Pragmatic.Endpoints
Source-generated HTTP endpoints for ASP.NET Core. Declare the shape, the generator writes the plumbing.
The Problem
Section titled “The Problem”Every ASP.NET Core endpoint needs the same ceremony: parse route parameters, bind the request body, validate inputs, check authorization, call business logic, map errors to HTTP status codes, configure OpenAPI metadata. With Minimal APIs you write this by hand. With Controllers you inherit it but lose flexibility. Either way, the plumbing-to-logic ratio grows with every endpoint.
// Without Pragmatic: 40+ lines of plumbing for a simple GETapp.MapGet("/api/products/{id}", async (Guid id, IProductRepository repo, IValidator<...> validator, IAuthorizationService auth, HttpContext ctx, CancellationToken ct) =>{ var authResult = await auth.AuthorizeAsync(ctx.User, "products.read"); if (!authResult.Succeeded) return Results.Forbid();
var product = await repo.GetByIdAsync(id, ct); if (product is null) return Results.NotFound(new ProblemDetails { ... });
return Results.Ok(ProductDto.FromEntity(product));}).WithName("GetProduct").WithTags("Products").Produces<ProductDto>(200).ProducesProblem(404);The Solution
Section titled “The Solution”With Pragmatic.Endpoints, you declare WHAT the endpoint does. The source generator handles HOW.
// With Pragmatic: declare the shape, the SG generates the rest[Endpoint(HttpVerb.Get, "/api/products/{id}")][RequirePermission("products.read")]public partial class GetProduct : Endpoint<ProductDto, NotFoundError>{ private IProductRepository _products = null!;
[FromRoute] public Guid Id { get; set; }
public override async Task<Result<ProductDto, NotFoundError>> HandleAsync(CancellationToken ct) { var product = await _products.GetByIdAsync(Id, ct); return product is not null ? ProductDto.FromEntity(product) : new NotFoundError("Product", Id.ToString()); }}The SG produces the handler registration, parameter binding, DI wiring, authorization enforcement, error-to-HTTP mapping, and OpenAPI metadata — all at compile time, zero reflection.
Installation
Section titled “Installation”dotnet add package Pragmatic.EndpointsAdd the source generator:
<ProjectReference Include="..\Pragmatic.SourceGenerator\...\Pragmatic.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />Quick Start
Section titled “Quick Start”The simplest endpoint returns no response body — a fire-and-forget operation:
[Endpoint(HttpVerb.Post, "/api/cache/clear")]public partial class ClearCacheEndpoint : VoidEndpoint{ private ICacheStack _cache = null!; // injected by SG
public override async Task<VoidResult> HandleAsync(CancellationToken ct) { await _cache.InvalidateByTagAsync("products", ct); return VoidResult.Success(); // → 204 No Content }}Most endpoints return data and may fail with typed errors. Error types inherit from the Error base record (which implements IError), giving you Code, StatusCode, Title, and Description:
[Endpoint(HttpVerb.Get, "/api/products/{id}")]public partial class GetProduct : Endpoint<ProductDto, NotFoundError>{ private IProductRepository _products = null!;
public Guid Id { get; set; } // auto-bound from route: matches {id}
public override async Task<Result<ProductDto, NotFoundError>> HandleAsync(CancellationToken ct) { var product = await _products.GetByIdAsync(Id, ct); return product is not null ? ProductDto.FromEntity(product) : new NotFoundError("Product", Id.ToString()); // → 404 }}The generator produces:
MapEndpoint()that registersGET /api/products/{id}with ASP.NET CoreSetDependencies()that resolves_productsfrom DI- OpenAPI metadata (produces 200 with
ProductDto, 404 withNotFoundError)
Base Classes
Section titled “Base Classes”Endpoint<TResponse>
Section titled “Endpoint<TResponse>”Returns a response on success. Implement HandleAsync().
[Endpoint(HttpVerb.Get, "/api/health")]public partial class HealthEndpoint : Endpoint<HealthStatus>{ public override Task<Result<HealthStatus>> HandleAsync(CancellationToken ct) => Task.FromResult<Result<HealthStatus>>(new HealthStatus("ok"));}VoidEndpoint
Section titled “VoidEndpoint”Returns 204 No Content on success. No response body. Use for operations where the client only needs to know “it worked.”
[Endpoint(HttpVerb.Delete, "/api/cache")]public partial class ClearCacheEndpoint : VoidEndpoint{ private ICacheStack _cache = null!;
public override async Task<VoidResult> HandleAsync(CancellationToken ct) { await _cache.InvalidateByTagAsync("products", ct); return VoidResult.Success(); }}You can also return typed errors from void endpoints:
[Endpoint(HttpVerb.Delete, "/api/orders/{id}")]public partial class CancelOrderEndpoint : VoidEndpoint<NotFoundError, ConflictError>{ // Can return NotFoundError (404) or ConflictError (409) on failure}Default success codes: VoidEndpoint = 204, POST = 201, GET/PUT/PATCH = 200. Override with [HttpStatus(code)]:
[Endpoint(HttpVerb.Post, "/api/jobs")][HttpStatus(201)] // 201 Created instead of default 204public partial class EnqueueJobEndpoint : VoidEndpoint { /* ... */ }Typed Errors
Section titled “Typed Errors”Both base classes support up to 6 typed error parameters. Error types must implement IError — the easiest way is to inherit from the Error abstract record in Pragmatic.Result, which gives you Code, StatusCode, Title, and Description:
// Built-in errors (from Pragmatic.Result):// NotFoundError : Error → 404// ValidationError : Error → 400// ConflictError : Error → 409// UnauthorizedError: Error → 401
// Custom error — inherit from Errorpublic sealed record InsufficientFundsError(decimal Available, decimal Required) : Error("INSUFFICIENT_FUNDS", 422, "Insufficient Funds");
// Use in endpoint — up to 6 error types[Endpoint(HttpVerb.Post, "/api/orders")]public partial class CreateOrderEndpoint : Endpoint<OrderDto, ValidationError, ConflictError>{ public override async Task<Result<OrderDto, ValidationError, ConflictError>> HandleAsync(CancellationToken ct) { if (orderExists) return new ConflictError("Order", orderId.ToString());
return new OrderDto { /* ... */ }; }}| Error Type | Default Status |
|---|---|
ValidationError | 400 |
UnauthorizedError | 401 |
ForbiddenError | 403 |
NotFoundError | 404 |
ConflictError | 409 |
Other IError | 422 |
Override with [HttpStatus(code)] on the error type.
Why multiple error types for the same status code? The HTTP status groups errors for the transport layer, but the code field in the response body identifies the specific error for the client:
// 422 from InsufficientFundsError{ "status": 422, "code": "INSUFFICIENT_FUNDS", "detail": "Available: €50, required: €100" }
// 422 from ExpiredCardError{ "status": 422, "code": "EXPIRED_CARD", "detail": "Card ending 4242 expired 2025-01" }The client switches on code, not status. Each typed error generates a distinct OpenAPI response schema, so API consumers see all possible error shapes in Swagger.
Request Binding
Section titled “Request Binding”Properties on the endpoint class are bound from the HTTP request. The SG infers the binding source automatically when possible:
Route parameters are matched by name. If your route has {id} and your class has a property named Id, the SG binds it automatically — no [FromRoute] needed:
[Endpoint(HttpVerb.Get, "/api/products/{id}")]public partial class GetProduct : Endpoint<ProductDto>{ public Guid Id { get; set; } // auto-bound from {id} in the route}For explicit control or when names differ, use binding attributes:
| Attribute | Source | Auto-inferred? | Example |
|---|---|---|---|
[FromRoute] | URL path segment | Yes, by name match | /products/{id} → public Guid Id |
[FromQuery] | Query string | No | ?page=1 → [FromQuery] public int Page |
[FromHeader] | HTTP header | No | [FromHeader("X-Api-Key")] public string ApiKey |
[FromBody] | Request body (JSON) | — | Force a property into the body |
[FromForm] | Form data / IFormFile | No | File uploads and form fields |
[FromClaim("sub")] | JWT claim | No | [FromClaim("sub")] public Guid UserId |
| (no attribute) | Generated body DTO | — | Remaining props → sealed record {Type}Body |
All binding attributes are in Pragmatic.Endpoints.Attributes — no FrameworkReference to ASP.NET Core needed in the declaring assembly.
Body DTO Generation
Section titled “Body DTO Generation”Properties without a binding attribute become a generated body DTO:
[Endpoint(HttpVerb.Post, "/api/products")]public partial class CreateProductEndpoint : Endpoint<ProductDto>{ [FromHeader("X-Tenant")] public string Tenant { get; set; } = "";
// These become CreateProductEndpointBody { Name, Price } public required string Name { get; set; } public decimal Price { get; set; }}The SG generates CreateProductEndpointBody as a sealed record and deserializes it from the request body.
Claim Binding
Section titled “Claim Binding”Supported claim types: string, Guid, int, long, bool, DateTimeOffset. When IsRequired = true (default), returns 401 if the claim is missing.
[FromClaim("sub")] public Guid UserId { get; set; }[FromClaim("tenant_id", IsRequired = false)] public string? TenantId { get; set; }Full reference: Binding Reference.
Dependency Injection
Section titled “Dependency Injection”Private fields on the endpoint class are resolved from DI automatically:
[Endpoint(HttpVerb.Get, "/api/orders/{id}")]public partial class GetOrderEndpoint : Endpoint<OrderDto>{ private IOrderRepository _orders = null!; // resolved from DI private ILogger<GetOrderEndpoint> _logger = null!;
[FromRoute] public Guid Id { get; set; }
public override async Task<Result<OrderDto>> HandleAsync(CancellationToken ct) { _logger.LogDebug("Getting order {Id}", Id); var order = await _orders.GetByIdAsync(Id, ct); // ... }}The SG generates SetDependencies() that calls GetRequiredService<T>() for each private field.
Endpoint Groups
Section titled “Endpoint Groups”As an API grows, you end up with dozens of endpoints sharing the same route prefix, OpenAPI tag, and authorization rules. Repeating /api/v1/products on every endpoint is fragile and noisy. Endpoint groups solve this — define the shared config once, then reference it:
Full guide: Endpoint Groups
[EndpointGroup("/api/v1/products", Tag = "Products")][ApiVersion("1.0")]public static class ProductsGroup;
[Endpoint(HttpVerb.Get, "/{id}", Group = typeof(ProductsGroup))]public partial class GetProductEndpoint : Endpoint<ProductDto> { /* ... */ }// Route: GET /api/v1/products/{id}
[Endpoint(HttpVerb.Post, "", Group = typeof(ProductsGroup))]public partial class CreateProductEndpoint : Endpoint<ProductDto> { /* ... */ }// Route: POST /api/v1/productsGroups support:
- Tag: OpenAPI tag for all endpoints in the group
- Version: API version applied to all endpoints
- Nesting:
Parent = typeof(ParentGroup)for hierarchical routes - Programmatic config: per-group auth, rate limiting, etc.
builder.Services.AddPragmaticEndpoints(options =>{ options.ConfigureGroup("Products", g => { g.RequireAuthorization = true; g.RateLimitPolicy = "standard"; });});OpenAPI Metadata
Section titled “OpenAPI Metadata”The SG auto-generates OpenAPI metadata from your endpoint class. Summary, description, tags, success/error response types — all discoverable by Swagger/Scalar without extra config. You control the documentation directly on the endpoint class:
[Endpoint(HttpVerb.Post, "/api/invoices/{id}/refund")][EndpointSummary("Refund Invoice")][EndpointDescription("Issues a full refund for a paid invoice. Supports **Markdown**.")][Tags("Invoices", "Billing")][HttpStatus(201)] // Override default success status codepublic partial class RefundEndpoint : Endpoint<decimal, NotFoundError, ForbiddenError>{ /// <summary>The invoice ID to refund.</summary> [FromRoute] public Guid Id { get; set; }
/// <summary>Reason for the refund. Required for audit trail.</summary> public required string Reason { get; set; }}- Default success codes: VoidEndpoint = 204, POST = 201, others = 200. Override with
[HttpStatus]. - Error responses: typed errors in the base class generics auto-generate produces metadata.
- Property docs: XML comments on body properties flow into the OpenAPI schema.
- Tags: default tag derived from route if not specified.
Authorization
Section titled “Authorization”Standard ASP.NET Core authorization works on any endpoint. The SG recognizes both Microsoft and Pragmatic attributes:
[Authorize] // Require authenticated user[Authorize(Policy = "AdminOnly")] // ASP.NET Core named policy[AllowAnonymous] // Public endpoint, override group authThe SG generates .RequireAuthorization() or .RequireAuthorization("PolicyName") on the route builder. You configure policies with standard ASP.NET Core APIs (AddAuthorizationBuilder(), AddPolicy()).
For Pragmatic permission-based authorization ([RequirePermission], [RequirePolicy<T>]), see Ecosystem Integration > Authorization below.
File Upload and Download
Section titled “File Upload and Download”File handling is built into the endpoint system — no manual multipart parsing needed. Use [FromForm] on IFormFile properties, add validation with [MaxFileSize] and [AllowedContentTypes], and the SG handles antiforgery, content-type negotiation, and error responses (413/415) automatically. Global defaults are configurable via PragmaticEndpointsOptions.
Full guide: File Upload and Download
Upload
Section titled “Upload”[Endpoint(HttpVerb.Post, "/api/photos")]public partial class UploadPhotoEndpoint : Endpoint<Uri>{ private IFileStorage _storage = null!;
[FromForm] [MaxFileSize(10_000_000)] // 10 MB limit → 413 if exceeded [AllowedContentTypes("image/*")] // Only images → 415 if wrong type public IFormFile Photo { get; set; } = null!;
[FromForm] public string? Caption { get; set; } [FromRoute] public Guid PropertyId { get; set; }
public override async Task<Result<Uri>> HandleAsync(CancellationToken ct) { /* ... */ }}[MaxFileSize] and [AllowedContentTypes] go on the IFormFile property — they’re validated before HandleAsync runs. The SG auto-disables antiforgery and configures multipart/form-data.
Download
Section titled “Download”Return FileResponse with optional ETag, LastModified, EnableRangeProcessing, Inline. Implement IETagSupport on response DTOs for conditional GET.
API Versioning
Section titled “API Versioning”APIs evolve — new fields, different response shapes, deprecated behaviors. Pragmatic builds on ASP.NET Core’s Asp.Versioning.Http package to generate versioned endpoints from convention-named methods. You write HandleAsyncV2, the SG generates separate route registrations with ApiVersionSet routing.
Full guide: Versioning
Convention-based versioning with multiple HandleAsync methods. The SG detects HandleAsyncV{n} methods and generates separate versioned route registrations:
[Endpoint(HttpVerb.Get, "/products/{id}")]public partial class GetProductEndpoint : Endpoint<ProductResponse>{ [FromRoute] public Guid Id { get; set; }
// v1.0 — base handler public override Task<Result<ProductResponse>> HandleAsync(CancellationToken ct) { /* ... */ }
// v2.0 — extended response public Task<Result<ProductResponse>> HandleAsyncV2(CancellationToken ct) { /* ... */ }}// Routes: GET /products/{id}?api-version=1.0 → HandleAsync// GET /products/{id}?api-version=2.0 → HandleAsyncV2Supports V{major}, V{major}_{minor}, and V{major}_{minor}_{patch} (e.g., HandleAsyncV2_1_3).
[ApiVersion] is optional — versions are inferred from methods. Use [ApiVersion] only for Deprecated or SunsetDate metadata.
[SinceVersion] on properties — controls which properties appear in each version’s body DTO:
public required string Name { get; set; } // available in all versions
[SinceVersion("2.0")]public string? LoyaltyMemberId { get; set; } // only in v2+ body DTORequires
Asp.Versioning.HttpNuGet package. Without it, only the base version handler is generated and PRAG0551 warns.
Rate Limiting and Response Caching
Section titled “Rate Limiting and Response Caching”Pragmatic builds on top of ASP.NET Core’s built-in rate limiting and output caching middleware. The SG generates the configuration boilerplate — you declare intent via attributes, the framework handles policy registration and middleware wiring. For distributed scenarios, both bridge to Pragmatic.Caching so rate limit counters and cached responses work across multiple app instances.
Full guides: Rate Limiting | Response Caching
Rate Limiting
Section titled “Rate Limiting”Three ways to configure:
1. Inline — the SG generates a fixed window limiter:
[Endpoint(HttpVerb.Get, "/limited")][RateLimit(Requests = 5, Window = "1m")]public partial class RateLimitedEndpoint : Endpoint<MyResponse> { /* ... */ }Window supports "30s", "1m", "5m", "1h", "1d". The SG generates AddFixedWindowLimiter(...) and .RequireRateLimiting(...) automatically. Exceeded requests return 429 (configurable via RateLimitRejectionStatusCode).
2. Named via Pragmatic options — configure strategy + parameters without raw ASP.NET Core APIs:
builder.Services.AddPragmaticEndpoints(options =>{ options.ConfigureRateLimiter("standard", limiter => { limiter.Strategy = RateLimiterStrategy.SlidingWindow; limiter.PermitLimit = 100; limiter.Window = TimeSpan.FromMinutes(1); limiter.SegmentsPerWindow = 6; });
options.ConfigureRateLimiter("api-burst", limiter => { limiter.Strategy = RateLimiterStrategy.TokenBucket; limiter.PermitLimit = 50; limiter.Window = TimeSpan.FromSeconds(10); limiter.TokensPerPeriod = 10; });});Reference on endpoints: [RateLimit(Policy = "standard")].
Available strategies: FixedWindow, SlidingWindow, TokenBucket, Concurrency.
3. Raw ASP.NET Core — for full control, use ASP.NET Core’s AddRateLimiter() directly.
All modes require app.UseRateLimiter() in the pipeline.
All modes are in-memory by default. For multi-instance deployments, bridge to Pragmatic.Caching for distributed counters:
builder.Services.AddPragmaticCaching();builder.Services.UseDistributedRateLimiterFromPragmaticCaching( "standard", permitLimit: 100, window: TimeSpan.FromMinutes(1));app.UseRateLimiter();This uses ICacheStack (HybridCache → Redis/SQL) for rate limit counters, partitioned by user identity or client IP.
Response Caching
Section titled “Response Caching”Uses ASP.NET Core Output Caching. Requires app.UseOutputCache().
[ResponseCache(Duration = 300)] // 5 min server-side cache[ResponseCache(Duration = 300, VaryByQueryKeys = new[] { "page" })][ResponseCache(Duration = 60, Location = ResponseCacheLocation.Client)] // Cache-Control: private[ResponseCache(NoStore = true)] // Cache-Control: no-storeStorage: in-memory by default. For distributed caching, bridge to Pragmatic.Caching:
builder.Services.AddPragmaticCaching(); // HybridCache (L1 memory + L2 Redis/SQL)builder.Services.UseOutputCacheFromPragmaticCaching(); // Output cache → same backendapp.UseOutputCache();This way [ResponseCache] on endpoints and [Cacheable] on domain actions share the same distributed backend.
[ResponseCache] vs [Cacheable]
Section titled “[ResponseCache] vs [Cacheable]”[ResponseCache] | [Cacheable] | |
|---|---|---|
| Layer | HTTP (transport) | Business (domain) |
| What it caches | Serialized HTTP response | Action/query result object |
| Scope | Endpoints only | Actions, queries, any service |
| Backend | ASP.NET Core Output Cache | Pragmatic.Caching (HybridCache) |
| Package | Pragmatic.Endpoints | Pragmatic.Caching (ecosystem) |
Both can coexist on the same endpoint — they cache at different layers. [Cacheable] is the recommended choice in the Pragmatic ecosystem; [ResponseCache] is useful for pure HTTP caching (CDN, browser, reverse proxy).
Pre/Post Processors
Section titled “Pre/Post Processors”Attach logic before or after the handler:
[PreProcessor<TenantValidationProcessor>][PostProcessor<AuditLogProcessor>]public partial class CreateOrderEndpoint : Endpoint<OrderDto> { /* ... */ }IEndpointPreProcessor: runs before handler, can short-circuit withPreProcessorResult.Fail(error)IEndpointPostProcessor: always runs after handler (for logging, metrics, etc.)- Typed variants
IEndpointPreProcessor<TEndpoint>/IEndpointPostProcessor<TEndpoint>provide access to the endpoint instance - Control order with
Orderproperty on the attribute
Global Configuration
Section titled “Global Configuration”builder.Services.AddPragmaticEndpoints(options =>{ options.RoutePrefix = "/api"; options.RequireAuthorizationByDefault = true; options.DefaultRateLimitPolicy = "standard"; options.MaxUploadFileSize = 10_000_000; // 10 MB global default options.DefaultAllowedContentTypes = ["image/*", "application/pdf"]; options.RateLimitRejectionStatusCode = 429;
options.ConfigureRateLimiter("standard", limiter => { limiter.Strategy = RateLimiterStrategy.SlidingWindow; limiter.PermitLimit = 100; limiter.Window = TimeSpan.FromMinutes(1); });
options.ConfigureGroup("Orders", group => { group.RoutePrefix = "/v1/orders"; group.RequireAuthorization = true; group.RateLimitPolicy = "standard"; group.Tags.Add("Orders"); });});| Option | Default | Description |
|---|---|---|
RoutePrefix | "" | Global route prefix — wraps all endpoints in MapGroup(prefix). |
RequireAuthorizationByDefault | false | All endpoints require authentication unless [AllowAnonymous]. Applied to the root MapGroup. |
DefaultRateLimitPolicy | null | Named rate limit policy applied to all endpoints. Applied to the root MapGroup. |
MaxUploadFileSize | null | Global max file upload size in bytes. Overridden by [MaxFileSize] per property. |
DefaultAllowedContentTypes | null | Global allowed MIME types for uploads. Overridden by [AllowedContentTypes] per property. |
RateLimitRejectionStatusCode | 429 | HTTP status code returned when rate limit is exceeded. |
ConfigureRateLimiter() | — | Register named rate limit policies (FixedWindow, SlidingWindow, TokenBucket, Concurrency). |
ConfigureGroup() | — | Programmatic group configuration (auth, tags, rate limit) applied at runtime to MapGroup. |
Auto-Wired ASP.NET Core Options
Section titled “Auto-Wired ASP.NET Core Options”These options are automatically applied by the SG-generated AddPragmaticEndpoints() — no separate ASP.NET Core configuration needed:
| Option | Default | What It Does |
|---|---|---|
UseCamelCaseJson | true | Auto-calls ConfigureHttpJsonOptions() with CamelCase naming policy. |
EnableResponseCompression | false | When true, auto-calls AddResponseCompression(). Add app.UseResponseCompression() in pipeline. |
IncludeStackTraceInErrors | false | Auto-calls AddProblemDetails() with stack trace in extensions. Development only. |
EnableOpenApi | true | SG always generates endpoint metadata (Produces, WithSummary, WithTags). Call AddOpenApi() to activate the document. |
OpenApiTitle | "API" | Passed to AddOpenApi() when you configure OpenAPI. |
DefaultApiVersion | null | Use [ApiVersion] on endpoints/groups. Add Asp.Versioning.Http for URL versioning. |
EnableRequestValidation | true | Validation runs per-endpoint via [Validate] attribute. |
Error Handling
Section titled “Error Handling”Errors convert to RFC 7807 ProblemDetails via Pragmatic.Endpoints.AspNetCore. Full guide: Error Handling.
What the SG Generates
Section titled “What the SG Generates”For each [Endpoint] class:
| Generated | Purpose |
|---|---|
MapEndpoint() | Static method registering the route with ASP.NET Core |
{TypeName}Body | Sealed record for body properties (when applicable) |
SetDependencies() | Resolves private fields from DI |
PragmaticEndpointContractAttribute | Assembly metadata for host discovery |
Attributes Reference
Section titled “Attributes Reference”| Attribute | Target | Description |
|---|---|---|
[Endpoint(HttpVerb, route)] | Class | HTTP endpoint with method and route |
[EndpointGroup(prefix)] | Class | Group with shared prefix, tag, version |
[FromRoute] | Property | Bind from URL path |
[FromQuery] | Property | Bind from query string |
[FromHeader] | Property | Bind from HTTP header |
[FromBody] | Property | Explicit body binding |
[FromForm] | Property | Form data / IFormFile |
[FromClaim(type)] | Property | JWT claim binding |
[EndpointSummary(text)] | Class | OpenAPI summary |
[EndpointDescription(text)] | Class | OpenAPI description (Markdown) |
[Tags(...)] | Class | OpenAPI tags |
[HttpStatus(code)] | Class/Error | Override HTTP status code |
[ApiVersion(ver)] | Class | API version (Deprecated, SunsetDate) |
[SinceVersion(ver)] | Property | Property introduced in version |
[AllowAnonymous] | Class | Override group authorization |
[RateLimit] | Class | Rate limiting |
[ResponseCache] | Class | Response caching |
[PreProcessor<T>] | Class | Pre-processor with Order |
[PostProcessor<T>] | Class | Post-processor with Order |
[MaxFileSize(bytes)] | Property | File size limit (413) |
[AllowedContentTypes(...)] | Property | MIME type filter (415) |
[Autocomplete] / [Autocomplete<TDto>] | Entity property | Search endpoint |
[ExposeEndpoint<T>] | Module class | Promote package action to HTTP |
Diagnostics
Section titled “Diagnostics”| ID | Severity | Description |
|---|---|---|
| PRAG0500 | Error | Endpoint class must be partial |
| PRAG0501 | Error | Must inherit from Endpoint or VoidEndpoint |
| PRAG0502 | Error | Route is required |
| PRAG0503 | Error | Too many error types (max 6) |
| PRAG0504 | Warning | Route parameter does not match any property |
| PRAG0505 | Error | Duplicate endpoint name |
| PRAG0506 | Error | Invalid rate limit configuration |
| PRAG0507 | Error | Endpoint group not found |
| PRAG0508 | Error | Processor must implement interface |
| PRAG0510 | Info | Endpoint registered |
| PRAG0512 | Info | Property implicitly binds to body |
| PRAG0515 | Error | Entity has no key for [Autocomplete] |
| PRAG0550 | Error | [Autocomplete] requires string property |
| PRAG0551 | Warning | Versioned Execute requires Asp.Versioning.Http |
Packages
Section titled “Packages”| Package | Purpose |
|---|---|
Pragmatic.Endpoints | Core: attributes, base classes, interfaces. No ASP.NET Core dependency. |
Pragmatic.Endpoints.AspNetCore | Runtime: ProblemDetails, FileResponse, EndpointContext. |
Pragmatic.SourceGenerator | Compile-time: generates handlers, DTOs, registration, metadata. |
Ecosystem Integration
Section titled “Ecosystem Integration”Pragmatic.Endpoints depends on Pragmatic.Actions at the package level. Most real endpoints also use Pragmatic.Persistence, Pragmatic.Validation, and Pragmatic.Mapping. The SG detects referenced packages and unlocks additional endpoint patterns automatically.
Authorization (Ecosystem)
Section titled “Authorization (Ecosystem)”Add Pragmatic.Authorization for permission-based access control. The attributes live in Pragmatic.Abstractions but enforcement requires the authorization runtime:
[RequirePermission("billing.invoice.read")] // User must have this permission[RequirePermission("a", "b")] // User must have ALL listed[RequireAnyPermission("a", "b")] // User must have at least one[RequirePolicy<MyPolicy>] // Composable ResourcePolicy checkWhen no explicit authorization is set and entity context is available, the SG auto-derives a CRUD permission (e.g., catalog.product.read for a GET on Product).
DomainAction Endpoints
Section titled “DomainAction Endpoints”Add Pragmatic.Actions to get a full pipeline (validation, authorization, logging, transactions) on your endpoint:
[DomainAction][Endpoint(HttpVerb.Post, "api/v1/reservations")][Validate][RequirePolicy<ReservationManagementPolicy>]public partial class CreateReservationAction : DomainAction<Guid, RoomUnavailableError>{ private IRepository<Reservation, Guid> _reservations = null!;
public required CreateReservationRequest Request { get; init; }
public override async Task<Result<Guid, IError>> Execute(CancellationToken ct) { /* ... */ }}The SG routes the request through the action invoker pipeline instead of calling HandleAsync() directly.
Convention-based versioning — same HandleAsyncV{n} pattern as plain endpoints, but using ExecuteV{n}:
public override async Task<Result<Guid, IError>> Execute(CancellationToken ct) { /* v1 */ }public async Task<Result<Guid, IError>> ExecuteV2(CancellationToken ct) { /* v2 */ }See Pragmatic.Actions.
Mutation Endpoints
Section titled “Mutation Endpoints”Add Pragmatic.Actions + Pragmatic.Persistence for entity CRUD with zero boilerplate:
[Mutation(Mode = MutationMode.Create)][Endpoint(HttpVerb.Post, "api/v1/amenities")][RequirePermission(CatalogPermissions.Amenity.Create)]public partial class CreateAmenityMutation : Mutation<Amenity>{ public required string Name { get; init; } public AmenityCategory Category { get; init; }}Modes: Create, Update, Delete (soft-delete), Restore. The SG generates load, validate, apply, persist. Override ApplyAsync for custom logic. See Query and Mutation Endpoints.
Query Endpoints
Section titled “Query Endpoints”Add Pragmatic.Persistence for paginated search with filtering, sorting, and projection:
[Query<Amenity, AmenityDto>][Endpoint(HttpVerb.Get, "api/v1/amenities/search")][Cacheable(Duration = "10m", Tags = ["amenities"])]public partial class SearchAmenitiesQuery{ [Filter(Operator = FilterOperator.Contains)] public string? Name { get; init; } [Filter] public AmenityCategory? Category { get; init; } [Sort(DefaultDirection = 0)] public SortDirection? NameSort { get; init; } public int Page { get; init; } = 1; public int PageSize { get; init; } = 50;}Properties with [Filter] and [Sort] become query string parameters. The SG generates the full query execution pipeline.
Autocomplete Endpoints
Section titled “Autocomplete Endpoints”Add Pragmatic.Persistence — [Autocomplete] on entity string properties generates search-as-you-type endpoints:
[Autocomplete]public string Name { get; private set; } = "";// Generates: GET /autocomplete/name?term=...&limit=10[Autocomplete<TDto>] projects to a custom DTO. Customize Route and DefaultLimit.
Package Endpoints
Section titled “Package Endpoints”Promote a package DomainAction to HTTP with [ExposeEndpoint] on a module:
[ExposeEndpoint<RegisterUser>(HttpVerb.Post, "api/identity/register")][ExposeEndpoint<LoginUser>(HttpVerb.Post, "api/identity/login")]public partial class BookingModule;Ecosystem Attributes
Section titled “Ecosystem Attributes”These attributes require ecosystem packages:
| Attribute | Requires | Description |
|---|---|---|
[RequirePermission(...)] | Pragmatic.Authorization | User must have all listed permissions |
[RequireAnyPermission(...)] | Pragmatic.Authorization | User must have at least one |
[RequirePolicy<T>] | Pragmatic.Authorization | Composable ResourcePolicy check |
[Validate] | Pragmatic.Validation | SG-generated input validation |
[Cacheable] | Pragmatic.Caching | Response caching with typed keys |
[ExposeEndpoint<T>] | Pragmatic.Actions | Promote package action to HTTP |
Cross-Module Integration
Section titled “Cross-Module Integration”| Module | What it adds to Endpoints |
|---|---|
| Pragmatic.Actions | DomainAction/Mutation endpoint patterns, invoker pipeline, convention versioning |
| Pragmatic.Validation | Request validation via [Validate] |
| Pragmatic.Persistence | Query/Mutation endpoints, repositories |
| Pragmatic.Mapping | Response DTOs with [MapFrom<T>] projection |
| Pragmatic.Result | Result<T, E> → HTTP status codes |
| Pragmatic.Authorization | [RequirePermission], CRUD auto-derivation |
| Pragmatic.Caching | [Cacheable] on Query endpoints |
Documentation
Section titled “Documentation”| Guide | What you will learn |
|---|---|
| Architecture and Concepts | Mental model, pipeline lifecycle, decision tree for choosing endpoint types |
| Getting Started | First endpoint from zero to HTTP response in 5 minutes |
Features
Section titled “Features”| Guide | What it covers |
|---|---|
| Binding Reference | Route, query, header, body, form, claim binding — all scenarios |
| Endpoint Groups | Shared route prefix, tags, auth, nesting |
| Endpoint Processors | Pre/post processor pipeline, ordering, short-circuit |
| API Versioning | HandleAsyncV2 convention, SinceVersion, versioned body DTOs |
| Rate Limiting | Inline, named policies, all 4 strategies, distributed |
| Response Caching | Output cache, VaryBy, distributed via Pragmatic.Caching |
| File Upload and Download | IFormFile, validation, FileResponse, ETag, range requests |
| Error Handling | Result types, ProblemDetails RFC 7807, custom errors |
| DomainAction Integration | DomainAction pipeline, invoker, convention versioning |
| Query and Mutation Endpoints | Query filters, Mutation CRUD, pagination |
| Guide | When to use |
|---|---|
| Common Mistakes | Wrong code → right code for the 12 most common pitfalls |
| Troubleshooting | Checklists for 404s, binding issues, auth failures, diagnostics |
Samples
Section titled “Samples”| Project | What it shows |
|---|---|
| Pragmatic.Endpoints.Samples | 17 endpoint patterns: CRUD, groups, auth, file upload, versioning |
| Showcase.Booking | Real-world: reservations, guests, check-in, cross-boundary events |
| Showcase.Billing | Invoices, payments, refunds with DomainAction endpoints |
| Showcase.Catalog | Properties, amenities, room types — full CRUD with GridFilter |
Requirements
Section titled “Requirements”- .NET 10.0+
- ASP.NET Core 10.0+ (for Pragmatic.Endpoints.AspNetCore)
- Pragmatic.SourceGenerator analyzer