rebus-org / Rebus.ServiceProvider

:bus: Microsoft Extensions Dependency Injection container adapter for Rebus
https://mookid.dk/category/rebus
Other
65 stars 32 forks source link

How to get ServiceScope #19

Closed kewinbrand closed 4 years ago

kewinbrand commented 5 years ago

Hello,

Is there any way to get the IServiceScope that resolved handlers for a message? The problem is that I create some scoped dependencies in a custom incoming step but obviously they are created in a different scope than the on used to resolved the handlers.

Cheers

vegar commented 5 years ago

This is very much relevant to my issue in the main project: https://github.com/rebus-org/Rebus/issues/789

I've created a modified handler activator that will check the incoming scope context for a existing scope, and use that to resolve the handler instead of creating a new scope.

My current problem is the outgoing pipeline...

kewinbrand commented 5 years ago

Hi Vegar,

Until I get an answer I copied this entire repo to my project and modified one line. I stash the IServiceProvider in the transaction context and the use it afterwards. IDK if it's the best option that's why didn't pull request it. I can show where I did it if you want to.

Cheers

vegar commented 5 years ago

Would love to see your modification.

kewinbrand commented 5 years ago

Hi,

In DependencyInjectionHandlerActivator I modified the method GetHandlers:

        /// <summary>
        /// Resolves all handlers for the given <typeparamref name="TMessage"/> message type
        /// </summary>
        /// <exception cref="System.InvalidOperationException"></exception>
        public Task<IEnumerable<IHandleMessages<TMessage>>> GetHandlers<TMessage>(TMessage message, ITransactionContext transactionContext)
        {
            var scope = _provider.CreateScope();

            transactionContext.Items["service-scope"] = scope; //this line

            var resolvedHandlerInstances = GetMessageHandlersForMessage<TMessage>(scope);

            transactionContext.OnDisposed(scope.Dispose);

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

and created a step:

public class ContainerScopeInitializerStep: IIncomingStep, IOutgoingStep
    {
        private static IServiceProviderAccessor ServiceProviderAccessor => IocManager.RootServiceProviderInstance.GetService<IServiceProviderAccessor>();

        private Task InternalProcess(StepContext context, Func<Task> next)
        {
            var serviceProvider = ServiceProviderAccessor.CurrentServiceProvider ?? context.TryGetServiceProvider();
            if(serviceProvider == null)
                throw new Exception($"Could not resolve a suitable ServiceProvider in ContainerScopeInitializerStep.Process({context.GetType().FullName} context). ServiceProvider should come from current HttpContext or ITransactionContext.");
            context.Save(serviceProvider);
            return next();
        }

        public async Task Process(IncomingStepContext context, Func<Task> next)
        {
            await InternalProcess(context, next);
        }

        public async Task Process(OutgoingStepContext context, Func<Task> next)
        {
            await InternalProcess(context, next);
        }
    }
}

In the subsequent steps I load it from the context. Example:

public class MultiTenancyMessageHeaderSetOutgoingStep : IOutgoingStep //can be IIncominStep
{

  public async Task Process(OutgoingStepContext context, Func<Task> next)
        {
            var message = context.Load<Message>();
            var serviceProvider = context.Load<IServiceProvider>();
           //get any service from the scoped IServiceProvider
            await next();
        }

}

Edit: forgot the TryGetServiceProvider extension method

public static IServiceProvider TryGetServiceProvider(this StepContext stepContext)
        {
            IServiceScope serviceScope = null;
            var transactionContext = stepContext.Load<ITransactionContext>();
            if (transactionContext != null && transactionContext.Items.ContainsKey("service-scope"))
            {
                serviceScope= transactionContext.Items["service-scope"] as IServiceScope;
            }

            return serviceScope?.ServiceProvider;
        }

Hope it helps.

vegar commented 5 years ago

Interesting. 🤔

What is this part:

private static IServiceProviderAccessor ServiceProviderAccessor => IocManager.RootServiceProviderInstance.GetService<IServiceProviderAccessor>();

This is not something I recognize from the built in asp.net core container?

The scope that you create in the GetHandlers-method - isn't that code run after the pipeline steps? That's what I've thought in my attempt at least...

In my attempt, I've created a scope in the incoming step and stored it on the incoming context. And then I get a problem in the outgoing steps...

kewinbrand commented 5 years ago

IocManager is a static class with one property RootServiceProviderInstance that is the set from the IApplicationBuilder.ApplicationServices. From this IServiceProvider you can resolve singleton dependencies (not sure if you can get transient, but I'm pretty sure you can't scoped ones). ServiceProviderAccessor is a simple class that tries to get the IServiceProvider from the IHttpContextAccessor. Basically it does this:

public IServiceProvider CurrentServiceProvider => _accessor.HttpContext?.RequestServices;

I need this because in the ContainerScopeInitializerStep I can't know if the processing messaging comes from the broker or from my controller for example.

This is all necessary because Rebus doesn't let me resolve steps with DI. By design every step that you inject in the pipeline is singleton and this can cause some problems, such as what if I need to get a scoped dependency from my steps.

vegar commented 5 years ago

There are things I don't quite understand, though:

Incoming step takes scope either from accessor or context. The accessor will not return a scope, since there are no http context for an incoming rebus message. The context will not contain any scope, since the scope is created in the handler activator, which is called after the incoming steps.

Or...? What part am I missing?

kewinbrand commented 5 years ago

ContainerScopeInitializerStep is positioned after the ActivateHandlersStep and before AssignDefaultHeadersStep.

Outgoing message, the pipeline should be: [Rebus steps] -> AssignDefaultHeadersStep -> ContainerScopeInitializerStep (acting as IOutgoingStep) scope comes from IHttpContextAccessor or from StepContext if the message was published from a handler -> My other custom steps resolve from OutgoingStepContext.Load

