SignalR in AspNetCore

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.
 
 

Picture of John Waters

John Waters

As a highly respected and recommended technology leader, entrepreneur, and software architect extraordinaire, John Waters provides guidance for Trailhead teams and projects. His remarkable project requirements gathering and project estimation skills allow him to oversee teams that deliver client projects on time and on budget. John’s background includes over two decades of systems architecting, implementations for a range of high-performance business solutions, and speaking at conferences worldwide.

Related Blog Posts

We hope you’ve found this to be helpful and are walking away with some new, useful insights. If you want to learn more, here are a couple of related articles that others also usually find to be interesting:

Our Gear Is Packed and We're Excited to Explore With You

Ready to come with us? 

Together, we can map your company’s software journey and start down the right trails. If you’re set to take the first step, simply fill out our contact form. We’ll be in touch quickly – and you’ll have a partner who is ready to help your company take the next step on its software journey. 

We can’t wait to hear from you! 

Main Contact

This field is for validation purposes and should be left unchanged.

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the form below. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Montage Portal

Montage Furniture Services provides furniture protection plans and claims processing services to a wide selection of furniture retailers and consumers.

Project Background

Montage was looking to build a new web portal for both Retailers and Consumers, which would integrate with Dynamics CRM and other legacy systems. The portal needed to be multi tenant and support branding and configuration for different Retailers. Trailhead architected the new Montage Platform, including the Portal and all of it’s back end integrations, did the UI/UX and then delivered the new system, along with enhancements to DevOps and processes.

Logistics

We’ve logged countless miles exploring the tech world. In doing so, we gained the experience that enables us to deliver your unique software and systems architecture needs. Our team of seasoned tech vets can provide you with:

Custom App and Software Development

We collaborate with you throughout the entire process because your customized tech should fit your needs, not just those of other clients.

Cloud and Mobile Applications

The modern world demands versatile technology, and this is exactly what your mobile and cloud-based apps will give you.

User Experience and Interface (UX/UI) Design

We want your end users to have optimal experiences with tech that is highly intuitive and responsive.

DevOps

This combination of Agile software development and IT operations provides you with high-quality software at reduced cost, time, and risk.

Trailhead stepped into a challenging project – building our new web architecture and redeveloping our portals at the same time the business was migrating from a legacy system to our new CRM solution. They were able to not only significantly improve our web development architecture but our development and deployment processes as well as the functionality and performance of our portals. The feedback from customers has been overwhelmingly positive. Trailhead has proven themselves to be a valuable partner.

– BOB DOERKSEN, Vice President of Technology Services
at Montage Furniture Services

Technologies Used

When you hit the trails, it is essential to bring appropriate gear. The same holds true for your digital technology needs. That’s why Trailhead builds custom solutions on trusted platforms like .NET, Angular, React, and Xamarin.

Expertise

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

  • Project Management
  • Architecture
  • Web App Development
  • Cloud Development
  • DevOps
  • Process Improvements
  • Legacy System Integration
  • UI Design
  • Manual QA
  • Back end/API/Database development

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

Our Gear Is Packed and We're Excited to Explore with You

Ready to come with us? 

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the contact form. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Thank you for reaching out.

You’ll be getting an email from our team shortly. If you need immediate assistance, please call (616) 371-1037.