13 KiB
13 KiB
HTTP Configuration
Configuration and customization for HTTP integration.
Basic Configuration
Minimal Setup
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
app.MapSvrntyCommands("my-commands");
// POST /my-commands/{name}
Custom Query Prefix
app.MapSvrntyQueries("my-queries");
// GET/POST /my-queries/{name}
Remove Prefix
app.MapSvrntyCommands("");
// POST /{commandName}
app.MapSvrntyQueries("");
// GET/POST /{queryName}
Versioned Routes
app.MapSvrntyCommands("v1/commands");
app.MapSvrntyQueries("v1/queries");
// POST /v1/commands/{name}
// GET/POST /v1/queries/{name}
CORS Configuration
Basic CORS
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
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
if (app.Environment.IsDevelopment())
{
app.UseCors(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
}
Authentication
JWT Bearer Authentication
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
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
// 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
app.MapSvrntyCommands().RequireAuthorization();
app.MapSvrntyQueries().RequireAuthorization();
Role-Based Authorization
app.MapSvrntyCommands().RequireAuthorization(policy =>
{
policy.RequireRole("Admin");
});
Policy-Based Authorization
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:
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
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
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
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
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
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
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
builder.Services.AddControllers()
.AddXmlSerializerFormatters();
HTTPS Configuration
Require HTTPS
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
HSTS
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
Logging
Request Logging
app.UseHttpLogging();
Custom Request Logging
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
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.MapSvrntyCommands();
app.MapSvrntyQueries();
Detailed Health Checks
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
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
app.MapSvrntyCommands();
app.MapSvrntyQueries();
Custom Error Handling
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
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
// Allow any CORS
app.UseCors(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
}
Production
if (app.Environment.IsProduction())
{
app.UseExceptionHandler("/error");
app.UseHsts();
app.UseHttpsRedirection();
// Strict CORS
app.UseCors("ProductionCorsPolicy");
}
Complete Example
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