Skip to content

API Setup in Momentum ​

Momentum provides pre-configured patterns for building REST APIs and gRPC services. The API setup includes automatic endpoint discovery, validation, error handling, and OpenAPI documentation.

Overview ​

API services in Momentum typically use the slim builder pattern for optimal performance:

csharp
using Momentum.ServiceDefaults;
using Momentum.ServiceDefaults.Api;

var builder = WebApplication.CreateSlimBuilder(args);

// Core service defaults
builder.AddServiceDefaults();

// API-specific configuration
builder.AddApiServiceDefaults();

var app = builder.Build();

// Configure API middleware and routing
app.ConfigureApiUsingDefaults();

await app.RunAsync(args);

API Service Defaults ​

The AddApiServiceDefaults() method configures:

  1. Controller Support - MVC controllers and API controllers
  2. OpenAPI/Swagger - Automatic API documentation
  3. CORS - Cross-origin resource sharing
  4. JSON Serialization - System.Text.Json with proper options
  5. Model Validation - Automatic model state validation
  6. Exception Handling - Global exception handling middleware
  7. Authentication/Authorization - JWT bearer token support

Basic API Configuration ​

csharp
// Minimal API setup
var builder = WebApplication.CreateSlimBuilder(args);

builder.AddServiceDefaults();
builder.AddApiServiceDefaults();

var app = builder.Build();

app.ConfigureApiUsingDefaults();

// Add your endpoints
app.MapGet("/", () => "Hello World");

await app.RunAsync(args);

API with Authentication ​

csharp
var builder = WebApplication.CreateSlimBuilder(args);

builder.AddServiceDefaults();

// Require authentication for all endpoints by default
// Individual endpoints can opt out with .AllowAnonymous()
builder.AddApiServiceDefaults(requireAuth: true);

var app = builder.Build();

app.ConfigureApiUsingDefaults();

// All endpoints require auth by default
app.MapGet("/protected", () => "Secured!");

// Opt out of auth for specific endpoints
app.MapGet("/public", () => "Open to all!")
   .AllowAnonymous();

await app.RunAsync(args);

Controller-Based APIs ​

Momentum works seamlessly with ASP.NET Core controllers:

Basic Controller ​

csharp
[ApiController]
[Route("api/[controller]")]
public class CashiersController : ControllerBase
{
    private readonly IMessageBus _messageBus;

    public CashiersController(IMessageBus messageBus)
    {
        _messageBus = messageBus;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Cashier>> Get(
        [FromRoute] Guid id, 
        [FromQuery] Guid tenantId,
        CancellationToken cancellationToken)
    {
        var query = new GetCashierQuery(tenantId, id);
        var result = await _messageBus.InvokeAsync(query, cancellationToken);

        if (result.IsSuccess)
        {
            return Ok(result.Value);
        }

        return BadRequest(result.Errors);
    }

    [HttpPost]
    public async Task<ActionResult<Cashier>> Create(
        [FromBody] CreateCashierRequest request,
        CancellationToken cancellationToken)
    {
        var command = new CreateCashierCommand(
            request.TenantId, 
            request.Name, 
            request.Email);

        var (result, integrationEvent) = await _messageBus.InvokeAsync(command, cancellationToken);

        if (result.IsSuccess)
        {
            return CreatedAtAction(
                nameof(Get), 
                new { id = result.Value.Id, tenantId = result.Value.TenantId }, 
                result.Value);
        }

        return BadRequest(result.Errors);
    }
}

Request/Response Models ​

Define clear request and response models:

csharp
// Request models
public record CreateCashierRequest(Guid TenantId, string Name, string Email);

public record UpdateCashierRequest(string Name, string Email);

// Response models can use domain models directly
public record CashierResponse(
    Guid Id,
    Guid TenantId,
    string Name,
    string Email,
    DateTime CreatedDate,
    DateTime UpdatedDate);

Advanced Controller Features ​

csharp
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class CashiersController : ControllerBase
{
    [HttpGet]
    [ProducesResponseType(typeof(PagedResult<Cashier>), 200)]
    [ProducesResponseType(400)]
    public async Task<ActionResult<PagedResult<Cashier>>> GetAll(
        [FromQuery] GetCashiersRequest request,
        CancellationToken cancellationToken)
    {
        var query = new GetCashiersQuery(
            request.TenantId,
            request.Page ?? 1,
            request.PageSize ?? 10);

        var result = await _messageBus.InvokeAsync(query, cancellationToken);

        return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Errors);
    }

