Skip to content

Binding Reference

Complete guide to request binding in Pragmatic.Endpoints.

Route parameters are extracted from the URL path. The SG matches properties to route segments by name (case-insensitive). If your route has {id} and your class has a property named Id, the binding happens automatically — [FromRoute] is optional:

[Endpoint(HttpVerb.Get, "/users/{id}")]
public partial class GetUserEndpoint : Endpoint<UserDto>
{
public Guid Id { get; set; } // auto-bound: matches {id} by name
// [FromRoute] is optional — only needed when the property name
// doesn't match the route parameter name
}
[Endpoint(HttpVerb.Get, "/customers/{customerId}/orders/{orderId}")]
public partial class GetOrderEndpoint : Endpoint<OrderDto>
{
[FromRoute]
public Guid CustomerId { get; set; }
[FromRoute]
public Guid OrderId { get; set; }
}

Route constraints are specified in the route template:

[Endpoint(HttpVerb.Get, "/products/{id:int}")]
public partial class GetProductEndpoint : Endpoint<ProductDto>
{
[FromRoute]
public int Id { get; set; }
}
[Endpoint(HttpVerb.Get, "/files/{**path}")] // Catch-all
public partial class GetFileEndpoint : Endpoint<FileResponse>
{
[FromRoute]
public string Path { get; set; } = null!;
}

Query parameters come from the URL query string.

[Endpoint(HttpVerb.Get, "/search")]
public partial class SearchEndpoint : Endpoint<SearchResults>
{
// Required - uses 'required' keyword
[FromQuery]
public required string Query { get; set; }
}
// GET /search?query=hello
[Endpoint(HttpVerb.Get, "/products")]
public partial class ListProductsEndpoint : Endpoint<ProductList>
{
// Optional - nullable type
[FromQuery]
public string? Category { get; set; }
// Optional - value type with nullable
[FromQuery]
public int? Page { get; set; }
// Optional - with default value
[FromQuery]
public int PageSize { get; set; } = 20;
}
// GET /products?category=electronics&page=2
[Endpoint(HttpVerb.Get, "/search")]
public partial class SearchEndpoint : Endpoint<SearchResults>
{
[FromQuery(Name = "q")]
public required string Query { get; set; }
[FromQuery(Name = "max_results")]
public int MaxResults { get; set; } = 10;
}
// GET /search?q=hello&max_results=50
[Endpoint(HttpVerb.Get, "/products")]
public partial class FilterProductsEndpoint : Endpoint<ProductList>
{
[FromQuery]
public List<string>? Tags { get; set; }
[FromQuery]
public int[]? Ids { get; set; }
}
// GET /products?tags=electronics&tags=sale&ids=1&ids=2&ids=3

Header parameters come from HTTP request headers.

[Endpoint(HttpVerb.Post, "/api/data")]
public partial class DataEndpoint : Endpoint<DataResponse>
{
// Required - uses 'required' keyword
[FromHeader(Name = "X-Api-Key")]
public required string ApiKey { get; set; }
}
[Endpoint(HttpVerb.Post, "/api/data")]
public partial class DataEndpoint : Endpoint<DataResponse>
{
// Optional - nullable type
[FromHeader(Name = "X-Correlation-Id")]
public string? CorrelationId { get; set; }
// Optional - with default
[FromHeader(Name = "X-Request-Timeout")]
public int RequestTimeout { get; set; } = 30;
}
[Endpoint(HttpVerb.Put, "/resources/{id}")]
public partial class UpdateResourceEndpoint : Endpoint<ResourceDto>
{
[FromRoute]
public Guid Id { get; set; }
// ETag for optimistic concurrency
[FromHeader(Name = "If-Match")]
public string? IfMatch { get; set; }
// Accept-Language for localization
[FromHeader(Name = "Accept-Language")]
public string? AcceptLanguage { get; set; }
}

Properties without binding attributes become the request body.

