Walfhand.QuickApi 3.0.0

dotnet add package Walfhand.QuickApi --version 3.0.0
                    
NuGet\Install-Package Walfhand.QuickApi -Version 3.0.0
                    
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="Walfhand.QuickApi" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Walfhand.QuickApi" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Walfhand.QuickApi" />
                    
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 Walfhand.QuickApi --version 3.0.0
                    
#r "nuget: Walfhand.QuickApi, 3.0.0"
                    
#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 Walfhand.QuickApi@3.0.0
                    
#: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=Walfhand.QuickApi&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Walfhand.QuickApi&version=3.0.0
                    
Install as a Cake Tool

QuickApi.Engine

NuGet Build Status

Overview

QuickApi is a lightweight library designed to simplify the development of Minimal APIs in .NET applications. It provides seamless integration with popular CQRS tools like MediatR and Wolverine, enabling developers to quickly set up and scale their APIs with minimal boilerplate code.

Features

  • Effortless Minimal API Setup: Simplifies configuration and reduces the boilerplate code.
  • Automatic Endpoint Discovery: Scans loaded assemblies and registers every non-abstract IMinimalEndpoint with a configurable lifetime.
  • CQRS Integration: Built-in support for MediatR and Wolverine for handling commands, queries, and events.
  • Configurable API Prefix & Tags: Routes are automatically prefixed (default api) and tagged using the first path segment for better OpenAPI grouping.
  • Response Conventions: Base endpoints wire sensible Produces(...)/ProducesProblem(...) metadata (e.g., 201 on POST, 204 on PUT/PATCH/DELETE, 404 on GET).
  • Paginated & File Helpers: PaginatedResult<T> and FileResult helpers for list and file endpoints.
  • Developer Friendly: Focuses on improving productivity and code readability.
  • NuGet Package: Easily installable via NuGet.

What's New (2026-03-24)

  • Added SseMinimalEndpoint<TRequest, TEvent> for native SSE endpoints (text/event-stream).
  • Added SSE OpenAPI convention: Produces(200, "text/event-stream").
  • Added a concrete example endpoint in QuickApi.Example: GET /api/v1/todos/stream.
  • Added SSE unit coverage with TestServer to validate status code, content type, and event payload format.

Installation

QuickApi is available on NuGet. Install the base package:

dotnet add package Walfhand.QuickApi

For MediatR integration, you'll also need to install:

dotnet add package Walfhand.QuickApi.Extensions.Mediatr

Getting Started

Step 1: Installation

Install the required packages using the .NET CLI as shown in the Installation section above.

Step 2: Register QuickApi in your Program.cs

Add the following lines to your Program.cs file:

// Basic setup
builder.Services.AddMinimalEndpoints();

// Or with MediatR integration
builder.Services.AddMinimalEndpoints(options =>
{
    options.AddMediatR(typeof(Program).Assembly);
});

app.UseMinimalEndpoints();

To change the global API prefix (defaults to api):

builder.Services.AddMinimalEndpoints(options =>
{
    options.SetBaseApiPath("api/v1");
});

All endpoints will then be exposed as /api/v1/<your-path>.

Step 3: Implement the IMessage Interface

If you're using MediatR integration (configured via options.AddMediatR()), you can skip this step as the implementation below is already provided for you.

For custom CQRS implementations, implement the IMessage interface. Here is an example of how the MediatR implementation looks like internally:

using MediatR;
using QuickApi.Engine.Web.Cqrs;

namespace QuickApi.Example.Cqrs;

// Note: This implementation is provided automatically when using options.AddMediatR()
// Only implement this if you're NOT using AddMediatR() or if you need a custom implementation
public class MessageService : IMessage
{
    private readonly IMediator _mediator;

