LowCodeHub.MinimalEndpoints 0.0.3

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LowCodeHub.MinimalEndpoints" Version="0.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LowCodeHub.MinimalEndpoints" Version="0.0.3" />
                    
Directory.Packages.props
<PackageReference Include="LowCodeHub.MinimalEndpoints" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add LowCodeHub.MinimalEndpoints --version 0.0.3
                    
#r "nuget: LowCodeHub.MinimalEndpoints, 0.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package LowCodeHub.MinimalEndpoints@0.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=LowCodeHub.MinimalEndpoints&version=0.0.3
                    
Install as a Cake Addin
#tool nuget:?package=LowCodeHub.MinimalEndpoints&version=0.0.3
                    
Install as a Cake Tool

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.

NuGet License: MIT

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

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.json
  • Resources/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
  • ErrorOr 2.0+ (included as a dependency)
  • StackExchange.Redis 2.12+ (included — required for Redis event bus)

License

MIT © Ahmed Abuelnour

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
0.0.6 130 5/18/2026
0.0.5 100 5/13/2026
0.0.4 104 5/12/2026
0.0.3 155 4/23/2026
0.0.2 1,705 3/27/2026
0.0.1 110 3/26/2026