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