Minimal API utilities with MediatR integration for .NET 9. Provides a class-based approach to endpoint definition with built-in CQRS support and SwaggerUI.
$ dotnet add package Moclawr.MinimalAPIMoclawr.MinimalAPI provides a class-based approach to ASP.NET Core Minimal APIs with automatic endpoint discovery, MediatR integration, and enhanced OpenAPI documentation. It bridges the gap between minimal APIs and traditional controller-based APIs by offering a structured, object-oriented approach while maintaining the performance benefits of minimal APIs.
SingleEndpointBase, CollectionEndpointBase)ICommand<T> and IQueryRequest<T> interfacesResponse<T>, ResponseCollection<T>, and custom response wrappersIFormFile and multi-file uploadsFromRoute, FromQuery, FromBody, FromForm, FromHeader attributesInstall the package via NuGet Package Manager:
dotnet add package Moclawr.MinimalAPI
In your Program.cs:
using MinimalAPI;
var builder = WebApplication.CreateBuilder(args);
// Configure enhanced versioning options
var versioningOptions = new DefaultVersioningOptions
{
Prefix = "v",
DefaultVersion = 1,
SupportedVersions = [1, 2],
IncludeVersionInRoute = true,
BaseRouteTemplate = "/api",
ReadingStrategy = VersionReadingStrategy.UrlSegment | VersionReadingStrategy.QueryString,
AssumeDefaultVersionWhenUnspecified = true
};
// Add MinimalAPI with comprehensive documentation
builder.Services.AddMinimalApiWithSwaggerUI(
title: "My API",
version: "v1",
description: "API built with MinimalAPI framework",
contactName: "Development Team",
contactEmail: "dev@company.com",
versioningOptions: versioningOptions,
assemblies: [typeof(Program).Assembly]
);
var app = builder.Build();
// Auto-discover and map all endpoints with versioning
app.MapMinimalEndpoints(versioningOptions, typeof(Program).Assembly);
// Enable comprehensive documentation
if (app.Environment.IsDevelopment())
{
app.UseMinimalApiDocs(
swaggerRoutePrefix: "docs",
enableTryItOut: true,
enableDeepLinking: true,
enableFilter: true
);
}
app.Run();
using MinimalAPI.Endpoints;
using MinimalAPI.Attributes;
using MediatR;
namespace MyApp.Endpoints.Users.Commands;
[OpenApiSummary("Create user account",
Description = "Creates a new user with validation and notification",
Tags = ["User Management"])]
[OpenApiParameter("sendEmail", typeof(bool),
Description = "Send welcome email", Location = ParameterLocation.Query)]
[OpenApiResponse(201, ResponseType = typeof(Response<UserDto>),
Description = "User created successfully")]
[OpenApiResponse(400, Description = "Invalid user data")]
[ApiVersion(1)]
public class CreateUserEndpoint(IMediator mediator)
: SingleEndpointBase<CreateUserCommand, UserDto>(mediator)
{
[HttpPost("users")]
public override async Task<Response<UserDto>> HandleAsync(
CreateUserCommand request, CancellationToken ct)
{
return await _mediator.Send(request, ct);
}
}
The framework now follows a clear priority system for parameter binding:
Priority Order:
[FromRoute], [FromQuery], [FromHeader], [FromForm], [FromBody]IFormFile types automatically use form bindingusing MediatR;
using MinimalAPI.Attributes;
using MinimalAPI.Handlers.Command;
// Command example - follows POST/PUT/PATCH rules
public record CreateUserCommand : ICommand<UserDto>
{
// These will be in request body (JSON) - default for commands
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
// This will be a query parameter - explicit attribute overrides default
[FromQuery] public bool SendWelcomeEmail { get; init; } = true;
// This will be from header - explicit attribute
[FromHeader("X-Client-Version")] public string? ClientVersion { get; init; }
// This will be from route parameter - explicit attribute
[FromRoute("tenantId")] public string? TenantId { get; init; }
}
// File upload example - auto-detects form data
public record UploadFileCommand : ICommand<FileDto>
{
// These will be form fields - explicit FromForm attributes
[FromForm] public IFormFile File { get; init; } = default!;
[FromForm] public string Description { get; init; } = string.Empty;
// This will be a query parameter - explicit attribute
[FromQuery] public string? Folder { get; init; }
}
// Mixed form and body example
public record UpdateProfileCommand : ICommand<UserDto>
{
// Route parameter
[FromRoute] public int UserId { get; init; }
// Form file upload
[FromForm] public IFormFile? Avatar { get; init; }
// Form fields
[FromForm] public string? Bio { get; init; }
// JSON body properties (remaining properties without explicit attributes)
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
// Query parameter
[FromQuery] public bool NotifyFollowers { get; init; } = true;
}
// Query example - follows GET rules
public record GetUsersQuery : IQueryCollectionRequest<UserDto>
{
// These will be query parameters automatically - default for queries
public string? Search { get; init; }
public int Page { get; init; } = 1;
public int Size { get; init; } = 10;
// This will be from route - explicit attribute overrides default
[FromRoute] public string TenantId { get; init; } = string.Empty;
}
Auto-Detection Rules:
IFormFile, IFormFile[], List<IFormFile> → Automatically treated as form dataThe framework automatically discovers and organizes endpoints based on your actual namespace structure:
No hardcoded feature names - the framework adapts to any organization:
// Any namespace pattern works:
namespace MyApp.Endpoints.UserManagement.Operations → "UserManagement Operations"
namespace CompanyApi.Endpoints.Finance.Reports → "Finance Reports"
namespace System.Endpoints.Authentication.Security → "Authentication Security"
namespace Api.Endpoints.S3.Commands → "S3 Commands"
namespace Project.Endpoints.Analytics.Dashboards → "Analytics Dashboards"
namespace sample.API.Endpoints.Todos.Commands → "Todos Commands"
namespace sample.API.Endpoints.Tags.Queries → "Tags Queries"
namespace sample.API.Endpoints.AutofacDemo.Commands → "AutofacDemo Commands"
Always use explicit tags when you want specific grouping:
[OpenApiSummary("Complex operation",
Tags = ["Custom Category", "Special Operations"])]
public class ComplexEndpoint : SingleEndpointBase<ComplexCommand, ComplexResponse>
{
// Explicit tags override namespace-based generation
}
Deterministic hash-based operation IDs ensure no conflicts:
Operation ID Format: {FeatureName}_{EndpointName}_{HttpMethod}_{Hash8}
Examples:
- Users_CreateUser_POST_a1b2c3d4
- S3_DeleteFile_DELETE_f9e8d7c6
- Orders_UpdateStatus_PUT_3c7b8a9d
- Products_GetDetails_GET_b5f2e1a8
The 8-character hash is generated deterministically from:
This ensures consistent, unique identifiers across deployments.
[ApiVersion(1)]
public class GetUsersV1Endpoint : CollectionEndpointBase<GetUsersQuery, UserDtoV1>
{
[HttpGet("users")]
public override async Task<ResponseCollection<UserDtoV1>> HandleAsync(...)
{
// Version 1 implementation with basic fields
}
}
[ApiVersion(2)]
public class GetUsersV2Endpoint : CollectionEndpointBase<GetUsersQueryV2, UserDtoV2>
{
[HttpGet("users")]
public override async Task<ResponseCollection<UserDtoV2>> HandleAsync(...)
{
// Version 2 with enhanced features and additional fields
}
}
The framework supports multiple versioning strategies:
var versioningOptions = new DefaultVersioningOptions
{
ReadingStrategy = VersionReadingStrategy.UrlSegment | // /api/v1/users
VersionReadingStrategy.QueryString | // /api/users?version=1
VersionReadingStrategy.Header, // X-API-Version: 1
// Route generation
BaseRouteTemplate = "/api",
IncludeVersionInRoute = true,
// Version validation
SupportedVersions = [1, 2, 3],
AssumeDefaultVersionWhenUnspecified = true
};
[OpenApiSummary("Upload user profile image",
Description = "Uploads and processes a user profile image with validation")]
[OpenApiParameter("userId", typeof(int),
Description = "User identifier", Required = true, Location = ParameterLocation.Path)]
[OpenApiParameter("generateThumbnail", typeof(bool),
Description = "Generate thumbnail", Location = ParameterLocation.Query)]
[OpenApiResponse(200, ResponseType = typeof(Response<ImageDto>),
Description = "Image uploaded successfully")]
[OpenApiResponse(400, Description = "Invalid file format")]
[OpenApiResponse(413, Description = "File too large")]
[ApiVersion(1)]
public class UploadProfileImageEndpoint : SingleEndpointBase<UploadImageCommand, ImageDto>
{
[HttpPost("users/{userId}/profile-image")]
public override async Task<Response<ImageDto>> HandleAsync(...) =>
await _mediator.Send(request, ct);
}
The framework automatically generates OpenAPI schemas for:
public record UploadDocumentsCommand : ICommand<List<DocumentDto>>
{
[FromForm] public List<IFormFile> Files { get; init; } = [];
[FromForm] public string Category { get; init; } = string.Empty;
[FromForm] public bool IsPublic { get; init; } = false;
[FromQuery] public string? FolderId { get; init; }
}
// Generates proper multipart/form-data schema in OpenAPI
public class UploadDocumentsEndpoint : SingleEndpointBase<UploadDocumentsCommand, List<DocumentDto>>
{
[HttpPost("documents/upload")]
public override async Task<Response<List<DocumentDto>>> HandleAsync(...) =>
await _mediator.Send(request, ct);
}
// Program.cs - Complete setup
builder.UseAutofacServiceProvider(containerBuilder =>
{
containerBuilder
.AddApplicationServices() // MediatR handlers
.AddInfrastructureServices(); // Repositories, services
});
builder.Services
.AddMinimalApiWithSwaggerUI(/* ... */)
.AddInfrastructureServices(configuration) // Traditional DI
.AddApplicationServices(configuration);
public class CreateUserHandler(
ICommandRepository repository,
ICacheService cache,
ILogger<CreateUserHandler> logger)
: ICommandHandler<CreateUserCommand, UserDto>
{
public async Task<Response<UserDto>> Handle(
CreateUserCommand request, CancellationToken cancellationToken)
{
var user = new User
{
Name = request.Name,
Email = request.Email,
CreatedAt = DateTime.UtcNow
};
await repository.AddAsync(user, cancellationToken);
await repository.SaveChangesAsync(cancellationToken);
// Invalidate cache
await cache.RemoveByPatternAsync("users:*");
logger.LogInformation("Created user {UserId} with email {Email}",
user.Id, user.Email);
return Response<UserDto>.Success(user.ToDto());
}
}
EndpointAbstractBase: Core abstract base with execution frameworkSingleEndpointBase<TRequest, TResponse>: For single item responsesCollectionEndpointBase<TRequest, TResponse>: For collection responsesSingleEndpointBase<TRequest>: For responses without specific dataICommand / ICommand<T>: Command pattern interfacesIQueryRequest<T> / IQueryCollectionRequest<T>: Query interfacesIEndpoint: Core endpoint interface with HttpContext and Definition@HttpGet/@HttpPost/@HttpPut/@HttpDelete: HTTP method routing@OpenApiSummary: Documentation and tagging@OpenApiParameter/@OpenApiResponse: Detailed API specification@FromRoute/@FromQuery/@FromBody/@FromForm/@FromHeader: Parameter binding@ApiVersion: Version specificationSeamless integration with the complete Moclawr framework:
Response<T> and ResponseCollection<T> modelsThis package is licensed under the MIT License.
Note: Version 2.1.2 represents a complete, production-ready MinimalAPI framework with zero hardcoded assumptions. The system dynamically adapts to any project structure while providing enterprise-grade features for API development, documentation, and deployment.