diff --git a/Microsoft.AspNetCore.Sockets.sln b/Microsoft.AspNetCore.Sockets.sln
index 1834ba9c07..61f6f31626 100644
--- a/Microsoft.AspNetCore.Sockets.sln
+++ b/Microsoft.AspNetCore.Sockets.sln
@@ -19,6 +19,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Socket
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ClientSample", "samples\ClientSample\ClientSample.xproj", "{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6A35B453-52EC-48AF-89CA-D4A69800F131}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Sockets.Tests", "test\Microsoft.AspNetCore.Sockets.Tests\Microsoft.AspNetCore.Sockets.Tests.xproj", "{AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -37,6 +41,10 @@ Global
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -45,5 +53,6 @@ Global
{C4AEAB04-F341-4539-B6C0-52368FB4BF9E} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
{1715EA8D-8E13-4ACF-8BCA-57D048E55ED8} = {DA69F624-5398-4884-87E4-B816698CDE65}
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
+ {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
EndGlobalSection
EndGlobal
diff --git a/src/Microsoft.AspNetCore.Sockets/EndPoint.cs b/src/Microsoft.AspNetCore.Sockets/EndPoint.cs
index 41afc43841..507db7862f 100644
--- a/src/Microsoft.AspNetCore.Sockets/EndPoint.cs
+++ b/src/Microsoft.AspNetCore.Sockets/EndPoint.cs
@@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Sockets
///
/// Represents an end point that multiple connections connect to. For HTTP, endpoints are URLs, for non HTTP it can be a TCP listener (or similar)
///
- public class EndPoint
+ public abstract class EndPoint
{
///
/// Live list of connections for this
@@ -17,9 +17,6 @@ namespace Microsoft.AspNetCore.Sockets
///
/// The new
/// A that represents the connection lifetime. When the task completes, the connection is complete.
- public virtual Task OnConnected(Connection connection)
- {
- return Task.CompletedTask;
- }
+ public abstract Task OnConnected(Connection connection);
}
}
diff --git a/src/Microsoft.AspNetCore.Sockets/HttpConnectionDispatcher.cs b/src/Microsoft.AspNetCore.Sockets/HttpConnectionDispatcher.cs
index af9ff72a65..663fab8c73 100644
--- a/src/Microsoft.AspNetCore.Sockets/HttpConnectionDispatcher.cs
+++ b/src/Microsoft.AspNetCore.Sockets/HttpConnectionDispatcher.cs
@@ -10,8 +10,14 @@ namespace Microsoft.AspNetCore.Sockets
{
public class HttpConnectionDispatcher
{
- private readonly ConnectionManager _manager = new ConnectionManager();
- private readonly ChannelFactory _channelFactory = new ChannelFactory();
+ private readonly ConnectionManager _manager;
+ private readonly ChannelFactory _channelFactory;
+
+ public HttpConnectionDispatcher(ConnectionManager manager, ChannelFactory factory)
+ {
+ _manager = manager;
+ _channelFactory = factory;
+ }
public async Task Execute(string path, HttpContext context) where TEndPoint : EndPoint
{
@@ -201,7 +207,7 @@ namespace Microsoft.AspNetCore.Sockets
return context.Request.Body.CopyToAsync(httpChannel.Input);
}
- return Task.CompletedTask;
+ throw new InvalidOperationException("Unknown connection id");
}
private ConnectionState GetOrCreateConnection(HttpContext context)
diff --git a/src/Microsoft.AspNetCore.Sockets/HttpDispatcherAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Sockets/HttpDispatcherAppBuilderExtensions.cs
index 51204bd5e8..97a87f5cc4 100644
--- a/src/Microsoft.AspNetCore.Sockets/HttpDispatcherAppBuilderExtensions.cs
+++ b/src/Microsoft.AspNetCore.Sockets/HttpDispatcherAppBuilderExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using Channels;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Sockets;
using Microsoft.AspNetCore.Sockets.Routing;
@@ -9,7 +10,9 @@ namespace Microsoft.AspNetCore.Builder
{
public static IApplicationBuilder UseSockets(this IApplicationBuilder app, Action callback)
{
- var dispatcher = new HttpConnectionDispatcher();
+ var manager = new ConnectionManager();
+ var factory = new ChannelFactory();
+ var dispatcher = new HttpConnectionDispatcher(manager, factory);
var routes = new RouteBuilder(app);
callback(new SocketRouteBuilder(routes, dispatcher));
diff --git a/src/Microsoft.AspNetCore.Sockets/project.json b/src/Microsoft.AspNetCore.Sockets/project.json
index 971f8b6547..d3d5bc09a2 100644
--- a/src/Microsoft.AspNetCore.Sockets/project.json
+++ b/src/Microsoft.AspNetCore.Sockets/project.json
@@ -7,6 +7,7 @@
},
"frameworks": {
"netstandard1.3": {
- }
+ },
+ "net46": { }
}
}
diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/ConnectionManagerTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/ConnectionManagerTests.cs
new file mode 100644
index 0000000000..9eec53d06f
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Sockets.Tests/ConnectionManagerTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Channels;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Sockets.Tests
+{
+ public class ConnectionManagerTests
+ {
+ [Fact]
+ public void ReservedConnectionsHaveConnectionId()
+ {
+ var connectionManager = new ConnectionManager();
+ var state = connectionManager.ReserveConnection();
+
+ Assert.NotNull(state.Connection);
+ Assert.NotNull(state.Connection.ConnectionId);
+ Assert.True(state.Active);
+ Assert.Null(state.Close);
+ Assert.Null(state.Connection.Channel);
+ }
+
+ [Fact]
+ public void ReservedConnectionsCanBeRetrieved()
+ {
+ var connectionManager = new ConnectionManager();
+ var state = connectionManager.ReserveConnection();
+
+ Assert.NotNull(state.Connection);
+ Assert.NotNull(state.Connection.ConnectionId);
+
+ ConnectionState newState;
+ Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState));
+ Assert.Same(newState, state);
+ }
+
+ [Fact]
+ public void AddNewConnection()
+ {
+ using (var factory = new ChannelFactory())
+ using (var channel = new HttpChannel(factory))
+ {
+ var connectionManager = new ConnectionManager();
+ var state = connectionManager.AddNewConnection(channel);
+
+ Assert.NotNull(state.Connection);
+ Assert.NotNull(state.Connection.ConnectionId);
+ Assert.NotNull(state.Connection.Channel);
+
+ ConnectionState newState;
+ Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState));
+ Assert.Same(newState, state);
+ Assert.Same(channel, newState.Connection.Channel);
+ }
+ }
+
+ [Fact]
+ public void RemoveConnection()
+ {
+ using (var factory = new ChannelFactory())
+ using (var channel = new HttpChannel(factory))
+ {
+ var connectionManager = new ConnectionManager();
+ var state = connectionManager.AddNewConnection(channel);
+
+ Assert.NotNull(state.Connection);
+ Assert.NotNull(state.Connection.ConnectionId);
+ Assert.NotNull(state.Connection.Channel);
+
+ ConnectionState newState;
+ Assert.True(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState));
+ Assert.Same(newState, state);
+ Assert.Same(channel, newState.Connection.Channel);
+
+ connectionManager.RemoveConnection(state.Connection.ConnectionId);
+ Assert.False(connectionManager.TryGetConnection(state.Connection.ConnectionId, out newState));
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs
new file mode 100644
index 0000000000..7048970d5d
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Channels;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Internal;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Sockets.Tests
+{
+ public class HttpConnectionDispatcherTests
+ {
+ [Fact]
+ public async Task GetIdReservesConnectionIdAndReturnsIt()
+ {
+ var manager = new ConnectionManager();
+ using (var factory = new ChannelFactory())
+ {
+ var dispatcher = new HttpConnectionDispatcher(manager, factory);
+ var context = new DefaultHttpContext();
+ var ms = new MemoryStream();
+ context.Request.Path = "/getid";
+ context.Response.Body = ms;
+ await dispatcher.Execute("", context);
+
+ var id = Encoding.UTF8.GetString(ms.ToArray());
+
+ ConnectionState state;
+ Assert.True(manager.TryGetConnection(id, out state));
+ Assert.Equal(id, state.Connection.ConnectionId);
+ }
+ }
+
+ [Fact]
+ public async Task SendingToReservedConnectionsThatHaveNotConnectedThrows()
+ {
+ var manager = new ConnectionManager();
+ var state = manager.ReserveConnection();
+
+ using (var factory = new ChannelFactory())
+ {
+ var dispatcher = new HttpConnectionDispatcher(manager, factory);
+ var context = new DefaultHttpContext();
+ context.Request.Path = "/send";
+ var values = new Dictionary();
+ values["id"] = state.Connection.ConnectionId;
+ var qs = new QueryCollection(values);
+ context.Request.Query = qs;
+ await Assert.ThrowsAsync(async () =>
+ {
+ await dispatcher.Execute("", context);
+ });
+ }
+ }
+
+ [Fact]
+ public async Task SendingToUnknownConnectionIdThrows()
+ {
+ var manager = new ConnectionManager();
+ using (var factory = new ChannelFactory())
+ {
+ var dispatcher = new HttpConnectionDispatcher(manager, factory);
+ var context = new DefaultHttpContext();
+ context.Request.Path = "/send";
+ var values = new Dictionary();
+ values["id"] = "unknown";
+ var qs = new QueryCollection(values);
+ context.Request.Query = qs;
+ await Assert.ThrowsAsync(async () =>
+ {
+ await dispatcher.Execute("", context);
+ });
+ }
+ }
+
+ [Fact]
+ public async Task SendingWithoutConnectionIdThrows()
+ {
+ var manager = new ConnectionManager();
+ using (var factory = new ChannelFactory())
+ {
+ var dispatcher = new HttpConnectionDispatcher(manager, factory);
+ var context = new DefaultHttpContext();
+ context.Request.Path = "/send";
+ await Assert.ThrowsAsync(async () =>
+ {
+ await dispatcher.Execute("", context);
+ });
+ }
+ }
+ }
+
+ public class TestEndPoint : EndPoint
+ {
+ public override Task OnConnected(Connection connection)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/LongPollingTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/LongPollingTests.cs
new file mode 100644
index 0000000000..d9442b3f9e
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Sockets.Tests/LongPollingTests.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Channels;
+using Microsoft.AspNetCore.Http;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Sockets.Tests
+{
+ public class LongPollingTests
+ {
+ [Fact]
+ public async Task Set204StatusCodeWhenChannelComplete()
+ {
+ using (var factory = new ChannelFactory())
+ {
+ var connection = new Connection();
+ connection.ConnectionId = Guid.NewGuid().ToString();
+ var channel = new HttpChannel(factory);
+ connection.Channel = channel;
+ var context = new DefaultHttpContext();
+ var poll = new LongPolling(connection);
+
+ channel.Output.CompleteWriter();
+
+ await poll.ProcessRequest(context);
+
+ Assert.Equal(204, context.Response.StatusCode);
+ }
+ }
+
+ [Fact]
+ public async Task NoFramingAddedWhenDataSent()
+ {
+ using (var factory = new ChannelFactory())
+ {
+ var connection = new Connection();
+ connection.ConnectionId = Guid.NewGuid().ToString();
+ var channel = new HttpChannel(factory);
+ connection.Channel = channel;
+ var context = new DefaultHttpContext();
+ var ms = new MemoryStream();
+ context.Response.Body = ms;
+ var poll = new LongPolling(connection);
+
+ await channel.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello World"));
+
+ channel.Output.CompleteWriter();
+
+ await poll.ProcessRequest(context);
+
+ Assert.Equal("Hello World", Encoding.UTF8.GetString(ms.ToArray()));
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/Microsoft.AspNetCore.Sockets.Tests.xproj b/test/Microsoft.AspNetCore.Sockets.Tests/Microsoft.AspNetCore.Sockets.Tests.xproj
new file mode 100644
index 0000000000..eb7ad5a0bd
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Sockets.Tests/Microsoft.AspNetCore.Sockets.Tests.xproj
@@ -0,0 +1,22 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ aad719d5-5e31-4ed1-a60f-6eb92efa66d9
+ Microsoft.AspNetCore.Sockets.Tests
+ .\obj
+ .\bin\
+ v4.5.2
+
+
+ 2.0
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.AspNetCore.Sockets.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..b3149d65f7
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Sockets.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Microsoft.AspNetCore.Sockets.Tests")]
+[assembly: AssemblyTrademark("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("aad719d5-5e31-4ed1-a60f-6eb92efa66d9")]
diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs
new file mode 100644
index 0000000000..3d1d9868fc
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Sockets.Tests/ServerSentEventsTests.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Channels;
+using Microsoft.AspNetCore.Http;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Sockets.Tests
+{
+ public class ServerSentEventsTests
+ {
+ [Fact]
+ public async Task SSESetsContentType()
+ {
+ using (var factory = new ChannelFactory())
+ {
+ var connection = new Connection();
+ connection.ConnectionId = Guid.NewGuid().ToString();
+ var channel = new HttpChannel(factory);
+ connection.Channel = channel;
+ var sse = new ServerSentEvents(connection);
+ var context = new DefaultHttpContext();
+
+ channel.Output.CompleteWriter();
+
+ await sse.ProcessRequest(context);
+
+ Assert.Equal("text/event-stream", context.Response.ContentType);
+ Assert.Equal("no-cache", context.Response.Headers["Cache-Control"]);
+ }
+ }
+
+ [Fact]
+ public async Task SSEAddsAppropriateFraming()
+ {
+ using (var factory = new ChannelFactory())
+ {
+ var connection = new Connection();
+ connection.ConnectionId = Guid.NewGuid().ToString();
+ var channel = new HttpChannel(factory);
+ connection.Channel = channel;
+ var sse = new ServerSentEvents(connection);
+ var context = new DefaultHttpContext();
+ var ms = new MemoryStream();
+ context.Response.Body = ms;
+
+ await channel.Output.WriteAsync(Encoding.UTF8.GetBytes("Hello World"));
+
+ channel.Output.CompleteWriter();
+
+ await sse.ProcessRequest(context);
+
+ var expected = "data: Hello World\n\n";
+ Assert.Equal(expected, Encoding.UTF8.GetString(ms.ToArray()));
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/project.json b/test/Microsoft.AspNetCore.Sockets.Tests/project.json
new file mode 100644
index 0000000000..c6ef6c82e4
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Sockets.Tests/project.json
@@ -0,0 +1,25 @@
+{
+ "buildOptions": {
+ "warningsAsErrors": true
+ },
+ "dependencies": {
+ "dotnet-test-xunit": "2.2.0-*",
+ "Microsoft.AspNetCore.Http": "1.1.0-*",
+ "Microsoft.AspNetCore.Sockets": {
+ "target": "project"
+ },
+ "xunit": "2.2.0-*"
+ },
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0",
+ "type": "platform"
+ }
+ }
+ },
+ "net46": {}
+ },
+ "testRunner": "xunit"
+}
\ No newline at end of file