Troubleshooting
Practical problem/solution guide for Pragmatic.Actions. Each section covers a common issue, the likely causes, and the fix.
Action Not Found at Runtime (DI Resolution Failure)
Section titled “Action Not Found at Runtime (DI Resolution Failure)”You have an action class that compiles, but resolving IDomainActionInvoker<T, TReturn> or IMutationInvoker<T, TEntity> throws InvalidOperationException at runtime.
Checklist
Section titled “Checklist”-
Does the class have
[DomainAction]or[Mutation]? Without the attribute, the SG does not generate an invoker. The class compiles fine but has no pipeline integration. -
Is the class
partial? DiagnosticPRAG0400fires if this is missing. Withoutpartial, the SG cannot generate the nestedInvokerclass. -
Does it inherit from the correct base class?
[DomainAction]requiresDomainAction<T>orVoidDomainAction.[Mutation]requiresMutation<TEntity>. Check forPRAG0401orPRAG0409. -
Is the SG analyzer referenced? In your
.csproj, thePragmatic.SourceGeneratormust be referenced withOutputItemType="Analyzer":<ProjectReference Include="..\Pragmatic.SourceGenerator\...\Pragmatic.SourceGenerator.csproj"OutputItemType="Analyzer"ReferenceOutputAssembly="false" /> -
Did you register the generated invokers in DI?
With Composition (automatic):
await PragmaticApp.RunAsync(args);Without Composition (manual):
builder.Services.AddPragmaticActions(); // Core pipelinebuilder.Services.AddMyAppActions(); // Generated DomainAction invokersbuilder.Services.AddMyAppMutations(); // Generated Mutation invokers -
Check the SG output. In Visual Studio, expand Dependencies > Analyzers > Pragmatic.SourceGenerator in Solution Explorer. Look for
{Type}.Invoker.g.csand_Infra.Actions.Registration.g.cs. If these files do not exist, the SG is not processing your class.
Dependencies Are Null Inside Execute()
Section titled “Dependencies Are Null Inside Execute()”Private fields are null when Execute() or ApplyAsync() runs, causing NullReferenceException.
Checklist
Section titled “Checklist”-
Are the fields
private? The SG only injects private fields. Public properties are treated as inputs, not dependencies. -
Did you remove
readonly? Fields declared asprivate readonlymay not be settable by the generatedSetDependencies(). Useprivate IMyService _service = null!;withoutreadonly. -
Are you accessing the field outside
Execute()? Dependencies are injected bySetDependencies()which runs insideInvokeAsync(). Property initializers, computed properties, and constructors all run before injection. -
Is the dependency registered in DI? If the service is not registered,
SetDependencies()resolvesnullfrom the container. Check your DI registrations. -
Is the action invoked through the pipeline? If you call
action.Execute(ct)directly without going through the invoker,SetDependencies()never runs. Always invoke viaIDomainActionInvoker<T>orIMutationInvoker<T>.
Mutation Mode Not Determined
Section titled “Mutation Mode Not Determined”The SG emits PRAG0410 — “Mutation mode could not be determined.”
Checklist
Section titled “Checklist”-
Does the class name start with a recognized prefix? The SG infers mode from the name:
Create*= Create,Update*= Update,Delete*= Delete,Restore*= Restore. -
Set the mode explicitly if the name does not match:
[Mutation(Mode = MutationMode.Update)]public partial class ConfirmReservation : Mutation<Reservation> { } -
For
CreateOrUpdate(upsert): This mode is not inferred from naming. Always set it explicitly:[Mutation(Mode = MutationMode.CreateOrUpdate)]public partial class UpsertProduct : Mutation<Product> { }
Validation Not Running
Section titled “Validation Not Running”The action has [Validate] or validation attributes on properties, but invalid input is not rejected.
Checklist
Section titled “Checklist”-
For DomainActions — is
[Validate]present? TheValidationFilteronly runs async validation when[Validate]is on the class. Sync validation (from attributes) runs by default.[DomainAction][Validate] // Required for async validationpublic partial class CreateReservation : DomainAction<Guid> { } -
Is
Pragmatic.Validationreferenced? The validation SG generatesISyncValidatorimplementations only when the package is in the project. -
For Mutations — validation is built-in. L1 validation runs automatically (no
[Validate]needed). Check that the mutation class implementsISyncValidator(generated from validation attributes on properties). -
Is the
ValidationFilterenabled? Check thatEnableValidationFilteris not disabled in options:services.AddPragmaticActions(options =>{options.EnableValidationFilter = true; // default}); -
Is
[NoValidation]present? This attribute explicitly disables all validation:[DomainAction][NoValidation] // Skips all validationpublic partial class InternalSync : DomainAction<bool> { }
Authorization Returns 403 on Internal Calls
Section titled “Authorization Returns 403 on Internal Calls”An action calls another action within the same boundary, but the inner action fails with ForbiddenError.
Checklist
Section titled “Checklist”-
Are you calling through the boundary interface? Internal call bypass only works when you call through the generated
I{Boundary}Actionsinterface. Direct invoker calls do not setIsInternalCall:// Correct: through boundary interface (internal call bypass active)await _bookingActions.ConfirmReservationAsync(mutation, ct);// Wrong: direct invoker call (authorization is checked)await _confirmInvoker.InvokeAsync(mutation, ct); -
Is
ActionCallContextregistered? It must be scoped in DI.AddPragmaticActions()registers it automatically. -
Is the calling action in the same boundary? Cross-boundary calls are not considered internal. Authorization runs for cross-boundary calls even through boundary interfaces.
Boundary Interface Missing Actions
Section titled “Boundary Interface Missing Actions”The generated I{Boundary}Actions interface does not include some of your actions.
Checklist
Section titled “Checklist”-
Is the action in the correct namespace? Boundaries capture actions by namespace prefix. An action in
MyApp.Catalog.Products.Mutationsis captured by a boundary inMyApp.Catalog. -
Is the action marked
Internal = true? Internal actions are excluded from the boundary interface:[DomainAction(Internal = true)] // Excluded from boundary interfacepublic partial class HelperAction : DomainAction<bool> { } -
Is the action marked
System = true? System actions are also excluded:[DomainAction(System = true)] // Excluded from boundary interfacepublic partial class HealthCheck : DomainAction<bool> { } -
Is there a namespace collision? If a sub-namespace has its own
[Boundary], it “subtracts” from the parent. Check that a child boundary is not capturing actions you expect in the parent. -
Is the boundary class
partial? DiagnosticPRAG0406fires if not. The SG cannot generate the interface implementation withoutpartial.
Mutation Entity Not Found (404 on Update/Delete)
Section titled “Mutation Entity Not Found (404 on Update/Delete)”An update or delete mutation returns NotFoundError even though the entity exists in the database.
Checklist
Section titled “Checklist”-
Is the
Idproperty correctly named? The SG looks for a public property namedIdon the mutation to use as the entity key. If your property has a different name, ensure the generatedLoadEntityAsyncresolves it. -
Are query filters blocking the load? Soft-delete filters may exclude the entity. For
MutationMode.Restore, the SG bypasses the soft-delete filter. ForUpdateandDelete, the entity must not be soft-deleted. -
Is the correct boundary/DbContext being used? If the mutation belongs to boundary A but the entity is in boundary B’s database, the load will fail. Check
[BelongsTo<T>]assignment. -
Does the entity have
[Include("navigation")]? If the mutation needs navigation properties, add[Include]to eagerly load them:[Mutation(Mode = MutationMode.Update)][Include("LineItems")]public partial class UpdateInvoice : Mutation<Invoice> { }
Generated Code Not Compiling
Section titled “Generated Code Not Compiling”The build fails with PRAG04xx diagnostics from the source generator.
Diagnostics Reference
Section titled “Diagnostics Reference”| ID | Severity | Cause | Fix |
|---|---|---|---|
| PRAG0400 | Error | Class is not partial | Add partial keyword to the class declaration |
| PRAG0401 | Error | Missing base class | Inherit from DomainAction<T> or VoidDomainAction |
| PRAG0402 | Info | No injectable dependencies | Add private fields, or ignore if intentional |
| PRAG0404 | Error | [LoadEntity] ID property not found | Verify the property name in [LoadEntity<T>(nameof(PropertyName))] |
| PRAG0405 | Error | [LoadEntity] key type not determined | Ensure the entity has a recognizable key (Id property or [Key]) |
| PRAG0406 | Error | [Boundary] class is not partial | Add partial to the boundary class |
| PRAG0407 | Error | [Boundary] class has no namespace | Move the class into a namespace |
| PRAG0409 | Error | Mutation base class wrong | Inherit from Mutation<TEntity>, not DomainAction<T> |
| PRAG0410 | Error | Mutation mode not determined | Set [Mutation(Mode = ...)] or use Create/Update/Delete prefix |
| PRAG0412 | Warning | SubBoundary nesting > 2 levels | Consider flattening the namespace structure |
| PRAG0413 | Info | SubBoundary inferred from namespace | Informational — the SG auto-detected a sub-boundary |
Check the Error List window in Visual Studio or the build output for diagnostic details and the affected source location.
Endpoint Returns 500 Instead of Business Error
Section titled “Endpoint Returns 500 Instead of Business Error”The action should return a typed error (404, 409, etc.) but the endpoint returns 500 Internal Server Error.
Common Causes
Section titled “Common Causes”-
Exception thrown instead of error returned. The Result pattern only works when you return error objects. Exceptions bypass the pipeline:
// Wrong: throws -- produces 500throw new InvalidOperationException("Not found");// Right: returns -- produces 404return NotFoundError.For<Invoice, Guid>(Id); -
DI resolution failure. A private dependency field is
nullbecause the service is not registered. TheNullReferenceExceptionpropagates as 500. Check all DI registrations. -
Missing middleware. Add
app.UseExceptionHandler()to convert unhandled exceptions into ProblemDetails responses.
Telemetry Not Appearing
Section titled “Telemetry Not Appearing”Actions run but no metrics or traces appear in your observability platform.
Checklist
Section titled “Checklist”-
Is the
ActivitySourcesubscribed?ActionsDiagnostics.ActivitySourcehas source name"Pragmatic.Actions". Your OTel configuration must include it:builder.Services.AddOpenTelemetry().WithTracing(tracing => tracing.AddSource("Pragmatic.Actions")); -
Is the
Metersubscribed? Metrics use meter name"Pragmatic.Actions":builder.Services.AddOpenTelemetry().WithMetrics(metrics => metrics.AddMeter("Pragmatic.Actions")); -
Is the
LoggingFilterenabled? Structured logging requires the filter to be active (default istrue).
Can I use DomainAction without an endpoint?
Section titled “Can I use DomainAction without an endpoint?”Yes. Actions work independently of endpoints. Invoke them programmatically via IDomainActionInvoker<T, TReturn> or through boundary interfaces. The [Endpoint] attribute is optional.
When should I override ApplyAsync on a Mutation?
Section titled “When should I override ApplyAsync on a Mutation?”Override ApplyAsync for logic that goes beyond property mapping: state machine transitions, business rule validation, child entity creation, or conditional updates. For simple property mapping, let the SG generate ApplyToEntity automatically.
Does ApplyToEntity run when I override ApplyAsync?
Section titled “Does ApplyToEntity run when I override ApplyAsync?”Yes. ApplyToEntity always runs before ApplyAsync. The pipeline calls mutation.ApplyToEntity(entity) first (auto-mapped properties), then mutation.ApplyAsync(entity, ct) (your custom logic). You do not need to call base.ApplyAsync().
How do I skip authorization for an internal helper action?
Section titled “How do I skip authorization for an internal helper action?”Mark it with [DomainAction(Internal = true)]. This excludes it from the boundary interface. When called from another action through the boundary interface, the ActionCallContext.IsInternalCall flag is set and authorization filters skip their checks.
Can Mutations dispatch domain events?
Section titled “Can Mutations dispatch domain events?”Yes. If the entity implements IHasDomainEvents, the pipeline dispatches all domain events after SaveChangesAsync. Add events in ApplyAsync or in entity domain methods:
entity.Cancel(reason); // entity.Cancel() adds a ReservationCancelledEvent internallyWhat is the difference between L1 and L2 validation?
Section titled “What is the difference between L1 and L2 validation?”L1 validates the mutation input (the properties on the mutation class) before the entity is loaded. L2 validates the entity state after ApplyToEntity and ApplyAsync have run. L1 uses ISyncValidator and IAsyncValidator<TMutation>. L2 uses IValidator<TEntity> with change-aware validation (only modified properties are checked on updates).
Can I use [LoadEntity<T>] on a Mutation?
Section titled “Can I use [LoadEntity<T>] on a Mutation?”No. [LoadEntity<T>] is for DomainAction classes that need entities pre-loaded before Execute(). Mutations have built-in entity loading via LoadEntityAsync() in the mutation pipeline.
How do I test an action without the full pipeline?
Section titled “How do I test an action without the full pipeline?”Call action.Execute(ct) directly, setting dependencies manually:
var action = new CreateReservation{ Request = new CreateReservationRequest { /* ... */ }};// Set dependencies directly (bypasses pipeline)action.SetDependencies(mockRepo, mockClock);var result = await action.Execute(ct);For integration testing with the full pipeline, resolve the invoker from a test IServiceProvider.
Getting Help
Section titled “Getting Help”- GitHub Issues: github.com/nicola-pragmatic/Pragmatic.Design/issues
- Showcase Examples: See the
Showcaseproject for working action and mutation implementations across all base class types. - Pipeline Reference: See pipeline.md for filter ordering, lifecycle hooks, and custom filters.
- Mutation Reference: See mutations.md for entity mutations, ApplyAsync, and auto-mapping.
- Boundary Reference: See boundaries.md for boundary interfaces, internal calls, and remote.