Home

Awesome

SignalW

Even simpler and faster real-time web for ASP.NET Core.

Install-Package Spreads.SignalW -Version 0.8.0-build1707052042
Install-Package Spreads.SignalW.Client -Version 0.8.0-build1707052042

SignalW is a simplified version of SignalR, with only WebSockets as a transport and MemoryStream as a message type.

Instead of multiple methods inside Hubs that clients invoked by name, in SignalW we have a single method async Task OnReceiveAsync(MemoryStream payload). If one uses Angular2 with @ngrx/store then a deserialized-to-JSON message will always have a type field, and one could use a custom JsonCreationConverter<IMessage> to deserialize a message to its correct .NET type. Then one could write multiple methods with the same name that differ only by its parameter type and use dynamic keyword to dispatch a message to a correct handler.

public class DispatchHub : Hub {
    public override async Task OnReceiveAsync(MemoryStream payload) {
        // Extension method ReadJsonMessage returns IMessage object instance based on the `type` field in JSON
		object message = payload.ReadJsonMessage();
        // dispose as soon as it is no longer used becasue it uses pooled buffers inside
        payload.Dispose();

        // dynamic will dispatch to the correct method
        dynamic dynMessage = message;
        await OnReceiveAsync(dynMessage);
    }

    public async void OnReceiveAsync(MessageFoo message) {
		var stream = message.WriteJson();
        await Clients.Group("foo").InvokeAsync(stream);
    }

    public async void OnReceiveAsync(MessageBar message) {
		var stream = message.WriteJson();
        await Clients.Group("bar").InvokeAsync(stream);
    }
}

On the Angular side, one could simply use a WebSocketSubject and forward messages to @ngrx/store directly if they have a type field. No dependencies, no custom deserialization, no hassle!

Authentication with a bearer token

From a C# client

var client = new ClientWebSocket();
var header = new AuthenticationHeaderValue("Bearer", _accessToken);
client.Options.RequestHeaders.Add("Authorization", header.ToString());

From JavaScript:

It is impossible to add headers to WebSocket constructor in JavaScript, but we could use protocol parameters for this. Here we are using RxJS WebSocketSubject:

import { WebSocketSubjectConfig, WebSocketSubject } from 'rxjs/observable/dom/WebSocketSubject';
...
let wsConfig: WebSocketSubjectConfig = {
    url: 'wss://example.com/api/signalw/chat',
    protocol: [
    'access_token',
    token
    ]
};
let ws = new WebSocketSubject<any>(wsConfig);

Then in the very beginning of OWIN pipeline (before any identity middleware) use this trick to populate the correct header:

app.Use((context, next) => {
    if (!context.Request.Headers.ContainsKey("Authorization")
        && context.Request.Headers.ContainsKey("Upgrade")) {
        if (context.WebSockets.WebSocketRequestedProtocols.Count >= 2) {
            var first = context.WebSockets.WebSocketRequestedProtocols[0];
            var second = context.WebSockets.WebSocketRequestedProtocols[1];
            if (first == "access_token") {
                context.Request.Headers.Add("Authorization", "Bearer " + second);
                context.Response.Headers.Add("Sec-WebSocket-Protocol", "access_token");
            }
        }
    }
    return next();
});

To use SignalW, create a custom Hub:

[Authorize]
public class Chat : Hub {
    public override Task OnConnectedAsync() {
        if (!Context.User.Identity.IsAuthenticated) {
            Context.Connection.Channel.TryComplete();
        }
        return Task.FromResult(0);
    }

    public override Task OnDisconnectedAsync(Exception exception) {
	
        return Task.FromResult(0);
    }

    public override async Task OnReceiveAsync(MemoryStream payload) {
        await Clients.All.InvokeAsync(payload);
    }
}

Then add SignalW to the OWIN pipeline and map hubs to a path. Here we use SignalR together with MVC on the "/api" path:

public void ConfigureServices(IServiceCollection services) {
	...
	services.AddSignalW();
	...
}

public void Configure(IApplicationBuilder app, ...){
	...
	app.Map("/api/signalw", signalw => {
		signalw.UseSignalW((config) => {
			config.MapHub<Chat>("chat", Format.Text);
		});
	});
	app.Map("/api", apiApp => {
		apiApp.UseMvc();
	});
	...
}

Open several pages of https://www.websocket.org/echo.html and connect to https://[host]/api/signalw/chat?connectionId=[any value]. Each page should broadcast messages to every other page and this is a simple chat.