mirror of
https://github.com/AlexMacocian/Net.Sdk.Web.Extensions.git
synced 2026-07-15 13:39:29 +00:00
Add project files.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.9.34714.143
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore.Extensions", "AspNetCore.Extensions\AspNetCore.Extensions.csproj", "{D0570CEC-F740-4332-AC10-8491974D1827}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D0570CEC-F740-4332-AC10-8491974D1827}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D0570CEC-F740-4332-AC10-8491974D1827}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D0570CEC-F740-4332-AC10-8491974D1827}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D0570CEC-F740-4332-AC10-8491974D1827}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7BB65352-F868-424E-BC0A-FD7CB202F791}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.1</Version>
|
||||
<Authors>Alexandru Macocian</Authors>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CorrelationVector" Version="1.0.42" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="SystemExtensions.NetCore" Version="1.0.1" />
|
||||
<PackageReference Include="SystemExtensions.NetStandard" Version="1.6.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace AspNetCore.Extensions.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Constructor)]
|
||||
public class DoNotInjectAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace AspNetCore.Extensions.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class OptionsNameAttribute : Attribute
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AspNetCore.Extensions.Websockets.Converters;
|
||||
|
||||
public sealed class Base64ToCertificateConverter : JsonConverter<X509Certificate2>
|
||||
{
|
||||
public override X509Certificate2? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.GetString() is not string base64)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot deserialize {nameof(X509Certificate2)} from {reader.GetString()}");
|
||||
}
|
||||
|
||||
var bytes = Convert.FromBase64String(base64);
|
||||
return new X509Certificate2(bytes);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, X509Certificate2 value, JsonSerializerOptions options)
|
||||
{
|
||||
var base64 = Convert.ToBase64String(value!.GetRawCertData());
|
||||
writer.WriteStringValue(base64);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AspNetCore.Extensions;
|
||||
|
||||
public class ForbiddenResponseActionResult : IActionResult
|
||||
{
|
||||
private readonly string reason;
|
||||
|
||||
public ForbiddenResponseActionResult(string reason)
|
||||
{
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
return context.HttpContext.Response.WriteAsync(reason, context.HttpContext.RequestAborted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.CorrelationVector;
|
||||
using System.Core.Extensions;
|
||||
|
||||
namespace AspNetCore.Extensions;
|
||||
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
private const string CorrelationVectorKey = "CorrelationVector";
|
||||
|
||||
public static void SetCorrelationVector(this HttpContext context, CorrelationVector cv)
|
||||
{
|
||||
context.ThrowIfNull()
|
||||
.Items.Add(CorrelationVectorKey, cv);
|
||||
}
|
||||
|
||||
public static CorrelationVector GetCorrelationVector(this HttpContext context)
|
||||
{
|
||||
context.ThrowIfNull();
|
||||
if (!context.Items.TryGetValue(CorrelationVectorKey, out var cvVal) ||
|
||||
cvVal is not CorrelationVector cv)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to extract API Key from context");
|
||||
}
|
||||
|
||||
return cv;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Macocian Alexandru Victor
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,34 @@
|
||||
using AspNetCore.Extensions.Options;
|
||||
using Microsoft.CorrelationVector;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Core.Extensions;
|
||||
|
||||
namespace AspNetCore.Extensions.Middleware;
|
||||
|
||||
public sealed class CorrelationVectorMiddleware : IMiddleware
|
||||
{
|
||||
private readonly CorrelationVectorOptions options;
|
||||
|
||||
public CorrelationVectorMiddleware() : this(Microsoft.Extensions.Options.Options.Create<CorrelationVectorOptions>(new()))
|
||||
{
|
||||
}
|
||||
|
||||
public CorrelationVectorMiddleware(IOptions<CorrelationVectorOptions> options)
|
||||
{
|
||||
this.options = options.ThrowIfNull().Value.ThrowIfNull();
|
||||
}
|
||||
|
||||
public Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
var cv = new CorrelationVector();
|
||||
if (context.Items.TryGetValue(this.options.Header, out var correlationVectorVal) &&
|
||||
correlationVectorVal is string correlationVectorStr)
|
||||
{
|
||||
cv = CorrelationVector.Parse(correlationVectorStr);
|
||||
cv.Increment();
|
||||
}
|
||||
|
||||
context.SetCorrelationVector(cv);
|
||||
return next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
|
||||
namespace AspNetCore.Extensions.Middleware;
|
||||
|
||||
public sealed class HeaderLoggingMiddleware : IMiddleware
|
||||
{
|
||||
private readonly ILogger<HeaderLoggingMiddleware> logger;
|
||||
|
||||
public HeaderLoggingMiddleware(
|
||||
ILogger<HeaderLoggingMiddleware> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger(nameof(this.InvokeAsync), string.Empty);
|
||||
scopedLogger.LogDebug(string.Join("\n", context.Request.Headers.Select(kvp => $"{kvp.Key}: {string.Join(",", kvp.Value.OfType<string>())}")));
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace AspNetCore.Extensions.Options;
|
||||
|
||||
public sealed class CorrelationVectorOptions
|
||||
{
|
||||
public string Header { get; set; } = "X-Correlation-Vector";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"AspNetCore.Extensions": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:54242;http://localhost:54243"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using AspNetCore.Extensions.Attributes;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
|
||||
namespace AspNetCore.Extensions;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder ConfigureExtended<TOptions>(this WebApplicationBuilder builder)
|
||||
where TOptions : class, new()
|
||||
{
|
||||
builder.ThrowIfNull()
|
||||
.Services.Configure<TOptions>(builder.Configuration.GetSection(GetOptionsName<TOptions>()));
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IConfigurationSection GetRequiredSection<TOptions>(this ConfigurationManager configurationManager)
|
||||
{
|
||||
configurationManager.ThrowIfNull();
|
||||
return configurationManager.GetRequiredSection(GetOptionsName<TOptions>());
|
||||
}
|
||||
|
||||
private static string GetOptionsName<TOptions>()
|
||||
{
|
||||
var maybeAttribute = typeof(TOptions).GetCustomAttributes(false).OfType<OptionsNameAttribute>().FirstOrDefault();
|
||||
if (maybeAttribute is not null &&
|
||||
maybeAttribute.Name?.IsNullOrWhiteSpace() is false)
|
||||
{
|
||||
return maybeAttribute.Name;
|
||||
}
|
||||
|
||||
return typeof(TOptions).Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using System.Core.Extensions;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Extensions;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace AspNetCore.Extensions.Websockets.Extensions;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
public static WebApplication MapWebSocket<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplication app, string route)
|
||||
where TWebSocketRoute : WebSocketRouteBase
|
||||
{
|
||||
app.ThrowIfNull();
|
||||
app.Map(route, async context =>
|
||||
{
|
||||
if (context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
var logger = app.Services.GetRequiredService<ILogger<WebSocketRouteBase>>();
|
||||
var route = GetRoute<TWebSocketRoute>(context);
|
||||
var routeFilters = GetRouteFilters<TWebSocketRoute>(context).ToList();
|
||||
|
||||
var actionContext = new ActionContext(
|
||||
context,
|
||||
new RouteData(),
|
||||
new ActionDescriptor());
|
||||
var actionExecutingContext = new ActionExecutingContext(
|
||||
actionContext,
|
||||
routeFilters,
|
||||
new Dictionary<string, object?>(),
|
||||
route);
|
||||
var actionExecutedContext = new ActionExecutedContext(
|
||||
actionContext,
|
||||
routeFilters,
|
||||
route);
|
||||
try
|
||||
{
|
||||
var processingTask = new Func<Task>(() => ProcessWebSocketRequest(route, context));
|
||||
await BeginProcessingPipeline(actionExecutingContext, actionExecutedContext, processingTask);
|
||||
}
|
||||
catch(WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
|
||||
{
|
||||
logger.LogInformation("Websocket closed prematurely. Marking as closed");
|
||||
}
|
||||
catch(OperationCanceledException)
|
||||
{
|
||||
logger.LogInformation("Websocket closed prematurely. Marking as closed");
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Encountered exception while handling websocket. Closing");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await route.SocketClosed();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task ProcessWebSocketRequest(WebSocketRouteBase route, HttpContext httpContext)
|
||||
{
|
||||
using var webSocket = await httpContext.WebSockets.AcceptWebSocketAsync();
|
||||
route.WebSocket = webSocket;
|
||||
route.Context = httpContext;
|
||||
await route.SocketAccepted(httpContext.RequestAborted);
|
||||
httpContext.Request.Scheme = httpContext.Request.Scheme == "https" ? "wss" : "ws";
|
||||
await HandleWebSocket(webSocket, route, httpContext.RequestAborted);
|
||||
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", httpContext.RequestAborted);
|
||||
}
|
||||
|
||||
private static async Task BeginProcessingPipeline(ActionExecutingContext actionExecutingContext, ActionExecutedContext actionExecutedContext, Func<Task> processWebSocket)
|
||||
{
|
||||
foreach (var filter in actionExecutingContext.Filters.OfType<IActionFilter>())
|
||||
{
|
||||
filter.OnActionExecuting(actionExecutingContext);
|
||||
if (actionExecutingContext.Result is IActionResult result)
|
||||
{
|
||||
await result.ExecuteResultAsync(actionExecutedContext);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ActionExecutionDelegate pipeline = async () =>
|
||||
{
|
||||
await processWebSocket();
|
||||
return actionExecutedContext;
|
||||
};
|
||||
|
||||
foreach (var filter in actionExecutingContext.Filters.OfType<IAsyncActionFilter>())
|
||||
{
|
||||
var next = pipeline;
|
||||
pipeline = async () =>
|
||||
{
|
||||
if (actionExecutingContext.Result is IActionResult result)
|
||||
{
|
||||
await result.ExecuteResultAsync(actionExecutedContext);
|
||||
return actionExecutedContext;
|
||||
}
|
||||
|
||||
await filter.OnActionExecutionAsync(actionExecutingContext, next);
|
||||
await (actionExecutingContext.Result?.ExecuteResultAsync(actionExecutingContext) ?? Task.CompletedTask);
|
||||
return actionExecutedContext;
|
||||
};
|
||||
}
|
||||
|
||||
await pipeline();
|
||||
}
|
||||
|
||||
private static async Task HandleWebSocket(WebSocket webSocket, WebSocketRouteBase route, CancellationToken cancellationToken)
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
using var memoryStream = new MemoryStream(1024);
|
||||
ValueWebSocketReceiveResult result;
|
||||
while(webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
do
|
||||
{
|
||||
var memory = new Memory<byte>(buffer);
|
||||
result = await webSocket.ReceiveAsync(memory, cancellationToken);
|
||||
await memoryStream.WriteAsync(buffer, 0, result.Count, cancellationToken);
|
||||
} while (!result.EndOfMessage);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await route.ExecuteAsync(result.MessageType, memoryStream.ToArray(), cancellationToken);
|
||||
memoryStream.SetLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
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<WebSocketRouteBase>();
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to resolve {typeof(TWebSocketRoute).Name}");
|
||||
}
|
||||
|
||||
private static IEnumerable<IFilterMetadata> GetRouteFilters<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(HttpContext context)
|
||||
where TWebSocketRoute : WebSocketRouteBase
|
||||
{
|
||||
foreach(var attribute in typeof(TWebSocketRoute).GetCustomAttributes(true).OfType<ServiceFilterAttribute>())
|
||||
{
|
||||
var filter = context.RequestServices.GetRequiredService(attribute.ServiceType);
|
||||
yield return filter.Cast<IFilterMetadata>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using AspNetCore.Extensions.Attributes;
|
||||
using System.Core.Extensions;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AspNetCore.Extensions.Websockets.Converters;
|
||||
|
||||
public class JsonWebSocketMessageConverter<T> : WebSocketMessageConverter<T>
|
||||
{
|
||||
private readonly JsonSerializerOptions? jsonSerializerOptions;
|
||||
|
||||
[DoNotInject]
|
||||
public JsonWebSocketMessageConverter()
|
||||
{
|
||||
}
|
||||
|
||||
public JsonWebSocketMessageConverter(JsonSerializerOptions options)
|
||||
{
|
||||
this.jsonSerializerOptions = options.ThrowIfNull();
|
||||
}
|
||||
|
||||
public override T ConvertTo(WebSocketConverterRequest request)
|
||||
{
|
||||
if (request.Type != System.Net.WebSockets.WebSocketMessageType.Text)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to deserialize message. Message is not text");
|
||||
}
|
||||
|
||||
var stringData = Encoding.UTF8.GetString(request.Payload!);
|
||||
var objData = JsonSerializer.Deserialize<T>(stringData, this.jsonSerializerOptions);
|
||||
return objData ?? throw new InvalidOperationException($"Unable to deserialize message to {typeof(T).Name}");
|
||||
}
|
||||
|
||||
public override WebSocketConverterResponse ConvertFrom(T message)
|
||||
{
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Text;
|
||||
|
||||
namespace AspNetCore.Extensions.Websockets.Converters;
|
||||
|
||||
public sealed class TextWebSocketMessageConverter : WebSocketMessageConverter<TextContent>
|
||||
{
|
||||
public override TextContent ConvertTo(WebSocketConverterRequest request)
|
||||
{
|
||||
if (request.Type is not System.Net.WebSockets.WebSocketMessageType.Text)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot parse message of type {request.Type}");
|
||||
}
|
||||
|
||||
var message = Encoding.UTF8.GetString(request.Payload!);
|
||||
return new TextContent { Text = message };
|
||||
}
|
||||
|
||||
public override WebSocketConverterResponse ConvertFrom(TextContent message)
|
||||
{
|
||||
return new WebSocketConverterResponse
|
||||
{
|
||||
Type = System.Net.WebSockets.WebSocketMessageType.Text,
|
||||
EndOfMessage = true,
|
||||
Payload = Encoding.UTF8.GetBytes(message.Text ?? string.Empty)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using AspNetCore.Extensions.Websockets.Converters;
|
||||
|
||||
namespace AspNetCore.Extensions.Websockets;
|
||||
|
||||
[WebSocketConverter<TextWebSocketMessageConverter, TextContent>]
|
||||
public sealed class TextContent
|
||||
{
|
||||
public string? Text { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace AspNetCore.Extensions.Websockets;
|
||||
|
||||
public abstract class WebSocketConverterAttributeBase : Attribute
|
||||
{
|
||||
public abstract Type ConverterType { get; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class WebSocketConverterAttribute<TConverter, TPayload> : WebSocketConverterAttributeBase
|
||||
where TConverter : WebSocketMessageConverter<TPayload>, new()
|
||||
{
|
||||
public override sealed Type ConverterType => typeof(TConverter);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace AspNetCore.Extensions.Websockets;
|
||||
|
||||
public sealed class WebSocketConverterRequest
|
||||
{
|
||||
public WebSocketMessageType Type { get; set; }
|
||||
public byte[]? Payload { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace AspNetCore.Extensions.Websockets;
|
||||
|
||||
public sealed class WebSocketConverterResponse
|
||||
{
|
||||
public WebSocketMessageType Type { get; set; }
|
||||
public byte[]? Payload { get; set; }
|
||||
public bool EndOfMessage { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Extensions;
|
||||
|
||||
namespace AspNetCore.Extensions.Websockets;
|
||||
|
||||
public abstract class WebSocketMessageConverterBase
|
||||
{
|
||||
public abstract object ConvertToObject(WebSocketConverterRequest request);
|
||||
public abstract WebSocketConverterResponse ConvertFromObject(object message);
|
||||
}
|
||||
|
||||
public abstract class WebSocketMessageConverter<T> : WebSocketMessageConverterBase
|
||||
{
|
||||
public sealed override object ConvertToObject(WebSocketConverterRequest request)
|
||||
{
|
||||
return ConvertTo(request)!;
|
||||
}
|
||||
public sealed override WebSocketConverterResponse ConvertFromObject(object message)
|
||||
{
|
||||
return this.ConvertFrom(message.Cast<T>());
|
||||
}
|
||||
|
||||
public abstract T ConvertTo(WebSocketConverterRequest request);
|
||||
public abstract WebSocketConverterResponse ConvertFrom(T message);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using AspNetCore.Extensions.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Extensions;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace AspNetCore.Extensions.Websockets;
|
||||
|
||||
public abstract class WebSocketRouteBase
|
||||
{
|
||||
public HttpContext? Context { get; internal set; }
|
||||
|
||||
public WebSocket? WebSocket { get; internal set; }
|
||||
|
||||
public virtual Task SocketAccepted(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task SocketClosed()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public abstract Task ExecuteAsync(WebSocketMessageType type, byte[] data, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public abstract class WebSocketRouteBase<TReceiveType> : WebSocketRouteBase
|
||||
where TReceiveType : class, new()
|
||||
{
|
||||
private readonly Lazy<WebSocketMessageConverterBase> converter;
|
||||
|
||||
public WebSocketRouteBase()
|
||||
{
|
||||
this.converter = new Lazy<WebSocketMessageConverterBase>(() =>
|
||||
{
|
||||
var attribute = typeof(TReceiveType).GetCustomAttributes(true).First(a => a is WebSocketConverterAttributeBase).Cast<WebSocketConverterAttributeBase>();
|
||||
var parsedConverter = GetConverter(attribute.ConverterType, this.Context!);
|
||||
return parsedConverter;
|
||||
});
|
||||
}
|
||||
|
||||
public sealed override Task ExecuteAsync(WebSocketMessageType type, byte[] data, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var objData = this.converter.Value.ConvertToObject(new WebSocketConverterRequest { Type = type, Payload = data }).Cast<TReceiveType>();
|
||||
return this.ExecuteAsync(objData, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var scoppedLogger = this.Context!.RequestServices.GetRequiredService<ILogger<WebSocketRouteBase<TReceiveType>>>().CreateScopedLogger(nameof(this.ExecuteAsync), string.Empty);
|
||||
scoppedLogger.LogError(ex, "Failed to process data");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Task ExecuteAsync(TReceiveType? type, CancellationToken cancellationToken);
|
||||
|
||||
internal static WebSocketMessageConverterBase GetConverter(Type converterType, HttpContext context)
|
||||
{
|
||||
var constructors = converterType.GetConstructors();
|
||||
foreach (var constructor in constructors)
|
||||
{
|
||||
if (constructor.GetCustomAttributes(false).Any(a => a is DoNotInjectAttribute))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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<WebSocketMessageConverterBase>();
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to resolve {converterType.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class WebSocketRouteBase<TReceiveType, TSendType> : WebSocketRouteBase<TReceiveType>
|
||||
where TReceiveType : class, new()
|
||||
{
|
||||
private readonly Lazy<WebSocketMessageConverterBase> converter = new(() =>
|
||||
{
|
||||
var attribute = typeof(TSendType).GetCustomAttributes(true).First(a => a is WebSocketConverterAttributeBase).Cast<WebSocketConverterAttributeBase>();
|
||||
var converter = Activator.CreateInstance(attribute.ConverterType)!.Cast<WebSocketMessageConverterBase>();
|
||||
return converter;
|
||||
});
|
||||
|
||||
public WebSocketRouteBase()
|
||||
{
|
||||
this.converter = new Lazy<WebSocketMessageConverterBase>(() =>
|
||||
{
|
||||
var attribute = typeof(TSendType).GetCustomAttributes(true).First(a => a is WebSocketConverterAttributeBase).Cast<WebSocketConverterAttributeBase>();
|
||||
var parsedConverter = GetConverter(attribute.ConverterType, this.Context!);
|
||||
return parsedConverter;
|
||||
});
|
||||
}
|
||||
|
||||
public Task SendMessage(TSendType sendType, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = this.converter.Value.ConvertFromObject(sendType!);
|
||||
return this.WebSocket!.SendAsync(response.Payload!, response.Type, response.EndOfMessage, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var scoppedLogger = this.Context!.RequestServices.GetRequiredService<ILogger<WebSocketRouteBase<TReceiveType, TSendType>>>().CreateScopedLogger(nameof(this.SendMessage), string.Empty);
|
||||
scoppedLogger.LogError(ex, "Failed to send data");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user