simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 155 forks source link

SimpleInjector integration with NServiceBus #953

Closed arnar-fived closed 2 years ago

arnar-fived commented 2 years ago

Hi

I have a question regarding NServiceBus and SimpleInjector and wondering if you could assist me.

I've been using SimpleInjector for quite some time now for my ASP.NET Core REST APIs, Azure WebJobs and some console background workers with good success.

I'm currently in the process now to add new functionality to our platform that uses NServiceBus endpoints.

After some investigation and google-ing I haven't been able to find concrete information on how to integrate the SimpleInjector DI container with the NServiceBus endpoints.

The NServiceBus endpoints are implemented on top of the NET Generic Host similar to this. https://docs.particular.net/samples/hosting/generic-host/

The "builder.UseNServiceBus" integrates with the Microsoft Generic Host and automatically uses the hosts managed dependency injection container. That's then either the native DI container or a 3rd party container that overrides the UseServiceProviderFactory on the generic host.

As I understand, SimpleInjector does NOT replace the .NET built in container. https://docs.simpleinjector.org/en/latest/servicecollectionintegration.html

Is there then a way for me to use SimpleInjector as my preferred DI container to register my application dependencies with my NServiceBus endpoints that I build on top of the Microsoft Generic Host?

With Version 7 of NServiceBus, they support overriding the container by invoking a method on the EndpointConfiguration called endpointConfiguration.UseContainer();

https://docs.particular.net/nservicebus/dependency-injection/#internally-managed-mode-using-a-third-party-container

https://github.com/WilliamBZA/NServicebus.SimpleInjector I think I saw a comment from you on some issue on Github that you adviced people not to use this package since it's neither an official NServiceBus nor SimpleInjector package.

However, the UseContainer will be removed from version 8 that is now in prerelease mode. So I don't want to spend time trying to figure out how to solve this with the UseContainer method.

Here are the DI upgrade guides going from version 7 to 8. https://docs.particular.net/nservicebus/upgrades/7to8/dependency-injection#externally-managed-container-mode

Will I need to create my own implementation of this IConfigureComponents abstraction?

https://github.com/simpleinjector/SimpleInjector/issues/798#issuecomment-583320729z -- And some comment where you are replying to a NSB dev regarding this topic.

So to wrap up, how could I use NServiceBus along side SimpleInjector with the changes they propose in version8?

I hope this is clear enough.

dotnetjunkie commented 2 years ago

Back in January of 2017 I had a long Skype call with @danielmarbach from the NServiceBus team about, among other things, integration of so called non-conforming containers. Perhaps Daniel can chime in on this conversation, but as far as I can see the current version still depends on the Conforming Container. What seems to be missing is an IMessageHandlerFactory abstraction that can be overridden.

This doesn't mean that all is lost, though, but it might require a bit of trickery to handlers resolved from Simple Injector. I don't have the time to build a runnable solution and test it, so instead I'll provide you with the basic idea. Hopefully you can build on top of that.

The trick -the idea- is to replace all handler registrations made in the IServiceCollection and replace them with registrations that forward their creation to Simple Injector. This can be done in an automated fashion, and might look as follows:

using SimpleInjector;

public static void ForwardHandlersToSimpleInjector(
    this IServiceCollection services,
    SimpleInjector.Container container)
{
    services.AddScoped<ScopeHolder>();

    for (int i = 0; i < services.Count; i++)
    {
        ServiceDescriptor descriptor = services[i];

        Type type = descriptor.ServiceType;

        bool shouldForward =
            !type.IsAbstract &&
            !type.IsGenericTypeDefinition &&
            type.IsClosedTypeOf(typeof(IHandleMessages<>));

        if (!shouldForward) continue;

        services[i] = new ServiceDescriptor(type, sp =>
            {
                var sh = sp.GetService<ScopeHolder>();
                sh.Scope ??= AsyncScopedLifestyle.BeginScope(container);
                return container.GetInstance(type);                
            },
            ServiceLifetime.Transient);
    }
}

sealed class ScopeHolder : IDisposable
{
    public SimpleInjector.Scope Scope { get; set; }
    public void Dispose() => this.Scope?.Dispose();
}

