460 lines
10 KiB
Markdown
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)
|