10 KiB
10 KiB
HTTP Integration Overview
Expose commands and queries via HTTP using ASP.NET Core Minimal API.
What is HTTP Integration?
The Svrnty.CQRS.MinimalApi package automatically generates HTTP endpoints for all registered commands and queries using ASP.NET Core Minimal API.
Key Features:
- ✅ Automatic endpoint generation - No manual controller code
- ✅ Convention-based routing - Predictable URL patterns
- ✅ Swagger/OpenAPI support - Automatic API documentation
- ✅ Flexible methods - POST for commands, GET/POST for queries
- ✅ Built-in validation - RFC 7807 Problem Details
- ✅ Authorization support - Integrated authorization services
Quick Start
Installation
dotnet add package Svrnty.CQRS.MinimalApi
Basic Setup
var builder = WebApplication.CreateBuilder(args);
// Register CQRS services
builder.Services.AddSvrntyCQRS();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();
// Register commands and queries
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
builder.Services.AddQuery<GetUserQuery, UserDto, GetUserQueryHandler>();
var app = builder.Build();
// Map CQRS endpoints
app.MapSvrntyCommands(); // POST /api/command/{name}
app.MapSvrntyQueries(); // GET/POST /api/query/{name}
app.Run();
This creates endpoints automatically:
POST /api/command/createUserGET /api/query/getUser?userId=123POST /api/query/getUser
How It Works
┌────────────────────┐
│ HTTP Request │
│ POST /api/command │
│ /createUser │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ Model Binding │
│ JSON → Command │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ Validation │
│ IValidator<T> │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ Authorization │
│ ICommandAuth... │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ Handler │
│ ICommandHandler │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ HTTP Response │
│ 200 OK / 400 Bad │
└────────────────────┘
Commands via HTTP
Command Without Result
public record DeleteUserCommand
{
public int UserId { get; init; }
}
public class DeleteUserCommandHandler : ICommandHandler<DeleteUserCommand>
{
public async Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken)
{
// Delete user logic
}
}
HTTP Request:
curl -X POST http://localhost:5000/api/command/deleteUser \
-H "Content-Type: application/json" \
-d '{"userId": 123}'
Response:
HTTP/1.1 204 No Content
Command With Result
public record CreateUserCommand
{
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
}
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
public async Task<int> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
{
// Create user and return ID
return newUserId;
}
}
HTTP Request:
curl -X POST http://localhost:5000/api/command/createUser \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "john@example.com"}'
Response:
42
Queries via HTTP
Queries support both GET and POST methods.
GET with Query String
GET /api/query/getUser?userId=123
Advantages:
- Cacheable
- Bookmarkable
- Simple for basic queries
Limitations:
- URL length limits
- No complex objects
- Visible in logs/browser history
POST with JSON Body
POST /api/query/getUser
Content-Type: application/json
{"userId": 123}
Advantages:
- Complex objects
- No URL length limits
- Sensitive data in body
Use Cases:
- Search with multiple filters
- Pagination parameters
- Complex query objects
Example Query
public record GetUserQuery
{
public int UserId { get; init; }
}
public record UserDto
{
public int Id { get; init; }
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
}
public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto>
{
public async Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
{
// Fetch and return user
}
}
GET Request:
curl http://localhost:5000/api/query/getUser?userId=123
POST Request:
curl -X POST http://localhost:5000/api/query/getUser \
-H "Content-Type: application/json" \
-d '{"userId": 123}'
Response:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
Endpoint Routing
Default Routes
Commands: POST /api/command/{commandName}
Queries: GET /api/query/{queryName}
POST /api/query/{queryName}
Custom Route Prefix
app.MapSvrntyCommands("my-commands"); // POST /my-commands/{name}
app.MapSvrntyQueries("my-queries"); // GET/POST /my-queries/{name}
Custom Command Names
[CommandName("users/create")]
public record CreateUserCommand { }
// Endpoint: POST /api/command/users/create
HTTP Status Codes
Success Responses
| Status | Scenario |
|---|---|
| 200 OK | Query success, Command with result |
| 201 Created | Command created a resource |
| 204 No Content | Command without result |
Error Responses
| Status | Scenario |
|---|---|
| 400 Bad Request | Validation failure (RFC 7807) |
| 401 Unauthorized | Missing/invalid authentication |
| 403 Forbidden | Authorization failure |
| 404 Not Found | Entity not found |
| 409 Conflict | Duplicate/constraint violation |
| 500 Internal Server Error | Unhandled exception |
Validation Errors
Validation failures return RFC 7807 Problem Details:
Request:
POST /api/command/createUser
{"name": "", "email": "invalid"}
Response:
{
"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"],
"Email": ["Valid email address is required"]
}
}
Authorization
Command Authorization
public class DeleteUserCommandAuthorization : ICommandAuthorizationService<DeleteUserCommand>
{
public Task<bool> CanExecuteAsync(
DeleteUserCommand command,
ClaimsPrincipal user,
CancellationToken cancellationToken)
{
// Only admins can delete users
return Task.FromResult(user.IsInRole("Admin"));
}
}
// Registration
builder.Services.AddScoped<ICommandAuthorizationService<DeleteUserCommand>, DeleteUserCommandAuthorization>();
Unauthorized Response:
HTTP/1.1 403 Forbidden
Documentation
Endpoint Mapping
How endpoints are generated:
- Discovery process
- Endpoint generation
- Route patterns
- HTTP methods
Naming Conventions
URL naming and customization:
- Default naming rules
- Custom endpoint names
- RESTful patterns
- Versioning strategies
HTTP Configuration
Configuration and customization:
- Route prefixes
- HTTP method selection
- CORS configuration
- Authentication/authorization
Swagger Integration
OpenAPI/Swagger setup:
- Swagger UI
- API documentation
- Response types
- Example requests
HTTP Troubleshooting
Common issues and solutions:
- 404 Not Found
- 400 Bad Request
- CORS errors
- Serialization issues
Complete Example
using Svrnty.CQRS;
using Svrnty.CQRS.MinimalApi;
using FluentValidation;
var builder = WebApplication.CreateBuilder(args);
// CQRS services
builder.Services.AddSvrntyCQRS();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();
// Commands
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
builder.Services.AddTransient<IValidator<CreateUserCommand>, CreateUserCommandValidator>();
// Queries
builder.Services.AddQuery<GetUserQuery, UserDto, GetUserQueryHandler>();
builder.Services.AddQuery<ListUsersQuery, List<UserDto>, ListUsersQueryHandler>();
// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Swagger UI
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// CQRS endpoints
app.MapSvrntyCommands();
app.MapSvrntyQueries();
app.Run();
Best Practices
✅ DO
- Use POST for commands (idempotent operations)
- Support both GET and POST for queries
- Return appropriate HTTP status codes
- Use RFC 7807 for validation errors
- Document endpoints with Swagger
- Use authorization services for security
- Handle 404 Not Found gracefully
❌ DON'T
- Don't use GET for commands (non-idempotent)
- Don't expose sensitive data in URLs (use POST)
- Don't skip validation
- Don't return 500 for business logic errors
- Don't bypass authorization
- Don't ignore content negotiation
What's Next?
- Endpoint Mapping - How endpoints are generated
- Naming Conventions - URL naming and customization
- HTTP Configuration - Configuration options
- Swagger Integration - API documentation
- HTTP Troubleshooting - Common issues