Pragmatic.Actions
Source-generated CQRS-style domain actions for .NET 10. Declare the operation, the generator writes the pipeline.
The Problem
Section titled “The Problem”Every business operation needs the same ceremony: resolve dependencies, validate inputs, check authorization, execute logic, handle errors, persist changes, log results, record telemetry. Across a service class with 10+ methods, this plumbing dwarfs the actual business logic.
// Without Pragmatic: 60+ lines per operation, most of it plumbingpublic class ReservationService( IRepository<Reservation, Guid> reservations, IReadRepository<Property, Guid> properties, IValidator<CreateReservationRequest> validator, IAuthorizationService auth, ILogger<ReservationService> logger, IUnitOfWork unitOfWork){ public async Task<Result<Guid>> CreateReservationAsync( CreateReservationRequest request, ClaimsPrincipal user, CancellationToken ct) { var authResult = await auth.AuthorizeAsync(user, "booking.reservation.create"); if (!authResult.Succeeded) return Result<Guid>.Failure(new ForbiddenError());
var validation = await validator.ValidateAsync(request, ct); if (!validation.IsValid) return Result<Guid>.Failure(new ValidationError(validation.Errors));
logger.LogInformation("Creating reservation for guest {GuestId}", request.GuestId);
var property = await properties.GetByIdAsync(request.PropertyId, ct); if (property is null) return Result<Guid>.Failure(new NotFoundError("Property"));
// ... 20 more lines of business logic ...
reservations.Add(reservation); await unitOfWork.SaveChangesAsync(ct); return reservation.Id; } // 10 more methods, each repeating the auth + validate + log + try/catch pattern...}The Solution
Section titled “The Solution”With Pragmatic.Actions, you declare WHAT the operation does. The source generator handles HOW — pipeline, DI, telemetry, and all.
// With Pragmatic: declare the shape, the SG generates the rest[DomainAction][RequirePolicy<ReservationManagementPolicy>][Endpoint(HttpVerb.Post, "api/v1/reservations")][Validate]public partial class CreateReservationAction : DomainAction<Guid, RoomUnavailableError>{ private IRepository<Reservation, Guid> _reservations = null!; private IReadRepository<Property, Guid> _properties = null!; private IClock _clock = null!;
public required CreateReservationRequest Request { get; init; }
public override async Task<Result<Guid, IError>> Execute(CancellationToken ct = default) { var property = await _properties.GetByIdAsync(Request.PropertyId, ct).ConfigureAwait(false); if (property is null) return NotFoundError.For<Property, Guid>(Request.PropertyId);
var reservation = Reservation.Create(/* ... */); _reservations.Add(reservation); return reservation.Id; }}The SG produces the invoker pipeline, DI wiring, field injection, authorization enforcement, validation, telemetry, and persistence — all at compile time, zero reflection.
For entity CRUD, it collapses even further:
[Mutation(Mode = MutationMode.Create)][RequirePermission(CatalogPermissions.Amenity.Create)][Endpoint(HttpVerb.Post, "api/v1/amenities")]public partial class CreateAmenityMutation : Mutation<Amenity>{ public required string Name { get; init; } public AmenityCategory Category { get; init; } public string? IconName { get; init; }}That is the entire class. The SG generates property mapping, entity lifecycle, validation, persistence, and DI registration.
Installation
Section titled “Installation”dotnet add package Pragmatic.ActionsAdd the source generator as a project reference:
<ProjectReference Include="..\Pragmatic.SourceGenerator\src\Pragmatic.SourceGenerator\Pragmatic.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />Quick Start
Section titled “Quick Start”DomainAction — Custom Business Logic
Section titled “DomainAction — Custom Business Logic”Use DomainAction<TReturn> when you need full control over the execution logic: reading from multiple repositories, orchestrating calls, or performing complex computations.
[DomainAction][BelongsTo<BookingBoundary>][Endpoint(HttpVerb.Post, "api/v1/reservations")][Validate]public partial class CreateReservationAction : DomainAction<Guid, RoomUnavailableError>{ // Dependencies -- injected automatically via generated SetDependencies() private IRepository<Reservation, Guid> _reservations; private IReadRepository<Property, Guid> _properties; private IClock _clock;
// Input properties -- bound from HTTP body or caller public required CreateReservationRequest Request { get; init; }
public override async Task<Result<Guid, IError>> Execute(CancellationToken ct = default) { var property = await _properties.GetByIdAsync(Request.PropertyId, ct); if (property is null) return NotFoundError.For<Property, Guid>(Request.PropertyId);
var reservation = Reservation.Create( Request.GuestId, Request.PropertyId, Request.CheckIn, Request.CheckOut, Request.NumberOfGuests);
_reservations.Add(reservation); return reservation.Id; }}Mutation — Entity CRUD with Pipeline
Section titled “Mutation — Entity CRUD with Pipeline”Use Mutation<TEntity> for standard entity operations. The MutationInvoker handles the full lifecycle: validate, load/create entity, apply changes, validate entity, persist, dispatch events.
[Mutation(Mode = MutationMode.Create)][RequirePermission(CatalogPermissions.Amenity.Create)][Endpoint(HttpVerb.Post, "api/v1/amenities")]public partial class CreateAmenityMutation : Mutation<Amenity>{ public required string Name { get; init; } public AmenityCategory Category { get; init; } public string? IconName { get; init; }}That is the entire class. The source generator:
- Detects matching properties between mutation and entity
- Generates
ApplyToEntity()that callsentity.SetName(this.Name), etc. - Generates a
MutationInvokerthat handles the full pipeline - Generates DI registration
- When paired with
[Endpoint], generates an HTTP endpoint
Three Base Classes
Section titled “Three Base Classes”Pragmatic.Actions provides three base classes. Choose based on your operation type.
DomainAction<TReturn> — Returns a Value
Section titled “DomainAction<TReturn> — Returns a Value”For queries, complex writes, orchestrations, or any operation that returns a result.
[DomainAction]public partial class GetInvoiceById : DomainAction<InvoiceDto>{ private IReadRepository<Invoice, Guid> _invoices; public required Guid Id { get; init; }
public override async Task<Result<InvoiceDto, IError>> Execute(CancellationToken ct) { var invoice = await _invoices.GetByIdAsync(Id, ct); if (invoice is null) return NotFoundError.For<Invoice, Guid>(Id); return invoice.ToDto(); }}Supports up to 6 typed error variants:
// One error typepublic partial class CreateReservation : DomainAction<Guid, RoomUnavailableError> { }
// Two error typespublic partial class TransferFunds : DomainAction<TransferResult, InsufficientFundsError, AccountFrozenError> { }
// Up to sixpublic partial class ComplexOp : DomainAction<Result, E1, E2, E3, E4, E5, E6> { }The IProducesError<T> marker interfaces are used by the source generator to produce OpenAPI error schemas.
VoidDomainAction — No Return Value
Section titled “VoidDomainAction — No Return Value”For operations that either succeed or fail with an error, but produce no return value.
[DomainAction][RequirePermission(BillingPermissions.Invoice.Update)][Endpoint(HttpVerb.Post, "/api/v1/invoices/{id}/pay")]public partial class MarkInvoicePaidAction : VoidDomainAction<NotFoundError>{ private IRepository<Invoice, Guid> _invoices; public required Guid Id { get; init; }
[FromClaim("sub", IsRequired = false)] public Guid PaidByUserId { get; set; }
public override async Task<VoidResult<IError>> Execute(CancellationToken ct) { var invoice = await _invoices.GetByIdAsync(Id, ct); if (invoice is null) return NotFoundError.For<Invoice, Guid>(Id);
invoice.MarkAsPaid(); invoice.SetPaidByUserId(PaidByUserId); return Success; // convenience property on VoidDomainAction }}Mutation<TEntity> — Entity Modifications
Section titled “Mutation<TEntity> — Entity Modifications”For standard entity CRUD operations. The MutationInvoker manages the full lifecycle automatically. See Mutations Guide for full details.
// Create -- properties auto-mapped to entity.SetX() calls[Mutation(Mode = MutationMode.Create)]public partial class CreateAmenityMutation : Mutation<Amenity>{ public required string Name { get; init; } public AmenityCategory Category { get; init; }}
// Update -- loads entity by Id, applies changes[Mutation(Mode = MutationMode.Update)]public partial class UpdateAmenityMutation : Mutation<Amenity>{ public required Guid Id { get; init; } public string? Name { get; init; } public AmenityCategory? Category { get; init; }}
// Delete -- loads entity, removes or soft-deletes[Mutation(Mode = MutationMode.Delete)]public partial class DeleteAmenityMutation : Mutation<Amenity>{ public required Guid Id { get; init; }}
// Restore -- undoes soft-delete[Mutation(Mode = MutationMode.Restore)]public partial class RestorePropertyMutation : Mutation<Property>{ public required Guid Id { get; init; }}Pipeline Architecture
Section titled “Pipeline Architecture”Every action executes through a filter pipeline. The invoker orchestrates the flow.
DomainAction Pipeline
Section titled “DomainAction Pipeline” InjectDependencies(action) | PrepareActionAsync(action) -- [LoadEntity] pre-loads | BeforeExecute filters (ascending order) 100: ValidationFilter -- sync + async validation 200: PermissionAuthorizationFilter -- [RequirePermission], [RequireAnyPermission] 210: PolicyEvaluationFilter -- [RequirePolicy<T>] 250: ResourceAuthorizationFilter -- IResourceAuthorizer<T> 1000: LoggingFilter -- structured logging | action.Execute(ct) | AfterExecute filters (descending order) | SaveChangesAsync(ct) -- boundary IUnitOfWorkAny BeforeExecute filter can short-circuit the pipeline by returning a failure result.
Mutation Pipeline
Section titled “Mutation Pipeline”The mutation pipeline is more structured, with two levels of validation:
0. InjectDependencies(mutation) -- SG-generated SetDependencies | 1. Permission check -- [RequirePermission] / [RequireAnyPermission] | (opt-in via PermissionAuthorizationFilter in DI) 2. L1 Sync validation -- ISyncValidator on mutation (from attributes) | 3. L1 Async validation -- IAsyncValidator<TMutation> | 4. IActionFilter<TMutation> -- typed business policy filters | 5. Load or Create entity -- mode-dependent (Create/Update/Delete/Restore) | 6. mutation.ApplyToEntity(entity) -- SG auto-mapped property setters | 7. mutation.ApplyAsync(entity) -- developer override for custom logic | (auto-mapped props already applied in step 6) 8. L2 Entity validation -- IValidator<TEntity> (change-aware) | 9. Persist -- Add if new, SaveChanges |10. Dispatch domain events -- IDomainEventDispatcher |11. Cache invalidation -- ICacheInvalidatorNote: Step 1 (permission check) only runs when
PermissionAuthorizationFilteris registered in DI. Mutations without[RequirePermission]skip the check automatically.
Filter Ordering
Section titled “Filter Ordering”Built-in filter order constants are defined in FilterOrder:
| Constant | Value | Filter | Purpose |
|---|---|---|---|
Validation | 100 | ValidationFilter | Sync + async input validation |
Authorization | 200 | PermissionAuthorizationFilter | [RequirePermission] checks |
| — | 210 | PolicyEvaluationFilter | [RequirePolicy<T>] checks |
| — | 250 | ResourceAuthorizationFilter | IResourceAuthorizer<T> checks |
Transaction | 300 | (reserved) | Transaction wrapping |
Caching | 400 | (reserved) | Cache read/write |
Logging | 1000 | LoggingFilter | Structured logging |
Filters run in ascending order for BeforeExecute and descending order for AfterExecute.
Internal Calls Skip Authorization
Section titled “Internal Calls Skip Authorization”When one action calls another within the same boundary (intra-boundary call), ActionCallContext.IsInternalCall is set to true. The PermissionAuthorizationFilter and PolicyEvaluationFilter skip their checks for internal calls. This prevents double-checking permissions when action A orchestrates action B.
Dependency Injection
Section titled “Dependency Injection”Automatic Field Detection
Section titled “Automatic Field Detection”The source generator scans private fields in your action class and generates a SetDependencies() method that resolves them from DI:
[DomainAction]public partial class CreateReservation : DomainAction<Guid>{ private IRepository<Reservation, Guid> _reservations; // resolved from DI private IClock _clock; // resolved from DI
public required Guid GuestId { get; init; } // NOT a dependency (public property) // ...}Convention: Private fields without initializers are treated as dependencies. Public properties are treated as input parameters.
Invoker Registration
Section titled “Invoker Registration”The source generator registers each invoker in DI. For a DomainAction, it registers:
services.AddScoped<IDomainActionInvoker<CreateReservation, Guid>, CreateReservation.Invoker>();For a Mutation:
services.AddScoped<IMutationInvoker<CreateAmenityMutation, Amenity>, CreateAmenityMutation.Invoker>();Invoking Actions Programmatically
Section titled “Invoking Actions Programmatically”Inject the invoker interface to call actions from other code:
public class PaymentOrchestrator( IMutationInvoker<CreateDraftInvoiceMutation, Invoice> createInvoice){ public async Task<Result<Invoice, IError>> CreateInvoiceAsync( Guid reservationId, decimal amount, CancellationToken ct) { var mutation = new CreateDraftInvoiceMutation { ReservationId = reservationId, TotalAmount = amount, // ... }; return await createInvoice.InvokeAsync(mutation, ct); }}Authorization Integration
Section titled “Authorization Integration”Pragmatic.Actions integrates with Pragmatic.Authorization through three pipeline filters, each addressing a different authorization concern.
Permission-Based — [RequirePermission]
Section titled “Permission-Based — [RequirePermission]”Enforces that the current user has specific permissions. All listed permissions must be present.
[RequirePermission(CatalogPermissions.Amenity.Create)]public partial class CreateAmenityMutation : Mutation<Amenity> { }Any-Permission — [RequireAnyPermission]
Section titled “Any-Permission — [RequireAnyPermission]”Passes if the user has at least one of the listed permissions.
[RequireAnyPermission("booking.reservation.read", "booking.admin")]public partial class ViewReservation : DomainAction<ReservationDto> { }Policy-Based — [RequirePolicy<T>]
Section titled “Policy-Based — [RequirePolicy<T>]”For rules more complex than a single permission check. The policy class must extend ResourcePolicy and implement Evaluate(ICurrentUser).
[RequirePolicy<ReservationManagementPolicy>]public partial class CreateReservationAction : DomainAction<Guid> { }Resource-Level — IResourceAuthorizer<T>
Section titled “Resource-Level — IResourceAuthorizer<T>”When authorization depends on the specific data being accessed (e.g., “can this user edit THIS invoice?”), register an IResourceAuthorizer<T> in DI. The ResourceAuthorizationFilter resolves it automatically.
Internal Call Bypass
Section titled “Internal Call Bypass”When actions call each other within the same boundary via generated boundary interfaces, they enter “internal call” mode. Authorization filters detect ActionCallContext.IsInternalCall and skip permission/policy checks, preventing redundant validation.
Boundaries
Section titled “Boundaries”Boundaries group related actions into a strongly-typed interface for cross-module composition.
Defining a Boundary
Section titled “Defining a Boundary”[Boundary]public partial class CatalogBoundary;The source generator captures all [DomainAction] and [Mutation] classes whose namespace starts with the boundary’s namespace (e.g., Showcase.Catalog.*). It generates an ICatalogActions interface with typed invoke methods for each action.
Assigning Actions to Boundaries
Section titled “Assigning Actions to Boundaries”Actions are assigned to boundaries by namespace prefix matching (default) or explicit attribute:
// Implicit: Showcase.Booking.Reservations.Mutations.* -> BookingBoundary[Mutation(Mode = MutationMode.Update)]public partial class CheckInGuestMutation : Mutation<Reservation> { }
// Explicit: override namespace-based assignment[DomainAction][BelongsTo<ShippingBoundary>]public partial class SpecialShipment : DomainAction<ShipmentId> { }Boundary Visibility
Section titled “Boundary Visibility”[Boundary(Visibility = BoundaryVisibility.Internal)]public partial class InternalServiceBoundary;BoundaryVisibility.Internal generates only an internal interface — the boundary cannot be called from other assemblies or exposed via endpoints.
Cross-Boundary Read Access
Section titled “Cross-Boundary Read Access”When a boundary needs to read entities from another boundary via SQL join (same physical database):
[Boundary][ReadAccess<Property>][ReadAccess<RoomType>]public partial class BookingBoundary;The persistence source generator adds DbSet<Property> to BookingBoundary’s DbContext with ExcludeFromMigrations().
Sub-Boundaries
Section titled “Sub-Boundaries”Large boundaries can be split into sub-groups:
[SubBoundary(Name = "Reservations")]public class ReservationActions { }Sub-boundaries generate their own interface and implementation, composed into the root boundary as a property.
Local vs Remote Boundaries
Section titled “Local vs Remote Boundaries”Boundaries can run in-process or be accessed via HTTP:
// Local -- same process, direct database accessservices.AddBoundary<CatalogBoundary>(cfg => cfg .UseLocal() .UseDatabase(opt => opt.UseNpgsql(connectionString)));
// Remote -- via HTTP APIservices.AddBoundary<BillingBoundary>(cfg => cfg .UseRemote("https://billing-api.example.com"));Topology Validation
Section titled “Topology Validation”Call ValidateBoundaryTopology() to verify at startup:
- No circular dependencies between boundaries
- All required dependencies are registered
- No duplicate boundary registrations
services.ValidateBoundaryTopology();See Boundaries Guide for full details.
Validation
Section titled “Validation”The ValidationFilter runs at Order 100 (first in the pipeline). Behavior depends on attributes:
| Attribute | Sync Validation | Async Validation |
|---|---|---|
| (none) | Yes | No |
[Validate] | Yes | Yes |
[Validate(AsyncOnly = true)] | No | Yes |
[NoValidation] | No | No |
Sync validation uses ISyncValidator (generated from validation attributes). Async validation uses IAsyncValidator<T> resolved from DI. Nested properties that implement ISyncValidator or have IAsyncValidator<T> registered are also validated automatically.
Action Versioning
Section titled “Action Versioning”Use [SinceVersion] on properties and implement ExecuteV2(), ExecuteV3(), etc. for versioned API evolution:
[DomainAction][Endpoint(HttpVerb.Post, "api/v1/reservations")]public partial class CreateReservation : DomainAction<Guid>{ public required CreateReservationRequest Request { get; init; }
[SinceVersion("2.0")] public string? LoyaltyMemberId { get; init; }
public override Task<Result<Guid, IError>> Execute(CancellationToken ct) { /* V1 logic */ }
public Task<Result<Guid, IError>> ExecuteV2(CancellationToken ct) { /* V2 logic */ }}The source generator creates separate versioned endpoints and routes to the correct Execute method.
Composite Actions
Section titled “Composite Actions”Use [CompositeAction] to orchestrate multiple mutations within a single transaction:
[CompositeAction]public partial class TransferGuest : DomainAction<TransferResult>{ // SG generates a transactional invoker that calls each mutation's // ExecuteWithoutSave and commits atomically}Entity Pre-Loading
Section titled “Entity Pre-Loading”Use [LoadEntity<T>] to automatically load entities before Execute() runs:
[DomainAction][LoadEntity<Invoice>(nameof(InvoiceId))][LoadEntity<Guest>(nameof(GuestId))]public partial class ProcessPayment : DomainAction<PaymentResult>{ public required Guid InvoiceId { get; init; } public required Guid GuestId { get; init; }
// _invoice and _guest are populated before Execute() via PrepareActionAsync public override async Task<Result<PaymentResult, IError>> Execute(CancellationToken ct) { // _invoice and _guest are already loaded }}The source generator creates the _invoice and _guest fields and overrides PrepareActionAsync to load them from their repositories, returning EntityNotFoundError if not found.
Telemetry
Section titled “Telemetry”ActionsDiagnostics provides built-in observability via System.Diagnostics:
ActivitySource (Pragmatic.Actions): Every action/mutation invocation creates an Activity with tags following OpenTelemetry semantic conventions (action.name, action.kind, action.result, error.code).
Meters:
| Instrument | Type | Description |
|---|---|---|
pragmatic.actions.duration | Histogram (ms) | Action execution duration |
pragmatic.actions.invocations | Counter | Total action invocations |
pragmatic.actions.failures | Counter | Business error count |
pragmatic.actions.filter_short_circuits | Counter | Pipeline short-circuits |
pragmatic.mutations.duration | Histogram (ms) | Mutation execution duration |
pragmatic.mutations.invocations | Counter | Total mutation invocations |
pragmatic.mutations.failures | Counter | Mutation failure count |
pragmatic.mutations.validation_failures | Counter | L1 + L2 validation failures |
Service Registration
Section titled “Service Registration”Core Services
Section titled “Core Services”services.AddPragmaticActions();This registers the ActionCallContext, ValidationFilter, LoggingFilter, PermissionAuthorizationFilter, ResourceAuthorizationFilter, and PolicyEvaluationFilter.
Configuration Options
Section titled “Configuration Options”services.AddPragmaticActions(options =>{ options.EnableValidationFilter = true; // default: true options.EnableLoggingFilter = true; // default: true options.EnablePermissionFilter = true; // default: true options.EnableResourceAuthorizationFilter = true; // default: true options.EnablePolicyFilter = true; // default: true});Custom Filters
Section titled “Custom Filters”Register global or action-specific filters:
// Global filter (applies to all actions)services.AddActionFilter<MyAuditFilter>();
// Action-specific filter (applies to one action type)services.AddActionFilter<MyBusinessRule, CreateReservation>();
// Typed filter with access to the resultservices.AddActionFilter<MyResultFilter, CreateReservation, Guid>();Feature Catalog
Section titled “Feature Catalog”| Problem | Solution |
|---|---|
| Business operations scattered across services | DomainAction<T> encapsulates operation + dependencies + result |
| Manual constructor injection grows unwieldy | Private fields auto-detected, SetDependencies() generated |
| No consistent error handling | Result<T, E> with up to 6 typed error variants |
| Validation duplicated or forgotten | [Validate] triggers sync + async validators via pipeline |
| Cross-cutting concerns need manual wiring | IActionFilter pipeline with ordered filters |
| Actions need HTTP endpoints | [Endpoint] generates full minimal API endpoint |
| JWT claims needed in business logic | [FromClaim("claim")] binds properties from JWT |
| Transaction isolation per domain | [BelongsTo<TBoundary>] routes to correct IUnitOfWork |
| Versioned APIs require different request shapes | [SinceVersion("2.0")] + ExecuteV2() |
| Cross-action composition | Inject IDomainActionInvoker<T, TReturn> for child actions |
| Entity pre-loading boilerplate | [LoadEntity<T>] auto-loads before Execute() |
| Entity CRUD boilerplate | Mutation<TEntity> with auto-mapped properties |
| State machine transitions | Override ApplyAsync for controlled state changes |
| Soft-delete with compensation | MutationMode.Delete + ISoftDelete, auto-rollback on failure |
| Restore soft-deleted entities | MutationMode.Restore resets ISoftDelete fields |
Attribute Reference
Section titled “Attribute Reference”| Attribute | Target | Purpose |
|---|---|---|
[DomainAction] | Class | Marks as a domain action, enables SG |
[DomainAction(Internal = true)] | Class | Excludes from boundary interface |
[DomainAction(System = true)] | Class | Infrastructure action, no boundary |
[Mutation] | Class | Marks as entity mutation |
[Mutation(Mode = ...)] | Class | Sets Create/Update/Delete/Restore mode |
[Mutation(ReturnType = ...)] | Class | Controls response: Id, LogicalKey, Entity |
[Mutation(SoftDelete = true)] | Class | Delete uses soft-delete |
[CompositeAction] | Class | Orchestrates multiple mutations atomically |
[BelongsTo<T>] | Class | Assigns action to a boundary |
[Boundary] | Class | Defines a boundary group |
[SubBoundary] | Class | Sub-grouping within a boundary |
[ReadAccess<T>] | Class | Boundary reads entity from another boundary |
[Validate] | Class | Enables async validation |
[NoValidation] | Class | Disables all validation |
[LoadEntity<T>] | Class | Pre-loads entity before Execute() |
[Include("nav")] | Class | Eagerly loads navigation for mutation entity |
[RequirePermission] | Class | Requires all listed permissions |
[RequireAnyPermission] | Class | Requires at least one permission |
[RequirePolicy<T>] | Class | Requires policy evaluation |
[SinceVersion("2.0")] | Property | Marks property as added in version |
[FromClaim("claim")] | Property | Binds from JWT claim |
Diagnostics
Section titled “Diagnostics”| ID | Severity | Description |
|---|---|---|
| PRAG0400 | Error | Action class must be partial |
| PRAG0401 | Error | Action must inherit from DomainAction<T> or VoidDomainAction |
| PRAG0402 | Info | Action has no injectable dependencies (private fields) |
| PRAG0404 | Error | [LoadEntity] ID property not found on the action class |
| PRAG0405 | Error | [LoadEntity] could not determine entity key type |
| PRAG0406 | Error | [Boundary] class must be partial |
| PRAG0407 | Error | [Boundary] class must be in a namespace |
| PRAG0409 | Error | Mutation class must inherit from Mutation<TEntity> |
| PRAG0410 | Error | Mutation mode could not be determined (specify [Mutation(Mode = ...)] or use Create/Update prefix) |
| PRAG0412 | Warning | SubBoundary nesting deeper than 2 levels — consider flattening |
| PRAG0413 | Info | SubBoundary inferred from namespace structure |
Samples
Section titled “Samples”See samples/Pragmatic.Actions.Samples/ for 5 pattern catalogs: action catalog (all SG output patterns), error handling (typed errors, implicit conversions, multi-error), mutation patterns (create, update, pipeline flow), DI wiring (registration, invoker, sub-boundary interfaces), and advanced patterns (CompositeAction, LoadEntity, Query).
| Architecture and Concepts | Why Actions exists, pipeline architecture, choosing the right base class | | Getting Started | Create your first DomainAction and Mutation | | Pipeline | Filter ordering, lifecycle hooks, custom filters | | Mutations | Entity mutations, ApplyAsync, auto-mapping | | Boundaries | Boundary interfaces, internal calls, remote | | Common Mistakes | Avoid the 12 most frequent pitfalls | | Troubleshooting | Problem/solution guide with diagnostics reference |
Cross-Module Integration
Section titled “Cross-Module Integration”| With Module | Integration |
|---|---|
| Pragmatic.Endpoints | [Endpoint] on action generates HTTP endpoint |
| Pragmatic.Validation | [Validate] runs sync + async validators |
| Pragmatic.Persistence | [BelongsTo<T>] provides boundary-keyed IUnitOfWork |
| Pragmatic.Authorization | [RequirePermission], [RequirePolicy<T>], IResourceAuthorizer<T> |
| Pragmatic.Mapping | [MapFrom<T>] DTOs as action input/output |
| Pragmatic.Caching | ICacheInvalidator on mutations |
| Pragmatic.Events | Domain events dispatched after mutation persist |
| Pragmatic.Composition | Module metadata for cross-assembly discovery |
Requirements
Section titled “Requirements”- .NET 10.0+
Pragmatic.SourceGeneratoranalyzer
License
Section titled “License”Part of the Pragmatic.Design ecosystem.