Incoming message, the pipeline should be: [Rebus steps] -> ActivateHandlersStep before this step the GetHandlers method is called and the scope gets stashed -> ContainerScopeInitializerStep (acting as IIncomingStep) scope comes from StepContext -> My other custom steps...

In my case I wanted to use the same scope from the one used to get the handlers because all dependencies that were injected in the them must be in the same scope as other dependencies that I get (in previous steps) to set some custom headers.

vegar commented 5 years ago

Wow. I didn't realize that the handler activation was part of the same pipeline.... In my head all the pipeline steps was executed and then the activation happened right before invoking it...

Except for that, we have the exact same problem that we are trying to solve: resolving dependencies in the pipeline steps with the same scope as the message handler it self.

I've sort of solved it a little different now, though... I've created the scope in the ContainerScopeInitializerStep and reused that scope in the activation handler. Now I need to decide if I'm gonna leave it as it is or if I should see if it becomes a little cleaner your way...

Anyway: Thank you so much for help and insight!

vegar commented 5 years ago

Anyway - having the service scope saved in the transaction context by the Rebus.ServiceProvider would saved a lot of hazzle... Would be nice to have that part as an official change...

kewinbrand commented 5 years ago

Yeah, I'm just waiting for an opinion from @mookid8000 on this

mookid8000 commented 5 years ago

Sounds to me like saving the service scope in the transaction context is the way to go – that way, it will be available in both the incoming and the outgoing context...

kewinbrand commented 5 years ago

Great.

Apart of this I feel a lack of supporting DI scopes out of the box in Rebus like unit of work. What you think?

Cheers

mookid8000 commented 5 years ago

I guess you're right.... I guess it would be kind of neat if all IoC container adapters (if they support it) would create scopes and resolve their handlers with them, and then make the scopes available in the transaction context.

And not only create scopes: They should stash it under a well-known key, and then if a scope already exists with that key, they should instead use that to create handlers.

vegar commented 5 years ago

The scenario of incoming messages seems quite 'simple': a scope should be created in the activation step, and made available for further steps through the transaction context. A custom incoming step might be added to also save the provider to the step context, but it's not necessary. The transaction context is available for the later steps anyway.

For the scenario of outgoing messages, I find it a little bit more complex: In @kewinbrand's code it will throw an exception if you're ever outside of the scope of either handling a web request or a incoming rebus message. The scope needs to come either from the transaction context or from the http context.

What if you want to send a message on service startup?

From what I see, there would be three ways of handling this:

  1. Not allow it, forcing a separate bus for such events - a bus without pipeline steps requiring service activation.
  2. Allow it, but silently ignore the missing service provider, forcing later steps to handle it how ever they want.
  3. Allow it, and store the root container in the step scope, letting later steps resolve services from root scope.

Option 2 doesn't look very robust... It will of cause depend on the nature of the step : some might be ok to ignore, some will have to throw error if services are needed. Option 3 also seems a little hard to live with.. this option will allow generic services to be resolved - like a ILogger instance, so that's good. But in the case of scoped services, this will either resolve a 'root-scoped' instance, shared between all non scoped requests, or it will fail - if the provider was created with validateScopes = true.

I think maybe I'll go for option 3... ...until I change my mind again...

mookid8000 commented 4 years ago

Hello all, here's a quick comment that might be of interest: the service provider's scope is not 😁 now saved under the incoming step context, thus making it available for the rest of the pipeline – it can be accessed like this:

var scope = context.Load<IServiceScope>();

// do stuff with it

Moreover, the handler activator (DependencyInjectionHandlerActivator) will REUSE THE SCOPE if it alread exists, meaning that the previous steps can leave a scope in the context (possibly because they need to resolve some scoped stuff themselves) and have it used to resolve handlers with.

🙂 Closing this issue for now. Please let me know if I've missed something important. 🙂

vegar commented 4 years ago

the service provider's scope is not saved under the incoming step context

I hope that should have been ...scope is *now* saved... ?

In what version of rebus is available?

mookid8000 commented 4 years ago

Thanks! 😁

Rebus.ServiceProvider 5.0.2 has it 🙂

arielmoraes commented 4 years ago

@mookid8000 by analysing the code I could see the Scope is created (if not provided previously) in the HandlerActivatorStep, isn't it better if the creation of the Scope was done on another Step? Asking this because that would avoid us having to create a step like CreateScopeIncomingStep in case of configurations that need to happen before the handler step.

mookid8000 commented 4 years ago

@arielmoraes does the current way of working prevent you from something?

mouradchama commented 8 months ago

After migrating to the last version I'm I'm receiving a null value when I call context.Load. You need just to check if there an async scope if the the result is null, I create an extension method :

public static IServiceProvider GetServiceProvider(this StepContext stepContext)
{
    var scope = stepContext.Load<IServiceScope>() ?? stepContext.Load<AsyncServiceScope?>();
    return scope?.ServiceProvider;
}
ilcorvo commented 1 month ago

Hi, I'm using the latest versions of both Rebus and Rebus.ServiceProvider, in a .net7 solution. I'm trying to use the following code: var serviceScope = stepContext.Load<IServiceScope>(); However, it returns null. My use case is that I get information from headers of incoming requests via middleware and storing them in a scoped service. I then aim to transmit this information to the message handlers by 2 pipelines steps (outgoing and incoming) that use message headers as a transport and deserialize the information again into a scoped service. Previously, using Rebus version 3.0.1 and SimpleInjector, I accomplished this easily, but now I'm struggling to find examples on how to access the 'IServiceScope' in the send and receive pipelines. I tried using 'ServiceProviderScopeStepContextExtensions' but I receive null. Am I missing some configuration?