Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jordiaragonzaragoza/JordiAragonZaragoza.SharedKernel/llms.txt

Use this file to discover all available pages before exploring further.

The JordiAragonZaragoza.SharedKernel.Presentation.HttpRestfulApi package provides a set of ASP.NET Core building blocks that sit between your application layer and the HTTP transport. It ships abstract base controllers wired to the ICommandBus and IQueryBus, middleware for global exception handling, user identity propagation, and multi-tenant partition context extraction, plus rich result-to-HTTP-response mapping helpers based on Ardalis.Result. A companion Contracts package supplies shared request/response types and typed HttpClient extension methods that consuming services can reference without taking a dependency on the full implementation package.

Packages

HttpRestfulApi

Controllers, middlewares, response helpers, and DI registration. Referenced by your API host project.

HttpRestfulApi.Contracts

Shared DTOs (PaginatedRequest, PaginatedCollectionResponse<T>, FileResponse) and HttpClient helpers. Safe to reference from client or integration-test projects.

Setting Up the Presentation Layer

1

Install the NuGet packages

Add the implementation package to your API host and the contracts package to any project that calls your API over HTTP.
dotnet add package JordiAragonZaragoza.SharedKernel.Presentation.HttpRestfulApi
dotnet add package JordiAragonZaragoza.SharedKernel.Presentation.HttpRestfulApi.Contracts
2

Register services

Call AddSharedKernelPresentationHttpRestfulApi from your Program.cs or service-registration extension. It returns the IServiceCollection for further chaining.
builder.Services.AddSharedKernelPresentationHttpRestfulApi();
The registration method currently acts as an extension point. Your own registrations (e.g. AddSharedKernelApplication, authentication, etc.) must be added separately and before this call if they are depended on by the middleware pipeline.
3

Add middleware in the correct order

Register the three custom middlewares after UseRouting() / UseAuthentication() but before MapControllers():
var app = builder.Build();

app.UseMiddleware<ExceptionMiddleware>();       // 1. Catch-all for unhandled exceptions
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<UserContextMiddleware>();     // 2. Populate IUserContextService
app.UseMiddleware<PartitionContextMiddleware>(); // 3. Populate IPartitionContextService

app.MapControllers();
app.Run();
4

Derive your controllers

Inherit from BaseApiCommandController or BaseApiQueryController and use the built-in CommandBus / QueryBus properties to dispatch requests.

Controllers

BaseApiCommandController

An abstract ControllerBase decorated with [ApiController] and [Authorize]. It lazily resolves ICommandBus from the current request’s IServiceProvider, so no constructor injection is needed in your concrete controller.
[ApiController]
[Authorize]
public abstract class BaseApiCommandController : ControllerBase
{
    private ICommandBus commandBus = null!;

    protected ICommandBus CommandBus
        => this.commandBus ??= this.HttpContext.RequestServices.GetRequiredService<ICommandBus>();
}
Inherit and add actions:
[Route("api/v1/orders")]
public sealed class OrdersController : BaseApiCommandController
{
    [HttpPost]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<Guid>> CreateOrder(
        [FromBody] CreateOrderRequest request,
        CancellationToken cancellationToken)
    {
        var command = new CreateOrderCommand(request.CustomerId, request.Items);

        var result = await CommandBus.SendAsync(command, cancellationToken);

        return result.ToActionResult(this);
    }
}

BaseApiQueryController

Identical structure to BaseApiCommandController, but exposes IQueryBus QueryBus instead.
[ApiController]
[Authorize]
public abstract class BaseApiQueryController : ControllerBase
{
    private IQueryBus queryBus = null!;

