diff --git a/Net.Sdk.Web.Extensions.sln b/Net.Sdk.Web.Extensions.sln
index 2186af4..a96077a 100644
--- a/Net.Sdk.Web.Extensions.sln
+++ b/Net.Sdk.Web.Extensions.sln
@@ -3,11 +3,12 @@ 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}") = "Net.Sdk.Web.Extensions", "Net.Sdk.Web.Extensions\Net.Sdk.Web.Extensions.csproj", "{D0570CEC-F740-4332-AC10-8491974D1827}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net.Sdk.Web.Extensions", "Net.Sdk.Web.Extensions\Net.Sdk.Web.Extensions.csproj", "{D0570CEC-F740-4332-AC10-8491974D1827}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{584B002D-4DFC-4034-8F3E-B4DA327B82B9}"
ProjectSection(SolutionItems) = preProject
LICENSE = LICENSE
+ README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{EF036121-B1EE-4F1C-AF84-7E39337BF039}"
diff --git a/Net.Sdk.Web.Extensions/HttpContextExtensions.cs b/Net.Sdk.Web.Extensions/HttpContextExtensions.cs
index f40a713..12c8104 100644
--- a/Net.Sdk.Web.Extensions/HttpContextExtensions.cs
+++ b/Net.Sdk.Web.Extensions/HttpContextExtensions.cs
@@ -6,6 +6,7 @@ namespace Net.Sdk.Web;
public static class HttpContextExtensions
{
private const string CorrelationVectorKey = "CorrelationVector";
+ private const string ClientIPKey = "ClientIP";
public static void SetCorrelationVector(this HttpContext context, CorrelationVector cv)
{
@@ -24,4 +25,22 @@ public static class HttpContextExtensions
return cv;
}
+
+ public static void SetClientIP(this HttpContext context, string ip)
+ {
+ context.ThrowIfNull()
+ .Items.Add(ClientIPKey, ip);
+ }
+
+ public static string GetClientIP(this HttpContext context)
+ {
+ context.ThrowIfNull();
+ if (!context.Items.TryGetValue(ClientIPKey, out var ip) ||
+ ip is not string ipStr)
+ {
+ throw new InvalidOperationException("Unable to extract IP from context");
+ }
+
+ return ipStr;
+ }
}
diff --git a/Net.Sdk.Web.Extensions/Middleware/IPExtractingMiddleware.cs b/Net.Sdk.Web.Extensions/Middleware/IPExtractingMiddleware.cs
new file mode 100644
index 0000000..1844695
--- /dev/null
+++ b/Net.Sdk.Web.Extensions/Middleware/IPExtractingMiddleware.cs
@@ -0,0 +1,34 @@
+namespace Net.Sdk.Web.Middleware;
+
+public sealed class IPExtractingMiddleware : IMiddleware
+{
+ private const string XForwardedForHeaderKey = "X-Forwarded-For";
+ private const string CFConnectingIPHeaderKey = "CF-Connecting-IP";
+
+ public IPExtractingMiddleware()
+ {
+ }
+
+ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
+ {
+ var address = context.Connection.RemoteIpAddress?.ToString();
+ context.Request.Headers.TryGetValue(CFConnectingIPHeaderKey, out var cfConnectingIpValues);
+ if (cfConnectingIpValues.FirstOrDefault() is string xCfIpAddress)
+ {
+ context.SetClientIP(xCfIpAddress);
+ await next(context);
+ return;
+ }
+
+ context.Request.Headers.TryGetValue(XForwardedForHeaderKey, out var xForwardedForValues);
+ if (xForwardedForValues.FirstOrDefault() is string xForwardedIpAddress)
+ {
+ context.SetClientIP(xForwardedIpAddress);
+ await next(context);
+ return;
+ }
+
+ context.SetClientIP(address ?? throw new InvalidOperationException("Unable to extract client IP address"));
+ await next(context);
+ }
+}
diff --git a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj
index 0193d53..f1af2ed 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.6.1
+ 0.7
Alexandru Macocian
LICENSE
true
diff --git a/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs b/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs
index befcc9a..fd7251b 100644
--- a/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs
+++ b/Net.Sdk.Web.Extensions/WebApplicationBuilderExtensions.cs
@@ -31,14 +31,22 @@ public static class WebApplicationBuilderExtensions
return builder;
}
- public static Net.Sdk.Web.Extensions.Http.HttpClientBuilder RegisterHttpClient(this WebApplicationBuilder builder)
+ public static WebApplicationBuilder WithIPExtraction(this WebApplicationBuilder builder)
+ {
+ builder.ThrowIfNull();
+ builder.Services.AddScoped();
+
+ return builder;
+ }
+
+ public static Extensions.Http.HttpClientBuilder RegisterHttpClient(this WebApplicationBuilder builder)
where T : class
{
_ = builder.ThrowIfNull();
- return new Net.Sdk.Web.Extensions.Http.HttpClientBuilder(builder);
+ return new Extensions.Http.HttpClientBuilder(builder);
}
- public static Net.Sdk.Web.Extensions.Http.HttpClientBuilder WithCorrelationVector(this Net.Sdk.Web.Extensions.Http.HttpClientBuilder httpClientBuilder)
+ public static Extensions.Http.HttpClientBuilder WithCorrelationVector(this Extensions.Http.HttpClientBuilder httpClientBuilder)
where T : class
{
return httpClientBuilder.ThrowIfNull()
diff --git a/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs b/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs
index 14b4125..63ac316 100644
--- a/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs
+++ b/Net.Sdk.Web.Extensions/WebApplicationExtensions.cs
@@ -19,6 +19,14 @@ public static class WebApplicationExtensions
return webApplication;
}
+ public static WebApplication UseIPExtraction(this WebApplication webApplication)
+ {
+ webApplication.ThrowIfNull()
+ .UseMiddleware();
+
+ return webApplication;
+ }
+
public static WebApplication MapWebSocket<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplication app, string route)
where TWebSocketRoute : WebSocketRouteBase
{
diff --git a/README.md b/README.md
index 928720b..4e4631c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,160 @@
-# AspNetCore.Extensions
\ No newline at end of file
+# Net.Sdk.Web.Extensions
+
+.NET library extending functionality for web applications
+
+# Features
+- [Improved Websocket Support](#better-websocket-support)
+- [Improved request tracing](#request-tracing-using-correlationvectors)
+- [Improved client IP handling](#client-ip-extraction)
+- [Improved configuration binding](#better-configuration-binding-support)
+- [Mockable IHttpClient builder](#mockable-httpclient)
+- [Support for base64 encoded certificates in configuration](#base64-to-x509certificate2-converter-for-options)
+
+## Better WebSocket support
+
+Net.Sdk.Web.Extensions provides a streamlined implementation of WebSockets to be used in a web app, in a syntax similar to Asp Mvc
+
+### Integration into `WebApplication`
+```C#
+var builder = WebApplication.CreateSlimBuilder(args);
+var app = builder.Build();
+app.MapWebSocker("custom-route");
+```
+
+### Message mapping
+#### Define your request
+```C#
+[WebSocketConverter, CustomRequest>]
+public class CustomRequest{
+}
+```
+
+#### Define your response
+```C#
+[WebSocketConverter, CustomResponse>]
+public class CustomResponse{
+}
+```
+
+#### Implement your WebSocketRoute
+```C#
+public class CustomRoute : WebSocketRouteBase
+{
+ public override Task SocketAccepted(CancellationToken cancellationToken)
+ {
+ return base.SocketAccepted(cancellationToken);
+ }
+
+ public override Task SocketClosed()
+ {
+ return base.SocketClosed();
+ }
+
+ public override async Task ExecuteAsync(CustomRequest? request, CancellationToken cancellationToken)
+ {
+ await this.SendMessage(new BotResponse(), cancellationToken);
+ }
+}
+```
+
+### Custom message converters
+If you need specialized converters, implement your own `WebSocketMessageConverter`
+
+## Request tracing using CorrelationVectors
+
+### Integration into `WebApplication`
+```C#
+var builder = WebApplication.CreateSlimBuilder(args);
+builder.WithCorrelationVector();
+var app = builder.Build();
+app.UseCorrelationVector();
+```
+
+### Accessing the correlation vector
+Take a dependency on `IHttpContextAccessor` and get the CV using the `HttpContextExtensions.GetCorrelationVector` method
+```C#
+var cv = accessor.HttpContext.GetCorrelationVector();
+```
+
+### Configuration
+Configure an instance of `CorrelationVectorOptions` to adjust the CV header
+
+### Requests and responses
+- On a request, the `CorrelationVectorMiddleware` retrieves the CV from the request header if present, otherwise it creates a new one
+- The CV is stored in `HttpContext.Items` under `CorrelationVector` key
+- Log the CV to follow traces of one application flow under multiple operations and across http requests
+
+### Integration with `HttpClient`
+- Pass the `CorrelationVectorHandler` to the `IHttpClientBuilder` to manage CVs across requests
+- On each `HttpClient` request, the CV is added to the configured header
+- After receiving the response, the `CorrelationVectorHandler` will receive and parse any existing CV from the headers and reapply it to the `HttpContext.Items`
+
+## Client IP extraction
+`IPExtractingMiddleware` figures out the IP of the client, being able to handle reverse proxying through `X-Forwarded-For` header as well as CloudFlare specific `CF-Connecting-IP` header
+
+### Integration into `WebApplication`
+```C#
+var builder = WebApplication.CreateSlimBuilder(args);
+builder.WithIPExtraction();
+var app = builder.Build();
+app.UseIPExtraction();
+```
+
+### Accessing the client IP
+Take a dependency on `IHttpContextAccessor` and get the CV using the `HttpContextExtensions.GetCorrelationVector` method
+```C#
+var cv = accessor.HttpContext.GetClientIP();
+```
+
+## Better configuration binding support
+
+### Integration into `WebApplication`
+```C#
+var builder = WebApplication.CreateSlimBuilder(args);
+builder.ConfigureExtended();
+
+public class CustomService
+{
+ public CustomService (IOptions options)
+ {
+ }
+}
+```
+
+### Customizing configuration key
+```C#
+[OptionsName(Name = "CustomKey")]
+public class CustomOptions
+{
+
+}
+```
+
+## Mockable HttpClient
+### Integration into `WebApplication`
+```C#
+var builder = WebApplication.CreateSlimBuilder(args);
+builder.RegisterHttpClient()
+ .WithTimeout(TimeSpan.FromSeconds(5))
+ .WithCorrelationVector()
+ .CreateBuilder();
+
+public class CustomService
+{
+ public CustomService (IHttpClient client)
+ {
+ }
+}
+```
+
+## Base64 to X509Certificate2 converter for options
+Use the `Base64ToCertificateConverter` to retrieve your SSL certificate from configuration and bind it to options, to allow for dynamic loading of certificates
+
+### Use `Base64ToCertificateConverter` on your `X509Certificate2` property
+```C#
+public class ServerOptions
+{
+ [JsonConverter(typeof(Base64ToCertificateConverter))]
+ public X509Certificate2 Certificate { get; set; } = default!;
+}
+```
\ No newline at end of file