Add mediator
This commit is contained in:
parent
3b446fa885
commit
9c5eaf2644
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Todo.Core.Interfaces.Persistence;
|
using Todo.Core.Interfaces.Persistence;
|
||||||
@ -17,8 +18,14 @@ public class TodosController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult<Core.Entities.Todo>> CreateTodo([FromBody] CreateTodoRequest request) =>
|
[Authorize]
|
||||||
Ok(await _todoRepository.CreateTodoAsync(request.Title, String.Empty));
|
public async Task<ActionResult<Core.Entities.Todo>> CreateTodo([FromBody] CreateTodoRequest request)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue("sub") ??
|
||||||
|
throw new InvalidOperationException("Could not get user, something has gone terribly wrong");
|
||||||
|
|
||||||
|
return Ok(await _todoRepository.CreateTodoAsync(request.Title, String.Empty, userId));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Todo.Api.Hubs.Models;
|
using Todo.Api.Hubs.Models;
|
||||||
|
using Todo.Core.Application.Commands.Todo;
|
||||||
|
using Todo.Core.Application.Services.UserConnectionStore;
|
||||||
using Todo.Core.Interfaces.Persistence;
|
using Todo.Core.Interfaces.Persistence;
|
||||||
|
using Todo.Core.Interfaces.User;
|
||||||
|
using Todo.Infrastructure;
|
||||||
|
|
||||||
namespace Todo.Api.Hubs;
|
namespace Todo.Api.Hubs;
|
||||||
|
|
||||||
@ -13,85 +17,59 @@ namespace Todo.Api.Hubs;
|
|||||||
public class TodoHub : Hub
|
public class TodoHub : Hub
|
||||||
{
|
{
|
||||||
private readonly ITodoRepository _todoRepository;
|
private readonly ITodoRepository _todoRepository;
|
||||||
|
private readonly IUserConnectionStore _userConnectionStore;
|
||||||
private static readonly ConcurrentDictionary<string, List<string>> ConnectedUsers = new();
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
public override Task OnConnectedAsync()
|
public override Task OnConnectedAsync()
|
||||||
{
|
{
|
||||||
var userId = Context.User.FindFirstValue("sub");
|
var userId = _currentUserService.GetUserId();
|
||||||
|
if (userId is not null)
|
||||||
// Try to get a List of existing user connections from the cache
|
_userConnectionStore.AddAsync(userId, Context.ConnectionId);
|
||||||
ConnectedUsers.TryGetValue(userId, out var existingUserConnectionIds);
|
|
||||||
|
|
||||||
// happens on the very first connection from the user
|
|
||||||
existingUserConnectionIds ??= new List<string>();
|
|
||||||
|
|
||||||
// First add to a List of existing user connections (i.e. multiple web browser tabs)
|
|
||||||
existingUserConnectionIds.Add(Context.ConnectionId);
|
|
||||||
|
|
||||||
|
|
||||||
// Add to the global dictionary of connected users
|
|
||||||
ConnectedUsers.TryAdd(userId, existingUserConnectionIds);
|
|
||||||
|
|
||||||
return base.OnConnectedAsync();
|
return base.OnConnectedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task OnDisconnectedAsync(Exception? exception)
|
public override Task OnDisconnectedAsync(Exception? exception)
|
||||||
{
|
{
|
||||||
var userId = Context.User.FindFirstValue("sub");
|
var userId = _currentUserService.GetUserId();
|
||||||
|
if (userId is not null)
|
||||||
ConnectedUsers.TryGetValue(userId, out var existingUserConnectionIds);
|
_userConnectionStore.RemoveAsync(userId, Context.ConnectionId);
|
||||||
|
|
||||||
// remove the connection id from the List
|
|
||||||
existingUserConnectionIds?.Remove(Context.ConnectionId);
|
|
||||||
|
|
||||||
// If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers).
|
|
||||||
if (existingUserConnectionIds?.Count == 0)
|
|
||||||
{
|
|
||||||
// if there are no connections for the user,
|
|
||||||
// just delete the userName key from the ConnectedUsers concurent dictionary
|
|
||||||
ConnectedUsers.TryRemove(userId, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.OnDisconnectedAsync(exception);
|
return base.OnDisconnectedAsync(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TodoHub(
|
||||||
public TodoHub(ITodoRepository todoRepository)
|
ITodoRepository todoRepository,
|
||||||
|
IUserConnectionStore userConnectionStore,
|
||||||
|
ICurrentUserService currentUserService,
|
||||||
|
IMediator mediator)
|
||||||
{
|
{
|
||||||
_todoRepository = todoRepository;
|
_todoRepository = todoRepository;
|
||||||
|
_userConnectionStore = userConnectionStore;
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateTodo(string todoTitle, string projectName)
|
public async Task CreateTodo(string todoTitle, string? projectName)
|
||||||
{
|
{
|
||||||
if (todoTitle is null)
|
if (todoTitle is null)
|
||||||
throw new ArgumentException("title cannot be null");
|
throw new ArgumentException("title cannot be null");
|
||||||
var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName);
|
|
||||||
|
|
||||||
var todos = await _todoRepository.GetNotDoneTodos();
|
//var userId = GetUserId();
|
||||||
var serializedTodos =
|
|
||||||
JsonSerializer.Serialize(todos
|
|
||||||
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Project = t.Project })
|
|
||||||
.ToList());
|
|
||||||
|
|
||||||
await RunOnUserConnections(async (connections) =>
|
//var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName, userId);
|
||||||
await Clients.Clients(connections).SendAsync("getInboxTodos", serializedTodos));
|
await _mediator.Send(new CreateTodoCommand(todoTitle, projectName));
|
||||||
|
|
||||||
|
await GetInboxTodos();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task UpdateTodo(string todoId, bool todoStatus)
|
public async Task UpdateTodo(string todoId, bool todoStatus)
|
||||||
{
|
{
|
||||||
await _todoRepository.UpdateTodoStatus(todoId, todoStatus);
|
var userId = GetUserId();
|
||||||
|
await _todoRepository.UpdateTodoStatus(todoId, todoStatus, userId);
|
||||||
|
|
||||||
var todos = await _todoRepository.GetNotDoneTodos();
|
await GetInboxTodos();
|
||||||
var serializedTodos =
|
|
||||||
JsonSerializer.Serialize(todos
|
|
||||||
.Select(t => new TodoResponse
|
|
||||||
{ Id = t.Id, Title = t.Title, Status = t.Status, Project = t.Project })
|
|
||||||
.ToList());
|
|
||||||
|
|
||||||
await RunOnUserConnections(async (connections) =>
|
|
||||||
await Clients.Clients(connections).SendAsync("getInboxTodos", serializedTodos));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task GetTodos()
|
public async Task GetTodos()
|
||||||
@ -157,17 +135,15 @@ public class TodoHub : Hub
|
|||||||
await Clients.Clients(connections).SendAsync("getTodo", serializedTodo));
|
await Clients.Clients(connections).SendAsync("getTodo", serializedTodo));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RunOnUserConnections(Func<IEnumerable<string>, Task> action)
|
private async Task RunOnUserConnections(Func<IEnumerable<string>, Task> action)
|
||||||
{
|
{
|
||||||
var userId = Context.User.FindFirstValue("sub");
|
var userId = GetUserId();
|
||||||
if (userId is null)
|
var connections = await _userConnectionStore.GetConnectionsAsync(userId);
|
||||||
throw new InvalidOperationException("User id was invalid. Something has gone terribly wrong");
|
|
||||||
|
|
||||||
ConnectedUsers.TryGetValue(userId, out var connections);
|
await action(connections);
|
||||||
|
|
||||||
if (connections is not null)
|
|
||||||
action(connections);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetUserId() =>
|
||||||
|
_currentUserService.GetUserId() ??
|
||||||
|
throw new InvalidOperationException("User id was invalid. Something has gone terribly wrong");
|
||||||
}
|
}
|
42
src/backend/server/src/Todo.Api/Publishers/TodoPublisher.cs
Normal file
42
src/backend/server/src/Todo.Api/Publishers/TodoPublisher.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Todo.Api.Hubs;
|
||||||
|
using Todo.Core.Application.Services.UserConnectionStore;
|
||||||
|
using Todo.Core.Interfaces.Publisher;
|
||||||
|
using Todo.Core.Interfaces.User;
|
||||||
|
|
||||||
|
namespace Todo.Api.Publishers;
|
||||||
|
|
||||||
|
public class TodoPublisher : ITodoPublisher
|
||||||
|
{
|
||||||
|
private readonly IHubContext<TodoHub> _hubContext;
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
private readonly IUserConnectionStore _userConnectionStore;
|
||||||
|
private readonly ILogger<TodoPublisher> _logger;
|
||||||
|
|
||||||
|
public TodoPublisher(
|
||||||
|
IHubContext<TodoHub> hubContext,
|
||||||
|
ICurrentUserService currentUserService,
|
||||||
|
IUserConnectionStore userConnectionStore,
|
||||||
|
ILogger<TodoPublisher> logger)
|
||||||
|
{
|
||||||
|
_hubContext = hubContext;
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
_userConnectionStore = userConnectionStore;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Publish(string todoId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = _currentUserService.GetUserId() ?? throw new InvalidOperationException("Cannot proceed without user");
|
||||||
|
var connections = await _userConnectionStore.GetConnectionsAsync(userId);
|
||||||
|
|
||||||
|
await _hubContext
|
||||||
|
.Clients
|
||||||
|
.Clients(connections)
|
||||||
|
.SendAsync("todoCreated", todoId , cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("todo created {TodoId}", todoId);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Todo.Core.Interfaces.User;
|
||||||
|
|
||||||
|
namespace Todo.Api.Services;
|
||||||
|
|
||||||
|
public class HttpContextCurrentUserService : ICurrentUserService
|
||||||
|
{
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
|
public HttpContextCurrentUserService(IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetUserId() => _httpContextAccessor.HttpContext?.User.FindFirstValue("sub");
|
||||||
|
}
|
@ -8,9 +8,14 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Todo.Api.Hubs;
|
using Todo.Api.Hubs;
|
||||||
|
using Todo.Api.Publishers;
|
||||||
|
using Todo.Api.Services;
|
||||||
|
using Todo.Core.Interfaces.User;
|
||||||
using Todo.Infrastructure;
|
using Todo.Infrastructure;
|
||||||
using Todo.Persistence;
|
using Todo.Persistence;
|
||||||
using Todo.Persistence.Mongo;
|
using Todo.Persistence.Mongo;
|
||||||
|
using Todo.Core;
|
||||||
|
using Todo.Core.Interfaces.Publisher;
|
||||||
|
|
||||||
namespace Todo.Api
|
namespace Todo.Api
|
||||||
{
|
{
|
||||||
@ -37,6 +42,10 @@ namespace Todo.Api
|
|||||||
.AllowAnyMethod());
|
.AllowAnyMethod());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddCore();
|
||||||
|
services.AddScoped<ICurrentUserService, HttpContextCurrentUserService>();
|
||||||
|
services.AddScoped<ITodoPublisher, TodoPublisher>();
|
||||||
|
|
||||||
services.AddSwaggerGen(c =>
|
services.AddSwaggerGen(c =>
|
||||||
{
|
{
|
||||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo.Api", Version = "v1" });
|
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo.Api", Version = "v1" });
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using MediatR;
|
||||||
|
using Todo.Core.Application.Notifications.Todo;
|
||||||
|
using Todo.Core.Interfaces.Persistence;
|
||||||
|
using Todo.Core.Interfaces.User;
|
||||||
|
|
||||||
|
namespace Todo.Core.Application.Commands.Todo;
|
||||||
|
|
||||||
|
public record CreateTodoCommand(string TodoTitle, string? TodoProject) : IRequest<string>
|
||||||
|
{
|
||||||
|
internal class Handler : IRequestHandler<CreateTodoCommand, string>
|
||||||
|
{
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
private readonly ITodoRepository _todoRepository;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public Handler(ICurrentUserService currentUserService, ITodoRepository todoRepository, IMediator mediator)
|
||||||
|
{
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
_todoRepository = todoRepository;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> Handle(CreateTodoCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = _currentUserService.GetUserId();
|
||||||
|
if (userId is null)
|
||||||
|
throw new InvalidOperationException("User was not found");
|
||||||
|
|
||||||
|
var todo = await _todoRepository.CreateTodoAsync(request.TodoTitle, request.TodoProject, userId);
|
||||||
|
|
||||||
|
await _mediator.Publish(new TodoCreated(todo.Id), cancellationToken);
|
||||||
|
|
||||||
|
return todo.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Todo.Core.Application.Services.UserConnectionStore;
|
||||||
|
using Todo.Core.Interfaces.Publisher;
|
||||||
|
using Todo.Core.Interfaces.User;
|
||||||
|
|
||||||
|
namespace Todo.Core.Application.Notifications.Todo;
|
||||||
|
|
||||||
|
public record TodoCreated([property: JsonPropertyName("todoId")] string TodoId) : INotification
|
||||||
|
{
|
||||||
|
internal class Handler : INotificationHandler<TodoCreated>
|
||||||
|
{
|
||||||
|
private readonly ITodoPublisher _todoPublisher;
|
||||||
|
|
||||||
|
public Handler(ITodoPublisher todoPublisher)
|
||||||
|
{
|
||||||
|
_todoPublisher = todoPublisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(TodoCreated notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _todoPublisher.Publish(JsonSerializer.Serialize(notification), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Todo.Core.Interfaces.Publisher;
|
||||||
|
|
||||||
|
public interface ITodoPublisher
|
||||||
|
{
|
||||||
|
Task Publish(string todoId, CancellationToken cancellationToken = new ());
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace Todo.Core.Application.Services.UserConnectionStore;
|
||||||
|
|
||||||
|
public interface IUserConnectionStore
|
||||||
|
{
|
||||||
|
Task AddAsync(string userId, string connectionId);
|
||||||
|
Task RemoveAsync(string userId, string connectionId);
|
||||||
|
Task<IEnumerable<string>> GetConnectionsAsync(string userId);
|
||||||
|
}
|
@ -2,9 +2,13 @@ global using System;
|
|||||||
global using System.Linq;
|
global using System.Linq;
|
||||||
global using System.Threading.Tasks;
|
global using System.Threading.Tasks;
|
||||||
global using System.Collections.Generic;
|
global using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Todo.Core;
|
namespace Todo.Core;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
|
public static IServiceCollection AddCore(this IServiceCollection services) => services.AddMediatR(Assembly.GetExecutingAssembly());
|
||||||
}
|
}
|
@ -3,9 +3,9 @@ namespace Todo.Core.Interfaces.Persistence;
|
|||||||
|
|
||||||
public interface ITodoRepository
|
public interface ITodoRepository
|
||||||
{
|
{
|
||||||
Task<Entities.Todo> CreateTodoAsync(string title, string projectName);
|
Task<Entities.Todo> CreateTodoAsync(string title, string projectName, string userId);
|
||||||
Task<IEnumerable<Entities.Todo>> GetTodosAsync();
|
Task<IEnumerable<Entities.Todo>> GetTodosAsync();
|
||||||
Task UpdateTodoStatus(string todoId, bool todoStatus);
|
Task UpdateTodoStatus(string todoId, bool todoStatus, string userId);
|
||||||
Task<IEnumerable<Entities.Todo>> GetNotDoneTodos();
|
Task<IEnumerable<Entities.Todo>> GetNotDoneTodos();
|
||||||
Task<Entities.Todo> GetTodoByIdAsync(string todoId);
|
Task<Entities.Todo> GetTodoByIdAsync(string todoId);
|
||||||
Task<Entities.Todo> UpdateTodoAsync(Entities.Todo todo);
|
Task<Entities.Todo> UpdateTodoAsync(Entities.Todo todo);
|
||||||
|
@ -4,5 +4,5 @@ namespace Todo.Core.Interfaces.Persistence;
|
|||||||
|
|
||||||
public interface IUserRepository
|
public interface IUserRepository
|
||||||
{
|
{
|
||||||
Task<User> Register(string email, string password);
|
Task<Entities.User> Register(string email, string password);
|
||||||
}
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Todo.Core.Interfaces.User;
|
||||||
|
|
||||||
|
public interface ICurrentUserService
|
||||||
|
{
|
||||||
|
string? GetUserId();
|
||||||
|
}
|
@ -2,6 +2,20 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Application\Services" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Mediatr" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Mediatr.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -3,7 +3,8 @@ using Microsoft.AspNetCore.Builder;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
using Todo.Core.Application.Services.UserConnectionStore;
|
||||||
|
using Todo.Infrastructure.UserConnectionStore;
|
||||||
|
|
||||||
namespace Todo.Infrastructure;
|
namespace Todo.Infrastructure;
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ public static class DependencyInjection
|
|||||||
.Bind(giteaOptions)
|
.Bind(giteaOptions)
|
||||||
.ValidateDataAnnotations();
|
.ValidateDataAnnotations();
|
||||||
|
|
||||||
|
services.AddSingleton<IUserConnectionStore, InMemoryUserConnectionStore>();
|
||||||
|
|
||||||
return services.AddAuthentication(options =>
|
return services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
|
@ -13,5 +13,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Todo.Core\Todo.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Todo.Core.Application.Services.UserConnectionStore;
|
||||||
|
|
||||||
|
namespace Todo.Infrastructure.UserConnectionStore;
|
||||||
|
|
||||||
|
class InMemoryUserConnectionStore : IUserConnectionStore
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, List<string>> ConnectedUsers = new();
|
||||||
|
|
||||||
|
public Task AddAsync(string userId, string connectionId)
|
||||||
|
{
|
||||||
|
// Try to get a List of existing user connections from the cache
|
||||||
|
ConnectedUsers.TryGetValue(userId, out var existingUserConnectionIds);
|
||||||
|
|
||||||
|
// happens on the very first connection from the user
|
||||||
|
existingUserConnectionIds ??= new List<string>();
|
||||||
|
|
||||||
|
// First add to a List of existing user connections (i.e. multiple web browser tabs)
|
||||||
|
existingUserConnectionIds.Add(connectionId);
|
||||||
|
|
||||||
|
// Add to the global dictionary of connected users
|
||||||
|
ConnectedUsers.TryAdd(userId, existingUserConnectionIds);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<string>> GetConnectionsAsync(string userId)
|
||||||
|
{
|
||||||
|
ConnectedUsers.TryGetValue(userId, out var connections);
|
||||||
|
return Task.FromResult(connections is null ? new List<string>().AsEnumerable() : connections.AsEnumerable());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveAsync(string userId, string connectionId)
|
||||||
|
{
|
||||||
|
ConnectedUsers.TryGetValue(userId, out var existingUserConnectionIds);
|
||||||
|
|
||||||
|
// remove the connection id from the List
|
||||||
|
existingUserConnectionIds?.Remove(connectionId);
|
||||||
|
|
||||||
|
// If there are no connection ids in the List, delete the user from the global cache (ConnectedUsers).
|
||||||
|
if (existingUserConnectionIds?.Count == 0)
|
||||||
|
{
|
||||||
|
// if there are no connections for the user,
|
||||||
|
// just delete the userName key from the ConnectedUsers concurent dictionary
|
||||||
|
ConnectedUsers.TryRemove(userId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
@ -12,4 +12,5 @@ public record MongoTodo
|
|||||||
[BsonRequired] public string Title { get; init; }
|
[BsonRequired] public string Title { get; init; }
|
||||||
[BsonRequired] public bool Status { get; set; }
|
[BsonRequired] public bool Status { get; set; }
|
||||||
public string ProjectName { get; set; } = String.Empty;
|
public string ProjectName { get; set; } = String.Empty;
|
||||||
|
public string AuthorId { get; set; }
|
||||||
}
|
}
|
@ -15,9 +15,9 @@ public class TodoRepository : ITodoRepository
|
|||||||
_todosCollection = database.GetCollection<MongoTodo>("todos");
|
_todosCollection = database.GetCollection<MongoTodo>("todos");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Core.Entities.Todo> CreateTodoAsync(string title, string projectName)
|
public async Task<Core.Entities.Todo> CreateTodoAsync(string title, string projectName, string userId)
|
||||||
{
|
{
|
||||||
var todo = new MongoTodo() { Title = title, ProjectName = projectName };
|
var todo = new MongoTodo() { Title = title, ProjectName = projectName, AuthorId = userId };
|
||||||
await _todosCollection.InsertOneAsync(todo);
|
await _todosCollection.InsertOneAsync(todo);
|
||||||
return new Core.Entities.Todo()
|
return new Core.Entities.Todo()
|
||||||
{ Id = todo.Id, Title = todo.Title, Status = false, Project = todo.ProjectName };
|
{ Id = todo.Id, Title = todo.Title, Status = false, Project = todo.ProjectName };
|
||||||
@ -32,10 +32,10 @@ public class TodoRepository : ITodoRepository
|
|||||||
new Core.Entities.Todo() { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.ProjectName });
|
new Core.Entities.Todo() { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.ProjectName });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateTodoStatus(string todoId, bool todoStatus)
|
public async Task UpdateTodoStatus(string todoId, bool todoStatus, string userId)
|
||||||
{
|
{
|
||||||
await _todosCollection
|
await _todosCollection
|
||||||
.UpdateOneAsync(t => t.Id == todoId,
|
.UpdateOneAsync(t => t.Id == todoId && t.AuthorId == userId,
|
||||||
Builders<MongoTodo>.Update.Set(t => t.Status, todoStatus));
|
Builders<MongoTodo>.Update.Set(t => t.Status, todoStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ export const TodoCheckmark: FC<TodoCheckmarkProps> = (props) => (
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.updateTodo({ ...props.todo, status: !props.todo.status })
|
props.updateTodo({ ...props.todo, status: !props.todo.status })
|
||||||
}
|
}
|
||||||
className={`todo-checkmark h-5 w-5 rounded-full border dark:border-gray-500 ${
|
className={`todo-checkmark h-5 w-5 rounded-full border dark:border-gray-500 cursor-pointer ${
|
||||||
props.todo.status === StatusState.done
|
props.todo.status === StatusState.done
|
||||||
? "bg-gray-300 dark:bg-gray-500"
|
? "bg-gray-300 dark:bg-gray-500"
|
||||||
: "hover:bg-gray-200 hover:dark:bg-gray-600"
|
: "hover:bg-gray-200 hover:dark:bg-gray-600"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user