658 lines
13 KiB
Markdown
658 lines
13 KiB
Markdown
# HTTP Configuration
|
|
|
|
Configuration and customization for HTTP integration.
|
|
|
|
## Basic Configuration
|
|
|
|
### Minimal Setup
|
|
|
|
```csharp
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Register CQRS services
|
|
builder.Services.AddSvrntyCQRS();
|
|
builder.Services.AddDefaultCommandDiscovery();
|
|
builder.Services.AddDefaultQueryDiscovery();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Map endpoints with default settings
|
|
app.MapSvrntyCommands(); // POST /api/command/{name}
|
|
app.MapSvrntyQueries(); // GET/POST /api/query/{name}
|
|
|
|
app.Run();
|
|
```
|
|
|
|
## Route Prefix Configuration
|
|
|
|
### Custom Command Prefix
|
|
|
|
```csharp
|
|
app.MapSvrntyCommands("my-commands");
|
|
// POST /my-commands/{name}
|
|
```
|
|
|
|
### Custom Query Prefix
|
|
|
|
```csharp
|
|
app.MapSvrntyQueries("my-queries");
|
|
// GET/POST /my-queries/{name}
|
|
```
|
|
|
|
### Remove Prefix
|
|
|
|
```csharp
|
|
app.MapSvrntyCommands("");
|
|
// POST /{commandName}
|
|
|
|
app.MapSvrntyQueries("");
|
|
// GET/POST /{queryName}
|
|
```
|
|
|
|
### Versioned Routes
|
|
|
|
```csharp
|
|
app.MapSvrntyCommands("v1/commands");
|
|
app.MapSvrntyQueries("v1/queries");
|
|
|
|
// POST /v1/commands/{name}
|
|
// GET/POST /v1/queries/{name}
|
|
```
|
|
|
|
## CORS Configuration
|
|
|
|
### Basic CORS
|
|
|
|
```csharp
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Services.AddCors(options =>
|
|
{
|
|
options.AddDefaultPolicy(policy =>
|
|
{
|
|
policy.WithOrigins("https://example.com")
|
|
.AllowAnyMethod()
|
|
.AllowAnyHeader();
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseCors(); // Must be before MapSvrntyCommands/Queries
|
|
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
|
|
app.Run();
|
|
```
|
|
|
|
### Named CORS Policy
|
|
|
|
```csharp
|
|
builder.Services.AddCors(options =>
|
|
{
|
|
options.AddPolicy("AllowSpecificOrigin", policy =>
|
|
{
|
|
policy.WithOrigins("https://app.example.com", "https://admin.example.com")
|
|
.WithMethods("GET", "POST")
|
|
.WithHeaders("Content-Type", "Authorization")
|
|
.AllowCredentials();
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseCors("AllowSpecificOrigin");
|
|
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
```
|
|
|
|
### Development CORS
|
|
|
|
```csharp
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseCors(policy =>
|
|
{
|
|
policy.AllowAnyOrigin()
|
|
.AllowAnyMethod()
|
|
.AllowAnyHeader();
|
|
});
|
|
}
|
|
```
|
|
|
|
## Authentication
|
|
|
|
### JWT Bearer Authentication
|
|
|
|
```csharp
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
.AddJwtBearer(options =>
|
|
{
|
|
options.Authority = "https://your-auth-server.com";
|
|
options.Audience = "your-api-resource";
|
|
});
|
|
|
|
builder.Services.AddAuthorization();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
|
|
app.Run();
|
|
```
|
|
|
|
### Cookie Authentication
|
|
|
|
```csharp
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
|
|
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
.AddCookie(options =>
|
|
{
|
|
options.LoginPath = "/login";
|
|
options.LogoutPath = "/logout";
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
```
|
|
|
|
### API Key Authentication
|
|
|
|
```csharp
|
|
// Custom API key middleware
|
|
app.Use(async (context, next) =>
|
|
{
|
|
if (!context.Request.Headers.TryGetValue("X-API-Key", out var apiKey))
|
|
{
|
|
context.Response.StatusCode = 401;
|
|
await context.Response.WriteAsync("API Key missing");
|
|
return;
|
|
}
|
|
|
|
// Validate API key
|
|
if (!IsValidApiKey(apiKey))
|
|
{
|
|
context.Response.StatusCode = 401;
|
|
await context.Response.WriteAsync("Invalid API Key");
|
|
return;
|
|
}
|
|
|
|
await next();
|
|
});
|
|
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
```
|
|
|
|
## Authorization
|
|
|
|
### Require Authentication for All Endpoints
|
|
|
|
```csharp
|
|
app.MapSvrntyCommands().RequireAuthorization();
|
|
app.MapSvrntyQueries().RequireAuthorization();
|
|
```
|
|
|
|
### Role-Based Authorization
|
|
|
|
```csharp
|
|
app.MapSvrntyCommands().RequireAuthorization(policy =>
|
|
{
|
|
policy.RequireRole("Admin");
|
|
});
|
|
```
|
|
|
|
### Policy-Based Authorization
|
|
|
|
```csharp
|
|
builder.Services.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy("RequireAdminRole", policy =>
|
|
{
|
|
policy.RequireRole("Admin");
|
|
});
|
|
|
|
options.AddPolicy("RequireVerifiedAccount", policy =>
|
|
{
|
|
policy.RequireClaim("EmailVerified", "true");
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
app.MapSvrntyCommands().RequireAuthorization("RequireAdminRole");
|
|
app.MapSvrntyQueries(); // No global authorization for queries
|
|
```
|
|
|
|
### Per-Command Authorization
|
|
|
|
Use `ICommandAuthorizationService` for fine-grained control:
|
|
|
|
```csharp
|
|
public class DeleteUserCommandAuthorization : ICommandAuthorizationService<DeleteUserCommand>
|
|
{
|
|
public Task<bool> CanExecuteAsync(
|
|
DeleteUserCommand command,
|
|
ClaimsPrincipal user,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// Only admins or the user themselves can delete
|
|
return Task.FromResult(
|
|
user.IsInRole("Admin") ||
|
|
user.FindFirst(ClaimTypes.NameIdentifier)?.Value == command.UserId.ToString());
|
|
}
|
|
}
|
|
|
|
// Registration
|
|
builder.Services.AddScoped<ICommandAuthorizationService<DeleteUserCommand>, DeleteUserCommandAuthorization>();
|
|
```
|
|
|
|
## Rate Limiting
|
|
|
|
### ASP.NET Core Rate Limiting
|
|
|
|
```csharp
|
|
using System.Threading.RateLimiting;
|
|
|
|
builder.Services.AddRateLimiter(options =>
|
|
{
|
|
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
|
|
{
|
|
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "anonymous";
|
|
|
|
return RateLimitPartition.GetFixedWindowLimiter(userId, _ =>
|
|
new FixedWindowRateLimiterOptions
|
|
{
|
|
PermitLimit = 100,
|
|
Window = TimeSpan.FromMinutes(1)
|
|
});
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseRateLimiter();
|
|
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
```
|
|
|
|
### Per-Endpoint Rate Limiting
|
|
|
|
```csharp
|
|
app.MapSvrntyCommands().RequireRateLimiting("fixed");
|
|
|
|
// Define named policy
|
|
builder.Services.AddRateLimiter(options =>
|
|
{
|
|
options.AddFixedWindowLimiter("fixed", limiterOptions =>
|
|
{
|
|
limiterOptions.PermitLimit = 10;
|
|
limiterOptions.Window = TimeSpan.FromSeconds(10);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Request Size Limits
|
|
|
|
### Global Request Size Limit
|
|
|
|
```csharp
|
|
builder.Services.Configure<IISServerOptions>(options =>
|
|
{
|
|
options.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
|
|
});
|
|
|
|
builder.Services.Configure<KestrelServerOptions>(options =>
|
|
{
|
|
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
|
|
});
|
|
```
|
|
|
|
### Per-Endpoint Size Limit
|
|
|
|
```csharp
|
|
app.MapPost("/api/command/uploadLargeFile", async (HttpContext context) =>
|
|
{
|
|
context.Features.Get<IHttpMaxRequestBodySizeFeature>().MaxRequestBodySize = 100 * 1024 * 1024; // 100 MB
|
|
// Handle large file upload
|
|
})
|
|
.DisableRequestSizeLimit();
|
|
```
|
|
|
|
## Compression
|
|
|
|
### Response Compression
|
|
|
|
```csharp
|
|
using Microsoft.AspNetCore.ResponseCompression;
|
|
|
|
builder.Services.AddResponseCompression(options =>
|
|
{
|
|
options.EnableForHttps = true;
|
|
options.Providers.Add<GzipCompressionProvider>();
|
|
options.Providers.Add<BrotliCompressionProvider>();
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseResponseCompression();
|
|
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
```
|
|
|
|
## Content Negotiation
|
|
|
|
### JSON Configuration
|
|
|
|
```csharp
|
|
using System.Text.Json;
|
|
|
|
builder.Services.ConfigureHttpJsonOptions(options =>
|
|
{
|
|
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
|
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
|
options.SerializerOptions.WriteIndented = app.Environment.IsDevelopment();
|
|
});
|
|
```
|
|
|
|
### XML Support
|
|
|
|
```csharp
|
|
builder.Services.AddControllers()
|
|
.AddXmlSerializerFormatters();
|
|
```
|
|
|
|
## HTTPS Configuration
|
|
|
|
### Require HTTPS
|
|
|
|
```csharp
|
|
if (!app.Environment.IsDevelopment())
|
|
{
|
|
app.UseHttpsRedirection();
|
|
}
|
|
```
|
|
|
|
### HSTS
|
|
|
|
```csharp
|
|
if (!app.Environment.IsDevelopment())
|
|
{
|
|
app.UseHsts();
|
|
}
|
|
```
|
|
|
|
## Logging
|
|
|
|
### Request Logging
|
|
|
|
```csharp
|
|
app.UseHttpLogging();
|
|
```
|
|
|
|
### Custom Request Logging
|
|
|
|
```csharp
|
|
app.Use(async (context, next) =>
|
|
{
|
|
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
|
|
|
|
logger.LogInformation(
|
|
"HTTP {Method} {Path} from {RemoteIp}",
|
|
context.Request.Method,
|
|
context.Request.Path,
|
|
context.Connection.RemoteIpAddress);
|
|
|
|
await next();
|
|
});
|
|
```
|
|
|
|
## Health Checks
|
|
|
|
### Basic Health Checks
|
|
|
|
```csharp
|
|
builder.Services.AddHealthChecks();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.MapHealthChecks("/health");
|
|
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
```
|
|
|
|
### Detailed Health Checks
|
|
|
|
```csharp
|
|
builder.Services.AddHealthChecks()
|
|
.AddDbContextCheck<ApplicationDbContext>();
|
|
|
|
app.MapHealthChecks("/health", new HealthCheckOptions
|
|
{
|
|
ResponseWriter = async (context, report) =>
|
|
{
|
|
context.Response.ContentType = "application/json";
|
|
var response = new
|
|
{
|
|
status = report.Status.ToString(),
|
|
checks = report.Entries.Select(e => new
|
|
{
|
|
name = e.Key,
|
|
status = e.Value.Status.ToString(),
|
|
description = e.Value.Description
|
|
})
|
|
};
|
|
await context.Response.WriteAsJsonAsync(response);
|
|
}
|
|
});
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Problem Details
|
|
|
|
```csharp
|
|
builder.Services.AddProblemDetails();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseExceptionHandler();
|
|
app.UseStatusCodePages();
|
|
|
|
app.MapSvrntyCommands();
|
|
app.MapSvrntyQueries();
|
|
```
|
|
|
|
### Custom Error Handling
|
|
|
|
```csharp
|
|
app.UseExceptionHandler(errorApp =>
|
|
{
|
|
errorApp.Run(async context =>
|
|
{
|
|
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
|
context.Response.ContentType = "application/json";
|
|
|
|
var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
|
|
var exception = exceptionHandlerFeature?.Error;
|
|
|
|
var problem = new
|
|
{
|
|
type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
|
|
title = "An error occurred",
|
|
status = 500,
|
|
detail = app.Environment.IsDevelopment() ? exception?.Message : "An internal error occurred"
|
|
};
|
|
|
|
await context.Response.WriteAsJsonAsync(problem);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Environment-Specific Configuration
|
|
|
|
### Development
|
|
|
|
```csharp
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseDeveloperExceptionPage();
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
|
|
// Allow any CORS
|
|
app.UseCors(policy =>
|
|
{
|
|
policy.AllowAnyOrigin()
|
|
.AllowAnyMethod()
|
|
.AllowAnyHeader();
|
|
});
|
|
}
|
|
```
|
|
|
|
### Production
|
|
|
|
```csharp
|
|
if (app.Environment.IsProduction())
|
|
{
|
|
app.UseExceptionHandler("/error");
|
|
app.UseHsts();
|
|
app.UseHttpsRedirection();
|
|
|
|
// Strict CORS
|
|
app.UseCors("ProductionCorsPolicy");
|
|
}
|
|
```
|
|
|
|
## Complete Example
|
|
|
|
```csharp
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// CQRS services
|
|
builder.Services.AddSvrntyCQRS();
|
|
builder.Services.AddDefaultCommandDiscovery();
|
|
builder.Services.AddDefaultQueryDiscovery();
|
|
|
|
// Authentication & Authorization
|
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
.AddJwtBearer(options =>
|
|
{
|
|
options.Authority = builder.Configuration["Auth:Authority"];
|
|
options.Audience = builder.Configuration["Auth:Audience"];
|
|
});
|
|
|
|
builder.Services.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
|
|
});
|
|
|
|
// CORS
|
|
builder.Services.AddCors(options =>
|
|
{
|
|
options.AddPolicy("AllowFrontend", policy =>
|
|
{
|
|
policy.WithOrigins(builder.Configuration["Frontend:Url"])
|
|
.AllowAnyMethod()
|
|
.AllowAnyHeader()
|
|
.AllowCredentials();
|
|
});
|
|
});
|
|
|
|
// Rate Limiting
|
|
builder.Services.AddRateLimiter(options =>
|
|
{
|
|
options.AddFixedWindowLimiter("api", limiterOptions =>
|
|
{
|
|
limiterOptions.PermitLimit = 100;
|
|
limiterOptions.Window = TimeSpan.FromMinutes(1);
|
|
});
|
|
});
|
|
|
|
// Health Checks
|
|
builder.Services.AddHealthChecks()
|
|
.AddDbContextCheck<ApplicationDbContext>();
|
|
|
|
// Swagger
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Middleware Pipeline
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
}
|
|
else
|
|
{
|
|
app.UseHsts();
|
|
app.UseHttpsRedirection();
|
|
}
|
|
|
|
app.UseCors("AllowFrontend");
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
app.UseRateLimiter();
|
|
|
|
// Health Checks
|
|
app.MapHealthChecks("/health");
|
|
|
|
// CQRS Endpoints
|
|
app.MapSvrntyCommands("v1/commands").RequireRateLimiting("api");
|
|
app.MapSvrntyQueries("v1/queries").RequireRateLimiting("api");
|
|
|
|
app.Run();
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### ✅ DO
|
|
|
|
- Configure authentication and authorization
|
|
- Use HTTPS in production
|
|
- Implement rate limiting
|
|
- Enable CORS appropriately
|
|
- Configure request size limits
|
|
- Use health checks
|
|
- Log requests in production
|
|
- Enable compression
|
|
- Use environment-specific settings
|
|
|
|
### ❌ DON'T
|
|
|
|
- Don't allow any CORS in production
|
|
- Don't skip authentication
|
|
- Don't expose detailed errors in production
|
|
- Don't use unlimited request sizes
|
|
- Don't skip rate limiting
|
|
- Don't ignore health checks
|
|
|
|
## See Also
|
|
|
|
- [HTTP Integration Overview](README.md)
|
|
- [Endpoint Mapping](endpoint-mapping.md)
|
|
- [Naming Conventions](naming-conventions.md)
|
|
- [Swagger Integration](swagger-integration.md)
|
|
- [HTTP Troubleshooting](http-troubleshooting.md)
|