Single server presence

This commit is contained in:
Pawel Kadluczka 2017-04-02 22:16:07 -07:00 committed by Pawel Kadluczka
parent 963fcd41ed
commit 632c8abf77
9 changed files with 196 additions and 18 deletions

View File

@ -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<THub> : IPresenceManager where THub : HubWithPresence<TClient>
public class DefaultPresenceManager<THub> : IPresenceManager where THub : HubWithPresence
{
private IHubContext<THub> _hubContext;
private HubLifetimeManager<THub> _lifetimeManager;
private readonly IServiceScopeFactory _serviceScopeFactory;
public DefaultPresenceManager(IHubContext<THub> hubContext, HubLifetimeManager<THub> lifetimeManager, IServiceScopeFactory serviceScopeFactory)
{
_hubContext = hubContext;
_lifetimeManager = lifetimeManager;
_serviceScopeFactory = serviceScopeFactory;
}
private readonly ConcurrentDictionary<Connection, UserDetails> usersOnline
= new ConcurrentDictionary<Connection, UserDetails>();
public IEnumerable<UserDetails> 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<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
{
// TODO: log
}
finally
{
hubActivator.Release(hub);
}
}
}
}
}
}

View File

@ -0,0 +1,55 @@
using Microsoft.AspNetCore.SignalR;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
namespace ChatSample
{
public class HubWithPresence : HubWithPresence<IClientProxy>
{
public HubWithPresence(IPresenceManager presenceManager)
: base(presenceManager)
{
}
}
public class HubWithPresence<TClient> : Hub<TClient>
{
private IPresenceManager _presenceManager;
public HubWithPresence(IPresenceManager presenceManager)
{
_presenceManager = presenceManager;
}
public IEnumerable<UserDetails> 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;
}
}
}

View File

@ -1,20 +1,19 @@
// 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.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic;
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace ChatSample.Hubs namespace ChatSample.Hubs
{ {
// TODO: Make this work // TODO: Make this work
[Authorize] [Authorize]
public class Chat : Hub public class Chat : HubWithPresence
{ {
private static readonly ConcurrentDictionary<string, string> usersOnline = new ConcurrentDictionary<string, string>(); public Chat(IPresenceManager presenceManager)
: base(presenceManager)
{
}
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
{ {
@ -23,16 +22,18 @@ namespace ChatSample.Hubs
Context.Connection.Dispose(); Context.Connection.Dispose();
} }
await Clients.Client(Context.ConnectionId).InvokeAsync("SetUsersOnline", usersOnline); await Clients.Client(Context.ConnectionId).InvokeAsync("SetUsersOnline", UsersOnline);
usersOnline.TryAdd(Context.ConnectionId, Context.User.Identity.Name); await base.OnConnectedAsync();
await Clients.All.InvokeAsync("OnConnected", Context.ConnectionId, Context.User.Identity.Name);
} }
public override Task OnDisconnectedAsync(Exception ex) public override Task OnUserJoined(UserDetails user)
{ {
usersOnline.TryRemove(Context.ConnectionId, out var value); return Clients.Client(Context.ConnectionId).InvokeAsync("UserJoined", user);
return Clients.All.InvokeAsync("OnDisconnected", Context.ConnectionId, Context.User.Identity.Name); }
public override Task OnUserLeft(UserDetails user)
{
return Clients.Client(Context.ConnectionId).InvokeAsync("UserLeft", user);
} }
public async Task Send(string message) public async Task Send(string message)

View File

@ -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<UserDetails> UsersOnline { get; }
Task UserJoined(Connection connection);
Task UserLeft(Connection connection);
}
}

View File

@ -8,6 +8,7 @@ using ChatSample.Services;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -56,6 +57,8 @@ namespace ChatSample
services.AddSignalR(); services.AddSignalR();
services.AddAuthentication(); services.AddAuthentication();
services.AddSingleton<IPresenceManager, DefaultPresenceManager<Chat>>();
} }
// 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.

View File

@ -30,9 +30,7 @@ connection.onClosed = e => {
}; };
connection.on('SetUsersOnline', function (usersOnline) { connection.on('SetUsersOnline', function (usersOnline) {
for (var user in usersOnline) { usersOnline.forEach(user => addUserOnline(user));
addUserOnline(user, usersOnline[user]);
}
}); });
connection.on('OnConnected', function (id, userName) { connection.on('OnConnected', function (id, userName) {
@ -43,6 +41,16 @@ connection.on('OnConnected', function (id, userName) {
connection.on('OnDisconnected', function (id, userName) { connection.on('OnDisconnected', function (id, userName) {
appendLine('User ' + userName + ' left the chat'); appendLine('User ' + userName + ' left the chat');
document.getElementById(id).outerHTML = ''; 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) { connection.on('Send', function (userName, message) {

View File

@ -9,7 +9,6 @@ namespace Microsoft.AspNetCore.SignalR
{ {
public class Hub : Hub<IClientProxy> public class Hub : Hub<IClientProxy>
{ {
} }
public class Hub<TClient> : IDisposable public class Hub<TClient> : IDisposable

View File

@ -32,9 +32,14 @@ namespace Microsoft.AspNetCore.Builder
_routes = routes; _routes = routes;
} }
public void MapHub<THub>(string path) where THub : Hub public void MapHub<THub>(string path) where THub : Hub<IClientProxy>
{ {
_routes.MapEndpoint<HubEndPoint<THub>>(path); _routes.MapEndpoint<HubEndPoint<THub>>(path);
} }
public void MapHub<THub, TClient>(string path) where THub : Hub<TClient>
{
_routes.MapEndpoint<HubEndPoint<THub, TClient>>(path);
}
} }
} }

View File

@ -14,6 +14,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddSockets(); services.AddSockets();
services.AddSingleton(typeof(HubLifetimeManager<>), typeof(DefaultHubLifetimeManager<>)); services.AddSingleton(typeof(HubLifetimeManager<>), typeof(DefaultHubLifetimeManager<>));
services.AddSingleton(typeof(IHubContext<>), typeof(HubContext<>)); 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(typeof(HubEndPoint<>), typeof(HubEndPoint<>));
services.AddSingleton<IConfigureOptions<SignalROptions>, SignalROptionsSetup>(); services.AddSingleton<IConfigureOptions<SignalROptions>, SignalROptionsSetup>();
services.AddSingleton<JsonNetInvocationAdapter>(); services.AddSingleton<JsonNetInvocationAdapter>();