diff --git a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj index a70cba8..f5442e8 100644 --- a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj +++ b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj @@ -6,7 +6,7 @@ enable Library true - 0.8.4 + 0.8.5 Alexandru Macocian LICENSE true @@ -25,6 +25,7 @@ + diff --git a/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs b/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs index fd7251b..e2d13d6 100644 --- a/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs +++ b/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs @@ -3,11 +3,20 @@ using Net.Sdk.Web.Middleware; using Net.Sdk.Web.Extensions.Http; using System.Core.Extensions; using System.Extensions; +using Net.Sdk.Web.Websockets; +using System.Diagnostics.CodeAnalysis; namespace Net.Sdk.Web; public static class WebApplicationBuilderExtensions { + public static WebApplicationBuilder WithWebSocketRoute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplicationBuilder builder) + where TWebSocketRoute : WebSocketRouteBase + { + builder.Services.AddScoped(); + return builder; + } + public static WebApplicationBuilder ConfigureExtended(this WebApplicationBuilder builder) where TOptions : class, new() { diff --git a/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs b/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs index 63ac316..c16080c 100644 --- a/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs +++ b/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs @@ -6,11 +6,14 @@ using System.Core.Extensions; using System.Diagnostics.CodeAnalysis; using System.Extensions; using System.Net.WebSockets; +using Microsoft.IO; namespace Net.Sdk.Web.Websockets.Extensions; public static class WebApplicationExtensions { + private static readonly RecyclableMemoryStreamManager StreamManager = new(); + public static WebApplication UseCorrelationVector(this WebApplication webApplication) { webApplication.ThrowIfNull() @@ -27,16 +30,16 @@ public static class WebApplicationExtensions return webApplication; } - public static WebApplication MapWebSocket<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplication app, string route) + public static IEndpointConventionBuilder UseWebSocketRoute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplication app, string route) where TWebSocketRoute : WebSocketRouteBase { app.ThrowIfNull(); - app.Map(route, async context => + return app.Map(route, async context => { if (context.WebSockets.IsWebSocketRequest) { - var logger = app.Services.GetRequiredService>(); - var route = GetRoute(context); + var logger = app.Services.GetRequiredService>(); + var route = app.Services.GetRequiredService(); var routeFilters = GetRouteFilters(context).ToList(); var actionContext = new ActionContext( @@ -79,8 +82,6 @@ public static class WebApplicationExtensions context.Response.StatusCode = StatusCodes.Status400BadRequest; } }); - - return app; } private static async Task ProcessWebSocketRequest(WebSocketRouteBase route, HttpContext httpContext) @@ -134,16 +135,19 @@ public static class WebApplicationExtensions private static async Task HandleWebSocket(WebSocket webSocket, WebSocketRouteBase route, CancellationToken cancellationToken) { - var buffer = new byte[1024]; - using var memoryStream = new MemoryStream(1024); + if (route.Context is null) + { + throw new InvalidOperationException("Route HttpContext is null"); + } + + using var ms = StreamManager.GetStream(route.Context.Session.Id); ValueWebSocketReceiveResult result; while(webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested) { do { - var memory = new Memory(buffer); - result = await webSocket.ReceiveAsync(memory, cancellationToken); - await memoryStream.WriteAsync(buffer, 0, result.Count, cancellationToken); + var buffer = ms.GetMemory(8 * 1024); + result = await webSocket.ReceiveAsync(buffer, cancellationToken); } while (!result.EndOfMessage); if (result.MessageType == WebSocketMessageType.Close) @@ -151,30 +155,10 @@ public static class WebApplicationExtensions return; } - await route.ExecuteAsync(result.MessageType, memoryStream.ToArray(), cancellationToken); - memoryStream.SetLength(0); + await route.ExecuteAsync(result.MessageType, ms.GetReadOnlySequence(), cancellationToken); } } - private static WebSocketRouteBase GetRoute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(HttpContext context) - where TWebSocketRoute : WebSocketRouteBase - { - var constructors = typeof(TWebSocketRoute).GetConstructors(); - foreach(var constructor in constructors) - { - var dependencies = constructor.GetParameters().Select(param => context.RequestServices.GetService(param.ParameterType)); - if (dependencies.Any(d => d is null)) - { - continue; - } - - var route = constructor.Invoke(dependencies.ToArray()); - return route.Cast(); - } - - throw new InvalidOperationException($"Unable to resolve {typeof(TWebSocketRoute).Name}"); - } - private static IEnumerable GetRouteFilters<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(HttpContext context) where TWebSocketRoute : WebSocketRouteBase { diff --git a/Net.Sdk.Web.Extensions/Websockets/Converters/JsonWebSocketMessageConverter.cs b/Net.Sdk.Web.Extensions/Websockets/Converters/JsonWebSocketMessageConverter.cs index 171000a..07a32be 100644 --- a/Net.Sdk.Web.Extensions/Websockets/Converters/JsonWebSocketMessageConverter.cs +++ b/Net.Sdk.Web.Extensions/Websockets/Converters/JsonWebSocketMessageConverter.cs @@ -1,4 +1,5 @@ using Net.Sdk.Web.Attributes; +using System.Buffers; using System.Core.Extensions; using System.Text; using System.Text.Json; @@ -26,7 +27,12 @@ public class JsonWebSocketMessageConverter : WebSocketMessageConverter throw new InvalidOperationException($"Unable to deserialize message. Message is not text"); } - var stringData = Encoding.UTF8.GetString(request.Payload!); + if (request.Payload is null) + { + throw new InvalidOperationException($"Unable to deserialize message. Payload is null"); + } + + var stringData = Encoding.UTF8.GetString(request.Payload.Value); var objData = JsonSerializer.Deserialize(stringData, this.jsonSerializerOptions); return objData ?? throw new InvalidOperationException($"Unable to deserialize message to {typeof(T).Name}"); } @@ -35,6 +41,7 @@ public class JsonWebSocketMessageConverter : WebSocketMessageConverter { var serialized = JsonSerializer.Serialize(message, this.jsonSerializerOptions); var data = Encoding.UTF8.GetBytes(serialized); - return new WebSocketConverterResponse { EndOfMessage = true, Type = System.Net.WebSockets.WebSocketMessageType.Text, Payload = data }; + var readOnlySequence = new ReadOnlySequence(data); + return new WebSocketConverterResponse { EndOfMessage = true, Type = System.Net.WebSockets.WebSocketMessageType.Text, Payload = readOnlySequence }; } } diff --git a/Net.Sdk.Web.Extensions/Websockets/Converters/TextWebSocketMessageConverter.cs b/Net.Sdk.Web.Extensions/Websockets/Converters/TextWebSocketMessageConverter.cs index 4d84960..027b750 100644 --- a/Net.Sdk.Web.Extensions/Websockets/Converters/TextWebSocketMessageConverter.cs +++ b/Net.Sdk.Web.Extensions/Websockets/Converters/TextWebSocketMessageConverter.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Buffers; +using System.Text; namespace Net.Sdk.Web.Websockets.Converters; @@ -11,7 +12,12 @@ public sealed class TextWebSocketMessageConverter : WebSocketMessageConverter(Encoding.UTF8.GetBytes(message.Text ?? string.Empty)) }; } } diff --git a/Net.Sdk.Web.Extensions/Websockets/WebSocketConverterRequest.cs b/Net.Sdk.Web.Extensions/Websockets/WebSocketConverterRequest.cs index 19482b0..ba7395e 100644 --- a/Net.Sdk.Web.Extensions/Websockets/WebSocketConverterRequest.cs +++ b/Net.Sdk.Web.Extensions/Websockets/WebSocketConverterRequest.cs @@ -1,10 +1,11 @@ -using System.Net.WebSockets; +using System.Buffers; +using System.Net.WebSockets; namespace Net.Sdk.Web.Websockets; public sealed class WebSocketConverterRequest { public WebSocketMessageType Type { get; set; } - public byte[]? Payload { get; set; } + public ReadOnlySequence? Payload { get; set; } } diff --git a/Net.Sdk.Web.Extensions/Websockets/WebSocketConverterResponse.cs b/Net.Sdk.Web.Extensions/Websockets/WebSocketConverterResponse.cs index 278128b..47010cf 100644 --- a/Net.Sdk.Web.Extensions/Websockets/WebSocketConverterResponse.cs +++ b/Net.Sdk.Web.Extensions/Websockets/WebSocketConverterResponse.cs @@ -1,10 +1,11 @@ -using System.Net.WebSockets; +using System.Buffers; +using System.Net.WebSockets; namespace Net.Sdk.Web.Websockets; public sealed class WebSocketConverterResponse { public WebSocketMessageType Type { get; set; } - public byte[]? Payload { get; set; } + public ReadOnlySequence? Payload { get; set; } public bool EndOfMessage { get; set; } } \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions/Websockets/WebSocketRouteBase.cs b/Net.Sdk.Web.Extensions/Websockets/WebSocketRouteBase.cs index 38676fa..c1249a0 100644 --- a/Net.Sdk.Web.Extensions/Websockets/WebSocketRouteBase.cs +++ b/Net.Sdk.Web.Extensions/Websockets/WebSocketRouteBase.cs @@ -1,4 +1,5 @@ using Net.Sdk.Web.Attributes; +using System.Buffers; using System.Extensions; using System.Net.WebSockets; @@ -20,7 +21,7 @@ public abstract class WebSocketRouteBase return Task.CompletedTask; } - public abstract Task ExecuteAsync(WebSocketMessageType type, byte[] data, CancellationToken cancellationToken); + public abstract Task ExecuteAsync(WebSocketMessageType type, ReadOnlySequence data, CancellationToken cancellationToken); } public abstract class WebSocketRouteBase : WebSocketRouteBase @@ -38,8 +39,13 @@ public abstract class WebSocketRouteBase : WebSocketRouteBase }); } - public sealed override Task ExecuteAsync(WebSocketMessageType type, byte[] data, CancellationToken cancellationToken) + public sealed override Task ExecuteAsync(WebSocketMessageType type, ReadOnlySequence data, CancellationToken cancellationToken) { + if (this.Context is null) + { + throw new InvalidOperationException("Route HttpContext is null"); + } + try { var objData = this.converter.Value.ConvertToObject(new WebSocketConverterRequest { Type = type, Payload = data }).Cast(); @@ -47,7 +53,7 @@ public abstract class WebSocketRouteBase : WebSocketRouteBase } catch (Exception ex) { - var scoppedLogger = this.Context!.RequestServices.GetRequiredService>>().CreateScopedLogger(nameof(this.ExecuteAsync), string.Empty); + var scoppedLogger = this.Context.RequestServices.GetRequiredService>>().CreateScopedLogger(nameof(this.ExecuteAsync), string.Empty); scoppedLogger.LogError(ex, "Failed to process data"); throw; } @@ -99,16 +105,37 @@ public abstract class WebSocketRouteBase : WebSocketRou }); } - public Task SendMessage(TSendType sendType, CancellationToken cancellationToken) + public async Task SendMessage(TSendType sendType, CancellationToken cancellationToken) { + _ = sendType ?? throw new ArgumentNullException(nameof(sendType)); + if (this.WebSocket is null) + { + throw new InvalidOperationException("Route WebSocket is null"); + } + + if (this.Context is null) + { + throw new InvalidOperationException("Route HttpContext is null"); + } + try { - var response = this.converter.Value.ConvertFromObject(sendType!); - return this.WebSocket!.SendAsync(response.Payload!, response.Type, response.EndOfMessage, cancellationToken); + var response = this.converter.Value.ConvertFromObject(sendType); + if (response.Payload is null) + { + throw new InvalidOperationException("Send payload is null"); + } + + SequencePosition pos = response.Payload.Value.Start; + while (response.Payload.Value.TryGet(ref pos, out var memory)) + { + var endOfMessage = pos.Equals(response.Payload.Value.End); + await this.WebSocket.SendAsync(memory, response.Type, endOfMessage && response.EndOfMessage, cancellationToken); + } } catch (Exception ex) { - var scoppedLogger = this.Context!.RequestServices.GetRequiredService>>().CreateScopedLogger(nameof(this.SendMessage), string.Empty); + var scoppedLogger = this.Context.RequestServices.GetRequiredService>>().CreateScopedLogger(nameof(this.SendMessage), string.Empty); scoppedLogger.LogError(ex, "Failed to send data"); throw; } diff --git a/README.md b/README.md index 29d9051..699dc32 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,9 @@ Net.Sdk.Web.Extensions provides a streamlined implementation of WebSockets to be ### Integration into `WebApplication` ```C# var builder = WebApplication.CreateSlimBuilder(args); +builder.WithWebSocketRoute(); var app = builder.Build(); -app.MapWebSocket("custom-route"); +app.UseWebSocketRoute("custom-route"); ``` ### Message mapping