This would be about it. But do note the following:

danielmarbach commented 2 years ago

Sorry I'm currently hiking in Sweden and don't have a lot of time. I can hopefully look into this a bit in more depth next week.

I would assume though @williambza has more context since he built the simpleinjector integration. He is currently also with me travelling so our responses might be spotty.

arnar-fived commented 2 years ago

Thanks for a quick and details answer.

I managed to get this working using your proposed solution with some minor changes.

Most of the happy path cases I tested worked fine like resolving both .NET and NSB dependencies from SimpleInjector. Assuming this will work fine for now then at least.

Thanks!

danielmarbach commented 2 years ago

Do note that when your handler gets injected with a NSB component, that component won't get resolved from the IServiceScope that is maintained by NSB when handling your messages. Instead, Simple Injector will create a new IServiceScope for that. If that doesn't work, this behavior can be overridden. Let me know.

FYI this can be problematic, especially when you are using the synchronized storage session to make sure you have good integration the persistence (sagas, outbox...) and the persistence you are using in your handlers. It might be dangerous to not integrate with that scope because you end up with unpredictable behavior.

arnar-fived commented 2 years ago

The plan is to use SqlPersistence, sagas and outbox. So I'm going to need to share the Database connection/transaction created by NServiceBus to guarantee the business data will be persisted in the same transaction as the outbox data. Will this current implementation then cause my problems?

dotnetjunkie commented 2 years ago

Thanks for sharing this insight @danielmarbach. That means we need to override the Simple Injector's default IServiceProviderAccessor, which is defined in the SimpleInjector.Integration.ServiceCollection library.

The following code replaces the code I posted previously:

using SimpleInjector;

public static void AddNServiceBus(
    this SimpleInjectorAddOptions options, SimpleInjector.Container container)
{
    var services = options.Services;

    services.AddScoped<ScopeHolder>();

    var accessor = new NsbProviderAccessor(options.ServiceProviderAccessor);
    options.ServiceProviderAccessor = accessor;

    for (int i = 0; i < services.Count; i++)
    {
        ServiceDescriptor descriptor = services[i];

        Type type = descriptor.ServiceType;

        // NOTE: IsClosedTypeOf is a Simple Injector-provided extension method.
        bool shouldForward =
            !type.IsAbstract &&
            !type.IsGenericTypeDefinition &&
            type.IsClosedTypeOf(typeof(IHandleMessages<>));

        if (!shouldForward) continue;

        // Replace the MS registration with one that forwards to S.I.
        services[i] = new ServiceDescriptor(type, sp =>
            {
                accessor.Provider.Value = sp;
                var sh = sp.GetService<ScopeHolder>();
                sh.Scope ??= AsyncScopedLifestyle.BeginScope(container);
                return container.GetInstance(type);
            },
            ServiceLifetime.Transient);

         // Register the handler in S.I.
         container.Register(type, type, Lifestyle.Transient);
    }
}

sealed class ScopeHolder : IDisposable
{
    public SimpleInjector.Scope Scope { get; set; }
    public void Dispose() => this.Scope?.Dispose();
}

sealed class NsbProviderAccessor : IServiceProviderAccessor
{
    private IServiceProviderAccessor original;

    public readonly AsyncLocal<IServiceProvider> Provider =
        new AsyncLocal<IServiceProvider>();

    public NsbProviderAccessor(IServiceProviderAccessor original) =>
        this.original = original;

    public IServiceProvider Current =>
        this.Provider.Value ?? this.original.Current;
}

Using this extension method, the integration with NSB would become something like the following:

using SimpleInjector;

public static void Main()
{
    var container = new SimpleInjector.Container();

    var builder = Host.CreateDefaultBuilder(args);

    ...

    builder.UseNServiceBus(...);    

    // Integrate with Simple Injector
    builder.Services.AddSimpleInjector(container, options =>
    {
        options.AddNServiceBus();
    });

    ...

    var host = builder.Build();

    host.Services
        .UseSimpleInjector();

    host.Run();
}
dotnetjunkie commented 2 years ago

@arnar-fived,

I managed to get this working using your proposed solution with some minor changes.

