Even simpler and faster real-time web for ASP.NET Core.
Even simpler and faster real-time web for ASP.NET Core.
SignalW is a simplified version of SignalR, with only WebSockets as a transport and
MemoryStreamas a message type.
RecyclableMemoryStreamthat pools internal buffers.
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/storethen a deserialized-to-JSON message will always have a
typefield, and one could use a custom
JsonCreationConverterto 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
dynamickeyword 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
WebSocketSubjectand forward messages to
@ngrx/storedirectly if they have a
typefield. No dependencies, no custom deserialization, no hassle!
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(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() { 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", 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.