Skip to content

Troubleshooting

Practical problem/solution guide for Pragmatic.Events. Each section covers a common issue, the likely causes, and the fix.


Entities raise events, SaveChangesAsync() succeeds, but handlers never fire.

  1. Did you register the dispatcher?

    builder.Services.AddInMemoryDomainEvents();

    Without this call, IDomainEventDispatcher is not in the DI container. The interceptor will fail when it tries to resolve it.

  2. Did you add the interceptor to your DbContext?

    services.AddDbContext<AppDbContext>((sp, options) =>
    {
    options.UseNpgsql(connectionString);
    options.UseDomainEvents(sp); // This adds the DomainEventsInterceptor
    });

    Note the (sp, options) overload — the service provider parameter is required. Using the (options) overload without sp means you cannot call UseDomainEvents(sp).

  3. Does your entity implement IHasDomainEvents? The interceptor scans ChangeTracker.Entries<IHasDomainEvents>(). If your entity inherits DomainEventSource, this is handled. If you implement events manually, verify the entity implements IHasDomainEvents.

  4. Are events actually being raised? Set a breakpoint in RaiseEvent() or check entity.DomainEvents.Count before SaveChangesAsync(). If the list is empty, the entity is not raising events in its domain operation.

  5. Is the entity tracked by the ChangeTracker? The interceptor only sees entities tracked by EF Core. If you create an entity with AsNoTracking() or detach it, the interceptor will not find its events.


The dispatcher fires, but a specific handler never runs.

  1. Is the handler registered in DI? Check one of these registration methods:

    // Individual (AOT-safe)
    services.AddDomainEventHandler<MyHandler, MyEvent>();
    // Assembly scan (not AOT-safe)
    services.AddDomainEventHandlersFromAssembly(typeof(MyHandler).Assembly);
    // SG-generated (requires [EventHandler] attribute)
    // Check that the generated AddPragmaticEventHandlers() is called
  2. Does the handler implement IDomainEventHandler<T> for the correct event type? A handler for ReservationConfirmed will not fire for ReservationCreated. Verify the generic type parameter matches the event being raised.

  3. Is the handler class public? Assembly scanning with AddDomainEventHandlersFromAssembly only discovers public types. Internal handlers must be registered individually.

  4. Check the handler’s Order property. If a preceding handler throws an OperationCanceledException, the entire dispatch chain stops. Non-OperationCanceledException exceptions do not block subsequent handlers, but cancellation does.

  5. Is the event type correct at dispatch time? The untyped batch dispatch (DispatchAsync(IEnumerable<IDomainEvent>)) resolves handlers based on the runtime type of each event. If you upcast the event to IDomainEvent before raising, the runtime type is preserved and dispatch works correctly.


A handler throws, but the caller of SaveChangesAsync() does not see the exception.

The InMemoryEventDispatcher uses a continue-on-failure strategy. Handler exceptions (except OperationCanceledException) are:

  1. Logged at Error level with the handler name and event name.
  2. Counted in the pragmatic.events.handler_failures metric.
  3. Recorded as an exception on the current OTel Activity.
  4. Not propagated to the caller.
  • Logs: Look for Error-level messages: "Handler {HandlerName} failed for event {EventName}".
  • Metrics: Monitor pragmatic.events.handler_failures counter, tagged by event.name and handler.name.
  • Traces: In your distributed tracing UI, look for exceptions recorded on Event.{EventName} activities.

The continue-on-failure policy is intentional for independent side effects. If you need a side effect that must succeed or fail atomically with the domain operation, do not use an event handler. Instead:

  • Include the logic directly in the domain action or service method.
  • Use a single handler that combines the critical and non-critical side effects with its own error handling.

InvalidOperationException: IDomainEventDispatcher Not Registered

Section titled “InvalidOperationException: IDomainEventDispatcher Not Registered”
System.InvalidOperationException: No service for type 'Pragmatic.Events.IDomainEventDispatcher' has been registered.

AddInMemoryDomainEvents() was not called before the service provider was built.

builder.Services.AddInMemoryDomainEvents();

This must appear before builder.Build(). If using Pragmatic.Composition with PragmaticApp.RunAsync(), the SG-generated host handles this automatically when the Events module is detected.


After a transient database failure, the retry succeeds but event handlers fire twice for the same events.

You are dispatching events manually before clearing them from the entity, and the retry re-collects them.

The DomainEventsInterceptor handles this correctly by design: it clears events from entities before dispatching. If you are dispatching manually (without the EF Core interceptor), follow the same pattern:

var events = entity.DomainEvents.ToList();
entity.ClearDomainEvents(); // Clear BEFORE dispatch
await dispatcher.DispatchAsync(events, ct).ConfigureAwait(false);

DomainEventsInterceptor Not Resolving From DI

Section titled “DomainEventsInterceptor Not Resolving From DI”

The UseDomainEvents(sp) call throws because the service provider does not contain IDomainEventDispatcher.

The AddDbContext overload you are using does not provide the service provider:

// Wrong: no service provider available
services.AddDbContext<AppDbContext>(options =>
{
options.UseDomainEvents(???); // Where does sp come from?
});

Use the (IServiceProvider, DbContextOptionsBuilder) overload:

services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseNpgsql(connectionString);
options.UseDomainEvents(sp);
});

Events Accumulating on Entities Without Dispatch

Section titled “Events Accumulating on Entities Without Dispatch”

The entity’s DomainEvents list keeps growing across multiple operations, but handlers never fire.

  1. No interceptor registered. See “Events Not Dispatching After SaveChanges” above.

  2. Entity not saved to database. If you modify an entity and raise events but never call SaveChangesAsync(), the interceptor never fires. Events only dispatch on successful persistence.

  3. Entity created outside of EF Core tracking. If you new an entity, raise events, but never Add it to a DbSet or the ChangeTracker, the interceptor will not see it.

Ensure the entity is tracked and persisted:

var reservation = Reservation.Create(/* ... */);
dbContext.Reservations.Add(reservation); // Now tracked
await dbContext.SaveChangesAsync(ct); // Interceptor fires

Handlers Run But Authorization Blocks Downstream Actions

Section titled “Handlers Run But Authorization Blocks Downstream Actions”

A handler calls a domain action via a boundary interface, but the action returns a ForbiddenError or UnauthorizedError.

The ICallContext is not being resolved, so the handler does not run as an internal call. Authorization filters see the HTTP user’s permissions (which may not include the permission needed by the handler’s action).

  1. Verify Pragmatic.Actions is referenced. ICallContext is registered by the Actions module. Without it, the dispatcher’s GetService<ICallContext>() returns null and internal call mode is not activated.

  2. Check that the boundary interface action uses the pipeline. If the action is invoked through IDomainActionInvoker<T> (the normal path), the pipeline respects IsInternalCall. Direct method calls bypass the pipeline entirely.

  3. Do not use a custom IDomainEventDispatcher that skips internal call context. The InMemoryEventDispatcher calls callContext?.EnterInternalCall() for each handler. A custom dispatcher must do the same.


The synchronous SaveChanges() path works but blocks the thread during event dispatch.

The DomainEventsInterceptor.SavedChanges (sync) override calls the async dispatch path via .GetAwaiter().GetResult(). This is a synchronous-over-async bridge — it blocks the calling thread.

Always use SaveChangesAsync() in async code paths (ASP.NET Core handlers, background services). The sync SaveChanges() fallback exists for legacy code paths and test scenarios where async is not available.


Can I have multiple handlers for the same event?

Section titled “Can I have multiple handlers for the same event?”

Yes. Register as many IDomainEventHandler<T> implementations as needed. They all run for every dispatch of that event type, sorted by Order.

What happens if no handlers are registered for an event?

Section titled “What happens if no handlers are registered for an event?”

Nothing. The dispatcher logs at Debug level: "No handlers registered for event {EventName}", sets the handler count to 0, and returns. No exception is thrown.

Yes, but those events are not automatically dispatched. If a handler modifies an entity tracked by EF Core and raises new events, those events will be dispatched on the next SaveChangesAsync() call. If the handler does not save, the new events accumulate on the entity.

Yes. Create the handler directly and call HandleAsync:

var handler = new ReservationConfirmedHandler(mockBillingActions);
await handler.HandleAsync(new ReservationConfirmed(/* ... */));

Or test with the full dispatcher:

var services = new ServiceCollection();
services.AddLogging();
services.AddInMemoryDomainEvents();
services.AddSingleton<IDomainEventHandler<ReservationConfirmed>>(handler);
var sp = services.BuildServiceProvider();
var dispatcher = sp.GetRequiredService<IDomainEventDispatcher>();
await dispatcher.DispatchAsync(new ReservationConfirmed(/* ... */));

Is InMemoryEventDispatcher suitable for production?

Section titled “Is InMemoryEventDispatcher suitable for production?”

Yes, for in-process side effects. It is the standard dispatcher for monolithic applications and single-process deployments. For distributed scenarios requiring at-least-once delivery, cross-service messaging, or retry semantics, pair it with an outbox pattern or wait for Pragmatic.Messaging.

Can I replace InMemoryEventDispatcher with a custom implementation?

Section titled “Can I replace InMemoryEventDispatcher with a custom implementation?”

Yes. Implement IDomainEventDispatcher and register your implementation:

services.AddScoped<IDomainEventDispatcher, MyCustomDispatcher>();

The EF Core interceptor depends on IDomainEventDispatcher, not on InMemoryEventDispatcher directly.


  • GitHub Issues: github.com/nicola-pragmatic/Pragmatic.Design/issues
  • Showcase Examples: See the Showcase project for working cross-boundary event implementations with Booking and Billing boundaries.
  • Internals Guide: See internals.md for dispatcher mechanics, handler ordering, and observability details.
  • Concepts Guide: See concepts.md for architecture decisions and design rationale.