From 632c8abf774ba05a092654d2ce8f847d566b2a58 Mon Sep 17 00:00:00 2001 From: Pawel Kadluczka Date: Sun, 2 Apr 2017 22:16:07 -0700 Subject: [PATCH] Single server presence --- samples/ChatSample/DefaultPresenceManager.cs | 78 +++++++++++++++++++ samples/ChatSample/HubWithPresence.cs | 55 +++++++++++++ samples/ChatSample/Hubs/Chat.cs | 27 +++---- samples/ChatSample/IPresenceManager.cs | 27 +++++++ samples/ChatSample/Startup.cs | 3 + samples/ChatSample/Views/Home/Index.cshtml | 14 +++- src/Microsoft.AspNetCore.SignalR/Hub.cs | 1 - .../SignalRAppBuilderExtensions.cs | 7 +- .../SignalRDependencyInjectionExtensions.cs | 2 + 9 files changed, 196 insertions(+), 18 deletions(-) create mode 100644 samples/ChatSample/DefaultPresenceManager.cs create mode 100644 samples/ChatSample/HubWithPresence.cs create mode 100644 samples/ChatSample/IPresenceManager.cs diff --git a/samples/ChatSample/DefaultPresenceManager.cs b/samples/ChatSample/DefaultPresenceManager.cs new file mode 100644 index 0000000000..b79a91397a --- /dev/null +++ b/samples/ChatSample/DefaultPresenceManager.cs @@ -0,0 +1,78 @@ + +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using System; +using Microsoft.AspNetCore.Sockets; + +namespace ChatSample +{ + // TODO: not possible to use TClient instead of (implicit) IClientProxy + // public class DefaultPresenceManager : IPresenceManager where THub : HubWithPresence + public class DefaultPresenceManager : IPresenceManager where THub : HubWithPresence + { + private IHubContext _hubContext; + private HubLifetimeManager _lifetimeManager; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public DefaultPresenceManager(IHubContext hubContext, HubLifetimeManager lifetimeManager, IServiceScopeFactory serviceScopeFactory) + { + _hubContext = hubContext; + _lifetimeManager = lifetimeManager; + _serviceScopeFactory = serviceScopeFactory; + } + + private readonly ConcurrentDictionary usersOnline + = new ConcurrentDictionary(); + + public IEnumerable UsersOnline => usersOnline.Values; + + public async Task UserJoined(Connection connection) + { + // `context.User?.Identity?.Name ?? string.Empty` ? + 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) + { + usersOnline.TryRemove(connection, out UserDetails user); + + await Notify(hub => hub.OnUserLeft(user)); + } + + 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 + { + // TODO: log + } + finally + { + hubActivator.Release(hub); + } + } + } + } + } +} diff --git a/samples/ChatSample/HubWithPresence.cs b/samples/ChatSample/HubWithPresence.cs new file mode 100644 index 0000000000..c955daabb9 --- /dev/null +++ b/samples/ChatSample/HubWithPresence.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.SignalR; +using System.Collections.Generic; +using System.Threading.Tasks; +using System; + +namespace ChatSample +{ + public class HubWithPresence : HubWithPresence + { + public HubWithPresence(IPresenceManager presenceManager) + : base(presenceManager) + { + } + } + + public class HubWithPresence : Hub + { + private IPresenceManager _presenceManager; + + public HubWithPresence(IPresenceManager presenceManager) + { + _presenceManager = presenceManager; + } + + public IEnumerable UsersOnline + { + get + { + return _presenceManager.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; + } + + public virtual Task OnUserLeft(UserDetails user) + { + return Task.CompletedTask; + } + } +} diff --git a/samples/ChatSample/Hubs/Chat.cs b/samples/ChatSample/Hubs/Chat.cs index 51664a150f..cbfa913838 100644 --- a/samples/ChatSample/Hubs/Chat.cs +++ b/samples/ChatSample/Hubs/Chat.cs @@ -1,20 +1,19 @@ // 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.Threading.Tasks; -using System.Collections.Generic; -using System.Collections.Concurrent; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.SignalR; namespace ChatSample.Hubs { // TODO: Make this work [Authorize] - public class Chat : Hub + public class Chat : HubWithPresence { - private static readonly ConcurrentDictionary usersOnline = new ConcurrentDictionary(); + public Chat(IPresenceManager presenceManager) + : base(presenceManager) + { + } public override async Task OnConnectedAsync() { @@ -23,16 +22,18 @@ namespace ChatSample.Hubs Context.Connection.Dispose(); } - await Clients.Client(Context.ConnectionId).InvokeAsync("SetUsersOnline", usersOnline); - usersOnline.TryAdd(Context.ConnectionId, Context.User.Identity.Name); - - await Clients.All.InvokeAsync("OnConnected", Context.ConnectionId, Context.User.Identity.Name); + await Clients.Client(Context.ConnectionId).InvokeAsync("SetUsersOnline", UsersOnline); + await base.OnConnectedAsync(); } - public override Task OnDisconnectedAsync(Exception ex) + public override Task OnUserJoined(UserDetails user) { - usersOnline.TryRemove(Context.ConnectionId, out var value); - return Clients.All.InvokeAsync("OnDisconnected", Context.ConnectionId, Context.User.Identity.Name); + return Clients.Client(Context.ConnectionId).InvokeAsync("UserJoined", user); + } + + public override Task OnUserLeft(UserDetails user) + { + return Clients.Client(Context.ConnectionId).InvokeAsync("UserLeft", user); } public async Task Send(string message) diff --git a/samples/ChatSample/IPresenceManager.cs b/samples/ChatSample/IPresenceManager.cs new file mode 100644 index 0000000000..c38e32e9d6 --- /dev/null +++ b/samples/ChatSample/IPresenceManager.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Sockets; + +namespace ChatSample +{ + public class UserDetails + { + public UserDetails(string connectionId, string name) + { + ConnectionId = connectionId; + Name = name; + } + + public string ConnectionId { get; } + public string Name { get; } + } + + public interface IPresenceManager + { + IEnumerable UsersOnline { get; } + Task UserJoined(Connection connection); + Task UserLeft(Connection connection); + } +} diff --git a/samples/ChatSample/Startup.cs b/samples/ChatSample/Startup.cs index 4e3ca6ba99..be33807081 100644 --- a/samples/ChatSample/Startup.cs +++ b/samples/ChatSample/Startup.cs @@ -8,6 +8,7 @@ using ChatSample.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -56,6 +57,8 @@ namespace ChatSample services.AddSignalR(); services.AddAuthentication(); + + services.AddSingleton>(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/samples/ChatSample/Views/Home/Index.cshtml b/samples/ChatSample/Views/Home/Index.cshtml index aed41a5725..6d267c5e47 100644 --- a/samples/ChatSample/Views/Home/Index.cshtml +++ b/samples/ChatSample/Views/Home/Index.cshtml @@ -30,9 +30,7 @@ connection.onClosed = e => { }; connection.on('SetUsersOnline', function (usersOnline) { - for (var user in usersOnline) { - addUserOnline(user, usersOnline[user]); - } + usersOnline.forEach(user => addUserOnline(user)); }); connection.on('OnConnected', function (id, userName) { @@ -43,6 +41,16 @@ connection.on('OnConnected', function (id, userName) { connection.on('OnDisconnected', function (id, userName) { appendLine('User ' + userName + ' left the chat'); document.getElementById(id).outerHTML = ''; +} + +connection.on('UserJoined', function (user) { + appendLine('User ' + userName + ' joined the chat'); + addUserOnline(user); +}); + +connection.on('UserLeft', function (user) { + appendLine('User ' + userName + ' left the chat'); + document.getElementById(user.ConnectionId).outerHTML = ''; }); connection.on('Send', function (userName, message) { diff --git a/src/Microsoft.AspNetCore.SignalR/Hub.cs b/src/Microsoft.AspNetCore.SignalR/Hub.cs index 733ff49ff3..5ff206d408 100644 --- a/src/Microsoft.AspNetCore.SignalR/Hub.cs +++ b/src/Microsoft.AspNetCore.SignalR/Hub.cs @@ -9,7 +9,6 @@ namespace Microsoft.AspNetCore.SignalR { public class Hub : Hub { - } public class Hub : IDisposable diff --git a/src/Microsoft.AspNetCore.SignalR/SignalRAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.SignalR/SignalRAppBuilderExtensions.cs index 33a8399713..5c8daf39b2 100644 --- a/src/Microsoft.AspNetCore.SignalR/SignalRAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.SignalR/SignalRAppBuilderExtensions.cs @@ -32,9 +32,14 @@ namespace Microsoft.AspNetCore.Builder _routes = routes; } - public void MapHub(string path) where THub : Hub + public void MapHub(string path) where THub : Hub { _routes.MapEndpoint>(path); } + + public void MapHub(string path) where THub : Hub + { + _routes.MapEndpoint>(path); + } } } diff --git a/src/Microsoft.AspNetCore.SignalR/SignalRDependencyInjectionExtensions.cs b/src/Microsoft.AspNetCore.SignalR/SignalRDependencyInjectionExtensions.cs index bceb79c04f..cab6726734 100644 --- a/src/Microsoft.AspNetCore.SignalR/SignalRDependencyInjectionExtensions.cs +++ b/src/Microsoft.AspNetCore.SignalR/SignalRDependencyInjectionExtensions.cs @@ -14,6 +14,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddSockets(); services.AddSingleton(typeof(HubLifetimeManager<>), typeof(DefaultHubLifetimeManager<>)); services.AddSingleton(typeof(IHubContext<>), typeof(HubContext<>)); + // TODO: this breaks because of hardcoded IClientProxy + // services.AddSingleton(typeof(IHubContext<,>), typeof(HubContext<,>)); services.AddSingleton(typeof(HubEndPoint<>), typeof(HubEndPoint<>)); services.AddSingleton, SignalROptionsSetup>(); services.AddSingleton();