Would you mind sharing the changes you made and later on report back on how this is working out for you, and if there are other findings. Using this feedback I'm hopefully able to transform this thread into official Simple Injector documentation.

arnar-fived commented 2 years ago

I've been stuck at another project at work so I've not had time to work on this as I've wanted.

I didn't have much time to test this or understand the whole flow, but here is the change I had to make.

Where you are overriding the ServiceDescriptor in the AddNServiceBus we need to resolve the handler from the SimpleInjector container.

So first off I guess we need to register our NServiceBus handlers to the SimpleInjector container?

Register(typeof(IHandleMessages<>), MyAssembly, Lifestyle.Transient);

Then I saw in the ServiceDescriptor, the type was the concrete implementation of the NServiceBus handler and not the generic definition.

 services[i] = new ServiceDescriptor(type, sp =>
    {
        accessor.Provider.Value = sp;
        var sh = sp.GetService<ScopeHolder>();
        sh.Scope ??= AsyncScopedLifestyle.BeginScope(container);
        return container.GetInstance(handlerType); --How to resolve this handlerType?
    },
    ServiceLifetime.Transient);

so type is mabye = CreateEmployeeHandler and not IHandleMessages<CreateEmployeeCommand>.

So I just needed to resolve the generic handler type from the concrete handler type.

Please inform me if there is a more finesse way of resolving the handler.

Ended up with something like this.

services[i] = new ServiceDescriptor(type, sp =>
   {
       accessor.Provider.Value = sp;
       var sh = sp.GetService<ScopeHolder>();
       sh.Scope ??= AsyncScopedLifestyle.BeginScope(container);
       var genericHandlerType = GetOpenGenericType(type);
       return container.GetInstance(genericHandlerType);                
   },
   ServiceLifetime.Transient);

GetOpenGenericType would then return IHandleMessages<CreateEmployeeCommand> where the input type is then CreateEmployeeHandler.

If you understand me.

dotnetjunkie commented 2 years ago

return container.GetInstance(handlerType); was a typo. I didn't bother to run my code through the compiler. It should have been return container.GetInstance(type); instead. I updated my samples.

This, however, doesn't work with your Register(typeof(IHandleMessages<>), MyAssembly, Lifestyle.Transient) registration, because that would make a set of registrations based on their interface. Although I would say that making registrations based on an abstraction is best (because it allows you to apply decorators), this doesn't work well with NServiceBus, as it expects that exact type. NServiceBus allows a single handler to implement multiple IHandleMessages<T> interfaces, and internally, it contains a mapping between messages and their handlers. That's why NSB requests for that concrete handler type.

This means that I think it's best to change your registration to the following:

var handlerTypes = container.GetTypesToRegister(typeof(IHandleMessages<>), MyAssembly);
foreach (Type type in handlerTypes) container.Register(type, type, Lifestyle.Transient);

This makes the registration of the concrete type itself, instead of a mapping from an interface to a concrete type. Although this disallows application of decorators, this might not be a problem, as most of the cross-cutting concerns you might want to apply will be provided by NSB anyway. It likely contains its own mechanism for most of them.

dotnetjunkie commented 2 years ago

If you need to get the implemented IHandleMessages<T> interface for a given handler, instead of calling your custom GetOpenGenericType(type) method, you also call Simple Injector's GetClosedTypeOf() extension method:

// GetClosedTypeOf will throw when there is more than one interface implemented.
var genericHandlerType = type.GetClosedTypeOf(typeof(IHandleMessages<>));

services[i] = new ServiceDescriptor(type, sp =>
   {
       accessor.Provider.Value = sp;
       var sh = sp.GetService<ScopeHolder>();
       sh.Scope ??= AsyncScopedLifestyle.BeginScope(container);
       return container.GetInstance(genericHandlerType);                
   },
   ServiceLifetime.Transient);
arnar-fived commented 2 years ago

Okay great, I'll register my handlers the way you propose. I've adjusted my code to your changes by overriding the ServiceProvider and they work (At least for a happy path). I've only created the scaffolding code for the endpoint and not started any complex business logic.

Thanks for the assistance and quick feedback, I'll let you know if I hit any walls when I start working on this project full time again.