    public MessageService(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task<TResult> InvokeAsync<TResult>(object message, CancellationToken ct = default)
    {
        return await _mediator.Send((IRequest<TResult>)message, ct);
    }

    public async Task InvokeAsync(object message, CancellationToken ct = default)
    {
        await _mediator.Send(message, ct);
    }
}

Step 4: Register the Message Service

If you're using MediatR integration (configured via options.AddMediatR()), you can skip this step.

For custom CQRS implementations, register your MessageService implementation in the Program.cs file:

builder.Services.AddScoped<IMessage, MessageService>();

Configuration Notes (MinimalApiOptions)

  • Endpoint lifetime: defaults to Scoped. Override with options.ServiceLifetime = ServiceLifetime.Transient; (or Singleton) when registering endpoints.
  • Assembly scanning (recommended): limit scanning for endpoint discovery with options.AddAssemblies(...) to improve startup determinism and performance:
builder.Services.AddMinimalEndpoints(options =>
{
    options.AddAssemblies(
        typeof(Program).Assembly,
        typeof(TodosAssemblyMarker).Assembly);
});
  • Route prefix & tags: routes are automatically prefixed with the configured base path and tagged using the first segment of the route (e.g., todos) for cleaner Swagger grouping.

Step 5: Adding Endpoints

With QuickApi, you can define endpoints anywhere in your code by implementing one of the provided endpoint base classes. AddMinimalEndpoints automatically discovers every non-abstract implementation of IMinimalEndpoint in the loaded assemblies and maps them with the configured lifetime.

Example: Adding a Todo Item without Policies

The following example uses MediatR's CQRS implementation (IRequest and IRequestHandler). The specific interfaces will vary depending on your chosen CQRS framework:

using MediatR;
using QuickApi.Engine.Web.Endpoints.Impl;
using QuickApi.Example.Features.Todos.Domain;

namespace QuickApi.Example.Features.Todos.AddTodo.Endpoints;

// MediatR specific: IRequest<Todo> (command)
public record AddTodoRequest(string Title, string? Description) : IRequest<Todo>;

public class AddTodoEndpoint() : PostMinimalEndpoint<AddTodoRequest, Todo>("todos")
{
}

// MediatR specific: IRequestHandler<AddTodoRequest, Todo>
public class AddTodoRequestHandler : IRequestHandler<AddTodoRequest, Todo>
{
    public Task<Todo> Handle(AddTodoRequest request, CancellationToken cancellationToken)
    {
        // Example logic for creating a Todo item
        var todo = new Todo { Title = request.Title, Description = request.Description };
        return Task.FromResult(todo);
    }
}
Example: Adding a Todo Item with Policies

Similar to the previous example, this uses MediatR's CQRS implementation:

using MediatR;
using QuickApi.Engine.Web.Endpoints.Impl;
using QuickApi.Example.Features.Todos.Domain;

namespace QuickApi.Example.Features.Todos.AddTodo.Endpoints;

// MediatR specific: IRequest<Todo> (command)
public record AddTodoRequest(string Title, string? Description) : IRequest<Todo>;

public class AddTodoEndpoint() : PostMinimalEndpoint<AddTodoRequest, Todo>(
    "todos",
    nameof(PoliciesEnum.Subscribed))
{
}

// MediatR specific: IRequestHandler<AddTodoRequest, Todo>
public class AddTodoRequestHandler : IRequestHandler<AddTodoRequest, Todo>
{
    public Task<Todo> Handle(AddTodoRequest request, CancellationToken cancellationToken)
    {
        // Example logic for creating a Todo item
        var todo = new Todo { Title = request.Title, Description = request.Description };
        return Task.FromResult(todo);
    }
}

public class PoliciesEnum
{
    public const string Subscribed = "Subscribed";
}
Example: Customizing an Endpoint without MediatR

You can bypass the IMessage interface and MediatR by customizing your endpoint directly:

public class FilterTodoEndpointCustom() : FilterMinimalEndpoint<FilterTodoRequest, Todo>("todos/custom")
{
    protected override RouteHandlerBuilder Configure(IEndpointRouteBuilder builder)
    {
        //call base configure and get routeHandlerBuilder
        var routeHandlerBuilder = base.Configure(builder);

        //add your customization
        routeHandlerBuilder.ProducesProblem(500);

        //return routeHandlerBuilder custom
        return routeHandlerBuilder;
    }