    [HttpPut("{id}")]
    [ProducesResponseType(typeof(Cashier), 200)]
    [ProducesResponseType(404)]
    [ProducesResponseType(400)]
    public async Task<ActionResult<Cashier>> Update(
        [FromRoute] Guid id,
        [FromQuery] Guid tenantId,
        [FromBody] UpdateCashierRequest request,
        CancellationToken cancellationToken)
    {
        var command = new UpdateCashierCommand(tenantId, id, request.Name, request.Email);
        var (result, integrationEvent) = await _messageBus.InvokeAsync(command, cancellationToken);

        if (result.IsSuccess)
        {
            return Ok(result.Value);
        }

        // Check if it's a not found error
        var notFoundError = result.Errors?.FirstOrDefault(e => e.PropertyName == "Id");
        if (notFoundError != null)
        {
            return NotFound(notFoundError.ErrorMessage);
        }

        return BadRequest(result.Errors);
    }

    [HttpDelete("{id}")]
    [ProducesResponseType(204)]
    [ProducesResponseType(404)]
    public async Task<ActionResult> Delete(
        [FromRoute] Guid id,
        [FromQuery] Guid tenantId,
        CancellationToken cancellationToken)
    {
        var command = new DeleteCashierCommand(tenantId, id);
        var (result, integrationEvent) = await _messageBus.InvokeAsync(command, cancellationToken);

        if (result.IsSuccess)
        {
            return NoContent();
        }

        var notFoundError = result.Errors?.FirstOrDefault(e => e.PropertyName == "Id");
        if (notFoundError != null)
        {
            return NotFound();
        }

        return BadRequest(result.Errors);
    }
}

Minimal APIs ​

For simpler scenarios, use Minimal APIs:

Basic Endpoints ​

csharp
var app = builder.Build();

app.ConfigureApiUsingDefaults();

// GET endpoint
app.MapGet("/api/cashiers/{id}", async (
    Guid id,
    Guid tenantId,
    IMessageBus messageBus,
    CancellationToken cancellationToken) =>
{
    var query = new GetCashierQuery(tenantId, id);
    var result = await messageBus.InvokeAsync(query, cancellationToken);

    return result.IsSuccess
        ? Results.Ok(result.Value)
        : Results.BadRequest(result.Errors);
})
.WithName("GetCashier");

// POST endpoint
app.MapPost("/api/cashiers", async (
    CreateCashierRequest request,
    IMessageBus messageBus,
    CancellationToken cancellationToken) =>
{
    var command = new CreateCashierCommand(request.TenantId, request.Name, request.Email);
    var (result, integrationEvent) = await messageBus.InvokeAsync(command, cancellationToken);

    return result.IsSuccess
        ? Results.CreatedAtRoute("GetCashier", new { id = result.Value.Id, tenantId = result.Value.TenantId }, result.Value)
        : Results.BadRequest(result.Errors);
})
.WithName("CreateCashier");

Grouped Endpoints ​

csharp
var cashierEndpoints = app.MapGroup("/api/cashiers")
    .WithTags("Cashiers");

cashierEndpoints.MapGet("/", async (
    Guid tenantId,
    int page,
    int pageSize,
    IMessageBus messageBus,
    CancellationToken cancellationToken) =>
{
    var query = new GetCashiersQuery(tenantId, page, pageSize);
    var result = await messageBus.InvokeAsync(query, cancellationToken);

    return result.IsSuccess 
        ? Results.Ok(result.Value)
        : Results.BadRequest(result.Errors);
});

cashierEndpoints.MapGet("/{id}", async (
    Guid id,
    Guid tenantId,
    IMessageBus messageBus,
    CancellationToken cancellationToken) =>
{
    var query = new GetCashierQuery(tenantId, id);
    var result = await messageBus.InvokeAsync(query, cancellationToken);

    return result.IsSuccess 
        ? Results.Ok(result.Value)
        : Results.BadRequest(result.Errors);
});

cashierEndpoints.MapPost("/", async (
    CreateCashierRequest request,
    IMessageBus messageBus,
    CancellationToken cancellationToken) =>
{
    var command = new CreateCashierCommand(request.TenantId, request.Name, request.Email);
    var (result, integrationEvent) = await messageBus.InvokeAsync(command, cancellationToken);

    return result.IsSuccess
        ? Results.Created($"/api/cashiers/{result.Value.Id}", result.Value)
        : Results.BadRequest(result.Errors);
});

gRPC Services ​

Momentum supports gRPC services alongside REST APIs:

gRPC Service Implementation ​

csharp
public class CashierService : CashierServiceBase
{
    private readonly IMessageBus _messageBus;
    private readonly ILogger<CashierService> _logger;

    public CashierService(IMessageBus messageBus, ILogger<CashierService> logger)
    {
        _messageBus = messageBus;
        _logger = logger;
    }

