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

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/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

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?

See Also