WebSockets CRUD ASP.NET CORE PART 1
WebSockets provide a bidirectional communication between a client and server .
The client establishes a WebSocket connection to the server. This process starts with the client sending a regular HTTP request to the server with an upgrade header.This process is call WebSocket handshake
Websocket, unlike http, involves both the server and client sending messages.
Websocket urls uses ws scheme and wss for secure connections.Below is an example of initial request from Client
GET ws://websocket.test.com/ HTTP/1.1
Origin: http://test.com
Connection: Upgrade
Host: websocket.test.com
Upgrade: websocket
Now Enough talking Lets dive into code.We will first write a webscocket connection manager.
public class WebSocketConnectionManager{
private ConcurrentDictionary<string, WebSocket> _socketConnection= new ConcurrentDictionary<string, WebSocket>();
public void AddSocket(WebSocket socket)
{
string socketId = CreateConnectionId();
while (!_socketConnection.TryAdd(socketId, socket))
{
socketId = CreateConnectionId();
}}public async Task RemoveSocket(string id)
{
try
{
WebSocket socket;
_socketConnection.TryRemove(id, out socket);
await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
}
catch (Exception)
{ }
}
public WebSocket GetSocketById(string id)
{
return _socketConnection.FirstOrDefault(m => m.Key == id).Value;
}
public ConcurrentDictionary<string, WebSocket> GetAll()
{
return _socketConnection;
}
public string GetSocketId(WebSocket socket)
{
return _socketConnection.FirstOrDefault(m => m.Value == socket).Key;
}
private string CreateConnectionId()
{
return Guid.NewGuid().ToString();
}}
Note: It’s worth noting that the client will only be notified about a new message once all of the frames have been received and the original message payload has been reconstructed.
Then we will write a webscocket handler.This handler will be inherited by all our handlers
public abstract class WebSocketHandler{
public WebSocketConnectionManager WebSocketConnectionManager { get; set; }public WebSocketHandler(WebSocketConnectionManager webSocketConnectionManager){WebSocketConnectionManager = webSocketConnectionManager;}public virtual async Task OnConnected(WebSocket socket){WebSocketConnectionManager.AddSocket(socket);await SendMessageAsync(socket, new JObject { { “Message”, “Accepted” } });}public virtual async Task OnDisconnected(WebSocket socket){await WebSocketConnectionManager.RemoveSocket(WebSocketConnectionManager.GetSocketId(socket));}public async Task SendMessageAsync(WebSocket socket, JObject message){if (socket.State != WebSocketState.Open)return;await socket.SendAsync(buffer: new ArraySegment<byte>(array: Encoding.ASCII.GetBytes(message.ToString()),offset: 0,count: message.ToString().Length),messageType: WebSocketMessageType.Text,endOfMessage: true,cancellationToken: System.Threading.CancellationToken.None);}public async Task SendMessageAsync(string socketId, JObject message){await SendMessageAsync(WebSocketConnectionManager.GetSocketById(socketId), message);}public async Task SendMessageToAllAsync(JObject message){foreach (var pair in WebSocketConnectionManager.GetAll()){if (pair.Value.State == WebSocketState.Open)await SendMessageAsync(pair.Value, message);}}public abstract Task ReceiveAsync(WebSocket socket, WebSocketReceiveResult result, byte[] buffer);}
Next we will create a middleware for the websocket as shown below
public class WebSocketManagerMiddleware{private readonly RequestDelegate _next;private WebSocketHandler _webSocketHandler { get; set; }public WebSocketManagerMiddleware(RequestDelegate next,WebSocketHandler webSocketHandler){_next = next;_webSocketHandler = webSocketHandler;}public async Task Invoke(HttpContext context){if (!context.WebSockets.IsWebSocketRequest)return;var socket = await context.WebSockets.AcceptWebSocketAsync();await _webSocketHandler.OnConnected(socket);await Receive(socket, async (result, buffer) =>{if (result.MessageType == WebSocketMessageType.Text){await _webSocketHandler.ReceiveAsync(socket, result, buffer);return;}else if (result.MessageType == WebSocketMessageType.Close){await _webSocketHandler.OnDisconnected(socket);return;}});//TODO — investigate the Kestrel exception thrown when this is the last middleware//await _next.Invoke(context);}private async Task Receive(WebSocket socket, Action<WebSocketReceiveResult, byte[]> handleMessage){try{var buffer = new byte[1024 * 4];while (socket.State == WebSocketState.Open){var result = await socket.ReceiveAsync(buffer: new ArraySegment<byte>(buffer),cancellationToken: CancellationToken.None);handleMessage(result, buffer);}}catch{}}}
In the asp.net main application add the extensions below.The purpose of the extension is to add webscocket manager to inject the dependency as transient service and add all our webscocket handlers as a singleton service
public static class WebSocketExtensions{public static IApplicationBuilder MapWebSocketManager(this IApplicationBuilder app,PathString path,WebSocketHandler handler){return app.Map(path, (_app) => _app.UseMiddleware<WebSocketManagerMiddleware>(handler));}public static IServiceCollection AddWebSocketManager(this IServiceCollection services){services.AddTransient<WebSocketConnectionManager>();foreach (var type in Assembly.GetEntryAssembly().ExportedTypes){if (type.GetTypeInfo().BaseType == typeof(WebSocketHandler)){services.AddSingleton(type);}}return services;}}
Once done adding the above classes into your project lets now create a basic websocket crud application.In this article we will keep things as simple as possible.We will create a student class as shown below
public class Student{public string Name { get; set; }public int Age { get; set; }}
Then we will create a context class and add it into the context class as shown
public class SocketExampleContext : DbContext{public SocketExampleContext(DbContextOptions<SocketExampleContext> options) : base(options){ }public DbSet<Student> Student { get; set; }}
Once done we will register the context dependency in the startup.cs class.we will also register the webscocket manager to the startup.cs.As shown below
public class Startup{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }public void ConfigureServices(IServiceCollection services){services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);services.AddWebSocketManager();services.AddDbContext<SocketExampleContext>(options =>options.UseSqlServer(Configuration.GetConnectionString(“DefaultConnection”)));}public void Configure(IApplicationBuilder app, IHostingEnvironment env){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}else{app.UseHsts();}app.UseHttpsRedirection();app.UseMvc();}}
All is set we can now write our Student websockect handler deriving from Websockethandler class.This student websocket handler creates and returns a list of students.
public class StudentWebsocketHandler : WebSocketHandler
{
private readonly IServiceScopeFactory _service;
public StudentWebsocketHandler(WebSocketConnectionManager webSocketConnectionManager, IServiceScopeFactory service) : base(webSocketConnectionManager){_service = service;}public override async Task ReceiveAsync(WebSocket socket, WebSocketReceiveResult result, byte[] buffer){Student data = JsonConvert.DeserializeObject<Student>(Encoding.UTF8.GetString(buffer, 0, result.Count));var responseObject = await GetResponseObjectAsync(data);await SendMessageToAllAsync(responseObject);}private async Task<JObject> GetResponseObjectAsync(Student payload){using (var scope = _service.CreateScope())
{var _Context = scope.ServiceProvider.GetService<SocketExampleContext>();await _Context.Student.AddAsync(payload);var list = await _Context.Student.ToListAsync();return JObject.FromObject(list);}}}
Now once done lets create the websocket endpoint. In startup.cs class under configure method register your websocket.Modify your startup.cs class as shown
public class Startup{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }// This method gets called by the runtime. Use this method to add services to the container.public void ConfigureServices(IServiceCollection services){services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);services.AddWebSocketManager();services.AddDbContext<SocketExampleContext>(options =>options.UseSqlServer(Configuration.GetConnectionString(“DefaultConnection”)));}// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}else{// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.app.UseHsts();}app.UseWebSockets();var wsOptions = new WebSocketOptions(){KeepAliveInterval = TimeSpan.FromSeconds(120),ReceiveBufferSize = 4 * 1024};// Whitelist the allowed websocket connection ips
//Comment out the line below if you need to whitelist ip//wsOptions.AllowedOrigins.Add(“ip”);app.UseWebSockets(wsOptions);app.MapWebSocketManager(“/student”, serviceProvider.GetService<StudentWebsocketHandler>());app.UseHttpsRedirection();app.UseMvc();}}
To test your connection download Websocket client chrome extension and input the url with your base url without http and Click open
ws://{url}/student
You should receive a message from the server saying
{ “Message”: “Accepted” }
To add a new student send this json object
{"Name":"name",
"Age":"age"
}
You should receive a student list object as a result
You can find the sample project on github here
From the sample in appsettings.json change your sql server connections and then run migrations.