    protected override Delegate Handler => EndpointHandler;

    private static async Task<IResult> EndpointHandler([AsParameters] FilterTodoRequest request, IDbContext context, CancellationToken ct)
    {
        var query = context.Set<Todo>().AsQueryable();
        if(request.IsCompleted.HasValue)
            query = query.Where(x => x.IsCompleted == request.IsCompleted);
        return Results.Ok(await query.ToListAsync(ct));
    }
}

Available Endpoint Base Classes

QuickApi provides several base classes to simplify the creation of endpoints (each adds default Produces(...) metadata for common status codes):

  • PostMinimalEndpoint: For POST requests.
  • GetMinimalEndpoint: For GET requests.
  • PutMinimalEndpoint: For PUT requests.
  • DeleteMinimalEndpoint: For DELETE requests.
  • PatchMinimalEndpoint: For PATCH requests.
  • FilterMinimalEndpoint: For filtered GET requests returning List<T>.
  • FilterPaginateMinimalEndpoint: For paginated and filtered GET requests returning PaginatedResult<T>.
  • GetFileMinimalEndpoint: For serving files using FileResult.
  • SseMinimalEndpoint: For SSE streams (text/event-stream) with IAsyncEnumerable<T> messages.
Paginated filtering example
using MediatR;
using QuickApi.Engine.Web.Endpoints.Impl;
using QuickApi.Engine.Web.Models;

public record FilterTodosPaginatedRequest(int PageIndex = 1, int PageSize = 20)
    : IRequest<PaginatedResult<Todo>>;

public class FilterTodosPaginatedEndpoint()
    : FilterPaginateMinimalEndpoint<FilterTodosPaginatedRequest, Todo>("todos/paginated");

PaginatedResult<T> exposes Items, TotalCount, PageIndex, and PageSize.

File endpoint example
using MediatR;
using QuickApi.Engine.Web.Endpoints.Impl;
using QuickApi.Engine.Web.Models;

public record DownloadInvoiceRequest(Guid Id) : IRequest<FileResult>;

public class DownloadInvoiceEndpoint()
    : GetFileMinimalEndpoint<DownloadInvoiceRequest, FileResult>("invoices/{id:guid}");
SSE endpoint example
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using QuickApi.Engine.Web.Endpoints.Impl;
using QuickApi.Example.Data.Contexts;
using QuickApi.Example.Features.Todos.Domain;

public record StreamTodosRequest
{
    [FromQuery] public bool? IsCompleted { get; set; }
    [FromQuery] public int PollIntervalSeconds { get; set; } = 3;
    [FromQuery] public bool IncludeHeartbeat { get; set; } = true;
}

public sealed record TodoStreamItem(Guid Id, string Title, string? Description, bool IsCompleted);
public sealed record StreamTodosEvent(long Sequence, string Name, int RetryMilliseconds, object Payload);

public class StreamTodosEndpoint : SseMinimalEndpoint<StreamTodosRequest, StreamTodosEvent>
{
    public StreamTodosEndpoint() : base("todos/stream")
    {
    }

    protected override async IAsyncEnumerable<StreamTodosEvent> Stream(
        StreamTodosRequest request,
        HttpContext httpContext,
        [EnumeratorCancellation] CancellationToken ct)
    {
        var dbContext = httpContext.RequestServices.GetRequiredService<IDbContext>();
        var intervalSeconds = Math.Clamp(request.PollIntervalSeconds, 1, 30);
        var retryMilliseconds = intervalSeconds * 1000;
        var sequence = 0L;

        while (!ct.IsCancellationRequested && !httpContext.RequestAborted.IsCancellationRequested)
        {
            var query = dbContext.Set<Todo>().AsNoTracking();
            if (request.IsCompleted.HasValue)
                query = query.Where(x => x.IsCompleted == request.IsCompleted.Value);

            var items = await query
                .OrderBy(x => x.Id.Value)
                .Select(x => new TodoStreamItem(x.Id.Value, x.Title, x.Description, x.IsCompleted))
                .ToListAsync(ct);

            sequence++;
            yield return new StreamTodosEvent(sequence, "todos-snapshot", retryMilliseconds, new
            {
                Items = items,
                SentAtUtc = DateTimeOffset.UtcNow
            });

            await Task.Delay(TimeSpan.FromSeconds(intervalSeconds), ct);
        }
    }

