Lord Zed
Lord Zed

Reputation: 780

Simple Injector, .NET Core 3.1, short lifetime of a Context object

I'm trying to pass correlationId that I would receive through controller to my IEventDispatcher and I want this object that holds correlationId to be disposed soon as EventHandler is done with it.

Also, I'm listening to ServiceBus queue and topic and I have instance of IEventDispatcher there as well and I'd like to apply same logic there.

I inject EventDispatcher in Controller where I set internal Context which I hope to have access to in EventHandler but not by passing context directly but accessing context through DI.

public class CommandController : ControllerBase
{
    private readonly IEventDispatcher eventDispatcher;

    public CommandController(IEventDispatcher eventDispatcher)
    {
        this.eventDispatcher = eventDispatcher;
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody]TAction action)
    {
        try
        {
            // I want this context to be disposed after handler 
            // that is called inside of eventDispatcher is done with execution
            var context = new HttpRequestContext(HttpContext); 

            await eventDispatcher.Dispatch<TAction>((TAction)action, context);
            return Ok();
        } catch (Exception e)
        {
            return BadRequest((new BadExceptionResult { Error = e.Message }));
        }
    }
}    

I use dotnet core 3.1 web api, and I have SimpleInjector setup like this:

class ServiceSetup    
    private Container container = new Container();

    public void ConfigureServices(IServiceCollection services)
    {
        container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
        // here I set RequestContextRegistrator 
        // and then I set this context inside EventHandler to set context 
        // and hope to have it available in handler
        container.Register<IRequestContextRegistrator, RequestContextRegistrator>(
            Lifestyle.Scoped);
        container.Register(() => container.GetInstance<IRequestContextRegistrator>().Get(),
            Lifestyle.Scoped);

        // ...
        // EventHandlers are registered just before EventDispatcher as EventDispatcher is
        // is depending on eventHandlers to be registered before eventDispatcher can send
        // a request to them
        var eventDispatcher = new EventDispatcher(container);

        services.AddSingleton<IEventDispatcher>(eventDispatcher);
        // ...
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseSimpleInjector(container);
        // ...
        container.Verify();
    }
}    

RequestContextRegistrator : IRequestContextRegistrator

internal class RequestContextRegistrator : IRequestContextRegistrator
{
    private readonly IContext context = new Context();

    public IContext RegisterContext(IContext context)
    {
        context.CorrelationId = new Guid().ToString();

        return context;
    }

    public Context Get()
    {
        return new Context()
        {
            CorrelationId = context.CorrelationId
        };
    }
}

Here it is how EventDispatcher kind of looks like

public class EventDispatcher : IEventDispatcher
{
    Container container;

    public EventDispatcher(Container container)
    {
        this.container = container;
    }

    public async Task Dispatch<TAction>(TAction action, IContext context)
    {
        using (AsyncScopedLifestyle.BeginScope(container))
        {
            // this is registered in `ConfigureServices` before  
            var handler = container.GetInstance(IEventHandler<TAction>);
        }
    }
}    

As you can see I use using (AsyncScopedLifestyle.BeginScope(container)) but in handler's constructor I never get the Context object injected, it's always null.

Upvotes: 0

Views: 475

Answers (1)

Steven
Steven

Reputation: 172606

You can store the IContext inside a Scoped components (such as your RequestContextRegistrator) at the start of your scope. For instance:

public async Task Dispatch<TAction>(TAction action, IContext context)
{
    using (AsyncScopedLifestyle.BeginScope(container))
    {
        container.GetInstance<IContextProvider>().SetContext(context);

        var handler = container.GetInstance<IEventHandler<TAction>>();
        await handler.Handle(action);
    }
}

Your handlers can now be injected with the IContextProvider to get access to the IContext:

public class OrderShippedHandler : IEventHandler<OrderShipped>
{
    private readonly IContextProvider provider;

    public OrderShippedHandler(IContextProvider provider) => this.provider = provider;

    public async Task Handle(OrderShipped e)
    {
        IContext context = this.provider.Context;
    }
}

Don't forget to register the IContextProvider implemenation as scoped.

This way of storing state inside object graphs is called the Closure Composition Model.

Upvotes: 1

Related Questions