rebus-org / Rebus.Events

:bus: Convenient event configuration extensions for Rebus
https://mookid.dk/category/rebus
Other
17 stars 1 forks source link

Events are raised using the Bus Scope #2

Closed arielmoraes closed 6 years ago

arielmoraes commented 6 years ago

If you use the Rebus.ServiceProvider library it will register the IBus as a Singleton, and as the Events are fired in the Singleton context it is not possible to get a Scoped service inside the event in .NET Core. The exception message is 'Cannot resolve scoped service 'ServiceName' from root provider.' I tried to add a new Pipeline Step but there is no injection available on it. How can I inject scoped services into Events or Pipeline Steps?

mookid8000 commented 6 years ago

How can I inject scoped services into Events or Pipeline Steps?

Rebus does not use your IoC container (ServiceProvider in this case) to resolve anything but your message handlers.

This is very deliberate, because there's simply too many subtle differences between container implementations to be able to make that work reliably. Also, pretty much all of Rebus' services are singletons, so it's not that interesting to have them resolved in an IoC container.

If you want to inject something into your own pipeline step, you need to inject it when the step is created, which is most likely during the registration:

Configure.With(...)
    .(...)
    .Options(o => {
        o.YourCustomConfigurationThingHere();
    })

public static void YourCustomConfigurationThingHere(this OptionsConfigurer configurer)
{
    configurer.Decorate<IPipeline>(c =>
    {
        var pipeline = c.Get<IPipeline>();
        var step = new YourOwnPipelineStep();
        return new PipelineStepInjector(pipeline)
            .OnSend(step, PipelineRelativePosition.Before, typeof(SerializeOutgoingMessageStep));
    });
}

So, if you want to have something resolved for each message, I suggest you pass a factory method into the YourOwnPipelineStep ctor, and then you resolve/release as appropriate.

arielmoraes commented 6 years ago

Let me explain a little more, we have a scenario where we have a multitenancy and one database per tenant, to know which tenant the message is for we've added a header in the message with the tenant id. We have created an ITenantAccessor to get the tenant for the current message being handled so that when we need to create our DbContext we know which Database we should point to. So as this is per message the ITenantAccessor was registered as Scoped. Any tips on how to achieve that when using Rebus?

mookid8000 commented 6 years ago

Which IoC container are you using? Windsor?

arielmoraes commented 6 years ago

I'm using the .NET Core Native IoC and the Rebus.ServiceProvider library.

mookid8000 commented 6 years ago

aaaahahahahaha, sorry!! I must not be completely awake 😁

You:

If you use the Rebus.ServiceProvider library (...)

Me:

Are you using Windsor?

Sorry about that 👍

What does it mean for a registration to be "scoped" with ServiceProvider?

arielmoraes commented 6 years ago

hahaha no problem.

When you call ServiceProvider.CreateScope() it will create a new scope, if you have a service called MyService registered with the container all calls to GetService<MyService> using the created scope will return the same instance. To understand it easily you can imagine it as "Instance per request" when using the ASP.NET Core.

mookid8000 commented 6 years ago

Well in that case, you should probably wrap the incoming pipeline in your scope somehow. You could probably get the most of the way by creating a nifty extensions method like this:

public static void EnableServiceProviderScopeThing(this OptionsConfigurer configurer)
{
    configurer.Decorate<IPipeline>(c =>
    {
        var pipeline = c.Get<IPipeline>();
        var step = new ServiceProviderScopeStep();
        return new PipelineStepInjector(pipeline)
            .OnReceive(step, PipelineRelativePosition.After, typeof(DeserializeIncomingMessageStep));
    });
}

and then enable it like this:

Configure.With(...)
    .(...)
    .Options(o => {
        o.EnableServiceProviderScopeThing();
    })

and then your ServiceProviderScopeStep could look somewhat like this:

public class ServiceProviderScopeStep : IIncomingStep
{
    public async Task Process(IncomingStepContext context, Func<Task> next) 
    {
        using(var scope = ServiceProvider.CreateScope())
        {
            await next();
        }   
    }
}

After creating the scope, it might be necessary to stash it in the IncomingStepContext somewhere, and then retrieve it again when you resolve your handlers. Not sure how ServiceProvider handles that part?

arielmoraes commented 6 years ago

Actually I needed to have access to the scope throughout the lifetime of a Message. I ended up creating my own ContainerAdapter and when the time comes to get the Handlers I add the created scope to the Transaction Context.

So instead of using the Container Adapter provided by the Rebus.ServiceProvider package now I use the following:

public Task<IEnumerable<IHandleMessages<TMessage>>> GetHandlers<TMessage>(TMessage message, ITransactionContext transactionContext)
{
    var scope = _provider.CreateScope();

    var resolvedHandlerInstances = GetMessageHandlersForMessage<TMessage>(scope);

    transactionContext.Items.TryAdd("CurrentScope", scope);
    transactionContext.OnDisposed(scope.Dispose);

    return Task.FromResult((IEnumerable<IHandleMessages<TMessage>>)resolvedHandlerInstances.ToArray());
}

So whenever I need to access that scope to retrieve a service outside the flow of the Handlers I access the current Transaction Context.

mookid8000 commented 6 years ago

That's a pretty good solution, much better than mine actually 😄

I see now that I was aiming for a "cross-the-river-to-get-water"-kind of solution(*) – it's much easier to simply create the scope and hook up with the transaction context's callbacks in the container adapter.

(*) It's a Danish saying that you should never cross the river to get water 😉

arielmoraes commented 6 years ago

We also have that saying here in Brazil hahaha, thanks for the tips once again.

danilobreda commented 4 years ago

With Simple Injector, it worked perfectly, I didn't have to create my own container adapter.