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, with no reflection on the executed path.
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(ForbiddenError.MissingPermission("booking.reservation.create")); var validation = await validator.ValidateAsync(request, ct); if (!validation.IsValid) return Result<Guid>.Failure(BadRequestError.Create(validation.ToString())); 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/reservations")]public partial class CreateReservationAction : DomainAction<Guid, NotFoundError, RoomUnavailableError>{ private IRepository<Reservation> _reservations = null!; // injected by the generator private IReadRepository<Property> _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(nameof(Property), 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/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: runs before
Execute()by default, the synchronous rules first and the async validators declared for it after;[Validate]only changes that default and[NoValidation]switches it off. - Boundaries: the namespace decides the boundary (and the sub-boundary group);
[BelongsTo<T>]names it explicitly, and it keys the unit of work / DbContext. - Action versioning, entity pre-loading, and composite (single-transaction) actions.
Status
Section titled “Status”Functional within 1.0.0-alpha: the DomainAction/Mutation pipeline, filters, authorization, validation, boundaries, and versioning. 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).
