DI enabled Microsoft.AspNetCore.Sockets (#47)

* DI enabled Microsoft.AspNetCore.Sockets
- Added AddSockets extension method to IServiceCollection
- Inject IApplicationLifetime into ConnectionManager to handle
graceful shutdown and added test.
- Call AddSockets from AddSignalR

* PR feedback
- Added AddSignalR overload that takes Action<SignalROptions>
This commit is contained in:
David Fowler 2016-11-26 23:09:38 -08:00 committed by GitHub
parent 7b814e8d92
commit 1b59fc6f80
7 changed files with 91 additions and 40 deletions

View File

@ -16,18 +16,17 @@ namespace SocketsSample
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddRouting();
services.AddSingleton<ProtobufInvocationAdapter>(); services.AddSingleton<ProtobufInvocationAdapter>();
services.AddSingleton<LineInvocationAdapter>(); services.AddSingleton<LineInvocationAdapter>();
services.AddSignalR() services.AddSockets();
.AddSignalROptions(options =>
services.AddSignalR(options =>
{ {
options.RegisterInvocationAdapter<ProtobufInvocationAdapter>("protobuf"); options.RegisterInvocationAdapter<ProtobufInvocationAdapter>("protobuf");
options.RegisterInvocationAdapter<LineInvocationAdapter>("line"); options.RegisterInvocationAdapter<LineInvocationAdapter>("line");
}); });
// .AddRedis(); // .AddRedis();
services.AddSingleton<ChatEndPoint>(); services.AddSingleton<ChatEndPoint>();
services.AddSingleton<ProtobufSerializer>(); services.AddSingleton<ProtobufSerializer>();

View File

@ -8,10 +8,11 @@ using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
{ {
public static class DependencyInjectionExtensions public static class SignalRDependencyInjectionExtensions
{ {
public static ISignalRBuilder AddSignalR(this IServiceCollection services) public static ISignalRBuilder AddSignalR(this IServiceCollection services)
{ {
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<>));
services.AddSingleton(typeof(HubEndPoint<>), typeof(HubEndPoint<>)); services.AddSingleton(typeof(HubEndPoint<>), typeof(HubEndPoint<>));
@ -22,9 +23,14 @@ namespace Microsoft.Extensions.DependencyInjection
return new SignalRBuilder(services); return new SignalRBuilder(services);
} }
public static ISignalRBuilder AddSignalROptions(this ISignalRBuilder builder, Action<SignalROptions> configure) public static ISignalRBuilder AddSignalR(this IServiceCollection services, Action<SignalROptions> setupAction)
{ {
builder.Services.Configure(configure); return services.AddSignalR().AddSignalROptions(setupAction);
}
public static ISignalRBuilder AddSignalROptions(this ISignalRBuilder builder, Action<SignalROptions> setupAction)
{
builder.Services.Configure(setupAction);
return builder; return builder;
} }
} }

View File

@ -5,17 +5,22 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading; using System.Threading;
using Microsoft.AspNetCore.Hosting;
namespace Microsoft.AspNetCore.Sockets namespace Microsoft.AspNetCore.Sockets
{ {
public class ConnectionManager : IDisposable public class ConnectionManager
{ {
private ConcurrentDictionary<string, ConnectionState> _connections = new ConcurrentDictionary<string, ConnectionState>(); private ConcurrentDictionary<string, ConnectionState> _connections = new ConcurrentDictionary<string, ConnectionState>();
private Timer _timer; private Timer _timer;
public ConnectionManager() public ConnectionManager(IApplicationLifetime lifetime)
{ {
_timer = new Timer(Scan, this, 0, 1000); _timer = new Timer(Scan, this, 0, 1000);
// We hook stopping because we need the requests to end, Dispose doesn't work since
// that happens after requests are drained
lifetime.ApplicationStopping.Register(CloseConnections);
} }
public bool TryGetConnection(string id, out ConnectionState state) public bool TryGetConnection(string id, out ConnectionState state)
@ -96,7 +101,7 @@ namespace Microsoft.AspNetCore.Sockets
} }
} }
public void Dispose() private void CloseConnections()
{ {
// Stop firing the timer // Stop firing the timer
_timer.Dispose(); _timer.Dispose();

View File

@ -2,13 +2,10 @@
// 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.IO.Pipelines;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Sockets; using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Routing; using Microsoft.AspNetCore.Sockets.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Builder namespace Microsoft.AspNetCore.Builder
{ {
@ -16,15 +13,7 @@ namespace Microsoft.AspNetCore.Builder
{ {
public static IApplicationBuilder UseSockets(this IApplicationBuilder app, Action<SocketRouteBuilder> callback) public static IApplicationBuilder UseSockets(this IApplicationBuilder app, Action<SocketRouteBuilder> callback)
{ {
var manager = new ConnectionManager(); var dispatcher = app.ApplicationServices.GetRequiredService<HttpConnectionDispatcher>();
var factory = new PipelineFactory();
var loggerFactory = app.ApplicationServices.GetService<ILoggerFactory>();
var dispatcher = new HttpConnectionDispatcher(manager, factory, loggerFactory);
// Dispose the connection manager when application shutdown is triggered
var lifetime = app.ApplicationServices.GetRequiredService<IApplicationLifetime>();
lifetime.ApplicationStopping.Register(state => ((IDisposable)state).Dispose(), manager);
var routes = new RouteBuilder(app); var routes = new RouteBuilder(app);

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Sockets;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection
{
public static class SocketsDependencyInjectionExtensions
{
public static IServiceCollection AddSockets(this IServiceCollection services)
{
services.AddRouting();
services.TryAddSingleton<ConnectionManager>();
services.TryAddSingleton<PipelineFactory>();
services.TryAddSingleton<HttpConnectionDispatcher>();
return services;
}
}
}

View File

@ -1,11 +1,9 @@
// 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.IO.Pipelines; using System.IO.Pipelines;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Internal;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Sockets.Tests namespace Microsoft.AspNetCore.Sockets.Tests
@ -15,7 +13,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
[Fact] [Fact]
public void ReservedConnectionsHaveConnectionId() public void ReservedConnectionsHaveConnectionId()
{ {
var connectionManager = new ConnectionManager(); var lifetime = new ApplicationLifetime();
var connectionManager = new ConnectionManager(lifetime);
var state = connectionManager.ReserveConnection(); var state = connectionManager.ReserveConnection();
Assert.NotNull(state.Connection); Assert.NotNull(state.Connection);
@ -28,7 +27,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
[Fact] [Fact]
public void ReservedConnectionsCanBeRetrieved() public void ReservedConnectionsCanBeRetrieved()
{ {
var connectionManager = new ConnectionManager(); var lifetime = new ApplicationLifetime();
var connectionManager = new ConnectionManager(lifetime);
var state = connectionManager.ReserveConnection(); var state = connectionManager.ReserveConnection();
Assert.NotNull(state.Connection); Assert.NotNull(state.Connection);
@ -43,10 +43,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests
public void AddNewConnection() public void AddNewConnection()
{ {
using (var factory = new PipelineFactory()) using (var factory = new PipelineFactory())
using (var channel = new HttpConnection(factory)) using (var connection = new HttpConnection(factory))
{ {
var connectionManager = new ConnectionManager(); var lifetime = new ApplicationLifetime();
var state = connectionManager.AddNewConnection(channel); var connectionManager = new ConnectionManager(lifetime);
var state = connectionManager.AddNewConnection(connection);
Assert.NotNull(state.Connection); Assert.NotNull(state.Connection);
Assert.NotNull(state.Connection.ConnectionId); Assert.NotNull(state.Connection.ConnectionId);
@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
ConnectionState newState; ConnectionState newState;
Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState)); Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState));
Assert.Same(newState, state); Assert.Same(newState, state);
Assert.Same(channel, newState.Connection.Channel); Assert.Same(connection, newState.Connection.Channel);
} }
} }
@ -63,10 +64,11 @@ namespace Microsoft.AspNetCore.Sockets.Tests
public void RemoveConnection() public void RemoveConnection()
{ {
using (var factory = new PipelineFactory()) using (var factory = new PipelineFactory())
using (var channel = new HttpConnection(factory)) using (var connection = new HttpConnection(factory))
{ {
var connectionManager = new ConnectionManager(); var lifetime = new ApplicationLifetime();
var state = connectionManager.AddNewConnection(channel); var connectionManager = new ConnectionManager(lifetime);
var state = connectionManager.AddNewConnection(connection);
Assert.NotNull(state.Connection); Assert.NotNull(state.Connection);
Assert.NotNull(state.Connection.ConnectionId); Assert.NotNull(state.Connection.ConnectionId);
@ -75,11 +77,34 @@ namespace Microsoft.AspNetCore.Sockets.Tests
ConnectionState newState; ConnectionState newState;
Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState)); Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState));
Assert.Same(newState, state); Assert.Same(newState, state);
Assert.Same(channel, newState.Connection.Channel); Assert.Same(connection, newState.Connection.Channel);
connectionManager.RemoveConnection(state.Connection.ConnectionId); connectionManager.RemoveConnection(state.Connection.ConnectionId);
Assert.False(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState)); Assert.False(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState));
} }
} }
[Fact]
public async Task ApplicationStoppingClosesConnections()
{
using (var factory = new PipelineFactory())
using (var connection = new HttpConnection(factory))
{
var lifetime = new ApplicationLifetime();
var connectionManager = new ConnectionManager(lifetime);
var state = connectionManager.AddNewConnection(connection);
var task = Task.Run(async () =>
{
var result = await connection.Input.ReadAsync();
Assert.True(result.IsCompleted);
});
lifetime.StopApplication();
await task;
}
}
} }
} }

