Now with multi connections
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing

This commit is contained in:
Kasper Juul Hermansen 2021-11-17 18:57:53 +01:00
parent fd79027b37
commit 2f537a7535
Signed by: kjuulh
GPG Key ID: DCD9397082D97069
4 changed files with 147 additions and 77 deletions

View File

@ -1,107 +1,173 @@
using System.Collections.Concurrent;
using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
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.Interfaces.Persistence; using Todo.Core.Interfaces.Persistence;
namespace Todo.Api.Hubs namespace Todo.Api.Hubs;
[Authorize]
public class TodoHub : Hub
{ {
[Authorize] private readonly ITodoRepository _todoRepository;
public class TodoHub : Hub
private static readonly ConcurrentDictionary<string, List<string>> ConnectedUsers = new();
public override Task OnConnectedAsync()
{ {
private readonly ITodoRepository _todoRepository; var userId = Context.User.FindFirstValue("sub");
public TodoHub(ITodoRepository todoRepository) // 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(Context.ConnectionId);
// Add to the global dictionary of connected users
ConnectedUsers.TryAdd(userId, existingUserConnectionIds);
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception? exception)
{
var userId = Context.User.FindFirstValue("sub");
ConnectedUsers.TryGetValue(userId, out var existingUserConnectionIds);
// 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)
{ {
_todoRepository = todoRepository; // if there are no connections for the user,
// just delete the userName key from the ConnectedUsers concurent dictionary
ConnectedUsers.TryRemove(userId, out _);
} }
public async Task CreateTodo(string todoTitle, string projectName) return base.OnDisconnectedAsync(exception);
{ }
if (todoTitle is null)
throw new ArgumentException("title cannot be null");
var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName);
var todos = await _todoRepository.GetNotDoneTodos();
var serializedTodos =
JsonSerializer.Serialize(todos
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Project = t.Project })
.ToList());
await Clients.Caller.SendAsync("getInboxTodos", serializedTodos); public TodoHub(ITodoRepository todoRepository)
} {
_todoRepository = todoRepository;
}
public async Task UpdateTodo(string todoId, bool todoStatus) public async Task CreateTodo(string todoTitle, string projectName)
{ {
await _todoRepository.UpdateTodoStatus(todoId, todoStatus); if (todoTitle is null)
throw new ArgumentException("title cannot be null");
var _ = await _todoRepository.CreateTodoAsync(todoTitle, projectName);
var todos = await _todoRepository.GetNotDoneTodos(); var todos = await _todoRepository.GetNotDoneTodos();
var serializedTodos = var serializedTodos =
JsonSerializer.Serialize(todos JsonSerializer.Serialize(todos
.Select(t => new TodoResponse .Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Project = t.Project })
{ Id = t.Id, Title = t.Title, Status = t.Status, Project = t.Project })
.ToList());
await Clients.Caller.SendAsync("getInboxTodos", serializedTodos);
}
public async Task GetTodos()
{
var todos = await _todoRepository.GetTodosAsync();
var serializedTodos = JsonSerializer.Serialize(todos
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.Project })
.ToList()); .ToList());
await Clients.Caller.SendAsync("todos", serializedTodos); await RunOnUserConnections(async (connections) =>
} await Clients.Clients(connections).SendAsync("getInboxTodos", serializedTodos));
}
public async Task GetInboxTodos()
{ public async Task UpdateTodo(string todoId, bool todoStatus)
var todos = await _todoRepository.GetNotDoneTodos(); {
var serializedTodos = JsonSerializer.Serialize(todos await _todoRepository.UpdateTodoStatus(todoId, todoStatus);
.Select(t => new TodoResponse { Id = t.Id, Title = t.Title, Status = t.Status, Project = t.Project })
var todos = await _todoRepository.GetNotDoneTodos();
var serializedTodos =
JsonSerializer.Serialize(todos
.Select(t => new TodoResponse
{ Id = t.Id, Title = t.Title, Status = t.Status, Project = t.Project })
.ToList()); .ToList());
await Clients.Caller.SendAsync("getInboxTodos", serializedTodos); await RunOnUserConnections(async (connections) =>
} await Clients.Clients(connections).SendAsync("getInboxTodos", serializedTodos));
}
public async Task GetTodo(string todoId) public async Task GetTodos()
{
var todos = await _todoRepository.GetTodosAsync();
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("todos", serializedTodos));
}
public async Task GetInboxTodos()
{
var todos = await _todoRepository.GetNotDoneTodos();
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 GetTodo(string todoId)
{
var todo = await _todoRepository.GetTodoByIdAsync(todoId);
var serializedTodo = JsonSerializer.Serialize(new TodoResponse()
{ {
var todo = await _todoRepository.GetTodoByIdAsync(todoId); Id = todo.Id,
var serializedTodo = JsonSerializer.Serialize(new TodoResponse() Project = todo.Project,
{ Status = todo.Status,
Id = todo.Id, Title = todo.Title,
Project = todo.Project, });
Status = todo.Status,
Title = todo.Title,
});
await Clients.Caller.SendAsync("getTodo", serializedTodo); await RunOnUserConnections(async (connections) =>
} await Clients.Clients(connections).SendAsync("getTodo", serializedTodo));
}
public async Task ReplaceTodo(string updateTodoRequest) public async Task ReplaceTodo(string updateTodoRequest)
{
var updateTodo = JsonSerializer.Deserialize<UpdateTodoRequest>(updateTodoRequest);
if (updateTodo is null)
throw new InvalidOperationException("Could not parse invalid updateTodo");
var updatedTodo = await _todoRepository.UpdateTodoAsync(new Core.Entities.Todo()
{ {
var updateTodo = JsonSerializer.Deserialize<UpdateTodoRequest>(updateTodoRequest); Id = updateTodo.Id,
if (updateTodo is null) Project = updateTodo.Project,
throw new InvalidOperationException("Could not parse invalid updateTodo"); Status = updateTodo.Status,
Title = updateTodo.Title
});
var updatedTodo = await _todoRepository.UpdateTodoAsync(new Core.Entities.Todo() var serializedTodo = JsonSerializer.Serialize(new TodoResponse()
{ {
Id = updateTodo.Id, Id = updatedTodo.Id,
Project = updateTodo.Project, Project = updatedTodo.Project,
Status = updateTodo.Status, Status = updatedTodo.Status,
Title = updateTodo.Title Title = updatedTodo.Title,
}); });
var serializedTodo = JsonSerializer.Serialize(new TodoResponse() await RunOnUserConnections(async (connections) =>
{ await Clients.Clients(connections).SendAsync("getTodo", serializedTodo));
Id = updatedTodo.Id, }
Project = updatedTodo.Project,
Status = updatedTodo.Status,
Title = updatedTodo.Title,
});
await Clients.Caller.SendAsync("getTodo", serializedTodo); private Task RunOnUserConnections(Func<IEnumerable<string>, Task> action)
} {
var userId = Context.User.FindFirstValue("sub");
if (userId is null)
throw new InvalidOperationException("User id was invalid. Something has gone terribly wrong");
ConnectedUsers.TryGetValue(userId, out var connections);
if (connections is not null)
action(connections);
return Task.CompletedTask;
} }
} }

View File

@ -27,6 +27,7 @@ namespace Todo.Api
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddControllers(); services.AddControllers();
services.AddHttpContextAccessor();
services.AddCors(options => services.AddCors(options =>
{ {
options.AddDefaultPolicy(builder => options.AddDefaultPolicy(builder =>

View File

@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -40,7 +40,9 @@ export const SocketProvider: FC = (props) => {
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:5000"; process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:5000";
const connection = new HubConnectionBuilder() const connection = new HubConnectionBuilder()
.withUrl(`${serverUrl}/hubs/todo`) .withUrl(`${serverUrl}/hubs/todo`, {
withCredentials: true
})
.withAutomaticReconnect() .withAutomaticReconnect()
.configureLogging(LogLevel.Information) .configureLogging(LogLevel.Information)
.build(); .build();