    public override async Task<GetCashierResponse> GetCashier(
        GetCashierRequest request,
        ServerCallContext context)
    {
        var query = new GetCashierQuery(
            Guid.Parse(request.TenantId),
            Guid.Parse(request.Id));

        var result = await _messageBus.InvokeAsync(query, context.CancellationToken);

        if (result.IsSuccess)
        {
            return new GetCashierResponse
            {
                Id = result.Value.Id.ToString(),
                TenantId = result.Value.TenantId.ToString(),
                Name = result.Value.Name,
                Email = result.Value.Email,
                CreatedDate = Timestamp.FromDateTime(result.Value.CreatedDate.ToUniversalTime()),
                UpdatedDate = Timestamp.FromDateTime(result.Value.UpdatedDate.ToUniversalTime())
            };
        }

        var error = result.Errors?.FirstOrDefault();
        throw new RpcException(new Status(StatusCode.NotFound, error?.ErrorMessage ?? "Cashier not found"));
    }

    public override async Task<CreateCashierResponse> CreateCashier(
        CreateCashierRequest request,
        ServerCallContext context)
    {
        var command = new CreateCashierCommand(
            Guid.Parse(request.TenantId),
            request.Name,
            request.Email);

        var (result, integrationEvent) = await _messageBus.InvokeAsync(command, context.CancellationToken);

        if (result.IsSuccess)
        {
            return new CreateCashierResponse
            {
                Id = result.Value.Id.ToString(),
                TenantId = result.Value.TenantId.ToString(),
                Name = result.Value.Name,
                Email = result.Value.Email
            };
        }

        var validationErrors = string.Join("; ", result.Errors?.Select(e => $"{e.PropertyName}: {e.ErrorMessage}") ?? []);
        throw new RpcException(new Status(StatusCode.InvalidArgument, validationErrors));
    }
}

gRPC Configuration ​

csharp
var builder = WebApplication.CreateSlimBuilder(args);

builder.AddServiceDefaults();
builder.AddApiServiceDefaults();

// Add gRPC services
builder.Services.AddGrpc();

var app = builder.Build();

app.ConfigureApiUsingDefaults();

// Map gRPC service
app.MapGrpcService<CashierService>();

// Optional: Add gRPC reflection for development
if (app.Environment.IsDevelopment())
{
    app.MapGrpcReflectionService();
}

await app.RunAsync(args);

Authentication and Authorization ​

JWT Bearer Authentication ​

csharp
var builder = WebApplication.CreateSlimBuilder(args);

builder.AddServiceDefaults();

// Require authentication for all endpoints by default
builder.AddApiServiceDefaults(requireAuth: true);

// Configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Auth:Authority"];
        options.Audience = builder.Configuration["Auth:Audience"];
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("RequireAdmin", policy =>
        policy.RequireClaim("role", "admin"));

var app = builder.Build();

app.ConfigureApiUsingDefaults();

// All endpoints require auth by default (from requireAuth: true)
// This endpoint additionally requires the "RequireAdmin" policy
app.MapGet("/admin/users", () => "Admin only!")
   .RequireAuthorization("RequireAdmin");

await app.RunAsync(args);

Custom Authorization ​

csharp
public class TenantAuthorizationHandler : AuthorizationHandler<TenantRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        TenantRequirement requirement)
    {
        var tenantId = context.User.FindFirst("tenant_id")?.Value;
        
        if (tenantId == requirement.TenantId.ToString())
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

// Register handler
builder.Services.AddScoped<IAuthorizationHandler, TenantAuthorizationHandler>();

Error Handling ​

Global Exception Handling ​

Momentum includes built-in exception handling:

csharp
app.ConfigureApiUsingDefaults(); // Includes global exception handling

// Exceptions are automatically converted to appropriate HTTP responses:
// - ValidationException -> 400 Bad Request
// - UnauthorizedAccessException -> 401 Unauthorized
// - NotFoundException -> 404 Not Found
// - Other exceptions -> 500 Internal Server Error

Custom Error Handling ​

csharp
public class CustomExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<CustomExceptionHandlingMiddleware> _logger;

    public CustomExceptionHandlingMiddleware(RequestDelegate next, ILogger<CustomExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception occurred");
            await HandleExceptionAsync(context, ex);
        }
    }

    private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var response = context.Response;
        response.ContentType = "application/json";

        var result = exception switch
        {
            BusinessException => new { error = exception.Message, statusCode = 400 },
            UnauthorizedAccessException => new { error = "Unauthorized", statusCode = 401 },
            NotFoundException => new { error = "Not found", statusCode = 404 },
            _ => new { error = "Internal server error", statusCode = 500 }
        };

        response.StatusCode = result.statusCode;
        await response.WriteAsync(JsonSerializer.Serialize(result));
    }
}

