Pragmatic.Actions
Source-generated CQRS-style domain actions for .NET 10. Declare the operation; the generator writes the pipeline — DI, validation, authorization, telemetry, persistence — at compile time, zero reflection.
The Problem
Section titled “The Problem”Every business operation repeats the same ceremony: resolve dependencies, validate, check authorization, execute, handle errors, persist, log, record telemetry. Across a service with 10+ methods, the plumbing dwarfs the logic.
// Without Pragmatic: 60+ lines per operation, most of it plumbingpublic async Task<Result<Guid>> CreateReservationAsync( CreateReservationRequest request, ClaimsPrincipal user, CancellationToken ct){ if (!(await auth.AuthorizeAsync(user, "booking.reservation.create")).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 {GuestId}", request.GuestId); // ... business logic ... reservations.Add(reservation); await unitOfWork.SaveChangesAsync(ct); return reservation.Id;}// ...repeated for every method.The Solution
Section titled “The Solution”Declare what the operation does; the generator handles how.
[DomainAction][RequirePolicy<ReservationManagementPolicy>][Endpoint(HttpVerb.Post, "api/v1/reservations")][Validate]public partial class CreateReservationAction : DomainAction<Guid, RoomUnavailableError>{ private IRepository<Reservation, Guid> _reservations = null!; // injected by the generator private IReadRepository<Property, Guid> _properties = 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); if (property is null) return NotFoundError.For<Property, Guid>(Request.PropertyId);
var reservation = Reservation.Create(/* ... */); _reservations.Add(reservation); return reservation.Id; }}The generator produces the invoker pipeline, DI/field injection, authorization enforcement, validation,
telemetry, and persistence. For entity CRUD it collapses even further — a Mutation<T> is the whole
class (property mapping, lifecycle, validation, persistence, DI all generated):
[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; }}Three base classes
Section titled “Three base classes”| Base class | For | Returns |
|---|---|---|
DomainAction<TReturn> (<TReturn, TError>) | Custom logic: orchestrate repos, compute, call services | Result<TReturn, …> |
VoidDomainAction | Commands with no return value | VoidResult |
Mutation<TEntity> | Entity CRUD (create/update/delete) — minimal boilerplate | the entity |
Operation taxonomy (Mutation / SideEffect / Query) and the pipeline (filters → execute → save → after-hooks) are covered in Concepts.
Installation
Section titled “Installation”dotnet add package Pragmatic.Actionsdotnet add package Pragmatic.SourceGenerator # the unified analyzer(Building inside this monorepo? See Monorepo Structure.)
What the generator gives you
Section titled “What the generator gives you”- Invoker pipeline — before/after filters, validation, authorization, save ordering, telemetry.
- DI & field injection — private fields resolved automatically; no constructor boilerplate.
- Authorization —
[RequirePermission]/[RequirePolicy]enforced in the pipeline. - Validation —
[Validate]runs L1/L2 beforeExecute(). - Boundaries —
[BelongsTo<T>]keys the unit of work / DbContext. - Action versioning, entity pre-loading, and composite (single-transaction) actions.
Status
Section titled “Status”Core DomainAction/Mutation pipeline, filters, authorization, validation, boundaries, and versioning are functional within the 0.8 preview. See the roadmap.
| Concepts | Pipeline lifecycle, operation taxonomy, base-class decision guide, composite actions |
| Getting Started | Your first DomainAction and Mutation |
| Mutations | Mutation<T>, modes, property mapping, entity lifecycle |
| Pipeline | Filters, ordering, authorization, validation, save/event ordering |
| Boundaries | [BelongsTo<T>], sub-boundaries, keyed unit of work |
| Common Mistakes | The most frequent action pitfalls |
| Troubleshooting | Problem/solution guide with diagnostics |
Cross-module integration
Section titled “Cross-module integration”Actions are invoked by Endpoints, persist via Persistence, validate via Validation, authorize via Authorization, and are wired by Composition.
Requirements
Section titled “Requirements”- .NET 10.0+
Pragmatic.SourceGeneratoranalyzer
License
Section titled “License”Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Actions is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).