dotnet-cqrs/docs/http-integration/http-configuration.md

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();
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

See Also