View File

@ -8,6 +8,7 @@ using System.IO.Pipelines;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
@ -20,7 +21,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
[Fact] [Fact]
public async Task GetIdReservesConnectionIdAndReturnsIt() public async Task GetIdReservesConnectionIdAndReturnsIt()
{ {
var manager = new ConnectionManager(); var lifetime = new ApplicationLifetime();
var manager = new ConnectionManager(lifetime);
using (var factory = new PipelineFactory()) using (var factory = new PipelineFactory())
{ {
var dispatcher = new HttpConnectionDispatcher(manager, factory, loggerFactory: null); var dispatcher = new HttpConnectionDispatcher(manager, factory, loggerFactory: null);
@ -41,7 +43,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
[Fact] [Fact]
public async Task SendingToReservedConnectionsThatHaveNotConnectedThrows() public async Task SendingToReservedConnectionsThatHaveNotConnectedThrows()
{ {
var manager = new ConnectionManager(); var lifetime = new ApplicationLifetime();
var manager = new ConnectionManager(lifetime);
var state = manager.ReserveConnection(); var state = manager.ReserveConnection();
using (var factory = new PipelineFactory()) using (var factory = new PipelineFactory())
@ -63,7 +66,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
[Fact] [Fact]
public async Task SendingToUnknownConnectionIdThrows() public async Task SendingToUnknownConnectionIdThrows()
{ {
var manager = new ConnectionManager(); var lifetime = new ApplicationLifetime();
var manager = new ConnectionManager(lifetime);
using (var factory = new PipelineFactory()) using (var factory = new PipelineFactory())
{ {
var dispatcher = new HttpConnectionDispatcher(manager, factory, loggerFactory: null); var dispatcher = new HttpConnectionDispatcher(manager, factory, loggerFactory: null);
@ -83,7 +87,8 @@ namespace Microsoft.AspNetCore.Sockets.Tests
[Fact] [Fact]
public async Task SendingWithoutConnectionIdThrows() public async Task SendingWithoutConnectionIdThrows()
{ {
var manager = new ConnectionManager(); var lifetime = new ApplicationLifetime();
var manager = new ConnectionManager(lifetime);
using (var factory = new PipelineFactory()) using (var factory = new PipelineFactory())
{ {
var dispatcher = new HttpConnectionDispatcher(manager, factory, loggerFactory: null); var dispatcher = new HttpConnectionDispatcher(manager, factory, loggerFactory: null);