// Register middleware
app.UseMiddleware<CustomExceptionHandlingMiddleware>();

OpenAPI/Swagger Configuration ​

Basic Swagger Setup ​

csharp
var builder = WebApplication.CreateSlimBuilder(args);

builder.AddServiceDefaults();
builder.AddApiServiceDefaults(); // Includes Swagger

var app = builder.Build();

app.ConfigureApiUsingDefaults();

// Swagger is automatically available at:
// - /swagger (Swagger UI)
// - /swagger/v1/swagger.json (OpenAPI spec)

await app.RunAsync(args);

Custom Swagger Configuration ​

csharp
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo 
    { 
        Title = "My API", 
        Version = "v1",
        Description = "API for managing cashiers and invoices"
    });

    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new string[] {}
        }
    });

    // Include XML comments
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
});

CORS Configuration ​

Basic CORS ​

csharp
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

var app = builder.Build();

app.UseCors();
app.ConfigureApiUsingDefaults();

Environment-Specific CORS ​

csharp
builder.Services.AddCors(options =>
{
    if (builder.Environment.IsDevelopment())
    {
        options.AddDefaultPolicy(policy =>
        {
            policy.AllowAnyOrigin()
                  .AllowAnyMethod()
                  .AllowAnyHeader();
        });
    }
    else
    {
        options.AddDefaultPolicy(policy =>
        {
            policy.WithOrigins("https://myapp.com", "https://api.myapp.com")
                  .WithMethods("GET", "POST", "PUT", "DELETE")
                  .WithHeaders("Content-Type", "Authorization");
        });
    }
});

Health Checks ​

API Health Endpoints ​

csharp
var app = builder.Build();

app.ConfigureApiUsingDefaults();

// Map default health check endpoints
app.MapDefaultHealthCheckEndpoints();

// Custom health check endpoint
app.MapHealthChecks("/health/detailed", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

await app.RunAsync(args);

Custom Health Checks ​

csharp
public class DatabaseHealthCheck : IHealthCheck
{
    private readonly AppDomainDb _db;

    public DatabaseHealthCheck(AppDomainDb db)
    {
        _db = db;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, 
        CancellationToken cancellationToken = default)
    {
        try
        {
            await _db.Database.CanConnectAsync(cancellationToken);
            return HealthCheckResult.Healthy("Database is accessible");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Database is not accessible", ex);
        }
    }
}

// Register health check
builder.Services.AddHealthChecks()
    .AddCheck<DatabaseHealthCheck>("database");

Testing API Endpoints ​

Integration Testing Setup ​

csharp
public class ApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;

    public ApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetCashier_ValidId_ReturnsOk()
    {
        // Arrange
        var tenantId = Guid.NewGuid();
        var cashierId = Guid.NewGuid();

        // Act
        var response = await _client.GetAsync($"/api/cashiers/{cashierId}?tenantId={tenantId}");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var cashier = await response.Content.ReadFromJsonAsync<Cashier>();
        cashier.Should().NotBeNull();
    }

    [Fact]
    public async Task CreateCashier_ValidRequest_ReturnsCreated()
    {
        // Arrange
        var request = new CreateCashierRequest(
            Guid.NewGuid(),
            "John Doe",
            "john@example.com");

        // Act
        var response = await _client.PostAsJsonAsync("/api/cashiers", request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);
        var cashier = await response.Content.ReadFromJsonAsync<Cashier>();
        cashier.Should().NotBeNull();
        cashier!.Name.Should().Be(request.Name);
    }
}

Best Practices ​

API Design ​

  1. Use consistent naming: Follow REST conventions for endpoints
  2. Version your APIs: Plan for API evolution with versioning
  3. Document everything: Use OpenAPI/Swagger for documentation
  4. Handle errors gracefully: Provide meaningful error messages

Performance ​

  1. Use async/await: All endpoints should be asynchronous
  2. Implement pagination: For endpoints that return collections
  3. Use appropriate HTTP status codes: Follow HTTP semantics
  4. Enable compression: Use response compression for better performance

Security ​

  1. Always validate input: Use model validation and FluentValidation
  2. Implement authentication: Protect sensitive endpoints
  3. Use HTTPS: Always use HTTPS in production
  4. Implement rate limiting: Protect against abuse

Monitoring ​

  1. Add health checks: Monitor API health and dependencies
  2. Use structured logging: Include request/response information
  3. Implement metrics: Track API performance and usage
  4. Set up alerts: Monitor for errors and performance issues

Next Steps ​

  • Learn about Service Defaults for comprehensive service configuration
  • Understand Observability for monitoring and telemetry
  • Explore CQRS patterns for commands and queries
  • See Testing for comprehensive testing strategies