    protected IQueryBus QueryBus
        => this.queryBus ??= this.HttpContext.RequestServices.GetRequiredService<IQueryBus>();
}
Inherit and add actions:
[Route("api/v1/orders")]
public sealed class OrdersQueryController : BaseApiQueryController
{
    [HttpGet("{orderId:guid}")]
    [ProducesResponseType(typeof(OrderResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<OrderResponse>> GetOrder(
        Guid orderId,
        CancellationToken cancellationToken)
    {
        var query = new GetOrderByIdQuery(orderId);

        var result = await QueryBus.SendAsync<GetOrderByIdQuery, OrderResponse>(query, cancellationToken);

        return result.ToActionResult(this);
    }
}
Split command and query controllers for the same resource into separate classes if you want to keep each file focused. The route prefix can be shared.

Middlewares

ExceptionMiddleware

A catch-all middleware that wraps the entire pipeline. Any unhandled Exception that escapes downstream middleware is caught and serialised as a 500 Internal Server Error JSON response containing the HTTP status code and the exception message.
public class ExceptionMiddleware
{
    private readonly RequestDelegate next;

    public ExceptionMiddleware(RequestDelegate next)
    {
        this.next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        ArgumentNullException.ThrowIfNull(httpContext);

        try
        {
            await this.next(httpContext);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        var errorDetails = new
        {
            context.Response.StatusCode,
            exception.Message,
        };

        await context.Response.WriteAsync(JsonSerializer.Serialize(errorDetails));
    }
}
Register in Program.cs:
app.UseMiddleware<ExceptionMiddleware>();
Register ExceptionMiddleware as the outermost middleware so it can catch exceptions from every subsequent middleware and controller in the pipeline.

UserContextMiddleware

Reads the KeyUserCorrelationId HTTP request header (referenced via UserConstants.UserId) and populates IUserContextService for use anywhere downstream (application layer, domain services, etc.).
public sealed class UserContextMiddleware
{
    private readonly RequestDelegate next;

    public UserContextMiddleware(RequestDelegate next)
    {
        this.next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext context, IUserContextService userContextService)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(userContextService);

        string? userId = context.Request.Headers[UserConstants.UserId];

        if (userId is null)
        {
            if (AnonymousRequestHelper.IsAnonymousAllowed(context))
            {
                userId = UserConstants.AnonymousUser;
            }
            else
            {
                context.Response.StatusCode = StatusCodes.Status400BadRequest;
                await context.Response.WriteAsync("Missing UserId headers.");
                return;
            }
        }

        userContextService.SetUserContext(userId!);

        await this.next(context);
    }
}
Register in Program.cs:
app.UseMiddleware<UserContextMiddleware>();
Anonymous access to /swagger, /health, and /metrics paths is automatically allowed by AnonymousRequestHelper.IsAnonymousAllowed. For all other paths, the KeyUserCorrelationId header is required or the middleware returns 400 Bad Request.

PartitionContextMiddleware

Reads TenantId and PartitionId request headers and populates IPartitionContextService. Both headers must be present for authenticated routes.
public sealed class PartitionContextMiddleware
{
    private readonly RequestDelegate next;

    public PartitionContextMiddleware(RequestDelegate next)
    {
        this.next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext context, IPartitionContextService partitionContextService)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(partitionContextService);

        string? tenantId  = context.Request.Headers[PartitionConstants.TenantId];
        string? partitionId = context.Request.Headers[PartitionConstants.PartitionId];

        if (tenantId is null || partitionId is null)
        {
            if (AnonymousRequestHelper.IsAnonymousAllowed(context))
            {
                tenantId    = PartitionConstants.AnonymousTenant;
                partitionId = PartitionConstants.AnonymousPartition;
            }
            else
            {
                context.Response.StatusCode = StatusCodes.Status400BadRequest;
                await context.Response.WriteAsync("Missing TenantId or PartitionId headers.");
                return;
            }
        }

        partitionContextService.SetPartitionContext(tenantId!, partitionId!);

        await this.next(context);
    }
}
Register in Program.cs:
app.UseMiddleware<PartitionContextMiddleware>();

AnonymousRequestHelper

A static helper used internally by UserContextMiddleware and PartitionContextMiddleware to decide whether a request path is allowed to bypass header validation.
public static class AnonymousRequestHelper
{
    public static bool IsAnonymousAllowed(HttpContext context)
    {
        ArgumentNullException.ThrowIfNull(context);

        var path = context.Request.Path.Value ?? string.Empty;

        // Infrastructure paths — no authentication or partition context required
        if (path.StartsWith("/swagger", StringComparison.InvariantCulture) ||
            path.StartsWith("/health",  StringComparison.InvariantCulture) ||
            path.StartsWith("/metrics", StringComparison.InvariantCulture))
        {
            return true;
        }

        return false;
    }
}
To apply different anonymous-access rules in your application — for example, to allow unauthenticated access to an api/v1/auth/login endpoint — extend the conditions inside IsAnonymousAllowed or add your own middleware that runs before UserContextMiddleware and PartitionContextMiddleware.

Response Mapping

ResultExtensions

Extension methods that convert an Ardalis.Result<T> or Ardalis.Result returned by the application layer into the appropriate ActionResult. The full status mapping is:
ResultStatusHTTP ResponseBody
Ok200 OK / 204 No ContentValue or empty
Created201 CreatedValue + Location header
NoContent204 No Content
Invalid400 Bad RequestValidationProblemDetails
NotFound404 Not FoundProblemDetails
Unauthorized401 UnauthorizedProblemDetails
Forbidden403 Forbidden
Conflict409 ConflictProblemDetails
Error422 Unprocessable EntityProblemDetails
CriticalError500 Internal Server ErrorProblemDetails
Unavailable503 Service UnavailableProblemDetails
Usage — generic result:
// In a controller action:
var result = await CommandBus.SendAsync(command, cancellationToken);
return result.ToActionResult(this);

// Or equivalently, call it on the controller:
return this.ToActionResult(result);
Usage — typed result:
Result<OrderResponse> result = await QueryBus.SendAsync<GetOrderByIdQuery, OrderResponse>(query, ct);
return result.ToActionResult(this);  // Returns ActionResult<OrderResponse>
Usage — file download:
Result<FileResponse> result = await QueryBus.SendAsync<DownloadInvoiceQuery, FileResponse>(query, ct);
return result.ToFileResult(this);  // Returns FileResult (200) or appropriate error

ResponseBuilderHelper

Converts a collection of FluentValidation.ValidationFailure objects into a properly structured ProblemDetails or ValidationProblemDetails response. Used when you need to return validation errors that are not part of an Ardalis.Result — for example in custom filter attributes.
public static object BuildResponse(
    IReadOnlyCollection<ValidationFailure> failures,
    HttpContext context,
    int statusCode)
The helper selects the response shape based on statusCode. The switch explicitly matches 422, 404, and 409; all other codes fall through to the default branch which returns ValidationProblemDetails:
Status CodeResponse TypeRFC Reference
422ProblemDetailsRFC 9110 §422
404ProblemDetailsRFC 7231 §6.5.4
409ProblemDetailsRFC 9110 §409
any otherValidationProblemDetailsRFC 7231 §6.5.1
// Example: manually build a 422 response from FluentValidation failures
var failures = new List<ValidationFailure>
{
    new("OrderId", "Order not found.")
};

var response = ResponseBuilderHelper.BuildResponse(failures, HttpContext, 422);
return UnprocessableEntity(response);

EndpointExtensions (FastEndpoints)

For teams using FastEndpoints instead of MVC controllers, EndpointExtensions.SendResponseAsync maps an Ardalis.IResult to the appropriate FastEndpoints response method.
public static async Task SendResponseAsync(
    this IEndpoint endpoint,
    IResult result,
    CancellationToken cancellationToken)
Status mapping mirrors ResultExtensions, adapting to FastEndpoints’ SendAsync, SendCreatedAtAsync, SendErrorsAsync, SendNoContentAsync, SendForbiddenAsync, and SendUnauthorizedAsync calls.
// Inside a FastEndpoints endpoint handler:
public override async Task HandleAsync(CreateOrderRequest req, CancellationToken ct)
{
    var command = new CreateOrderCommand(req.CustomerId, req.Items);
    var result  = await CommandBus.SendAsync(command, ct);

    await this.SendResponseAsync(result, ct);
}

Contracts Package

PaginatedRequest

A base record for paginated list queries. Bind it from query-string parameters in your controller or endpoint.
public record class PaginatedRequest
{
    [DefaultValue(1)]
    public int PageNumber { get; init; }

    [DefaultValue(10)]
    public int PageSize { get; init; }
}
Usage:
[HttpGet]
public async Task<ActionResult<PaginatedCollectionResponse<OrderSummaryResponse>>> GetOrders(
    [FromQuery] PaginatedRequest request,
    CancellationToken cancellationToken)
{
    var query  = new GetOrdersQuery(request.PageNumber, request.PageSize);
    var result = await QueryBus.SendAsync<GetOrdersQuery, PaginatedCollectionResponse<OrderSummaryResponse>>(query, cancellationToken);

    return result.ToActionResult(this);
}

PaginatedCollectionResponse<T>

The standard paginated response envelope returned by list queries.
public record class PaginatedCollectionResponse<T>(
    int ActualPage,
    int TotalPages,
    int TotalItems,
    IEnumerable<T> Items);

FileResponse

Carries a file download result from the application layer to the controller, which calls result.ToFileResult(this).
public record class FileResponse(
    byte[] FileContents,
    string ContentType,
    string FileDownloadName);

HttpClient Helpers

The Contracts package ships a set of typed HttpClient helpers that are useful in integration tests, API gateways, or any service that calls another service over HTTP.

EndpointRouteHelpers

Builds a Uri from a route template, substituting path parameters first and appending any remainder as query-string entries.
public static Uri BuildUriWithQueryParameters(
    string basePath,
    params (string Key, string Value)[] queryParams)
// Substitutes {orderId} in the path template; PageNumber/PageSize go to the query string
Uri uri = EndpointRouteHelpers.BuildUriWithQueryParameters(
    "/api/v1/orders/{orderId}/items",
    ("orderId",   orderId.ToString()),
    ("PageNumber", "1"),
    ("PageSize",  "20"));
// → /api/v1/orders/abc-123/items?PageNumber=1&PageSize=20

HttpClientGetExtensionMethods

Performs a GET request and deserialises the JSON response body using camelCase naming policy.
public static async Task<T> GetAndDeserializeAsync<T>(
    this HttpClient client,
    string requestUri,
    CancellationToken cancellationToken = default)

public static async Task<T> GetAndDeserializeAsync<T>(
    this HttpClient client,
    Uri requestUri,
    CancellationToken cancellationToken = default)
var order = await httpClient.GetAndDeserializeAsync<OrderResponse>(
    $"/api/v1/orders/{orderId}",
    cancellationToken);

HttpClientPutExtensionMethods

Performs a PUT request with an HttpContent body and deserialises the JSON response.
public static async Task<T> PutAndDeserializeAsync<T>(
    this HttpClient client,
    string requestUri,
    HttpContent content,
    CancellationToken cancellationToken = default)

public static async Task<T> PutAndDeserializeAsync<T>(
    this HttpClient client,
    Uri requestUri,
    HttpContent content,
    CancellationToken cancellationToken = default)
var content  = StringContentHelpers.FromModelAsJson(new UpdateOrderRequest(...));
var response = await httpClient.PutAndDeserializeAsync<OrderResponse>(
    $"/api/v1/orders/{orderId}",
    content,
    cancellationToken);

StringContentHelpers

Serialises any object to a JSON StringContent with UTF-8 encoding and application/json content-type.
public static StringContent FromModelAsJson(object model)
HttpContent content = StringContentHelpers.FromModelAsJson(new CreateOrderRequest(
    CustomerId: customerId,
    Items: items));

var response = await httpClient.PostAsync("/api/v1/orders", content, cancellationToken);

Complete Program.cs Example

var builder = WebApplication.CreateBuilder(args);

// Application & infrastructure registrations
builder.Services.AddSharedKernelApplication(typeof(Program).Assembly);
builder.Services.AddSharedKernelPresentationHttpRestfulApi();

builder.Services.AddAuthentication(/* ... */);
builder.Services.AddAuthorization();

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Middleware pipeline — order matters
app.UseMiddleware<ExceptionMiddleware>();       // must be outermost

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.UseMiddleware<UserContextMiddleware>();      // after auth
app.UseMiddleware<PartitionContextMiddleware>(); // after user context

app.MapControllers();
app.Run();

Build docs developers (and LLMs) love