Skip to content

Pragmatic.Endpoints

Source-generated HTTP endpoints for ASP.NET Core. Declare the shape, the generator writes the plumbing.

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 GET
app.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);

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.

Terminal window
dotnet add package Pragmatic.Endpoints

Add the source generator:

<ProjectReference Include="..\Pragmatic.SourceGenerator\...\Pragmatic.SourceGenerator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

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 registers GET /api/products/{id} with ASP.NET Core
  • SetDependencies() that resolves _products from DI
  • OpenAPI metadata (produces 200 with ProductDto, 404 with NotFoundError)

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"));
}

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 204
public partial class EnqueueJobEndpoint : VoidEndpoint { /* ... */ }

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 Error
public 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 TypeDefault Status
ValidationError400
UnauthorizedError401
ForbiddenError403
NotFoundError404
ConflictError409
Other IError422

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.


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:

AttributeSourceAuto-inferred?Example
[FromRoute]URL path segmentYes, by name match/products/{id}public Guid Id
[FromQuery]Query stringNo?page=1[FromQuery] public int Page
[FromHeader]HTTP headerNo[FromHeader("X-Api-Key")] public string ApiKey
[FromBody]Request body (JSON)Force a property into the body
[FromForm]Form data / IFormFileNoFile uploads and form fields
[FromClaim("sub")]JWT claimNo[FromClaim("sub")] public Guid UserId
(no attribute)Generated body DTORemaining props → sealed record {Type}Body

All binding attributes are in Pragmatic.Endpoints.Attributes — no FrameworkReference to ASP.NET Core needed in the declaring assembly.

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.

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.


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.


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/products

Groups 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";
});
});

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 code
public 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.

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 auth

The 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 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

[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.

Return FileResponse with optional ETag, LastModified, EnableRangeProcessing, Inline. Implement IETagSupport on response DTOs for conditional GET.


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 → HandleAsyncV2

Supports 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 DTO

Requires Asp.Versioning.Http NuGet package. Without it, only the base version handler is generated and PRAG0551 warns.


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

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.

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-store

Storage: 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 backend
app.UseOutputCache();

This way [ResponseCache] on endpoints and [Cacheable] on domain actions share the same distributed backend.

[ResponseCache][Cacheable]
LayerHTTP (transport)Business (domain)
What it cachesSerialized HTTP responseAction/query result object
ScopeEndpoints onlyActions, queries, any service
BackendASP.NET Core Output CachePragmatic.Caching (HybridCache)
PackagePragmatic.EndpointsPragmatic.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).


Attach logic before or after the handler:

[PreProcessor<TenantValidationProcessor>]
[PostProcessor<AuditLogProcessor>]
public partial class CreateOrderEndpoint : Endpoint<OrderDto> { /* ... */ }
  • IEndpointPreProcessor: runs before handler, can short-circuit with PreProcessorResult.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 Order property on the attribute

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");
});
});
OptionDefaultDescription
RoutePrefix""Global route prefix — wraps all endpoints in MapGroup(prefix).
RequireAuthorizationByDefaultfalseAll endpoints require authentication unless [AllowAnonymous]. Applied to the root MapGroup.
DefaultRateLimitPolicynullNamed rate limit policy applied to all endpoints. Applied to the root MapGroup.
MaxUploadFileSizenullGlobal max file upload size in bytes. Overridden by [MaxFileSize] per property.
DefaultAllowedContentTypesnullGlobal allowed MIME types for uploads. Overridden by [AllowedContentTypes] per property.
RateLimitRejectionStatusCode429HTTP 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.

These options are automatically applied by the SG-generated AddPragmaticEndpoints() — no separate ASP.NET Core configuration needed:

OptionDefaultWhat It Does
UseCamelCaseJsontrueAuto-calls ConfigureHttpJsonOptions() with CamelCase naming policy.
EnableResponseCompressionfalseWhen true, auto-calls AddResponseCompression(). Add app.UseResponseCompression() in pipeline.
IncludeStackTraceInErrorsfalseAuto-calls AddProblemDetails() with stack trace in extensions. Development only.
EnableOpenApitrueSG always generates endpoint metadata (Produces, WithSummary, WithTags). Call AddOpenApi() to activate the document.
OpenApiTitle"API"Passed to AddOpenApi() when you configure OpenAPI.
DefaultApiVersionnullUse [ApiVersion] on endpoints/groups. Add Asp.Versioning.Http for URL versioning.
EnableRequestValidationtrueValidation runs per-endpoint via [Validate] attribute.

Errors convert to RFC 7807 ProblemDetails via Pragmatic.Endpoints.AspNetCore. Full guide: Error Handling.


For each [Endpoint] class:

GeneratedPurpose
MapEndpoint()Static method registering the route with ASP.NET Core
{TypeName}BodySealed record for body properties (when applicable)
SetDependencies()Resolves private fields from DI
PragmaticEndpointContractAttributeAssembly metadata for host discovery

