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.
// 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<string, string> usersOnline = new ConcurrentDictionary<string, string>();
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)

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.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<IPresenceManager, DefaultPresenceManager<Chat>>();
}
// 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) {
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) {

View File

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

View File

@ -32,9 +32,14 @@ namespace Microsoft.AspNetCore.Builder
_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);
}
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.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<IConfigureOptions<SignalROptions>, SignalROptionsSetup>();
services.AddSingleton<JsonNetInvocationAdapter>();