Skip to content

Commit 32ef3eb

Browse files
authored
Hub<T> (aspnet#689)
1 parent 3a1d4c5 commit 32ef3eb

File tree

10 files changed

+450
-0
lines changed

10 files changed

+450
-0
lines changed

build/dependencies.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<XunitVersion>2.3.0-beta2-*</XunitVersion>
1717
<RxVersion>3.1.1</RxVersion>
1818
<MsgPackVersion>0.9.0-beta2</MsgPackVersion>
19+
<SystemReflectionEmitVersion>4.3.0</SystemReflectionEmitVersion>
1920
<!--
2021
TODO remove in next update of xunit
2122
Prevent bug in xunit.analyzer from failing the build.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.SignalR;
7+
8+
namespace SocketsSample.Hubs
9+
{
10+
public class HubTChat : Hub<IChatClient>
11+
{
12+
public override async Task OnConnectedAsync()
13+
{
14+
await Clients.All.Send($"{Context.ConnectionId} joined");
15+
}
16+
17+
public override async Task OnDisconnectedAsync(Exception ex)
18+
{
19+
await Clients.All.Send($"{Context.ConnectionId} left");
20+
}
21+
22+
public Task Send(string message)
23+
{
24+
return Clients.All.Send($"{Context.ConnectionId}: {message}");
25+
}
26+
27+
public Task SendToGroup(string groupName, string message)
28+
{
29+
return Clients.Group(groupName).Send($"{Context.ConnectionId}@{groupName}: {message}");
30+
}
31+
32+
public async Task JoinGroup(string groupName)
33+
{
34+
await Groups.AddAsync(Context.ConnectionId, groupName);
35+
36+
await Clients.Group(groupName).Send($"{Context.ConnectionId} joined {groupName}");
37+
}
38+
39+
public async Task LeaveGroup(string groupName)
40+
{
41+
await Groups.RemoveAsync(Context.ConnectionId, groupName);
42+
43+
await Clients.Group(groupName).Send($"{Context.ConnectionId} left {groupName}");
44+
}
45+
46+
public Task Echo(string message)
47+
{
48+
return Clients.Client(Context.ConnectionId).Send($"{Context.ConnectionId}: {message}");
49+
}
50+
}
51+
52+
public interface IChatClient
53+
{
54+
Task Send(string message);
55+
}
56+
}

samples/SocketsSample/Startup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
5050
routes.MapHub<DynamicChat>("dynamic");
5151
routes.MapHub<Chat>("default");
5252
routes.MapHub<Streaming>("streaming");
53+
routes.MapHub<HubTChat>("hubT");
5354
});
5455

