From 7f3baf5ce6d29c01c686a4af4e09dc04297e302b Mon Sep 17 00:00:00 2001 From: Pawel Kadluczka Date: Tue, 25 Apr 2017 16:43:08 -0700 Subject: [PATCH] Splitting PresenceManager --- samples/ChatSample/DefaultPresenceManager.cs | 84 ---------------- samples/ChatSample/HubWithPresence.cs | 27 ++--- samples/ChatSample/Hubs/Chat.cs | 4 +- .../{IPresenceManager.cs => IUserTracker.cs} | 10 +- samples/ChatSample/InMemoryUserTracker.cs | 39 ++++++++ .../ChatSample/PresenceHubLifetimeManager.cs | 99 +++++++++++++++++++ samples/ChatSample/RedisPresenceManager.cs | 12 ++- samples/ChatSample/Startup.cs | 7 +- 8 files changed, 164 insertions(+), 118 deletions(-) delete mode 100644 samples/ChatSample/DefaultPresenceManager.cs rename samples/ChatSample/{IPresenceManager.cs => IUserTracker.cs} (57%) create mode 100644 samples/ChatSample/InMemoryUserTracker.cs create mode 100644 samples/ChatSample/PresenceHubLifetimeManager.cs diff --git a/samples/ChatSample/DefaultPresenceManager.cs b/samples/ChatSample/DefaultPresenceManager.cs deleted file mode 100644 index c895ae7f1e..0000000000 --- a/samples/ChatSample/DefaultPresenceManager.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.SignalR; -using Microsoft.AspNetCore.Sockets; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace ChatSample -{ - public class DefaultPresenceManager : IPresenceManager where THub : HubWithPresence - { - private IHubContext _hubContext; - private HubLifetimeManager _lifetimeManager; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly ILogger _logger; - - public DefaultPresenceManager(IHubContext hubContext, HubLifetimeManager lifetimeManager, - IServiceScopeFactory serviceScopeFactory, ILoggerFactory loggerFactory) - { - _hubContext = hubContext; - _lifetimeManager = lifetimeManager; - _serviceScopeFactory = serviceScopeFactory; - _logger = loggerFactory.CreateLogger>(); - } - - private readonly ConcurrentDictionary _usersOnline - = new ConcurrentDictionary(); - - public Task> UsersOnline() - => Task.FromResult(_usersOnline.Values.AsEnumerable()); - - public async Task UserJoined(Connection connection) - { - var user = new UserDetails(connection.ConnectionId, connection.User.Identity.Name); - - await Notify(hub => hub.OnUserJoined(user)); - - _usersOnline.TryAdd(connection, user); - } - - public async Task UserLeft(Connection connection) - { - if (_usersOnline.TryRemove(connection, out var userDetails)) - { - await Notify(hub => hub.OnUserLeft(userDetails)); - } - } - - private async Task Notify(Func invocation) - { - foreach (var connection in _usersOnline.Keys) - { - using (var scope = _serviceScopeFactory.CreateScope()) - { - var hubActivator = scope.ServiceProvider.GetRequiredService>(); - var hub = hubActivator.Create(); - - hub.Clients = _hubContext.Clients; - hub.Context = new HubCallerContext(connection); - hub.Groups = new GroupManager(connection, _lifetimeManager); - - try - { - await invocation(hub); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Presence notification failed."); - } - finally - { - hubActivator.Release(hub); - } - } - } - } - } -} diff --git a/samples/ChatSample/HubWithPresence.cs b/samples/ChatSample/HubWithPresence.cs index e1c3480405..283655ec27 100644 --- a/samples/ChatSample/HubWithPresence.cs +++ b/samples/ChatSample/HubWithPresence.cs @@ -10,41 +10,28 @@ namespace ChatSample { public class HubWithPresence : HubWithPresence { - public HubWithPresence(IPresenceManager presenceManager) - : base(presenceManager) - { - } + public HubWithPresence(IUserTracker userTracker) + : base(userTracker) + { } } public class HubWithPresence : Hub { - private IPresenceManager _presenceManager; + private IUserTracker> _userTracker; - public HubWithPresence(IPresenceManager presenceManager) + public HubWithPresence(IUserTracker> userTracker) { - _presenceManager = presenceManager; + _userTracker = userTracker; } public Task> UsersOnline { get { - return _presenceManager.UsersOnline(); + return _userTracker.UsersOnline(); } } - public override Task OnConnectedAsync() - { - _presenceManager.UserJoined(Context.Connection); - return base.OnConnectedAsync(); - } - - public override Task OnDisconnectedAsync(Exception exception) - { - _presenceManager.UserLeft(Context.Connection); - return base.OnDisconnectedAsync(exception); - } - public virtual Task OnUserJoined(UserDetails user) { return Task.CompletedTask; diff --git a/samples/ChatSample/Hubs/Chat.cs b/samples/ChatSample/Hubs/Chat.cs index bcc86def43..e2c23218f7 100644 --- a/samples/ChatSample/Hubs/Chat.cs +++ b/samples/ChatSample/Hubs/Chat.cs @@ -10,8 +10,8 @@ namespace ChatSample.Hubs [Authorize] public class Chat : HubWithPresence { - public Chat(IPresenceManager presenceManager) - : base(presenceManager) + public Chat(IUserTracker userTracker) + : base(userTracker) { } diff --git a/samples/ChatSample/IPresenceManager.cs b/samples/ChatSample/IUserTracker.cs similarity index 57% rename from samples/ChatSample/IPresenceManager.cs rename to samples/ChatSample/IUserTracker.cs index c73d59a94b..efcebacfd4 100644 --- a/samples/ChatSample/IPresenceManager.cs +++ b/samples/ChatSample/IUserTracker.cs @@ -1,16 +1,20 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Sockets; namespace ChatSample { - public interface IPresenceManager + public interface IUserTracker { Task> UsersOnline(); - Task UserJoined(Connection connection); - Task UserLeft(Connection connection); + Task AddUser(Connection connection, UserDetails user); + Task RemoveUser(Connection connection); + + event Action UserJoined; + event Action UserLeft; } } diff --git a/samples/ChatSample/InMemoryUserTracker.cs b/samples/ChatSample/InMemoryUserTracker.cs new file mode 100644 index 0000000000..67668502fa --- /dev/null +++ b/samples/ChatSample/InMemoryUserTracker.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Sockets; + +namespace ChatSample +{ + public class InMemoryUserTracker : IUserTracker + { + private readonly ConcurrentDictionary _usersOnline + = new ConcurrentDictionary(); + + public event Action UserJoined; + public event Action UserLeft; + + public Task> UsersOnline() + => Task.FromResult(_usersOnline.Values.AsEnumerable()); + + public Task AddUser(Connection connection, UserDetails user) + { + _usersOnline.TryAdd(connection, user); + UserJoined(user); + + return Task.CompletedTask; + } + + public Task RemoveUser(Connection connection) + { + if (_usersOnline.TryRemove(connection, out var userDetails)) + { + UserLeft(userDetails); + } + + return Task.FromResult(userDetails); + } + } +} diff --git a/samples/ChatSample/PresenceHubLifetimeManager.cs b/samples/ChatSample/PresenceHubLifetimeManager.cs new file mode 100644 index 0000000000..9d551e4566 --- /dev/null +++ b/samples/ChatSample/PresenceHubLifetimeManager.cs @@ -0,0 +1,99 @@ + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Sockets; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ChatSample +{ + public class PresenceHubLifetimeManager : DefaultHubLifetimeManager, IDisposable + where THub : HubWithPresence + { + private readonly ConnectionList _connections = new ConnectionList(); + private readonly IUserTracker _userTracker; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private IHubContext _hubContext; + + public PresenceHubLifetimeManager(InvocationAdapterRegistry registry, IUserTracker userTracker, + IServiceScopeFactory serviceScopeFactory, ILoggerFactory loggerFactory, IServiceProvider serviceProvider) + : base(registry) + { + _userTracker = userTracker; + _userTracker.UserJoined += OnUserJoined; + _userTracker.UserLeft += OnUserLeft; + + _serviceScopeFactory = serviceScopeFactory; + _serviceProvider = serviceProvider; + _logger = loggerFactory.CreateLogger>(); + } + + public override async Task OnConnectedAsync(Connection connection) + { + await base.OnConnectedAsync(connection); + _connections.Add(connection); + await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, connection.User.Identity.Name)); + } + + public override async Task OnDisconnectedAsync(Connection connection) + { + await base.OnDisconnectedAsync(connection); + _connections.Remove(connection); + await _userTracker.RemoveUser(connection); + } + + private async void OnUserJoined(UserDetails userDetails) + { + await Notify(hub => hub.OnUserJoined(userDetails)); + } + + private async void OnUserLeft(UserDetails userDetails) + { + await Notify(hub => hub.OnUserLeft(userDetails)); + } + + private async Task Notify(Func invocation) + { + foreach (var connection in _connections) + { + using (var scope = _serviceScopeFactory.CreateScope()) + { + var hubActivator = scope.ServiceProvider.GetRequiredService>(); + var hub = hubActivator.Create(); + + if (_hubContext == null) + { + // Cannot be injected due to circular dependency + _hubContext = _serviceProvider.GetRequiredService>(); + } + + hub.Clients = _hubContext.Clients; + hub.Context = new HubCallerContext(connection); + hub.Groups = new GroupManager(connection, this); + + try + { + await invocation(hub); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Presence notification failed."); + } + finally + { + hubActivator.Release(hub); + } + } + } + } + + public void Dispose() + { + _userTracker.UserJoined -= OnUserJoined; + _userTracker.UserLeft -= OnUserLeft; + } + } +} diff --git a/samples/ChatSample/RedisPresenceManager.cs b/samples/ChatSample/RedisPresenceManager.cs index 05110a4600..6a3aefdd66 100644 --- a/samples/ChatSample/RedisPresenceManager.cs +++ b/samples/ChatSample/RedisPresenceManager.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +/* using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -19,7 +20,7 @@ using StackExchange.Redis; namespace ChatSample { - public class RedisPresenceManager : IDisposable, IPresenceManager where THub : HubWithPresence + public class RedisPresenceManager : IDisposable, IUserTracker where THub : HubWithPresence { private readonly RedisKey UsersOnlineRedisKey = "UsersOnline"; private readonly RedisChannel _redisChannel; @@ -32,7 +33,7 @@ namespace ChatSample private readonly ISubscriber _redisSubscriber; private readonly ILogger _logger; - private readonly ConcurrentDictionary connections + private readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); // TODO: subscribe and handle lifecycle events/disconnects @@ -92,7 +93,7 @@ namespace ChatSample public Task UserJoined(Connection connection) { - connections.TryAdd(connection, null); + _connections.TryAdd(connection, null); var database = _redisConnection.GetDatabase(_redisDatabase); var user = $"{connection.ConnectionId}|{connection.User.Identity.Name}"; @@ -105,7 +106,7 @@ namespace ChatSample public Task UserLeft(Connection connection) { - connections.TryRemove(connection, out object _); + _connections.TryRemove(connection, out object _); var database = _redisConnection.GetDatabase(_redisDatabase); var user = $"{connection.ConnectionId}|{connection.User.Identity.Name}"; @@ -118,7 +119,7 @@ namespace ChatSample private async Task Notify(Func invocation) { - foreach (var connection in connections.Keys) + foreach (var connection in _connections.Keys) { using (var scope = _serviceScopeFactory.CreateScope()) { @@ -174,3 +175,4 @@ namespace ChatSample } } } +*/ \ No newline at end of file diff --git a/samples/ChatSample/Startup.cs b/samples/ChatSample/Startup.cs index 3f2ec901a0..d2a02b8f0b 100644 --- a/samples/ChatSample/Startup.cs +++ b/samples/ChatSample/Startup.cs @@ -54,15 +54,14 @@ namespace ChatSample services.AddTransient(); services.AddTransient(); - // To use Redis scaleout uncomment .AddRedis and register RedisPresenceManager - // instead of DefaultPresenceManager + // To use Redis scaleout uncomment .AddRedis services.AddSignalR() // .AddRedis() ; services.AddAuthentication(); - services.AddSingleton>(); - // services.AddSingleton>(); + services.AddSingleton(typeof(HubLifetimeManager<>), typeof(PresenceHubLifetimeManager<>)); + services.AddSingleton(typeof(IUserTracker<>), typeof(InMemoryUserTracker<>)); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.