# Endpoint Mapping How HTTP endpoints are automatically generated from commands and queries. ## Overview The `MapSvrntyCommands()` and `MapSvrntyQueries()` extension methods use metadata discovery to automatically create Minimal API endpoints for all registered commands and queries. **No manual controller code required!** ## Discovery Process ``` ┌──────────────────────────────┐ │ Application Startup │ └─────────────┬────────────────┘ │ ▼ ┌──────────────────────────────┐ │ MapSvrntyCommands() │ │ 1. Get ICommandDiscovery │ │ 2. Get all registered │ │ command metadata │ └─────────────┬────────────────┘ │ ▼ ┌──────────────────────────────┐ │ For Each Command: │ │ 1. Check [IgnoreCommand] │ │ 2. Get command name │ │ 3. Get handler type │ │ 4. Get result type │ │ 5. Create endpoint │ └─────────────┬────────────────┘ │ ▼ ┌──────────────────────────────┐ │ Generated Endpoint: │ │ POST /api/command/{name} │ │ - Model binding │ │ - Validation │ │ - Authorization │ │ - Handler invocation │ │ - Response formatting │ └──────────────────────────────┘ ``` ## Command Mapping ### MapSvrntyCommands ```csharp app.MapSvrntyCommands(); ``` **This generates:** ```csharp // For each registered command app.MapPost("/api/command/{commandName}", async ( [FromBody] TCommand command, [FromServices] ICommandHandler handler, [FromServices] IValidator? validator, [FromServices] ICommandAuthorizationService? authService, HttpContext httpContext, CancellationToken cancellationToken) => { // Validate if (validator != null) { var validationResult = await validator.ValidateAsync(command, cancellationToken); if (!validationResult.IsValid) { return Results.ValidationProblem(validationResult.ToDictionary()); } } // Authorize if (authService != null) { var canExecute = await authService.CanExecuteAsync( command, httpContext.User, cancellationToken); if (!canExecute) { return Results.Forbid(); } } // Execute var result = await handler.HandleAsync(command, cancellationToken); // Return result return result != null ? Results.Ok(result) : Results.NoContent(); }) .WithTags("Commands") .WithOpenApi(); ``` ### Custom Route Prefix ```csharp app.MapSvrntyCommands("my-commands"); // POST /my-commands/{commandName} ``` ### Ignore Specific Commands ```csharp [IgnoreCommand] public record InternalCommand { } // No endpoint generated ``` ## Query Mapping ### MapSvrntyQueries ```csharp app.MapSvrntyQueries(); ``` **This generates TWO endpoints per query:** #### GET Endpoint ```csharp app.MapGet("/api/query/{queryName}", async ( [AsParameters] TQuery query, [FromServices] IQueryHandler handler, [FromServices] IQueryAuthorizationService? authService, HttpContext httpContext, CancellationToken cancellationToken) => { // Authorize if (authService != null) { var canExecute = await authService.CanExecuteAsync( query, httpContext.User, cancellationToken); if (!canExecute) { return Results.Forbid(); } } // Execute var result = await handler.HandleAsync(query, cancellationToken); return Results.Ok(result); }) .WithTags("Queries") .WithOpenApi(); ``` #### POST Endpoint ```csharp app.MapPost("/api/query/{queryName}", async ( [FromBody] TQuery query, [FromServices] IQueryHandler handler, [FromServices] IQueryAuthorizationService? authService, HttpContext httpContext, CancellationToken cancellationToken) => { // Same as GET but query comes from body }) .WithTags("Queries") .WithOpenApi(); ``` ## Naming Resolution ### Default Naming Command/Query class names are converted to endpoints: | Class Name | Endpoint | |------------|----------| | `CreateUserCommand` | `/api/command/createUser` | | `GetUserQuery` | `/api/query/getUser` | | `SearchProductsQuery` | `/api/query/searchProducts` | | `UpdateOrderStatusCommand` | `/api/command/updateOrderStatus` | **Rules:** 1. Remove "Command" or "Query" suffix 2. Convert to lowerCamelCase 3. Preserve numbers and underscores ### Custom Names Use `[CommandName]` or `[QueryName]` attributes: ```csharp [CommandName("users/create")] public record CreateUserCommand { } // Endpoint: POST /api/command/users/create ``` ```csharp [QueryName("products/search")] public record SearchProductsQuery { } // Endpoints: // GET /api/query/products/search // POST /api/query/products/search ``` ## Model Binding ### Commands (POST only) ```csharp POST /api/command/createUser Content-Type: application/json { "name": "John Doe", "email": "john@example.com" } ``` Model binding deserializes JSON to command object. ### Queries (GET) ```csharp GET /api/query/searchProducts?category=Electronics&minPrice=100&maxPrice=500 ``` Model binding maps query string parameters to query properties. ### Queries (POST) ```csharp POST /api/query/searchProducts Content-Type: application/json { "category": "Electronics", "minPrice": 100, "maxPrice": 500 } ``` Model binding deserializes JSON to query object. ## Validation Integration ### Automatic Validation If `IValidator` is registered: ```csharp builder.Services.AddCommand(); builder.Services.AddTransient, CreateUserCommandValidator>(); ``` The endpoint automatically validates before calling the handler. **Validation failure:** ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Name": ["Name is required"] } } ``` ## Authorization Integration ### Automatic Authorization If authorization service is registered: ```csharp builder.Services.AddScoped, DeleteUserCommandAuthorization>(); ``` The endpoint checks authorization before execution. **Authorization failure:** ``` HTTP/1.1 403 Forbidden ``` ## Response Types ### Commands Without Result ```csharp public class DeleteUserCommandHandler : ICommandHandler { public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken) { // Delete user } } ``` **Response:** ``` HTTP/1.1 204 No Content ``` ### Commands With Result ```csharp public class CreateUserCommandHandler : ICommandHandler { public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { return newUserId; } } ``` **Response:** ```json HTTP/1.1 200 OK Content-Type: application/json 42 ``` ### Queries ```csharp public class GetUserQueryHandler : IQueryHandler { public async Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken) { return userDto; } } ``` **Response:** ```json HTTP/1.1 200 OK Content-Type: application/json { "id": 123, "name": "John Doe", "email": "john@example.com" } ``` ## OpenAPI Integration ### Automatic Tags All command endpoints are tagged with "Commands": ```json { "paths": { "/api/command/createUser": { "post": { "tags": ["Commands"], ... } } } } ``` All query endpoints are tagged with "Queries": ```json { "paths": { "/api/query/getUser": { "get": { "tags": ["Queries"], ... }, "post": { "tags": ["Queries"], ... } } } } ``` ### Request/Response Schemas Swagger automatically documents request and response types: ```json { "components": { "schemas": { "CreateUserCommand": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" } } }, "UserDto": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" } } } } } } ``` ## Content Negotiation ### Default: JSON ``` Accept: application/json Content-Type: application/json ``` ### XML Support (Optional) ```csharp builder.Services.AddControllers() .AddXmlSerializerFormatters(); ``` ``` Accept: application/xml Content-Type: application/xml ``` ## Error Handling ### Validation Errors (400) ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { ... } } ``` ### Authorization Failures (403) ``` HTTP/1.1 403 Forbidden ``` ### Not Found (404) ```csharp throw new KeyNotFoundException("User not found"); ``` ``` HTTP/1.1 404 Not Found ``` ### Unhandled Exceptions (500) ``` HTTP/1.1 500 Internal Server Error ``` ## Customization ### Custom Endpoint Configuration ```csharp app.MapSvrntyCommands(options => { options.RoutePrefix = "my-commands"; options.RequireAuthorization = true; options.AllowAnonymous = false; }); ``` ### Per-Endpoint Customization After mapping, you can customize individual endpoints: ```csharp var commandEndpoints = app.MapSvrntyCommands(); // Customize specific endpoint commandEndpoints .Where(e => e.DisplayName == "CreateUser") .RequireAuthorization("AdminOnly"); ``` ## Testing Endpoints ### Integration Tests ```csharp public class CreateUserCommandTests : IClassFixture> { private readonly HttpClient _client; public CreateUserCommandTests(WebApplicationFactory factory) { _client = factory.CreateClient(); } [Fact] public async Task CreateUser_WithValidData_Returns200() { var command = new { name = "John Doe", email = "john@example.com" }; var response = await _client.PostAsJsonAsync("/api/command/createUser", command); response.EnsureSuccessStatusCode(); var userId = await response.Content.ReadFromJsonAsync(); Assert.True(userId > 0); } [Fact] public async Task CreateUser_WithInvalidData_Returns400() { var command = new { name = "", email = "invalid" }; var response = await _client.PostAsJsonAsync("/api/command/createUser", command); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } } ``` ## Best Practices ### ✅ DO - Use MapSvrntyCommands() and MapSvrntyQueries() - Let the framework handle endpoint generation - Use [IgnoreCommand]/[IgnoreQuery] for internal operations - Rely on automatic validation and authorization - Use OpenAPI tags for organization - Test endpoints with integration tests ### ❌ DON'T - Don't create manual controllers for CQRS operations - Don't bypass validation or authorization - Don't expose internal commands via HTTP - Don't skip error handling - Don't ignore HTTP status codes ## See Also - [HTTP Integration Overview](README.md) - [Naming Conventions](naming-conventions.md) - [HTTP Configuration](http-configuration.md) - [Swagger Integration](swagger-integration.md) - [Metadata Discovery](../architecture/metadata-discovery.md)