mirror of
https://github.com/AlexMacocian/Net.Sdk.Web.Extensions.git
synced 2026-07-21 00:19:30 +00:00
Improve README
IPExtractingMiddleware
This commit is contained in:
@@ -3,11 +3,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.9.34714.143
|
VisualStudioVersion = 17.9.34714.143
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{584B002D-4DFC-4034-8F3E-B4DA327B82B9}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{584B002D-4DFC-4034-8F3E-B4DA327B82B9}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
LICENSE = LICENSE
|
LICENSE = LICENSE
|
||||||
|
README.md = README.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{EF036121-B1EE-4F1C-AF84-7E39337BF039}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{EF036121-B1EE-4F1C-AF84-7E39337BF039}"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace Net.Sdk.Web;
|
|||||||
public static class HttpContextExtensions
|
public static class HttpContextExtensions
|
||||||
{
|
{
|
||||||
private const string CorrelationVectorKey = "CorrelationVector";
|
private const string CorrelationVectorKey = "CorrelationVector";
|
||||||
|
private const string ClientIPKey = "ClientIP";
|
||||||
|
|
||||||
public static void SetCorrelationVector(this HttpContext context, CorrelationVector cv)
|
public static void SetCorrelationVector(this HttpContext context, CorrelationVector cv)
|
||||||
{
|
{
|
||||||
@@ -24,4 +25,22 @@ public static class HttpContextExtensions
|
|||||||
|
|
||||||
return cv;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
<Version>0.6.1</Version>
|
<Version>0.7</Version>
|
||||||
<Authors>Alexandru Macocian</Authors>
|
<Authors>Alexandru Macocian</Authors>
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||||
|
|||||||
@@ -31,14 +31,22 @@ public static class WebApplicationBuilderExtensions
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Net.Sdk.Web.Extensions.Http.HttpClientBuilder<T> RegisterHttpClient<T>(this WebApplicationBuilder builder)
|
public static WebApplicationBuilder WithIPExtraction(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.ThrowIfNull();
|
||||||
|
builder.Services.AddScoped<IPExtractingMiddleware>();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Extensions.Http.HttpClientBuilder<T> RegisterHttpClient<T>(this WebApplicationBuilder builder)
|
||||||
where T : class
|
where T : class
|
||||||
{
|
{
|
||||||
_ = builder.ThrowIfNull();
|
_ = builder.ThrowIfNull();
|
||||||
return new Net.Sdk.Web.Extensions.Http.HttpClientBuilder<T>(builder);
|
return new Extensions.Http.HttpClientBuilder<T>(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Net.Sdk.Web.Extensions.Http.HttpClientBuilder<T> WithCorrelationVector<T>(this Net.Sdk.Web.Extensions.Http.HttpClientBuilder<T> httpClientBuilder)
|
public static Extensions.Http.HttpClientBuilder<T> WithCorrelationVector<T>(this Extensions.Http.HttpClientBuilder<T> httpClientBuilder)
|
||||||
where T : class
|
where T : class
|
||||||
{
|
{
|
||||||
return httpClientBuilder.ThrowIfNull()
|
return httpClientBuilder.ThrowIfNull()
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ public static class WebApplicationExtensions
|
|||||||
return webApplication;
|
return webApplication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static WebApplication UseIPExtraction(this WebApplication webApplication)
|
||||||
|
{
|
||||||
|
webApplication.ThrowIfNull()
|
||||||
|
.UseMiddleware<IPExtractingMiddleware>();
|
||||||
|
|
||||||
|
return webApplication;
|
||||||
|
}
|
||||||
|
|
||||||
public static WebApplication MapWebSocket<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplication app, string route)
|
public static WebApplication MapWebSocket<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplication app, string route)
|
||||||
where TWebSocketRoute : WebSocketRouteBase
|
where TWebSocketRoute : WebSocketRouteBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1 +1,160 @@
|
|||||||
# AspNetCore.Extensions
|
# 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<CustomRoute>("custom-route");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message mapping
|
||||||
|
#### Define your request
|
||||||
|
```C#
|
||||||
|
[WebSocketConverter<JsonWebSocketMessageConverter<CustomRequest>, CustomRequest>]
|
||||||
|
public class CustomRequest{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Define your response
|
||||||
|
```C#
|
||||||
|
[WebSocketConverter<JsonWebSocketMessageConverter<CustomResponse>, CustomResponse>]
|
||||||
|
public class CustomResponse{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Implement your WebSocketRoute
|
||||||
|
```C#
|
||||||
|
public class CustomRoute : WebSocketRouteBase<CustomRequest, CustomResponse>
|
||||||
|
{
|
||||||
|
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<T>`
|
||||||
|
|
||||||
|
## 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<CustomOptions>();
|
||||||
|
|
||||||
|
public class CustomService
|
||||||
|
{
|
||||||
|
public CustomService (IOptions<CustomOptions> options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customizing configuration key
|
||||||
|
```C#
|
||||||
|
[OptionsName(Name = "CustomKey")]
|
||||||
|
public class CustomOptions
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mockable HttpClient
|
||||||
|
### Integration into `WebApplication`
|
||||||
|
```C#
|
||||||
|
var builder = WebApplication.CreateSlimBuilder(args);
|
||||||
|
builder.RegisterHttpClient<CustomService>()
|
||||||
|
.WithTimeout(TimeSpan.FromSeconds(5))
|
||||||
|
.WithCorrelationVector()
|
||||||
|
.CreateBuilder();
|
||||||
|
|
||||||
|
public class CustomService
|
||||||
|
{
|
||||||
|
public CustomService (IHttpClient<CustomService> 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!;
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user