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:
- Controller Support - MVC controllers and API controllers
- OpenAPI/Swagger - Automatic API documentation
- CORS - Cross-origin resource sharing
- JSON Serialization - System.Text.Json with proper options
- Model Validation - Automatic model state validation
- Exception Handling - Global exception handling middleware
- 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();
builder.AddApiServiceDefaults();
var app = builder.Build();
// Require authentication
app.ConfigureApiUsingDefaults(requireAuth: true);
// Protected endpoint
app.MapGet("/protected", () => "Secured!")
.RequireAuthorization();
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")
.WithOpenApi();
// 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")
.WithOpenApi();
Grouped Endpoints
csharp
var cashierEndpoints = app.MapGroup("/api/cashiers")
.WithTags("Cashiers")
.WithOpenApi();
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();
builder.AddApiServiceDefaults();
// Configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth:Authority"];
options.Audience = builder.Configuration["Auth:Audience"];
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy =>
policy.RequireClaim("role", "admin"));
});
var app = builder.Build();
app.ConfigureApiUsingDefaults(requireAuth: true);
// Protected endpoint
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
- Use consistent naming: Follow REST conventions for endpoints
- Version your APIs: Plan for API evolution with versioning
- Document everything: Use OpenAPI/Swagger for documentation
- Handle errors gracefully: Provide meaningful error messages
Performance
- Use async/await: All endpoints should be asynchronous
- Implement pagination: For endpoints that return collections
- Use appropriate HTTP status codes: Follow HTTP semantics
- Enable compression: Use response compression for better performance
Security
- Always validate input: Use model validation and FluentValidation
- Implement authentication: Protect sensitive endpoints
- Use HTTPS: Always use HTTPS in production
- Implement rate limiting: Protect against abuse
Monitoring
- Add health checks: Monitor API health and dependencies
- Use structured logging: Include request/response information
- Implement metrics: Track API performance and usage
- 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