Walfhand.QuickApi
3.0.0
dotnet add package Walfhand.QuickApi --version 3.0.0
NuGet\Install-Package Walfhand.QuickApi -Version 3.0.0
<PackageReference Include="Walfhand.QuickApi" Version="3.0.0" />
<PackageVersion Include="Walfhand.QuickApi" Version="3.0.0" />
<PackageReference Include="Walfhand.QuickApi" />
paket add Walfhand.QuickApi --version 3.0.0
#r "nuget: Walfhand.QuickApi, 3.0.0"
#:package Walfhand.QuickApi@3.0.0
#addin nuget:?package=Walfhand.QuickApi&version=3.0.0
#tool nuget:?package=Walfhand.QuickApi&version=3.0.0
QuickApi.Engine
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
IMinimalEndpointwith 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>andFileResulthelpers 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
TestServerto 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 withoptions.ServiceLifetime = ServiceLifetime.Transient;(orSingleton) 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 returningList<T>.FilterPaginateMinimalEndpoint: For paginated and filtered GET requests returningPaginatedResult<T>.GetFileMinimalEndpoint: For serving files usingFileResult.SseMinimalEndpoint: For SSE streams (text/event-stream) withIAsyncEnumerable<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 theIMessagepipeline, 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
GetMinimalEndpointreturns200+404metadata.PostMinimalEndpointreturns201+ validation problem metadata.PutMinimalEndpoint,PatchMinimalEndpoint, andDeleteMinimalEndpointreturn204+404+ validation problem metadata.FilterMinimalEndpointreturns200 List<T>;FilterPaginateMinimalEndpointreturns200 PaginatedResult<T>.SseMinimalEndpointreturns200 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 | Versions 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. |
-
net10.0
- Scrutor (>= 6.1.0)
- Walfhand.QuickApi.Abstractions (>= 2.0.0)
-
net8.0
- Scrutor (>= 6.1.0)
- Walfhand.QuickApi.Abstractions (>= 2.0.0)
-
net9.0
- Scrutor (>= 6.1.0)
- Walfhand.QuickApi.Abstractions (>= 2.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.