[Endpoint(HttpVerb.Post, "/users")]
public partial class CreateUserEndpoint : Endpoint<UserDto>
{
// These properties become the request body DTO
public required string Name { get; set; }
public required string Email { get; set; }
public string? Phone { get; set; }
}
// Generated body DTO:
// public sealed record CreateUserEndpointBody
// {
// public required string Name { get; init; }
// public required string Email { get; init; }
// public string? Phone { get; init; }
// }
[Endpoint(HttpVerb.Post, "/orders")]
public partial class CreateOrderEndpoint : Endpoint<OrderDto>
{
public required Guid CustomerId { get; set; }
public required List<OrderItem> Items { get; set; }
public Address? ShippingAddress { get; set; }
public PaymentInfo? Payment { get; set; }
}
public record OrderItem(Guid ProductId, int Quantity);
public record Address(string Street, string City, string Country);
public record PaymentInfo(string CardNumber, string Expiry);
[Endpoint(HttpVerb.Post, "/articles")]
public partial class CreateArticleEndpoint : Endpoint<ArticleDto>
{
/// <summary>
/// The article title. Must be unique.
/// </summary>
public required string Title { get; set; }
/// <summary>
/// The article content in Markdown format.
/// </summary>
public required string Content { get; set; }
/// <summary>
/// Tags for categorization. Maximum 5 tags allowed.
/// </summary>
public List<string>? Tags { get; set; }
}

Combine multiple binding sources in one endpoint:

[Endpoint(HttpVerb.Put, "/customers/{customerId}/orders/{orderId}")]
[EndpointSummary("Update Order")]
public partial class UpdateOrderEndpoint : Endpoint<OrderDto>
{
// Route parameters
[FromRoute]
public Guid CustomerId { get; set; }
[FromRoute]
public Guid OrderId { get; set; }
// Required header
[FromHeader(Name = "X-Idempotency-Key")]
public required string IdempotencyKey { get; set; }
// Optional header
[FromHeader(Name = "X-Correlation-Id")]
public string? CorrelationId { get; set; }
// Query parameter
[FromQuery]
public bool? Notify { get; set; }
// Body properties
public required List<OrderItem> Items { get; set; }
public string? Notes { get; set; }
}
// Request:
// PUT /customers/123/orders/456?notify=true
// Headers:
// X-Idempotency-Key: abc123
// X-Correlation-Id: corr-789
// Body:
// { "items": [...], "notes": "Rush order" }

The generator automatically orders parameters correctly:

  1. Route parameters - First (positional in URL)
  2. Required headers - Next (mandatory)
  3. Required body - If using body DTO
  4. Required query - If any required
  5. Dependencies - DI injected services
  6. HttpContext - If needed
  7. CancellationToken - Always last required
  8. Optional headers - With = default
  9. Optional query - With = default

This ensures C# parameter ordering rules (required before optional) are satisfied.

Combine with Pragmatic.Validation:

[Endpoint(HttpVerb.Post, "/users")]
public partial class CreateUserEndpoint : Endpoint<UserDto>
{
[Required]
[Email]
public required string Email { get; set; }
[Required]
[MinLength(2)]
[MaxLength(50)]
public required string Name { get; set; }
[Phone]
public string? Phone { get; set; }
}

Validation runs automatically before HandleAsync via the DomainAction pipeline.

[Endpoint(HttpVerb.Post, "/files")]
public partial class UploadFileEndpoint : Endpoint<FileInfo>
{
[FromForm]
public required IFormFile File { get; set; }
[FromForm]
public string? Description { get; set; }
}
  1. Use required for mandatory parameters - Clear intent and compile-time safety
  2. Use nullable types for optional - string?, int? instead of defaults
  3. Name headers explicitly - [FromHeader(Name = "X-...")]
  4. Document body properties - XML comments become OpenAPI descriptions
  5. Group related endpoints - Use [EndpointGroup] for common prefixes
  6. Keep endpoints focused - One responsibility per endpoint class