We’ve all been waiting for the first production release of SignalR for AspNetCore, and supposedly it’s going to be out there any day now. Impatient as I am, I started using 1.0.0-preview2-final – and it rocks! Here are some tips and tricks, including use of Dependency Injection and handling Multi Tenant scenarios. But first… a demo! See update at end of blog for a couple of changes in RC1.
As I mentioned, this is a multi tenant app, so when something happens in a tenant, I want to broadcast that to ONLY users logged into the same tenant. Also, when the user in Browser A clicks Re-sequence, when the back end is done, I want to push out some messages to all browsers.
The back end is an AspNetCore WebAPI, and the front end is an Angular 5 client. The Angular client uses the same version of SignalR as the server (important for these prereleases that the versions match!). Here is snippet from the Angular code, which sets up the connection with the hub:
import {EventEmitter} from "@angular/core"; import {each} from 'lodash-es'; import {HubConnection, TransportType} from "@aspnet/signalr"; import {AppConstants} from "../../constants/app.constants"; import {HubEvent} from "../../models/hub-event.model"; import {SessionData} from "../../interfaces/api/session-data.interface"; import {AppSession} from "../core/app-session.service"; export abstract class BaseHub { eventBus: EventEmitter<HubEvent> = new EventEmitter<HubEvent>(); private _hubConnection: HubConnection; private _hubName: string; private _events: Array<any>; constructor(hubName: string, events: Array<any>) { const sessionData: SessionData = AppSession.get(); this._hubName = hubName; this._events = events; this._hubConnection = new HubConnection(`${AppConstants.signalR.baseUrl}/${this._hubName}?token=${sessionData.token}`, { transport: TransportType.WebSockets }); this.subscribeToEvents(); this._hubConnection.start(); } invoke(hubEvent: HubEvent) { this._hubConnection.invoke(hubEvent.name, hubEvent.data); } private subscribeToEvents(): void { each(this._events, (event) => { this.setupHubEventListener(event); }); } private setupHubEventListener(eventName: string) { const evt = eventName; this._hubConnection.on(evt, (data: any) => { this.eventBus.emit( new HubEvent(evt, data) ); }); } }
Worth noting here is that:
- The connection call passes in a session token on the URL. This comes from a previous call to the login API, which returns a JWT, and I pass this on the Query String to the connect call.
- The transport is forced to Web Sockets (as in this version there seem to be some issues with the HTTP based negotiation protocol resulting in some 405s)
- All incoming messages are routed through a single event emitter, with a data payload, you will see what that means for the server shortly.
Let’s follow that token through to the server. The server is configured to use a JwtBearer for authentication:
public void ConfigureServices(IServiceCollection services) { services .AddOptions() .AddConfig(ConfigurationRoot) .AddCORS() .AddEF(ConfigurationRoot) .ConfigureApplicationInjection() .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { var tokenOptions = ConfigurationRoot.GetSection("Authentication").Get<TokenOptions>(); options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = tokenOptions.Issuer, ValidAudience = tokenOptions.Audience, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(tokenOptions.SigningKey)) }; }); services .AddMvc() .AddJsonOptions(opt => { opt.SerializerSettings.NullValueHandling = NullValueHandling.Include; opt.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include; opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); services.AddSignalR(); services.AddSwagger(); services.AddAutoMapper(); }
Here you also see the addition of SignalR to the services collection, after Authentication and Mvc. And below, you see SignalR added to the HTTP pipeline, after Authentication, and also that we are configuring CORS.
public void Configure(IApplicationBuilder app) { app .UseTokenInQueryString() .UseAuthentication() .UseConfigureSession() .UseSwagger( c => { c.PreSerializeFilters.Add( (swagger, httpReq) => swagger.Host = httpReq.Host.Value); }) .UseSwaggerUI(c => { var basePath = Environment.GetEnvironmentVariable("ASPNETCORE_APPL_PATH"); c.SwaggerEndpoint(basePath + "swagger/v1/swagger.json", "V1 Docs"); c.DocExpansion(DocExpansion.None); }) .UseSignalR( routes => { routes.MapHub<StopsHub>( "/stophub", options => options.Transports = TransportType.All ); }) .UseDeveloperExceptionPage() .UseHttpException() .UseCors("AllowAllCorsPolicy") .UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); var config = new MapperConfiguration(cfg => { cfg.AddProfile<MappingProfile>(); }); }
public static IServiceCollection AddCORS(this IServiceCollection services) { services.AddCors(action => action.AddPolicy("AllowAllCorsPolicy", builder => builder .AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials())); return services; }
Note the AllowCredentials() for CORS.
OK – so the server is set for JWT bearer tokens – but it looks for these in the HTTP Authorization header. We are passing it on the query string. To solve this, see the UseTokenInQueryString call above? This installs a Middleware that takes care of this for us:
using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace NorthStar.API.Middleware { /// <summary> /// Middleware that grabs any token from the query string and places it in an auth header /// </summary> public class GetTokenFromQueryStringMiddleware { private readonly RequestDelegate _next; /// <summary> /// constructor /// </summary> /// <param name="next">the next middleware in chain</param> public GetTokenFromQueryStringMiddleware(RequestDelegate next) { _next = next; } /// <summary> /// sets the header /// </summary> /// <param name="context">Current HTTP context</param> /// <returns></returns> public async Task InvokeAsync(HttpContext context) { if (string.IsNullOrWhiteSpace(context.Request.Headers["Authorization"])) { if (context.Request.QueryString.HasValue) { var token = context.Request.QueryString.Value.Split('&') .SingleOrDefault(x => x.Contains("token"))?.Split('=')[1]; if (!string.IsNullOrWhiteSpace(token)) { context.Request.Headers.Add("Authorization", new[] { $"Bearer {token}" }); } } } await _next.Invoke(context); } } }
Perfect – now we can get our SignalR hub calls authenticated, a good practice and of course a MUST for multi tenant! Otherwise we don’t know which tenant the connected client belongs to… unless we rely on the client to tell us, which is iffy.
Let’s take a look at the Hub that we configured above with Routes.MapHub<StopsHub> :
namespace NorthStar.Logic.Helpers { /// <inheritdoc /> /// <summary> /// SignalR hub for stop related messages /// </summary> public class StopsHub : HubBase { } }
Hmm – nothing there! It’s all in a generic base class for all my Hubs:
using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; namespace NorthStar.Logic.Helpers { /// <summary> /// Base class for all hubs /// </summary> [Authorize] public class HubBase : Hub { protected int CurrentTenantId(ClaimsPrincipal user) => GetClaim<int>(user, "tenantid"); protected T GetClaim<T>(ClaimsPrincipal user, string claimName) => ClaimsHelper.GetClaim<T>(user, claimName); /// <summary> /// Called when an authenticated client connects, store in group for tenant /// </summary> /// <returns>A task</returns> public override async Task OnConnectedAsync() { // add the connection to a group for this tenant await Groups.AddAsync(Context.ConnectionId, CurrentTenantId(Context.User).ToString()); await base.OnConnectedAsync(); } /// <summary> /// Calls when an authenticated client disconnects /// </summary> /// <param name="exception">Any exception that occucrred</param> /// <returns>A task</returns> public override async Task OnDisconnectedAsync(Exception exception) { // remove from tenant group await Groups.RemoveAsync(Context.ConnectionId, CurrentTenantId(Context.User).ToString()); await base.OnDisconnectedAsync(exception); } } }
Don’t forget that [Authorize] attribute to force authenticated calls only!
In OnConnectedAsync, an authenticated client connects, and I extract the TenantId from the Claims embedded in the JWT. GetClaim<T> gets it for me:
public static T GetClaim<T>(ClaimsPrincipal user, string claimName) { if (user == null) return default(T); var c = user.Claims.FirstOrDefault(i => string.Compare(i.Type, claimName.ToLower(), StringComparison.OrdinalIgnoreCase) == 0); if (c == null) return default(T); return (T)Convert.ChangeType(c.Value, typeof(T)); }
I pass in the User that has the claims, which comes form Context.User (since we have authenticated users).
Once I have the Tenant ID, I add this client connection to a SignalR Hub Group named TenantID.ToString(), so later on I can send messages to all clients of this tenant. When the client disconnects, I remove it from the group. That’s all there is to the Hub… so where is the logic for sending messages?
In my middle tier code, I need to be able to message the clients from arbitrary classes. The code looks something like this:
await _hubHelper.BroadcastMessageAsync( $"Route {trip.Route.Name} sequenced, {trip.Stops.Count - 2} stops"); await _hubHelper.BroadcastTripEventAsync( trip.Route.Id, trip.Id, TripEvents.Sequenced);
Here, _hubHelper is an injected class that does the sending of messages. Note I am not passing the current Tenant ID to it… how does it know where to send the message? And how does it get a handle to the StopHub, where the Groups are defined?
In previous SignalR versions, you would do that by calling GetHubContext like this:
GlobalHost.ConnectionManager.GetHubContext<StopsHub>()
In AspNetCore, it all happens magically through Dependency Injection! Here is my HubHelper class being configured for DI, along with the all important HttpContextAccessor:
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHubHelper, HubHelper>();
And here is the class definition, where I store the context accessor that is injected into the constructor. Note I also declare an IHubContext<StopsHub> in the constructor. That’s all I need to do to get the StopHub context injected into my helper.
public class HubHelper : IHubHelper { private readonly IHubContext<StopsHub> _stopsHub; private readonly IHttpContextAccessor _contextAccessor; /// <summary> /// Constructor /// </summary> /// <param name="stopHub">Injected stop hub context</param> /// <param name="contextAccessor">Context accessor</param> public HubHelper(IHubContext<StopsHub> stopHub, IHttpContextAccessor contextAccessor) { _stopsHub = stopHub; _contextAccessor = contextAccessor; }
Armed with the HttpContext and HubContext, I can now implement those tenant aware messages:
public async Task BroadcastSystemMessageAsync(string message) { await CurrentTenantGroup.SendAsync("messageTicker", message); } private int CurrentTenantId => ClaimsHelper.GetClaim<int>( _contextAccessor.HttpContext.User, "tenantid"); private IClientProxy CurrentTenantGroup => _stopsHub.Clients.Group(CurrentTenantId.ToString()); }
To find the current tenant, I get the TenantID claim from the User of the HttpContext, just like I did when the client connected.
Then, I send the message to the clients of that Tenant (remember, the group name is the TenantID.ToString()). Pretty simple… you saw the result in the demo!
Oh – and as far as the comment about the Angular client routing all events through a common emitter – here is the server side implication. Instead of passing a series of arguments that are different for each message (messageTicker, stopStatusChange, tripEvent), I just pass an anonymous type as the single parameter:
public async Task BroadcastMessageAsync(string message) { await CurrentTenantGroup.SendAsync("messageTicker", message ); } public async Task BroadcastStopStatusChangeAsync(int routeId, int tripId, int stopId, int newStatusId) { await CurrentTenantGroup.SendAsync("stopStatusChange", new { tenantId = CurrentTenantId, tripId, stopId, newStatusId }); } public async Task BroadcastTripEventAsync(int routeId, int tripId, TripEvents tripEvent) { await CurrentTenantGroup.SendAsync("tripEvent", new { routeId, tripId, tripEvent }); }
That’s all there is to it – multi tenant SignalR with an Angular client in AspNetCore – enjoy!
Breaking news – AspNetCore RC1 was released at Build today. There were a couple of naming changes – in the Hubs, Groups.AddAsync and RemoveAsync became AddToGroupAsync and RemoveFromGroupAsync. In your Startup.cs file, when doing UseSignalR, the options.Transports became options.HttpTransportType. On the Angular client side, similar changes were made to naming. Also, the way you create a Hub connection changed to this:
this._hubConnection = new HubConnectionBuilder() .withUrl(`${AppConstants.signalR.baseUrl}/${this._hubName}?token=${sessionData.token}`, HttpTransportType.WebSockets) .build();
Don’t forget to update your package reference client side to
"@aspnet/signalr": "^1.0.0-rc1-final",
And do an npm install.