Skip to content

Plugin event dispatch races the request scope and silently drops events #234

Description

@PianoNic

Running the published stack, this appears in the backend log whenever a query completes:

fail: Schuly.Application.Behaviors.PluginEventBehavior[0]
      Plugin event dispatch failed for GetSchoolSystemsQuery
      System.ObjectDisposedException: Cannot access a disposed object.
      Object name: 'IServiceProvider'.
         at Schuly.Application.Behaviors.PluginEventBehavior`2...<Handle>b__0

The behaviour injects IServiceProvider and then uses it from a fire-and-forget task:

public class PluginEventBehavior<TRequest, TResponse>(IServiceProvider serviceProvider, ...)
...
    _ = Task.Run(async () =>
    {
        using var scope = serviceProvider.CreateScope();

The injected provider is the request scope. Task.Run returns immediately, the request finishes, ASP.NET disposes that scope, and the continuation then calls CreateScope() on a disposed provider. Whether it wins the race is pure timing, so plugin event handlers fire sometimes and not others, and the failure is only visible as a log line - the caller still gets its 200.

Injecting IServiceScopeFactory instead of IServiceProvider fixes it: the factory is a singleton and outlives the request, so the detached task can create its own scope.

The wider question is whether these events should be fire-and-forget at all. Nothing awaits or retries them, so an event lost to a restart or an exception is simply gone - which may matter more once plugin work moves onto a real scheduler (#233).

Reproduced against ghcr.io/schulydev/schuly:1.3.3 by calling GET /api/app/school-systems.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions