Splitting PresenceManager
This commit is contained in:
parent
19b390c5ab
commit
7f3baf5ce6
|
|
@ -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<THub> : IPresenceManager where THub : HubWithPresence
|
|
||||||
{
|
|
||||||
private IHubContext<THub> _hubContext;
|
|
||||||
private HubLifetimeManager<THub> _lifetimeManager;
|
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
public DefaultPresenceManager(IHubContext<THub> hubContext, HubLifetimeManager<THub> lifetimeManager,
|
|
||||||
IServiceScopeFactory serviceScopeFactory, ILoggerFactory loggerFactory)
|
|
||||||
{
|
|
||||||
_hubContext = hubContext;
|
|
||||||
_lifetimeManager = lifetimeManager;
|
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
|
||||||
_logger = loggerFactory.CreateLogger<DefaultPresenceManager<THub>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<Connection, UserDetails> _usersOnline
|
|
||||||
= new ConcurrentDictionary<Connection, UserDetails>();
|
|
||||||
|
|
||||||
public Task<IEnumerable<UserDetails>> 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<THub, Task> invocation)
|
|
||||||
{
|
|
||||||
foreach (var connection in _usersOnline.Keys)
|
|
||||||
{
|
|
||||||
using (var scope = _serviceScopeFactory.CreateScope())
|
|
||||||
{
|
|
||||||
var hubActivator = scope.ServiceProvider.GetRequiredService<IHubActivator<THub, IClientProxy>>();
|
|
||||||
var hub = hubActivator.Create();
|
|
||||||
|
|
||||||
hub.Clients = _hubContext.Clients;
|
|
||||||
hub.Context = new HubCallerContext(connection);
|
|
||||||
hub.Groups = new GroupManager<THub>(connection, _lifetimeManager);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await invocation(hub);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Presence notification failed.");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
hubActivator.Release(hub);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,41 +10,28 @@ namespace ChatSample
|
||||||
{
|
{
|
||||||
public class HubWithPresence : HubWithPresence<IClientProxy>
|
public class HubWithPresence : HubWithPresence<IClientProxy>
|
||||||
{
|
{
|
||||||
public HubWithPresence(IPresenceManager presenceManager)
|
public HubWithPresence(IUserTracker<HubWithPresence> userTracker)
|
||||||
: base(presenceManager)
|
: base(userTracker)
|
||||||
{
|
{ }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HubWithPresence<TClient> : Hub<TClient>
|
public class HubWithPresence<TClient> : Hub<TClient>
|
||||||
{
|
{
|
||||||
private IPresenceManager _presenceManager;
|
private IUserTracker<HubWithPresence<TClient>> _userTracker;
|
||||||
|
|
||||||
public HubWithPresence(IPresenceManager presenceManager)
|
public HubWithPresence(IUserTracker<HubWithPresence<TClient>> userTracker)
|
||||||
{
|
{
|
||||||
_presenceManager = presenceManager;
|
_userTracker = userTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IEnumerable<UserDetails>> UsersOnline
|
public Task<IEnumerable<UserDetails>> UsersOnline
|
||||||
{
|
{
|
||||||
get
|
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)
|
public virtual Task OnUserJoined(UserDetails user)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ namespace ChatSample.Hubs
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class Chat : HubWithPresence
|
public class Chat : HubWithPresence
|
||||||
{
|
{
|
||||||
public Chat(IPresenceManager presenceManager)
|
public Chat(IUserTracker<Chat> userTracker)
|
||||||
: base(presenceManager)
|
: base(userTracker)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Sockets;
|
using Microsoft.AspNetCore.Sockets;
|
||||||
|
|
||||||
namespace ChatSample
|
namespace ChatSample
|
||||||
{
|
{
|
||||||
public interface IPresenceManager
|
public interface IUserTracker<out THub>
|
||||||
{
|
{
|
||||||
Task<IEnumerable<UserDetails>> UsersOnline();
|
Task<IEnumerable<UserDetails>> UsersOnline();
|
||||||
Task UserJoined(Connection connection);
|
Task AddUser(Connection connection, UserDetails user);
|
||||||
Task UserLeft(Connection connection);
|
Task<UserDetails> RemoveUser(Connection connection);
|
||||||
|
|
||||||
|
event Action<UserDetails> UserJoined;
|
||||||
|
event Action<UserDetails> UserLeft;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<THub> : IUserTracker<THub>
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<Connection, UserDetails> _usersOnline
|
||||||
|
= new ConcurrentDictionary<Connection, UserDetails>();
|
||||||
|
|
||||||
|
public event Action<UserDetails> UserJoined;
|
||||||
|
public event Action<UserDetails> UserLeft;
|
||||||
|
|
||||||
|
public Task<IEnumerable<UserDetails>> UsersOnline()
|
||||||
|
=> Task.FromResult(_usersOnline.Values.AsEnumerable());
|
||||||
|
|
||||||
|
public Task AddUser(Connection connection, UserDetails user)
|
||||||
|
{
|
||||||
|
_usersOnline.TryAdd(connection, user);
|
||||||
|
UserJoined(user);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<UserDetails> RemoveUser(Connection connection)
|
||||||
|
{
|
||||||
|
if (_usersOnline.TryRemove(connection, out var userDetails))
|
||||||
|
{
|
||||||
|
UserLeft(userDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(userDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<THub> : DefaultHubLifetimeManager<THub>, IDisposable
|
||||||
|
where THub : HubWithPresence
|
||||||
|
{
|
||||||
|
private readonly ConnectionList _connections = new ConnectionList();
|
||||||
|
private readonly IUserTracker<THub> _userTracker;
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private IHubContext<THub> _hubContext;
|
||||||
|
|
||||||
|
public PresenceHubLifetimeManager(InvocationAdapterRegistry registry, IUserTracker<THub> userTracker,
|
||||||
|
IServiceScopeFactory serviceScopeFactory, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
|
||||||
|
: base(registry)
|
||||||
|
{
|
||||||
|
_userTracker = userTracker;
|
||||||
|
_userTracker.UserJoined += OnUserJoined;
|
||||||
|
_userTracker.UserLeft += OnUserLeft;
|
||||||
|
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = loggerFactory.CreateLogger<PresenceHubLifetimeManager<THub>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<THub, Task> invocation)
|
||||||
|
{
|
||||||
|
foreach (var connection in _connections)
|
||||||
|
{
|
||||||
|
using (var scope = _serviceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var hubActivator = scope.ServiceProvider.GetRequiredService<IHubActivator<THub, IClientProxy>>();
|
||||||
|
var hub = hubActivator.Create();
|
||||||
|
|
||||||
|
if (_hubContext == null)
|
||||||
|
{
|
||||||
|
// Cannot be injected due to circular dependency
|
||||||
|
_hubContext = _serviceProvider.GetRequiredService<IHubContext<THub>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
hub.Clients = _hubContext.Clients;
|
||||||
|
hub.Context = new HubCallerContext(connection);
|
||||||
|
hub.Groups = new GroupManager<THub>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
/*
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -19,7 +20,7 @@ using StackExchange.Redis;
|
||||||
|
|
||||||
namespace ChatSample
|
namespace ChatSample
|
||||||
{
|
{
|
||||||
public class RedisPresenceManager<THub> : IDisposable, IPresenceManager where THub : HubWithPresence
|
public class RedisPresenceManager<THub> : IDisposable, IUserTracker<THub> where THub : HubWithPresence
|
||||||
{
|
{
|
||||||
private readonly RedisKey UsersOnlineRedisKey = "UsersOnline";
|
private readonly RedisKey UsersOnlineRedisKey = "UsersOnline";
|
||||||
private readonly RedisChannel _redisChannel;
|
private readonly RedisChannel _redisChannel;
|
||||||
|
|
@ -32,7 +33,7 @@ namespace ChatSample
|
||||||
private readonly ISubscriber _redisSubscriber;
|
private readonly ISubscriber _redisSubscriber;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<Connection, object> connections
|
private readonly ConcurrentDictionary<Connection, object> _connections
|
||||||
= new ConcurrentDictionary<Connection, object>();
|
= new ConcurrentDictionary<Connection, object>();
|
||||||
|
|
||||||
// TODO: subscribe and handle lifecycle events/disconnects
|
// TODO: subscribe and handle lifecycle events/disconnects
|
||||||
|
|
@ -92,7 +93,7 @@ namespace ChatSample
|
||||||
|
|
||||||
public Task UserJoined(Connection connection)
|
public Task UserJoined(Connection connection)
|
||||||
{
|
{
|
||||||
connections.TryAdd(connection, null);
|
_connections.TryAdd(connection, null);
|
||||||
|
|
||||||
var database = _redisConnection.GetDatabase(_redisDatabase);
|
var database = _redisConnection.GetDatabase(_redisDatabase);
|
||||||
var user = $"{connection.ConnectionId}|{connection.User.Identity.Name}";
|
var user = $"{connection.ConnectionId}|{connection.User.Identity.Name}";
|
||||||
|
|
@ -105,7 +106,7 @@ namespace ChatSample
|
||||||
|
|
||||||
public Task UserLeft(Connection connection)
|
public Task UserLeft(Connection connection)
|
||||||
{
|
{
|
||||||
connections.TryRemove(connection, out object _);
|
_connections.TryRemove(connection, out object _);
|
||||||
|
|
||||||
var database = _redisConnection.GetDatabase(_redisDatabase);
|
var database = _redisConnection.GetDatabase(_redisDatabase);
|
||||||
var user = $"{connection.ConnectionId}|{connection.User.Identity.Name}";
|
var user = $"{connection.ConnectionId}|{connection.User.Identity.Name}";
|
||||||
|
|
@ -118,7 +119,7 @@ namespace ChatSample
|
||||||
|
|
||||||
private async Task Notify(Func<THub, Task> invocation)
|
private async Task Notify(Func<THub, Task> invocation)
|
||||||
{
|
{
|
||||||
foreach (var connection in connections.Keys)
|
foreach (var connection in _connections.Keys)
|
||||||
{
|
{
|
||||||
using (var scope = _serviceScopeFactory.CreateScope())
|
using (var scope = _serviceScopeFactory.CreateScope())
|
||||||
{
|
{
|
||||||
|
|
@ -174,3 +175,4 @@ namespace ChatSample
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
@ -54,15 +54,14 @@ namespace ChatSample
|
||||||
services.AddTransient<IEmailSender, AuthMessageSender>();
|
services.AddTransient<IEmailSender, AuthMessageSender>();
|
||||||
services.AddTransient<ISmsSender, AuthMessageSender>();
|
services.AddTransient<ISmsSender, AuthMessageSender>();
|
||||||
|
|
||||||
// To use Redis scaleout uncomment .AddRedis and register RedisPresenceManager
|
// To use Redis scaleout uncomment .AddRedis
|
||||||
// instead of DefaultPresenceManager
|
|
||||||
services.AddSignalR()
|
services.AddSignalR()
|
||||||
// .AddRedis()
|
// .AddRedis()
|
||||||
;
|
;
|
||||||
services.AddAuthentication();
|
services.AddAuthentication();
|
||||||
|
|
||||||
services.AddSingleton<IPresenceManager, DefaultPresenceManager<Chat>>();
|
services.AddSingleton(typeof(HubLifetimeManager<>), typeof(PresenceHubLifetimeManager<>));
|
||||||
// services.AddSingleton<IPresenceManager, RedisPresenceManager<Chat>>();
|
services.AddSingleton(typeof(IUserTracker<>), typeof(InMemoryUserTracker<>));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue