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

460 lines
10 KiB
Markdown

# 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
```bash
dotnet add package Svrnty.CQRS.MinimalApi
```
### Basic Setup
```csharp
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/createUser`
- `GET /api/query/getUser?userId=123`
- `POST /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
```csharp
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:**
```bash
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
```csharp
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:**
```bash
curl -X POST http://localhost:5000/api/command/createUser \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "john@example.com"}'
```
**Response:**
```json
42
```
## Queries via HTTP
Queries support **both GET and POST** methods.
### GET with Query String
```bash
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
```bash
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
```csharp
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:**
```bash
curl http://localhost:5000/api/query/getUser?userId=123
```
**POST Request:**
```bash
curl -X POST http://localhost:5000/api/query/getUser \
-H "Content-Type: application/json" \
-d '{"userId": 123}'
```
**Response:**
```json
{
"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
```csharp
app.MapSvrntyCommands("my-commands"); // POST /my-commands/{name}
app.MapSvrntyQueries("my-queries"); // GET/POST /my-queries/{name}
```
### Custom Command Names
```csharp
[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:**
```bash
POST /api/command/createUser
{"name": "", "email": "invalid"}
```
**Response:**
```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"],
"Email": ["Valid email address is required"]
}
}
```
## Authorization
### Command Authorization
```csharp
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](endpoint-mapping.md)
How endpoints are generated:
- Discovery process
- Endpoint generation
- Route patterns
- HTTP methods
### [Naming Conventions](naming-conventions.md)
URL naming and customization:
- Default naming rules
- Custom endpoint names
- RESTful patterns
- Versioning strategies
### [HTTP Configuration](http-configuration.md)
Configuration and customization:
- Route prefixes
- HTTP method selection
- CORS configuration
- Authentication/authorization
### [Swagger Integration](swagger-integration.md)
OpenAPI/Swagger setup:
- Swagger UI
- API documentation
- Response types
- Example requests
### [HTTP Troubleshooting](http-troubleshooting.md)
Common issues and solutions:
- 404 Not Found
- 400 Bad Request
- CORS errors
- Serialization issues
## Complete Example
```csharp
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](endpoint-mapping.md)** - How endpoints are generated
- **[Naming Conventions](naming-conventions.md)** - URL naming and customization
- **[HTTP Configuration](http-configuration.md)** - Configuration options
- **[Swagger Integration](swagger-integration.md)** - API documentation
- **[HTTP Troubleshooting](http-troubleshooting.md)** - Common issues
## See Also
- [Commands Overview](../core-features/commands/README.md)
- [Queries Overview](../core-features/queries/README.md)
- [Validation Overview](../core-features/validation/README.md)
- [gRPC Integration](../grpc-integration/README.md)
- [Getting Started: Choosing HTTP or gRPC](../getting-started/06-choosing-http-or-grpc.md)