5556
app.UseSockets(routes =>

samples/SocketsSample/wwwroot/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ <h1>ASP.NET Core SignalR (Dynamic Hubs)</h1>
2424
<li><a href="hubs.html?transport=ServerSentEvents&hubType=dynamic">Server Sent Events</a></li>
2525
<li><a href="hubs.html?transport=WebSockets&hubType=dynamic">Web Sockets</a></li>
2626
</ul>
27+
<h1>ASP.NET Core SignalR (Hub&lt;T&gt;)</h1>
28+
<ul>
29+
<li><a href="hubs.html?transport=LongPolling&hubType=hubT">Long polling</a></li>
30+
<li><a href="hubs.html?transport=ServerSentEvents&hubType=hubT">Server Sent Events</a></li>
31+
<li><a href="hubs.html?transport=WebSockets&hubType=hubT">Web Sockets</a></li>
32+
</ul>
2733
<h1>ASP.NET Core SignalR (Streaming)</h1>
2834
<ul>
2935
<li><a href="streaming.html?transport=LongPolling">Long polling</a></li>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.SignalR
5+
{
6+
public class Hub<T> : Hub where T : class
7+
{
8+
private IHubClients<T> _clients;
9+
10+
public new IHubClients<T> Clients
11+
{
12+
get
13+
{
14+
if (_clients == null)
15+
{
16+
_clients = new TypedHubClients<T>(base.Clients);
17+
}
18+
return _clients;
19+
}
20+
set { _clients = value; }
21+
}
22+
}
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
8+
namespace Microsoft.AspNetCore.SignalR
9+
{
10+
public interface IHubClients<T>
11+
{
12+
T All { get; }
13+
14+
T Client(string connectionId);
15+
16+
T Group(string groupName);
17+
18+
T User(string userId);
19+
}
20+
}

src/Microsoft.AspNetCore.SignalR/Microsoft.AspNetCore.SignalR.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(AspNetCoreVersion)" />
2020
<PackageReference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
2121
<PackageReference Include="Microsoft.Extensions.ObjectMethodExecutor.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
22+
<PackageReference Include="System.Reflection.Emit" Version="$(SystemReflectionEmitVersion)" />
2223
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
2324
</ItemGroup>
2425

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Linq;
8+
using System.Reflection;
9+
using System.Reflection.Emit;
10+
using System.Threading.Tasks;
11+
12+
namespace Microsoft.AspNetCore.SignalR
13+
{
14+
internal static class TypedClientBuilder<T>
15+
{
16+
private const string ClientModuleName = "Microsoft.AspNetCore.SignalR.TypedClientBuilder";
17+
18+
// There is one static instance of _builder per T
19+
private static Lazy<Func<IClientProxy, T>> _builder = new Lazy<Func<IClientProxy, T>>(() => GenerateClientBuilder());
20+
21+
public static T Build(IClientProxy proxy)
22+
{
23+
return _builder.Value(proxy);
24+
}
25+
26+
public static void Validate()
27+
{
28+
// The following will throw if T is not a valid type
29+
_ = _builder.Value;
30+
}
31+
32+
private static Func<IClientProxy, T> GenerateClientBuilder()
33+
{
34+
VerifyInterface(typeof(T));
35+
36+
var assemblyName = new AssemblyName(ClientModuleName);
37+
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
38+
var moduleBuilder = assemblyBuilder.DefineDynamicModule(ClientModuleName);
39+
var clientType = GenerateInterfaceImplementation(moduleBuilder);
40+
41+
return proxy => (T)Activator.CreateInstance(clientType, proxy);
42+
}
43+
44+
private static Type GenerateInterfaceImplementation(ModuleBuilder moduleBuilder)
45+
{
46+
var type = moduleBuilder.DefineType(
47+
ClientModuleName + "." + typeof(T).Name + "Impl",
48+
TypeAttributes.Public,
49+
typeof(Object),
50+
new[] { typeof(T) });
51+
52+
var proxyField = type.DefineField("_proxy", typeof(IClientProxy), FieldAttributes.Private);
53+
54+
BuildConstructor(type, proxyField);
55+
56+
foreach (var method in GetAllInterfaceMethods(typeof(T)))
57+
{
58+
BuildMethod(type, method, proxyField);
59+
}
60+
61+
return type.CreateTypeInfo();
62+
}
63+
64+
private static IEnumerable<MethodInfo> GetAllInterfaceMethods(Type interfaceType)
65+
{
66+
foreach (var parent in interfaceType.GetInterfaces())
67+
{
68+
foreach (var parentMethod in GetAllInterfaceMethods(parent))
69+
{
70+
yield return parentMethod;
71+
}
72+
}
73+
74+
foreach (var method in interfaceType.GetMethods())
75+
{
76+
yield return method;
77+
}
78+
}
79+
80+
private static void BuildConstructor(TypeBuilder type, FieldInfo proxyField)
81+
{
82+
var method = type.DefineMethod(".ctor", System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.HideBySig);
83+
84+
var ctor = typeof(object).GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
85+
null, new Type[] { }, null);
86+
87+
method.SetReturnType(typeof(void));
88+
method.SetParameters(typeof(IClientProxy));
89+
90+
var generator = method.GetILGenerator();
91+
92+
// Call object constructor
93+
generator.Emit(OpCodes.Ldarg_0);
94+
generator.Emit(OpCodes.Call, ctor);
95+
96+
// Assign constructor argument to the proxyField
97+
generator.Emit(OpCodes.Ldarg_0); // type
98+
generator.Emit(OpCodes.Ldarg_1); // type proxyfield
99+
generator.Emit(OpCodes.Stfld, proxyField); // type.proxyField = proxyField
100+
generator.Emit(OpCodes.Ret);
101+
}
102+
103+
private static void BuildMethod(TypeBuilder type, MethodInfo interfaceMethodInfo, FieldInfo proxyField)
104+
{
105+
var methodAttributes =
106+
MethodAttributes.Public
107+
| MethodAttributes.Virtual
108+
| MethodAttributes.Final
109+
| MethodAttributes.HideBySig
110+
| MethodAttributes.NewSlot;
111+
112+
ParameterInfo[] parameters = interfaceMethodInfo.GetParameters();
113+
Type[] paramTypes = parameters.Select(param => param.ParameterType).ToArray();
114+
115+
var methodBuilder = type.DefineMethod(interfaceMethodInfo.Name, methodAttributes);
116+
117+
var invokeMethod = typeof(IClientProxy).GetMethod(
118+
"InvokeAsync", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null,
119+
new Type[] { typeof(string), typeof(object[]) }, null);
120+
121+
methodBuilder.SetReturnType(interfaceMethodInfo.ReturnType);
122+
methodBuilder.SetParameters(paramTypes);
123+
124+
// Sets the number of generic type parameters
125+
var genericTypeNames =
126+
paramTypes.Where(p => p.IsGenericParameter).Select(p => p.Name).Distinct().ToArray();
127+
128+
if (genericTypeNames.Any())
129+
{
130+
methodBuilder.DefineGenericParameters(genericTypeNames);
131+
}
132+
133+
var generator = methodBuilder.GetILGenerator();
134+
135+
// Declare local variable to store the arguments to IClientProxy.InvokeAsync
136+
generator.DeclareLocal(typeof(object[]));
137+
138+
// Get IClientProxy
139+
generator.Emit(OpCodes.Ldarg_0);
140+
generator.Emit(OpCodes.Ldfld, proxyField);
141+
142+
// The first argument to IClientProxy.InvokeAsync is this method's name
143+
generator.Emit(OpCodes.Ldstr, interfaceMethodInfo.Name);
144+
145+
// Create an new object array to hold all the parameters to this method
146+
generator.Emit(OpCodes.Ldc_I4, parameters.Length); // Stack:
147+
generator.Emit(OpCodes.Newarr, typeof(object)); // allocate object array
148+
generator.Emit(OpCodes.Stloc_0);
149+
150+
// Store each parameter in the object array
151+
for (int i = 0; i < paramTypes.Length; i++)
152+
{
153+
generator.Emit(OpCodes.Ldloc_0); // Object array loaded
154+
generator.Emit(OpCodes.Ldc_I4, i);
155+
generator.Emit(OpCodes.Ldarg, i + 1); // i + 1
156+
generator.Emit(OpCodes.Box, paramTypes[i]);
157+
generator.Emit(OpCodes.Stelem_Ref);
158+
}
159+
160+
// Call InvokeAsync
161+
generator.Emit(OpCodes.Ldloc_0);
162+
generator.Emit(OpCodes.Callvirt, invokeMethod);
163+
164+
if (interfaceMethodInfo.ReturnType == typeof(void))
165+
{
166+
// void return
167+
generator.Emit(OpCodes.Pop);
168+
}
169+
170+
generator.Emit(OpCodes.Ret); // Return
171+
}
172+
173+
private static void VerifyInterface(Type interfaceType)
174+
{
175+
if (!interfaceType.IsInterface)
176+
{
177+
throw new InvalidOperationException("Type must be an interface.");
178+
}
179+
180+
if (interfaceType.GetProperties().Length != 0)
181+
{
182+
throw new InvalidOperationException("Type must not contain properties.");
183+
}
184+
185+
if (interfaceType.GetEvents().Length != 0)
186+
{
187+
throw new InvalidOperationException("Type can not contain events.");
188+
}
189+
190+
foreach (var method in interfaceType.GetMethods())
191+
{
192+
VerifyMethod(interfaceType, method);
193+
}
194+
195+
foreach (var parent in interfaceType.GetInterfaces())
196+
{
197+
VerifyInterface(parent);
198+
}
199+
}
200+
201+
private static void VerifyMethod(Type interfaceType, MethodInfo interfaceMethod)
202+
{
203+
if (interfaceMethod.ReturnType != typeof(void) && interfaceMethod.ReturnType != typeof(Task))
204+
{
205+
throw new InvalidOperationException("Method must return Void or Task.");
206+
}
207+
208+
foreach (var parameter in interfaceMethod.GetParameters())
209+
{
210+
VerifyParameter(interfaceType, interfaceMethod, parameter);
211+
}
212+
}
213+
214+
private static void VerifyParameter(Type interfaceType, MethodInfo interfaceMethod, ParameterInfo parameter)
215+
{
216+
if (parameter.IsOut)
217+
{
218+
throw new InvalidOperationException("Method must not take out parameters.");
219+
}
220+
221+
if (parameter.ParameterType.IsByRef)
222+
{
223+
throw new InvalidOperationException("Method must not take reference parameters.");
224+
}
225+
}
226+
}
227+
}

0 commit comments

Comments
 (0)