Skip to content

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.

  1. 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.

  2. Is the class partial? Diagnostic PRAG0400 fires if this is missing. Without partial, the SG cannot generate the nested Invoker class.

  3. Does it inherit from the correct base class? [DomainAction] requires DomainAction<T> or VoidDomainAction. [Mutation] requires Mutation<TEntity>. Check for PRAG0401 or PRAG0409.

  4. Is the SG analyzer referenced? In your .csproj, the Pragmatic.SourceGenerator must be referenced with OutputItemType="Analyzer":

    <ProjectReference Include="..\Pragmatic.SourceGenerator\...\Pragmatic.SourceGenerator.csproj"
    OutputItemType="Analyzer"
    ReferenceOutputAssembly="false" />
  5. Did you register the generated invokers in DI?

    With Composition (automatic):

    await PragmaticApp.RunAsync(args);

    Without Composition (manual):

    builder.Services.AddPragmaticActions(); // Core pipeline
    builder.Services.AddMyAppActions(); // Generated DomainAction invokers
    builder.Services.AddMyAppMutations(); // Generated Mutation invokers
  6. Check the SG output. In Visual Studio, expand Dependencies > Analyzers > Pragmatic.SourceGenerator in Solution Explorer. Look for {Type}.Invoker.g.cs and _Infra.Actions.Registration.g.cs. If these files do not exist, the SG is not processing your class.


Private fields are null when Execute() or ApplyAsync() runs, causing NullReferenceException.

  1. Are the fields private? The SG only injects private fields. Public properties are treated as inputs, not dependencies.

  2. Did you remove readonly? Fields declared as private readonly may not be settable by the generated SetDependencies(). Use private IMyService _service = null!; without readonly.

  3. Are you accessing the field outside Execute()? Dependencies are injected by SetDependencies() which runs inside InvokeAsync(). Property initializers, computed properties, and constructors all run before injection.

  4. Is the dependency registered in DI? If the service is not registered, SetDependencies() resolves null from the container. Check your DI registrations.

  5. Is the action invoked through the pipeline? If you call action.Execute(ct) directly without going through the invoker, SetDependencies() never runs. Always invoke via IDomainActionInvoker<T> or IMutationInvoker<T>.


The SG emits PRAG0410 — “Mutation mode could not be determined.”

  1. Does the class name start with a recognized prefix? The SG infers mode from the name: Create* = Create, Update* = Update, Delete* = Delete, Restore* = Restore.

  2. Set the mode explicitly if the name does not match:

    [Mutation(Mode = MutationMode.Update)]
    public partial class ConfirmReservation : Mutation<Reservation> { }
  3. For CreateOrUpdate (upsert): This mode is not inferred from naming. Always set it explicitly:

    [Mutation(Mode = MutationMode.CreateOrUpdate)]
    public partial class UpsertProduct : Mutation<Product> { }

The action has [Validate] or validation attributes on properties, but invalid input is not rejected.

  1. For DomainActions — is [Validate] present? The ValidationFilter only runs async validation when [Validate] is on the class. Sync validation (from attributes) runs by default.

    [DomainAction]
    [Validate] // Required for async validation
    public partial class CreateReservation : DomainAction<Guid> { }
  2. Is Pragmatic.Validation referenced? The validation SG generates ISyncValidator implementations only when the package is in the project.

  3. For Mutations — validation is built-in. L1 validation runs automatically (no [Validate] needed). Check that the mutation class implements ISyncValidator (generated from validation attributes on properties).

  4. Is the ValidationFilter enabled? Check that EnableValidationFilter is not disabled in options:

    services.AddPragmaticActions(options =>
    {
    options.EnableValidationFilter = true; // default
    });
  5. Is [NoValidation] present? This attribute explicitly disables all validation:

    [DomainAction]
    [NoValidation] // Skips all validation
    public 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.

  1. Are you calling through the boundary interface? Internal call bypass only works when you call through the generated I{Boundary}Actions interface. Direct invoker calls do not set IsInternalCall:

    // 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);
  2. Is ActionCallContext registered? It must be scoped in DI. AddPragmaticActions() registers it automatically.

  3. 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.