AttributeTargetDescription
[Endpoint(HttpVerb, route)]ClassHTTP endpoint with method and route
[EndpointGroup(prefix)]ClassGroup with shared prefix, tag, version
[FromRoute]PropertyBind from URL path
[FromQuery]PropertyBind from query string
[FromHeader]PropertyBind from HTTP header
[FromBody]PropertyExplicit body binding
[FromForm]PropertyForm data / IFormFile
[FromClaim(type)]PropertyJWT claim binding
[EndpointSummary(text)]ClassOpenAPI summary
[EndpointDescription(text)]ClassOpenAPI description (Markdown)
[Tags(...)]ClassOpenAPI tags
[HttpStatus(code)]Class/ErrorOverride HTTP status code
[ApiVersion(ver)]ClassAPI version (Deprecated, SunsetDate)
[SinceVersion(ver)]PropertyProperty introduced in version
[AllowAnonymous]ClassOverride group authorization
[RateLimit]ClassRate limiting
[ResponseCache]ClassResponse caching
[PreProcessor<T>]ClassPre-processor with Order
[PostProcessor<T>]ClassPost-processor with Order
[MaxFileSize(bytes)]PropertyFile size limit (413)
[AllowedContentTypes(...)]PropertyMIME type filter (415)
[Autocomplete] / [Autocomplete<TDto>]Entity propertySearch endpoint
[ExposeEndpoint<T>]Module classPromote package action to HTTP

IDSeverityDescription
PRAG0500ErrorEndpoint class must be partial
PRAG0501ErrorMust inherit from Endpoint or VoidEndpoint
PRAG0502ErrorRoute is required
PRAG0503ErrorToo many error types (max 6)
PRAG0504WarningRoute parameter does not match any property
PRAG0505ErrorDuplicate endpoint name
PRAG0506ErrorInvalid rate limit configuration
PRAG0507ErrorEndpoint group not found
PRAG0508ErrorProcessor must implement interface
PRAG0510InfoEndpoint registered
PRAG0512InfoProperty implicitly binds to body
PRAG0515ErrorEntity has no key for [Autocomplete]
PRAG0550Error[Autocomplete] requires string property
PRAG0551WarningVersioned Execute requires Asp.Versioning.Http

PackagePurpose
Pragmatic.EndpointsCore: attributes, base classes, interfaces. No ASP.NET Core dependency.
Pragmatic.Endpoints.AspNetCoreRuntime: ProblemDetails, FileResponse, EndpointContext.
Pragmatic.SourceGeneratorCompile-time: generates handlers, DTOs, registration, metadata.

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.

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 check

When 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).

See Pragmatic.Authorization.

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.

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.

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.

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.

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;

These attributes require ecosystem packages:

AttributeRequiresDescription
[RequirePermission(...)]Pragmatic.AuthorizationUser must have all listed permissions
[RequireAnyPermission(...)]Pragmatic.AuthorizationUser must have at least one
[RequirePolicy<T>]Pragmatic.AuthorizationComposable ResourcePolicy check
[Validate]Pragmatic.ValidationSG-generated input validation
[Cacheable]Pragmatic.CachingResponse caching with typed keys
[ExposeEndpoint<T>]Pragmatic.ActionsPromote package action to HTTP
ModuleWhat it adds to Endpoints
Pragmatic.ActionsDomainAction/Mutation endpoint patterns, invoker pipeline, convention versioning
Pragmatic.ValidationRequest validation via [Validate]
Pragmatic.PersistenceQuery/Mutation endpoints, repositories
Pragmatic.MappingResponse DTOs with [MapFrom<T>] projection
Pragmatic.ResultResult<T, E> → HTTP status codes
Pragmatic.Authorization[RequirePermission], CRUD auto-derivation
Pragmatic.Caching[Cacheable] on Query endpoints

GuideWhat you will learn
Architecture and ConceptsMental model, pipeline lifecycle, decision tree for choosing endpoint types
Getting StartedFirst endpoint from zero to HTTP response in 5 minutes
GuideWhat it covers
Binding ReferenceRoute, query, header, body, form, claim binding — all scenarios
Endpoint GroupsShared route prefix, tags, auth, nesting
Endpoint ProcessorsPre/post processor pipeline, ordering, short-circuit
API VersioningHandleAsyncV2 convention, SinceVersion, versioned body DTOs
Rate LimitingInline, named policies, all 4 strategies, distributed
Response CachingOutput cache, VaryBy, distributed via Pragmatic.Caching
File Upload and DownloadIFormFile, validation, FileResponse, ETag, range requests
Error HandlingResult types, ProblemDetails RFC 7807, custom errors
DomainAction IntegrationDomainAction pipeline, invoker, convention versioning
Query and Mutation EndpointsQuery filters, Mutation CRUD, pagination
GuideWhen to use
Common MistakesWrong code → right code for the 12 most common pitfalls
TroubleshootingChecklists for 404s, binding issues, auth failures, diagnostics
ProjectWhat it shows
Pragmatic.Endpoints.Samples17 endpoint patterns: CRUD, groups, auth, file upload, versioning
Showcase.BookingReal-world: reservations, guests, check-in, cross-boundary events
Showcase.BillingInvoices, payments, refunds with DomainAction endpoints
Showcase.CatalogProperties, amenities, room types — full CRUD with GridFilter
  • .NET 10.0+
  • ASP.NET Core 10.0+ (for Pragmatic.Endpoints.AspNetCore)
  • Pragmatic.SourceGenerator analyzer