Pragmatic.Validation
Zero-allocation, source-generated validation for .NET 10.
The Problem
Section titled “The Problem”Validation in .NET applications scatters across layers and styles. Inline if checks bury validation inside business logic and get duplicated across actions. DataAnnotations uses reflection on every call, cannot do async validation, and handles cross-property rules awkwardly via IValidatableObject. FluentValidation is expressive but runtime-only — rules are evaluated through expression trees, and the validator class lives separately from the model it validates.
In all three approaches, validation rules are either scattered, reflection-based, or disconnected from the model. None integrates natively with a DomainAction pipeline, entity change tracking, or compile-time code generation.
The Solution
Section titled “The Solution”Declare validation rules as attributes on the model itself. The source generator produces the exact validation code at compile time — no reflection, no runtime expression evaluation, no separate validator class to maintain.
public partial class CreateReservationRequest{ [Required] [NotWhiteSpace] public string GuestName { get; init; } = "";
[Required] [Email] public string Email { get; init; } = "";
[FutureDate] public DateTimeOffset CheckIn { get; init; }
[GreaterThanProperty(nameof(CheckIn))] public DateTimeOffset CheckOut { get; init; }
[Positive] [Range(1, 20)] public int NumberOfGuests { get; init; } = 1;}The SG generates ISyncValidator.Validate() with inline if statements. For database checks, implement IAsyncValidator<T> with [Validator] and the SG wires up DI and CompositeValidator<T> automatically. Add [Validate] on a DomainAction and the pipeline runs validation before Execute().
Overview
Section titled “Overview”Validation runs at two levels in the Pragmatic stack:
- Level 1 (Input): Validates input DTOs before any database access. Sync attributes (
[Required],[Email],[Range]) run first, then async validators (IAsyncValidator<T>) for uniqueness or external checks. - Level 2 (Entity): Validates entity invariants after mutation application, ensuring the entity is consistent before persistence. Change-aware validation only re-validates modified properties.
The [Validate] attribute on a DomainAction triggers the full pipeline automatically via ValidationFilter.
Installation
Section titled “Installation”dotnet add package Pragmatic.ValidationThe Pragmatic.SourceGenerator analyzer must be referenced for code generation:
<ProjectReference Include="..\Pragmatic.SourceGenerator\src\Pragmatic.SourceGenerator\Pragmatic.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />Quick Start
Section titled “Quick Start”1. Define a DTO with validation attributes
Section titled “1. Define a DTO with validation attributes”Decorate properties on a partial class (or record) with validation attributes. The source generator detects them and generates ISyncValidator.Validate():
using Pragmatic.Validation.Attributes;
public partial class RegisterGuestRequest{ [Required] [NotWhiteSpace] public string FirstName { get; init; } = "";
[Required] [NotWhiteSpace] public string LastName { get; init; } = "";
[Required] [Email] public string Email { get; init; } = "";
[Phone] public string? Phone { get; init; }}The SG generates a Validate() method on RegisterGuestRequest that implements ISyncValidator. You can call it directly:
var request = new RegisterGuestRequest{ FirstName = "", LastName = "Doe", Email = "not-an-email"};
var result = request.Validate();// result.IsFailure == true// result.Issues contains:// - FirstName: "validation.required"// - FirstName: "validation.notwhitespace"// - Email: "validation.email"2. Add an async validator for database checks
Section titled “2. Add an async validator for database checks”For validation that requires I/O (database lookups, external services), implement IAsyncValidator<T> and mark it with [Validator]:
using Pragmatic.Validation;using Pragmatic.Validation.Attributes;using Pragmatic.Validation.Types;
[Validator]public class CreateReservationValidator : IAsyncValidator<CreateReservationRequest>{ private readonly IReadRepository<RoomType, Guid> _roomTypes;
public CreateReservationValidator(IReadRepository<RoomType, Guid> roomTypes) => _roomTypes = roomTypes;
public async Task<ValidationError> ValidateAsync( CreateReservationRequest request, CancellationToken ct = default) { var roomType = await _roomTypes.GetByIdAsync(request.RoomTypeId, ct) .ConfigureAwait(false);
if (roomType is null) return ValidationError.For("RoomTypeId", "validation.room_type.not_found");
if (request.NumberOfGuests > roomType.MaxOccupancy) return ValidationError.For("NumberOfGuests", "validation.room_type.max_occupancy");
return ValidationError.Valid; }}The SG auto-registers this validator in DI and generates a CompositeValidator<T> that runs sync validation first (from attributes), then async validation.
3. Register validation services
Section titled “3. Register validation services”// In your IStartupStep or Program.csbuilder.Services.AddPragmaticValidation();
// With options:builder.Services.AddPragmaticValidation(options =>{ options.FailFast = true; // Stop on first error});4. Use in a DomainAction
Section titled “4. Use in a DomainAction”[DomainAction][Validate]public partial class CreateReservation : DomainAction<Reservation>{ // ... properties with validation attributes // ValidationFilter runs automatically before Execute()}How It Works
Section titled “How It Works”Source Generator Pipeline
Section titled “Source Generator Pipeline”The unified Pragmatic.SourceGenerator detects validation attributes on partial types and generates:
ISyncValidator.Validate()— A method on the type itself that checks all attribute rules. No reflection at runtime; conditions are inlined asifstatements.- DI registration —
[Validator]classes are auto-registered asIAsyncValidator<T>. The SG also registersCompositeValidator<T>asIValidator<T>when both sync and async validation exist. IAsyncValidatorBindings<T>— For[AsyncValidate<T>]bindings, the SG generates metadata that tellsCompositeValidatorwhich async validators to invoke based on which properties changed.- Body DTO validation — For
[Endpoint]actions, the SG predicts which body DTOs will be generated and addsISyncValidatorto them.
Validation Order
Section titled “Validation Order”When IValidator<T>.ValidateAsync() is called (via CompositeValidator<T>):
1. Sync validation (ISyncValidator.Validate) - Runs all attribute checks (Required, Email, Range, etc.) - Fast, in-memory, no I/O | v2. If FailFast=true AND sync failed --> return errors immediately | v3. Async validation (IAsyncValidator<T>.ValidateAsync) - Database lookups, external API calls - Filtered by IAsyncValidatorBindings<T> if available | v4. Combine errors from both phases --> return ValidationErrorChange-Aware Validation (Entities)
Section titled “Change-Aware Validation (Entities)”When the validated type has [Entity], the SG generates an overload Validate(IReadOnlySet<string>? modifiedProperties):
- When
modifiedPropertiesisnull(create mode), all properties are validated. - When
modifiedPropertiesis provided (update mode), only rules for modified properties run. - Cross-property dependencies are tracked: if
EndDatehas[GreaterThanProperty(nameof(StartDate))], modifyingStartDatealso re-validatesEndDate.
Core Types
Section titled “Core Types”ValidationError
Section titled “ValidationError”A readonly struct that implements IError (400 Bad Request) with Result-like semantics:
// Empty (valid) stateValidationError error = ValidationError.Valid;error.IsSuccess // trueerror.IsFailure // false
// Single issue (property-first syntax)var error = ValidationError.For("Email", "validation.required");
// Fluent accumulationvar error = ValidationError.Valid .WithFor("Email", "validation.required") .WithFor("Name", "validation.minlength", ("min", 2));
// Collection expression syntaxValidationError errors = [ new ValidationIssue("validation.required", "Email"), new ValidationIssue("validation.minlength", "Name", ("min", 3))];
// Combine two errorsvar combined = errorA.Combine(errorB);
// Nested paths (collections)var error = ValidationError.Valid .WithNested("Items", 0, "ProductId", "validation.required");// PropertyPath = "Items[0].ProductId"
// Pattern matchingerror.Match( onValid: () => "All good", onInvalid: issues => $"Failed: {issues.Count} issues");
// Convert to ResultVoidResult<ValidationError> result = error.ToResult();ValidationIssue
Section titled “ValidationIssue”A readonly record struct representing a single validation failure:
public readonly record struct ValidationIssue{ public string MessageKey { get; } // "validation.required" public string? PropertyPath { get; } // "Email", "Items[0].Name", null for object-level public IReadOnlyDictionary<string, object>? Parameters { get; } // {"min": 3}}
// Factory methodsvar issue = ValidationIssue.For("Email", "validation.required");var nested = ValidationIssue.ForNested("Items", 0, "ProductId", "validation.required");var objectLevel = ValidationIssue.ForObject("validation.date_range_invalid");Interfaces
Section titled “Interfaces”| Interface | Purpose |
|---|---|
ISyncValidator | Generated by SG. Has Validate() and Validate(IReadOnlySet<string>?) for change-aware validation. |
IAsyncValidator<T> | Manual implementation for I/O-bound validation (DB, external services). |
IValidator<T> | Combined sync + async. Implemented by CompositeValidator<T>. Inject this in services. |
IAsyncValidatorBindings<T> | Generated by SG from [AsyncValidate<T>]. Maps validators to trigger properties. |
CompositeValidator<T>
Section titled “CompositeValidator<T>”The runtime orchestrator that combines sync and async validation:
- Runs
ISyncValidator.Validate()first (if the instance implements it). - If
FailFastis enabled and sync failed, returns immediately. - Runs all
IAsyncValidator<T>instances (filtered byIAsyncValidatorBindings<T>when available). - Combines all errors and returns a single
ValidationError. - Emits structured logging via
[LoggerMessage]and OpenTelemetry metrics/traces.
ValidationOptions
Section titled “ValidationOptions”builder.Services.AddPragmaticValidation(options =>{ // Stop at the first error (default: false -- accumulate all errors) options.FailFast = true;
// Include full property path in nested errors (default: true) // "Items[0].ProductId" vs just "ProductId" options.IncludePropertyPath = true;});Attribute Reference
Section titled “Attribute Reference”All attributes are in the Pragmatic.Validation.Attributes namespace. Null values pass validation for all attributes except [Required] — use [Required] to enforce non-null.
Presence
Section titled “Presence”| Attribute | Description | Message Key |
|---|---|---|
[Required] | Value must not be null (or empty string by default). AllowEmptyStrings property controls empty string behavior. | validation.required |
[NotEmpty] | String must have length > 0, collection must have count > 0. | validation.notempty |
[NotWhiteSpace] | String must not be null, empty, or whitespace only. Stricter than [Required]. | validation.notwhitespace |
public partial class CreateUserRequest{ [Required] public string Email { get; init; } = "";
[Required(AllowEmptyStrings = true)] // Allows "" public string Name { get; init; } = "";
[Required] [NotEmpty] public List<string> Tags { get; init; } = [];
[NotWhiteSpace] // Rejects " " public string? Bio { get; init; }}String Length
Section titled “String Length”| Attribute | Description | Constructor | Message Key |
|---|---|---|---|
[MinLength(n)] | Minimum character count (or collection count). | MinLength(int length) | validation.minlength |
[MaxLength(n)] | Maximum character count (or collection count). | MaxLength(int length) | validation.maxlength |
[Length(min, max)] | Character count must be in range. Combines MinLength + MaxLength. | Length(int min, int max) | validation.length |
public partial class CreateProductRequest{ [MinLength(2)] [MaxLength(100)] public string Name { get; init; } = "";
[Length(2, 100)] // Equivalent to above public string Description { get; init; } = "";}Format
Section titled “Format”| Attribute | Description | Message Key |
|---|---|---|
[Email] | Valid email format (practical regex, not RFC 5322). | validation.email |
[Phone] | Valid phone number (digits, spaces, hyphens, optional +). 7-15 digits. | validation.phone |
[Url] | Valid URL. AllowedSchemes defaults to ["http", "https"]. RequireAbsolute defaults to true. | validation.url |
[CreditCard] | Valid credit card number (Luhn algorithm). 13-19 digits. | validation.creditcard |
[Regex(pattern)] | Matches a regular expression. Compiled with 250ms timeout (ReDoS protection). | validation.regex |
[Guid] | Valid GUID format string. | validation.guid |
public partial class PaymentRequest{ [Required] [Email] public string Email { get; init; } = "";
[Phone] public string? Phone { get; init; }
[Required] [CreditCard] public string CardNumber { get; init; } = "";
[Url] public string? Website { get; init; }
[Url(AllowedSchemes = new[] { "http", "https", "ftp" })] public string? FileUrl { get; init; }
[Regex(@"^[A-Z]{2}-\d{4}$")] public string? ProductCode { get; init; }
[Guid] public string? ExternalId { get; init; }}Numeric
Section titled “Numeric”| Attribute | Description | Constructor | Message Key |
|---|---|---|---|
[Range(min, max)] | Value must be in range (inclusive). Supports int and double constructors. | Range(int min, int max) or Range(double min, double max) | validation.range |
[Positive] | Value must be > 0. Zero is not positive. | None | validation.positive |
[Negative] | Value must be < 0. Zero is not negative. | None | validation.negative |
[GreaterThan(n)] | Value must be > n (exclusive). | GreaterThan(int value) or GreaterThan(double value) | validation.greaterthan |
[GreaterThanOrEqual(n)] | Value must be >= n (inclusive). | GreaterThanOrEqual(int value) or GreaterThanOrEqual(double value) | validation.greaterthanorequal |
[LessThan(n)] | Value must be < n (exclusive). | LessThan(int value) or LessThan(double value) | validation.lessthan |
[LessThanOrEqual(n)] | Value must be <= n (inclusive). | LessThanOrEqual(int value) or LessThanOrEqual(double value) | validation.lessthanorequal |
All numeric attributes support: int, long, short, byte, sbyte, uint, ulong, ushort, float, double, decimal.
public partial class CreateReservationRequest{ [Positive] [Range(1, 20)] public int NumberOfGuests { get; init; } = 1;
[GreaterThan(0)] public decimal Price { get; init; }
[LessThanOrEqual(100)] public decimal DiscountPercent { get; init; }}Collection
Section titled “Collection”| Attribute | Description | Constructor | Message Key |
|---|---|---|---|
[MinCount(n)] | Collection must have >= n elements. | MinCount(int count) | validation.mincount |
[MaxCount(n)] | Collection must have <= n elements. | MaxCount(int count) | validation.maxcount |
[Count(min, max)] | Element count must be in range. Combines MinCount + MaxCount. | Count(int min, int max) | validation.count |
[ValidateElements] | Validate each element in the collection. Element type must implement ISyncValidator. | None | N/A (delegates to element validation) |
[ValidateElements] has a StopOnFirstError property (default false) that controls whether to stop at the first invalid element or validate all.
Error paths include the index: Items[0].ProductId, Items[1].Quantity.
public partial class PlaceOrderRequest{ [Required] [NotEmpty] [MinCount(1)] [MaxCount(50)] [ValidateElements] public List<OrderItemRequest> Items { get; init; } = [];}
public partial class OrderItemRequest{ [Required] public Guid ProductId { get; init; }
[Range(1, 100)] public int Quantity { get; init; }}Comparison (Cross-Property)
Section titled “Comparison (Cross-Property)”These attributes compare the decorated property against another property on the same type. They require RequiresInstance = true and use PropertyAccessorCache at runtime (compiled expression delegates, no per-call reflection).
| Attribute | Description | Message Key |
|---|---|---|
[EqualTo(nameof(Other))] | Value must equal the other property’s value. | validation.equalto |
[NotEqualTo(nameof(Other))] | Value must differ from the other property’s value. | validation.notequalto |
[GreaterThanProperty(nameof(Other))] | Value must be > the other property (uses IComparable). | validation.greaterthanproperty |
[LessThanProperty(nameof(Other))] | Value must be < the other property (uses IComparable). | validation.lessthanproperty |
public partial class ChangePasswordRequest{ [Required] public string CurrentPassword { get; init; } = "";
[Required] [NotEqualTo(nameof(CurrentPassword))] public string NewPassword { get; init; } = "";
[Required] [EqualTo(nameof(NewPassword))] public string ConfirmPassword { get; init; } = "";}
public partial class CreateReservationRequest{ [FutureDate] public DateTimeOffset CheckIn { get; init; }
[GreaterThanProperty(nameof(CheckIn))] public DateTimeOffset CheckOut { get; init; }}Conditional
Section titled “Conditional”| Attribute | Description | Message Key |
|---|---|---|
[RequiredIf(nameof(Other), value)] | Required when the other property equals the expected value. AllowEmptyStrings controls empty string behavior. | validation.requiredif |
[RequiredIfNot(nameof(Other), value)] | Required when the other property does NOT equal the specified value. | validation.requiredifnot |
Both support AllowMultiple = true — you can have multiple conditions on the same property.
public partial class PaymentRequest{ [Required] public PaymentMethod Method { get; init; }
[RequiredIf(nameof(Method), PaymentMethod.CreditCard)] public string? CardNumber { get; init; }
[RequiredIf(nameof(Method), PaymentMethod.BankTransfer)] public string? BankAccount { get; init; }}Extended / Type-Specific
Section titled “Extended / Type-Specific”| Attribute | Description | Message Key |
|---|---|---|
[ValidEnum] | Value must be a defined enum member. Prevents (Status)999. | validation.enum |
[OneOf("a", "b", "c")] | Value must be one of the specified allowed values. | validation.oneof |
[FutureDate] | DateTime/DateTimeOffset must be in the future (compared to UtcNow). | validation.future_date |
[PastDate] | DateTime/DateTimeOffset must be in the past (compared to UtcNow). | validation.past_date |
public partial class ScheduleRequest{ [Required] [ValidEnum] public Priority Priority { get; init; }
[Required] [OneOf("draft", "published", "archived")] public string Status { get; init; } = "";
[FutureDate] public DateTimeOffset StartDate { get; init; }
[PastDate] public DateTime? BirthDate { get; init; }}Custom Message Keys
Section titled “Custom Message Keys”Every attribute supports a MessageKey property to override the default localization key:
[Required(MessageKey = "custom.user.name_required")]public string Name { get; init; } = "";
[Email(MessageKey = "custom.invalid_email_format")]public string Email { get; init; } = "";Custom Validators
Section titled “Custom Validators”Sync: Custom ValidationAttribute
Section titled “Sync: Custom ValidationAttribute”Extend ValidationAttribute to create custom sync validation:
public sealed class FiscalCodeAttribute : ValidationAttribute{ public override string DefaultMessageKey => "validation.fiscalcode";
public override bool IsValid(object? value) { if (value is not string s) return true; // null handled by [Required] return FiscalCodeValidator.IsValid(s); }}For cross-property validation, override RequiresInstance and IsValid(object?, object):
public sealed class DateRangeAttribute : ValidationAttribute{ public string EndProperty { get; }
public DateRangeAttribute(string endProperty) => EndProperty = endProperty;
public override string DefaultMessageKey => "validation.daterange"; public override bool RequiresInstance => true;
public override bool IsValid(object? value, object instance) { if (value is not DateTime start) return true; var end = PropertyAccessorCache.GetValue(instance, EndProperty); return end is DateTime endDate && start < endDate; }}Async: IAsyncValidator<T>
Section titled “Async: IAsyncValidator<T>”Implement IAsyncValidator<T> for validation requiring I/O:
[Validator]public class CreateUserValidator : IAsyncValidator<CreateUserRequest>{ private readonly IReadRepository<User, Guid> _users;
public CreateUserValidator(IReadRepository<User, Guid> users) => _users = users;
public async Task<ValidationError> ValidateAsync( CreateUserRequest request, CancellationToken ct = default) { if (await _users.ExistsByEmailAsync(request.Email, ct)) return ValidationError.For("Email", "validation.email.exists");
return ValidationError.Valid; }}The [Validator] attribute:
- Auto-registers the class in DI as
IAsyncValidator<T>. - Configurable
Lifetime(default:ServiceLifetime.Scoped). - Automatically generates
CompositeValidator<T>registration asIValidator<T>.
Change-Aware Async Validation with [AsyncValidate<T>]
Section titled “Change-Aware Async Validation with [AsyncValidate<T>]”Bind async validators to specific properties so they only fire when those properties change:
// Property-level: fires only when Email changespublic partial class Reservation{ [AsyncValidate<EmailUniquenessValidator>] public string Email { get; private set; }
[AsyncValidate<AvailabilityValidator>] public DateTimeOffset CheckIn { get; private set; }}
// Entity-level: fires on any modification[AsyncValidate<ReservationConsistencyValidator>]public partial class Reservation { ... }The SG generates IAsyncValidatorBindings<Reservation> that CompositeValidator<T> uses to filter which validators to invoke. In create mode (modifiedProperties = null), all bound validators run.
DI Registration
Section titled “DI Registration”Manual Registration Methods
Section titled “Manual Registration Methods”// Register async validator + CompositeValidator as IValidator<T>services.AddValidatorWithComposite<CreateUserValidator, CreateUserRequest>();
// Register async validator only (without CompositeValidator)services.AddAsyncValidator<CreateUserValidator, CreateUserRequest>();
// Register a custom IValidator<T> implementationservices.AddValidator<MyCustomValidator, MyType>();
// Register CompositeValidator for sync-only validation (no async validator)services.AddSyncOnlyValidator<CreateUserRequest>();
// Register async validator bindingsservices.AddAsyncValidatorBindings<ReservationAsyncValidatorBindings, Reservation>();Using IValidator<T> in Services
Section titled “Using IValidator<T> in Services”public class UserService(IValidator<CreateUserRequest> validator){ public async Task<Result<User, IError>> CreateAsync( CreateUserRequest request, CancellationToken ct) { var validation = await validator.ValidateAsync(request, ct); if (validation.IsFailure) return validation; // Implicit conversion: ValidationError -> IError
// ... create user }}Integration with Other Modules
Section titled “Integration with Other Modules”Pragmatic.Actions
Section titled “Pragmatic.Actions”Add [Validate] to a DomainAction to trigger ValidationFilter automatically:
[DomainAction][Validate]public partial class CreateUser : DomainAction<User>{ [Required] [Email] public string Email { get; set; } = "";
[Required] [MinLength(2)] public string Name { get; set; } = "";
public override async Task<Result<User, IError>> Execute(CancellationToken ct) { // If we get here, validation has already passed // ... }}Pragmatic.Endpoints
Section titled “Pragmatic.Endpoints”Body DTOs generated by the Endpoints SG automatically get ISyncValidator validation. The endpoint returns HTTP 400 with the ValidationError when validation fails.
Pragmatic.Result
Section titled “Pragmatic.Result”ValidationError implements IError with StatusCode = 400. You can return it directly from a Result<T, IError>:
Result<User, IError> result = ValidationError.For("Email", "validation.required");Observability
Section titled “Observability”CompositeValidator<T> emits OpenTelemetry metrics and traces:
| Instrument | Name | Type | Description |
|---|---|---|---|
| Histogram | pragmatic.validation.duration | ms | Duration of validation execution |
| Counter | pragmatic.validation.executions | count | Total validation executions |
| Counter | pragmatic.validation.failures | count | Total validation failures |
Activity source: Pragmatic.Validation with activities named Validate.{TypeName}.
Structured logging via [LoggerMessage] covers: validation start, sync pass/fail, async validator execution/skip/fail, completion.
Diagnostics
Section titled “Diagnostics”| ID | Severity | Description |
|---|---|---|
PRAG0200 | Error | Type with validation attributes must be partial. |
PRAG0201 | Error | [Validator] class must implement IAsyncValidator<T>. |
PRAG0202 | Warning | Property has validation attributes but type is not partial. |
PRAG0203 | Error | Referenced property in comparison attribute not found on type. |
PRAG0204 | Warning | [ValidateElements] on a non-collection type. |
PRAG0205 | Error | Collection element type does not implement ISyncValidator. |
PRAG0206 | Warning | [Validator] class does not validate a type with ISyncValidator. |
PRAG0209 | Warning | Incompatible types in comparison attributes (e.g., int vs string). |
Samples
Section titled “Samples”See samples/Pragmatic.Validation.Samples/ for 8 runnable scenarios: basic validation, cross-property ([EqualTo], [RequiredIf], [GreaterThanProperty]), format/date ([Regex], [Guid], [FutureDate], [PastDate], [ValidEnum], [OneOf]), nested collection ([ValidateElements]), ValidationError API (fluent building, combining, collection expressions), async validator with DI, change-aware validation, and deep nested with custom MessageKey.
| Concepts | Architecture, validation pipeline, core types, integration with other modules. | | Getting Started | Add validation to your first action, step by step. | | Attributes Reference | Complete reference for all 30+ validation attributes. | | Custom Validators | ISyncValidator, IAsyncValidator, async bindings patterns. | | Common Mistakes | The 10 most frequent errors and how to fix them. | | Troubleshooting | Problem/solution guide with diagnostics reference and FAQ. |
Feature Catalog
Section titled “Feature Catalog”| Problem | Solution |
|---|---|
| Validation requires reflection at runtime | Source-generated Validate() method — zero reflection |
| Input validation is forgotten or inconsistent | 30+ declarative attributes |
| Database uniqueness checks need special handling | IAsyncValidator<T> for DB/API checks |
| Validation not integrated into action pipeline | [Validate] on DomainAction triggers ValidationFilter |
| Error messages need localization | Message keys follow validation.{attribute} pattern |
| Cross-property validation (password confirmation, date ranges) | [EqualTo], [GreaterThanProperty], [RequiredIf] |
| Collection elements need individual validation | [ValidateElements] on collection properties |
| Custom validation rules beyond built-in attributes | Extend ValidationAttribute with IsValid() |
| Expensive async validators run unnecessarily on updates | [AsyncValidate<T>] binds validators to trigger properties |
| Entity validation re-validates unchanged properties | Change-aware Validate(IReadOnlySet<string>?) for entities |
Requirements
Section titled “Requirements”- .NET 10.0+
Pragmatic.SourceGeneratoranalyzer
License
Section titled “License”Part of the Pragmatic.Design ecosystem.