The generated I{Boundary}Actions interface does not include some of your actions.

  1. Is the action in the correct namespace? Boundaries capture actions by namespace prefix. An action in MyApp.Catalog.Products.Mutations is captured by a boundary in MyApp.Catalog.

  2. Is the action marked Internal = true? Internal actions are excluded from the boundary interface:

    [DomainAction(Internal = true)] // Excluded from boundary interface
    public partial class HelperAction : DomainAction<bool> { }
  3. Is the action marked System = true? System actions are also excluded:

    [DomainAction(System = true)] // Excluded from boundary interface
    public partial class HealthCheck : DomainAction<bool> { }
  4. 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.

  5. Is the boundary class partial? Diagnostic PRAG0406 fires if not. The SG cannot generate the interface implementation without partial.


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.

  1. Is the Id property correctly named? The SG looks for a public property named Id on the mutation to use as the entity key. If your property has a different name, ensure the generated LoadEntityAsync resolves it.

  2. Are query filters blocking the load? Soft-delete filters may exclude the entity. For MutationMode.Restore, the SG bypasses the soft-delete filter. For Update and Delete, the entity must not be soft-deleted.

  3. 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.

  4. 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> { }

The build fails with PRAG04xx diagnostics from the source generator.

IDSeverityCauseFix
PRAG0400ErrorClass is not partialAdd partial keyword to the class declaration
PRAG0401ErrorMissing base classInherit from DomainAction<T> or VoidDomainAction
PRAG0402InfoNo injectable dependenciesAdd private fields, or ignore if intentional
PRAG0404Error[LoadEntity] ID property not foundVerify the property name in [LoadEntity<T>(nameof(PropertyName))]
PRAG0405Error[LoadEntity] key type not determinedEnsure the entity has a recognizable key (Id property or [Key])
PRAG0406Error[Boundary] class is not partialAdd partial to the boundary class
PRAG0407Error[Boundary] class has no namespaceMove the class into a namespace
PRAG0409ErrorMutation base class wrongInherit from Mutation<TEntity>, not DomainAction<T>
PRAG0410ErrorMutation mode not determinedSet [Mutation(Mode = ...)] or use Create/Update/Delete prefix
PRAG0412WarningSubBoundary nesting > 2 levelsConsider flattening the namespace structure
PRAG0413InfoSubBoundary inferred from namespaceInformational — 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.

  1. Exception thrown instead of error returned. The Result pattern only works when you return error objects. Exceptions bypass the pipeline:

    // Wrong: throws -- produces 500
    throw new InvalidOperationException("Not found");
    // Right: returns -- produces 404
    return NotFoundError.For<Invoice, Guid>(Id);
  2. DI resolution failure. A private dependency field is null because the service is not registered. The NullReferenceException propagates as 500. Check all DI registrations.

  3. Missing middleware. Add app.UseExceptionHandler() to convert unhandled exceptions into ProblemDetails responses.


Actions run but no metrics or traces appear in your observability platform.

  1. Is the ActivitySource subscribed? ActionsDiagnostics.ActivitySource has source name "Pragmatic.Actions". Your OTel configuration must include it:

    builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
    .AddSource("Pragmatic.Actions"));
  2. Is the Meter subscribed? Metrics use meter name "Pragmatic.Actions":

    builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
    .AddMeter("Pragmatic.Actions"));
  3. Is the LoggingFilter enabled? Structured logging requires the filter to be active (default is true).


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.

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 internally

What 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).

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.


  • GitHub Issues: github.com/nicola-pragmatic/Pragmatic.Design/issues
  • Showcase Examples: See the Showcase project 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.