๐Ÿšฆ Event Listeners, Processors, and DbContext โ€” Pitfalls & Best Practices – .NET 10

์ž‘์„ฑ์ž

์นดํ…Œ๊ณ ๋ฆฌ:

โ† ํ”ผ๋“œ๋กœ
DEV Community ยท Rahul Kumar Jha ยท 2026-08-04 ๊ฐœ๋ฐœ(SW)

Rahul Kumar Jha

Hereโ€™s a short, easy-to-digest article that explains the pitfalls and best practices of mixing lifetimes in .NET dependency injection.

๐Ÿšฆ Event Listeners, Processors, and DbContext โ€” Pitfalls & Best Practices

Imagine you have two buddies in your app:

  • EventListener โ†’ sits around forever, waiting for events (a singleton).
  • EventProcessor โ†’ does the actual work when an event arrives.

Now the big question: what lifetime should EventProcessor have, especially if it needs a DbContext?

โŒ Pitfall: Singleton Processor + Scoped DbContext

services.AddSingleton<IEventProcessor, EventProcessor>();
services.AddDbContext<AppDbContext>();

Enter fullscreen mode Exit fullscreen mode

And inside EventProcessor:

public class EventProcessor : IEventProcessor
{
    private readonly AppDbContext _db;

    public EventProcessor(AppDbContext db)
    {
        _db = db;
    }

    public Task ProcessAsync(Event evt)
    {
        _db.Events.Add(evt);
        return _db.SaveChangesAsync();
    }
}

Enter fullscreen mode Exit fullscreen mode

What goes wrong?

  • DbContext is scoped (new per request).
  • EventProcessor is singleton (one forever).
  • You end up holding onto one DbContext instance for the entire app lifetime. โ†’ That DbContext gets stale, tracks too much, and blows up with concurrency errors. โ†’ Basically, youโ€™re trying to use a toothbrush for the whole office โ€” gross and unsafe.

โœ… Best Practice #1: Keep Processor Singleton, Use Scope Factory

services.AddSingleton<IEventProcessor, EventProcessor>();
services.AddDbContext<AppDbContext>();

Enter fullscreen mode Exit fullscreen mode

public class EventProcessor : IEventProcessor
{
    private readonly IServiceScopeFactory _scopeFactory;

    public EventProcessor(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task ProcessAsync(Event evt)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        db.Events.Add(evt);
        await db.SaveChangesAsync();
    }
}

Enter fullscreen mode Exit fullscreen mode

๐Ÿ‘‰ Each event gets a fresh DbContext. No stale data, no concurrency nightmares.

โœ… Best Practice #2: Make Processor Scoped

services.AddScoped<IEventProcessor, EventProcessor>();
services.AddSingleton<IEventListener, EventListener>();

Enter fullscreen mode Exit fullscreen mode

And inside the listener:

public class EventListener
{
    private readonly IServiceScopeFactory _scopeFactory;

    public EventListener(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task OnEventAsync(Event evt)
    {
        using var scope = _scopeFactory.CreateScope();
        var processor = scope.ServiceProvider.GetRequiredService<IEventProcessor>();
        await processor.ProcessAsync(evt);
    }
}

Enter fullscreen mode Exit fullscreen mode

๐Ÿ‘‰ The listener stays singleton, but it spins up a scoped processor (and DbContext) per event.

๐Ÿ“ TL;DR

  • Never inject scoped services (like DbContext) directly into a singleton.
  • If your processor is stateless โ†’ make it singleton.
  • If it needs DbContext โ†’ either:
    • Use IServiceScopeFactory inside the singleton processor, or
    • Make the processor scoped and resolve it per event.

Think of it like this:

  • Singleton = one toothbrush for life.
  • Scoped = one toothbrush per person/request.
  • Transient = disposable toothbrush every time.

You wouldnโ€™t share one toothbrush forever, right? Same rule applies to DbContext.

์›๋ฌธ์—์„œ ๊ณ„์† โ†—

์ถ”์ถœ ๋ณธ๋ฌธ ยท ์ถœ์ฒ˜: dev.to ยท https://dev.to/rahul1994jh/event-listeners-processors-and-dbcontext-pitfalls-best-practices-net-10-185m

์ฝ”๋ฉ˜ํŠธ

๋‹ต๊ธ€ ๋‚จ๊ธฐ๊ธฐ

์ด๋ฉ”์ผ ์ฃผ์†Œ๋Š” ๊ณต๊ฐœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•„์ˆ˜ ํ•„๋“œ๋Š” *๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค