LowCodeHub.MinimalEndpoints
0.0.3
See the version list below for details.
dotnet add package LowCodeHub.MinimalEndpoints --version 0.0.3
NuGet\Install-Package LowCodeHub.MinimalEndpoints -Version 0.0.3
<PackageReference Include="LowCodeHub.MinimalEndpoints" Version="0.0.3" />
<PackageVersion Include="LowCodeHub.MinimalEndpoints" Version="0.0.3" />
<PackageReference Include="LowCodeHub.MinimalEndpoints" />
paket add LowCodeHub.MinimalEndpoints --version 0.0.3
#r "nuget: LowCodeHub.MinimalEndpoints, 0.0.3"
#:package LowCodeHub.MinimalEndpoints@0.0.3
#addin nuget:?package=LowCodeHub.MinimalEndpoints&version=0.0.3
#tool nuget:?package=LowCodeHub.MinimalEndpoints&version=0.0.3
LowCodeHub.MinimalEndpoints
A comprehensive utility library for ASP.NET Core Minimal APIs — modular endpoint registration, validation and logging filters, language/timezone awareness, JSON localization, in-memory and Redis-backed event bus, CQRS-lite operation dispatcher, typed result extensions, and a global exception handler. Everything you need to build production Minimal APIs without boilerplate.
Why This Library?
| Feature | LowCodeHub.MinimalEndpoints | Raw Minimal APIs | MediatR |
|---|---|---|---|
| Modular registration | MapModules<T>() auto-discovery |
Manual MapGet/Post |
Manual |
| Validation | Endpoint filter — sync + async | Manual | Pipeline behavior |
| Operations (CQRS) | Built-in IOperation + ErrorOr<T> |
Manual | Full MediatR |
| Event bus | In-memory + Redis with DLQ | Manual | INotification |
| Manual mapper | IMapper + handler auto-discovery |
Manual | AutoMapper |
| Localization | JSON-based IStringLocalizer |
Resource files | N/A |
| Language/timezone | Middleware — header-driven context | Manual | N/A |
| Correlation ID | Middleware — multi-header support | Manual | N/A |
| Typed results | EndpointResult<T> with error mapping |
Results.Ok() etc. |
N/A |
| Exception handling | ProblemDetails global handler | Manual | N/A |
| Observability | OpenTelemetry metrics + traces for event bus | Manual | N/A |
Installation
dotnet add package LowCodeHub.MinimalEndpoints
Quick Start
builder.Services.AddOperations<Program>();
builder.Services.AddValidators<Program>();
builder.Services.AddEventBus<Program>();
builder.Services.AddCorrelationId();
builder.Services.AddLanguageAwareness(defaultLanguage: "en");
builder.Services.AddDefault500ExceptionHandler();
app.UseCorrelationId();
app.UseLanguageAwareness();
app.MapModules<Program>();
Table of Contents
- Modular Endpoint Registration
- Endpoint Filters
- Operations (CQRS-Lite)
- Typed Results
- Domain Event Bus
- Manual Mapper
- Correlation ID
- Language Awareness
- Timezone Awareness
- JSON Localization
- Global Exception Handler
- How It Works
- Requirements
- License
Modular Endpoint Registration
Define modules implementing IModule and register them automatically:
public sealed class OrdersModule : IModule
{
public static void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/orders", GetOrders);
app.MapPost("/orders", CreateOrder).AddValidator<CreateOrderRequest>();
}
}
// Auto-discover all IModule implementations in the assembly
app.MapModules<Program>();
// Or specify assemblies explicitly
app.MapModules(typeof(Program).Assembly, typeof(SharedLib).Assembly);
For single endpoints, use IMinimalEndpoint:
public sealed class GetOrderEndpoint : IMinimalEndpoint
{
public static void AddRoute(IEndpointRouteBuilder app)
{
app.MapGet("/orders/{id}", (int id) => Results.Ok());
}
}
app.MapEndpoint<GetOrderEndpoint>();
Endpoint Filters
Validation Filter
Register validators and apply them as endpoint filters:
// Register all validators from assembly
builder.Services.AddValidators<Program>();
// Implement a validator
public sealed class CreateOrderValidator : IMinimalValidator<CreateOrderRequest>
{
public IEnumerable<ValidationFailure> Validate(CreateOrderRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
yield return new ValidationFailure("Name", "Name is required");
}
}
// Async validators are also supported
public sealed class UniqueOrderValidator : IAsyncMinimalValidator<CreateOrderRequest>
{
public async IAsyncEnumerable<ValidationFailure> ValidateAsync(
CreateOrderRequest request, CancellationToken ct)
{
if (await _repo.ExistsAsync(request.Name, ct))
yield return new ValidationFailure("Name", "Order already exists");
}
}
// Apply to endpoints
app.MapPost("/orders", CreateOrder)
.AddValidator<CreateOrderRequest>();
Logging Filter
Add request timing and logging to any endpoint:
app.MapPost("/orders", CreateOrder)
.AddLogging<CreateOrderEndpoint>();
Operations (CQRS-Lite)
The operations pattern provides a structured way to define units of work that return ErrorOr<TResult>. Each operation is a self-contained handler with single responsibility, dispatched through a single IOperation service.
Define Request and Handler
// Request defines the return type
public record CreateOrderRequest(string Name, List<Item> Items) : IOperationRequest<OrderDto>;
// One handler per operation
public class CreateOrderHandler(IOrderRepository repo) : IOperationHandler<CreateOrderRequest, OrderDto>
{
public async Task<ErrorOr<OrderDto>> HandleAsync(
CreateOrderRequest request, CancellationToken ct)
{
if (await repo.ExistsAsync(request.Name, ct))
return Error.Conflict("Order.Duplicate", "Order already exists");
var order = await repo.CreateAsync(request, ct);
return new OrderDto(order.Id, order.Name);
}
}
// Register all handlers from assembly
builder.Services.AddOperations<Program>();
Use in Endpoints
app.MapPost("/orders", async (CreateOrderRequest req, IOperation op, CancellationToken ct) =>
{
var result = await op.ExecuteAsync(req, ct);
return result.IsError
? EndpointResult.Failure(result.FirstError).ToBusinessFailure()
: EndpointResult<OrderDto>.Success(result.Value).ToCreated($"/orders/{result.Value.Id}");
});
Chain Operations
Short-circuit on first error using ThenAsync:
app.MapPost("/orders", async (CreateOrderRequest req, IOperation op, CancellationToken ct) =>
{
var result = await op.ExecuteAsync(req, ct)
.ThenAsync(order => op.ExecuteAsync(new SendConfirmationRequest(order.Id), ct));
return result.IsError
? EndpointResult.Failure(result.FirstError).ToBusinessFailure()
: EndpointResult.Success().ToOk();
});
Typed Results
EndpointResult and EndpointResult<T> provide a structured response pattern with error mapping:
// Success with data
EndpointResult<OrderDto>.Success(order).ToOk();
EndpointResult<OrderDto>.Success(order).ToCreated("/orders/123");
// Failure from ErrorOr
EndpointResult.Failure(error).ToBusinessFailure(); // auto-maps error type to HTTP status
EndpointResult.Failure(error).ToBadRequest();
EndpointResult.Failure(error).ToNotFound();
EndpointResult.Failure(error).ToUnprocessableEntity();
EndpointResult.Failure(error).ToInternalServerError();
Domain Event Bus
In-Memory Event Bus
builder.Services.AddEventBus<Program>();
- Bounded channel for backpressure
- Hosted background processor
- Handler auto-registration from scanned assemblies
// Define events and handlers
public record OrderCreated(Guid OrderId) : IDomainEvent;
public sealed class OrderCreatedHandler : IDomainEventHandler<OrderCreated>
{
public ValueTask Handle(OrderCreated domainEvent, CancellationToken ct)
{
// Handle event...
return ValueTask.CompletedTask;
}
}
// Publish
await eventBus.Publish(new OrderCreated(orderId), ct);
Redis-Backed Event Bus
For multi-instance deployments:
builder.Services.AddRedisEventBus(options =>
{
options.ConnectionString = "localhost:6379";
options.QueueKey = "domain-events";
options.ProcessingQueueKey = "domain-events:processing";
options.PopTimeoutSeconds = 5;
options.MaxRetryAttempts = 3;
}, typeof(Program).Assembly);
Redis mode includes:
- Reliable processing queue (
BRPOPLPUSH) - Ack on success
- Nack + requeue on failure
Dead Letter Queue
Map admin endpoints for inspecting and reprocessing failed events:
app.MapDeadLetterEndpoints("/admin/dlq");
Provides GET (list), DELETE (remove), and POST (reprocess) endpoints.
Event Bus Health Check
The Redis-backed event bus registers a health check automatically:
| Check | Tags |
|---|---|
eventbus-redis |
eventbus, redis, readiness |
app.MapHealthChecks("/health/ready", new() { Predicate = hc => hc.Tags.Contains("readiness") });
The health check verifies that the Redis connection is available. The in-memory event bus does not register a health check.
Event Bus Observability
Built-in OpenTelemetry instrumentation under the LowCodeHub.MinimalEndpoints.EventBus source:
Metrics
| Metric | Type | Description |
|---|---|---|
eventbus.events.published |
Counter | Domain events published to the bus |
eventbus.events.publish.failed |
Counter | Events that failed to publish |
eventbus.events.processed |
Counter | Events successfully processed by all handlers |
eventbus.events.failed |
Counter | Events where at least one handler failed |
eventbus.events.retried |
Counter | Events requeued for retry |
eventbus.events.deadlettered |
Counter | Events sent to the DLQ after exceeding max retries |
eventbus.events.processing.duration |
Histogram (ms) | Handler processing duration |
Traces
| Activity | Kind | Description |
|---|---|---|
eventbus.publish |
Producer | Publishing a domain event |
eventbus.process |
Consumer | Processing a domain event |
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource("LowCodeHub.MinimalEndpoints.EventBus"))
.WithMetrics(m => m.AddMeter("LowCodeHub.MinimalEndpoints.EventBus"));
Manual Mapper
A lightweight DI-based mapping system for converting between types without reflection-based mappers. All handlers are auto-discovered from assemblies at startup.
IMapper and Handlers
Define mapping logic by implementing IMapHandler<TSource, TDestination> (sync) or IMapAsyncHandler<TSource, TDestination> (async):
// Sync handler
public sealed class OrderToOrderDtoHandler : IMapHandler<Order, OrderDto>
{
public OrderDto Handler(Order source)
=> new OrderDto(source.Id, source.Name, source.Total);
}
// Async handler (e.g., needs DB lookup)
public sealed class UserToUserDtoHandler : IMapAsyncHandler<User, UserDto>
{
private readonly IPermissionRepository _repo;
public UserToUserDtoHandler(IPermissionRepository repo) => _repo = repo;
public async Task<UserDto> Handler(User source, CancellationToken ct)
{
var permissions = await _repo.GetPermissionsAsync(source.Id, ct);
return new UserDto(source.Id, source.Name, permissions);
}
}
Register all handlers from your assembly:
builder.Services.AddManualMapper<Program>();
Inject IMapper and call Map or MapAsync:
public class OrderService(IMapper mapper)
{
public OrderDto ToDto(Order order)
=> mapper.Map<Order, OrderDto>(order);
public async Task<UserDto> ToDtoAsync(User user, CancellationToken ct)
=> await mapper.MapAsync<User, UserDto>(user, ct);
}
IDualMapper
For bidirectional mapping between two types, implement IDualMapper<TFrom, TTo>:
public sealed class OrderDualMapper : IDualMapper<Order, OrderDto>
{
public OrderDto MapTo(Order from)
=> new OrderDto(from.Id, from.Name, from.Total);
public Order MapFrom(OrderDto to)
=> new Order { Id = to.Id, Name = to.Name, Total = to.Total };
}
Register all dual mappers from your assembly:
builder.Services.AddDualMappers<Program>();
Inject the specific IDualMapper<TFrom, TTo> interface:
public class OrderController(IDualMapper<Order, OrderDto> mapper)
{
public OrderDto ToDto(Order order) => mapper.MapTo(order);
public Order FromDto(OrderDto dto) => mapper.MapFrom(dto);
}
Correlation ID
Track requests across services with correlation IDs:
builder.Services.AddCorrelationId(options =>
{
options.HeaderNames = ["X-Correlation-ID", "X-Request-ID"];
options.ResponseHeaderName = "X-Correlation-ID";
});
app.UseCorrelationId();
If no header is present, a new correlation ID is generated. Access it anywhere via CorrelationContext.CorrelationId.
Language Awareness
Reads language from request headers and sets the request culture:
builder.Services.AddLanguageAwareness(defaultLanguage: "en");
app.UseLanguageAwareness();
Supported headers (checked in order): Language, Accept-Language, X-Culture.
Access the current language via LanguageContext.Language.
The library also registers LanguageRequestCultureProvider — an ASP.NET Core IRequestCultureProvider that resolves the request culture from the same language headers (Language, Accept-Language, X-Culture). It integrates automatically with the .NET localization pipeline when UseJsonLocalization or UseRequestLocalization is used.
Timezone Awareness
Reads timezone from configured headers and stores it in request context:
builder.Services.AddTimeZoneAwareness("TimeZone", "X-TimeZone");
app.UseTimeZoneAwareness();
Access the current timezone via TimeZoneContext.TimeZone.
Use the built-in JSON converters for timezone-aware serialization:
builder.Services.ConfigureHttpJsonOptions(options =>
{
// Converts DateTimeOffset to the caller's timezone on serialization
options.SerializerOptions.Converters.Add(new TimeZoneAwareDateTimeOffsetConverter());
// Same behavior for DateTimeOffset? (nullable)
options.SerializerOptions.Converters.Add(new TimeZoneAwareNullableDateTimeOffsetConverter());
});
Both converters read the timezone from TimeZoneContext.TimeZone (populated by UseTimeZoneAwareness()) and apply it when serializing DateTimeOffset values. If no timezone is set, values are returned as-is.
JSON Localization
JSON-based IStringLocalizer implementation:
builder.Services.AddJsonLocalization(options =>
{
options.ResourcesPath = "Resources";
});
app.UseJsonLocalization("en", "ar");
Resource files:
Resources/en.jsonResources/ar.json
Supports nested keys with dot notation:
{
"Errors": {
"USER_NOT_FOUND": "User not found"
}
}
Access: localizer["Errors.USER_NOT_FOUND"]
Global Exception Handler
Register a ProblemDetails-based global exception handler:
builder.Services.AddDefault500ExceptionHandler();
Unhandled exceptions are caught and returned as RFC 7807 ProblemDetails with a 500 status code — no stack trace leakage in production.
How It Works
┌─────────────────────────────────────────────────────────┐
│ ASP.NET Core Minimal API Pipeline │
│ │
│ ┌─── Middleware ───────────────────────────────────┐ │
│ │ CorrelationId → Language → TimeZone │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─── Endpoint Filters ────────────────────────────┐ │
│ │ Validation Filter → Logging Filter │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─── Endpoint Handler ───────────────────────────┐ │
│ │ IOperation.ExecuteAsync(request) │ │
│ │ ↓ │ │
│ │ IOperationHandler<TReq, TResult>.HandleAsync() │ │
│ │ ↓ │ │
│ │ ErrorOr<TResult> │ │
│ │ ↓ │ │
│ │ EndpointResult<T>.ToOk() / .ToBusinessFailure() │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─── Background ─────────────────────────────────┐ │
│ │ EventBus (In-Memory / Redis) │ │
│ │ → IDomainEventHandler<TEvent> │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Requirements
- .NET 10 or later
ErrorOr2.0+ (included as a dependency)StackExchange.Redis2.12+ (included — required for Redis event bus)
License
MIT © Ahmed Abuelnour
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- ErrorOr (>= 2.0.1)
- StackExchange.Redis (>= 2.12.14)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on LowCodeHub.MinimalEndpoints:
| Package | Downloads |
|---|---|
|
LowCodeHub.MinimalEndpoints.Mcp
Model Context Protocol integration for LowCodeHub.MinimalEndpoints, exposing opt-in API contracts as MCP tools over ASP.NET Core Streamable HTTP. |
GitHub repositories
This package is not used by any popular GitHub repositories.