    protected override async Task WriteEventAsync(HttpResponse response, StreamTodosEvent payload, CancellationToken ct)
    {
        await response.WriteAsync($"id: {payload.Sequence}\n", ct);
        await response.WriteAsync($"retry: {payload.RetryMilliseconds}\n", ct);
        await response.WriteAsync($"event: {payload.Name}\n", ct);
        await response.WriteAsync($"data: {JsonSerializer.Serialize(payload.Payload)}\n\n", ct);
    }
}

QuickApi.Example also shows heartbeat + change-detection mode in: src/QuickApi.Example/Features/Todos/StreamTodos/Endpoints/StreamTodosEndpoint.cs

Commands/Queries & Model Binding

  • Your request types (commands/queries) are plain classes/records that implement MediatR’s IRequest/IRequest<T> (or your CQRS equivalent). They are passed directly to the IMessage pipeline, so handlers stay unchanged.
  • You can compose requests with standard ASP.NET binding attributes:
using MediatR;
using Microsoft.AspNetCore.Mvc;

// Command mixing route + body binding
public record UpdateTodoRequest(
    [FromRoute] Guid Id,
    [FromBody] UpdateTodoBody Body) : IRequest;

public record UpdateTodoBody(string Title, string? Description);

public class UpdateTodoEndpoint()
    : PutMinimalEndpoint<UpdateTodoRequest>("todos/{id:guid}");
using MediatR;
using Microsoft.AspNetCore.Mvc;

// Query with query-string binding
public record FilterTodosRequest(
    [FromQuery] bool? IsCompleted,
    [FromQuery] int PageIndex = 1,
    [FromQuery] int PageSize = 20) : IRequest<PaginatedResult<Todo>>;

public class FilterTodosEndpoint()
    : FilterPaginateMinimalEndpoint<FilterTodosRequest, Todo>("todos");

Response conventions

  • GetMinimalEndpoint returns 200 + 404 metadata.
  • PostMinimalEndpoint returns 201 + validation problem metadata.
  • PutMinimalEndpoint, PatchMinimalEndpoint, and DeleteMinimalEndpoint return 204 + 404 + validation problem metadata.
  • FilterMinimalEndpoint returns 200 List<T>; FilterPaginateMinimalEndpoint returns 200 PaginatedResult<T>.
  • SseMinimalEndpoint returns 200 text/event-stream.

Example Project

A fully working example project is available. You can clone it and start it with the following command:

docker compose up --build

The application will be accessible at https://api.localhost/scalar/v1, where you can test more advanced features.

Note

When inheriting from a class like PostMinimalEndpoint, you can add security policies to your endpoint by passing them as a params array in the constructor after the route.

Packages will follow to simplify this integration for tools like MediatR and Wolverine.

Contributing

Contributions are welcome! If you find an issue or have an idea for improvement, feel free to submit a pull request or open an issue on the GitHub repository.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 427 3/24/2026
2.0.1 115 3/24/2026
2.0.0 550 11/15/2025
1.1.0 1,946 8/7/2025
1.0.7 309 8/7/2025
1.0.6 199 3/1/2025
1.0.5 308 1/9/2025
1.0.4 184 1/9/2025
1.0.3 181 1/9/2025
1.0.2 212 12/16/2024
1.0.1 184 12/14/2024
1.0.0 186 12/14/2024