Search code examples
c#.net-6.0simple-injector

SimpleInjector CrossWiring confusion


I've been looking through the Simple Injector documentation and thought I was doing things right regarding Crosswiring, but alas...

I have the following which fails to resolve my App through the IServiceProvider

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SimpleInjector;
using SimpleInjector.Lifestyles;
using SimpleInjectorTesting;

Container container = new Container { Options = { DefaultScopedLifestyle =
    new AsyncScopedLifestyle()}};

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Information))
    .ConfigureServices((hostBuilderContext, services) =>
    {
        container.RegisterSingleton<App>();

        services.AddSingleton<ISaySomethingService, SaySomethingService>();
        services.AddSimpleInjector(container);   
    })
    .Build();

host.Services.UseSimpleInjector(container);

container.Verify();

var app = host.Services.GetRequiredService<App>();
//var app = container.GetInstance<App>();
await app.Run();

If I switch to use the container.GetInstance<App>() call the it works fine, SI can resolve services registered from IServiceCollection.

If I set options.AddLogging() to SI, an IServiceProvider registered type can't receive ILogger even if I'm using container.GetInstance<App>()

System.InvalidOperationException: 'The configuration is invalid. Creating the instance for type App failed. The registered delegate for type ISaySomethingService threw an exception. Failed to resolve ISaySomethingService. ISaySomethingService is a cross-wired service, meaning that Simple Injector forwarded the request the framework's IServiceProvider in order to get an instance. The used Microsoft.Extensions.DependencyInjection.ServiceProvider, however, failed with the following message: "Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger' while attempting to activate 'SimpleInjectorTesting.SaySomethingService'.". This error might indicate a misconfiguration of services in the framework's IServiceCollection.'

So I have this library that does a bunch of stuff using SI and leverages decorators, conditionals, etc that standard .NET DI makes difficult but I want this library to be usable by anyone who may not be using Simple Injector. I have an extension method to Register my framework and then another to use it which does the container.Verify() call to get the SI specific stuff tucked away. But then I can't resolve through IServiceProvider as per the code above. Is this even possible with SI and if so what am I missing?

Update: This is my App class

public class App
{
    private readonly ISaySomethingService _saySomethingService;

    public App(ISaySomethingService saySomethingService)
    {
        _saySomethingService = saySomethingService;
    }

    public async Task Run(CancellationToken cancellationToken = default)
    {
        Console.WriteLine(_saySomethingService.Message());

    }

}

Solution

  • When it comes to integration with an .NET Core or ASP.NET Core application, Simple Injector works very differently compared to for instance Autofac. This has to do with the design choices made by Microsoft and their incompatibility with Simple Injector's design. I discussed this in detail here and here.

    But this is why choosing Autofac for your DI container in an ASP.NET Core application, Autofac ends up replacing the entire DI infrastructure, which means all application, framework, and third-party registrations will be made in Autofac.

    When choosing Simple Injector, on the other hand, the Simple Injector container will run side-by-side with the original MS.DI container. This means that you, as an application developer, will register your application components as part of Simple Injector, while framework and third-party libraries that depend on the IServiceCollection abstraction, will effectively use MS.DI as their backing container.

    The Simple Injector integration package for .NET Core, however, does allow a feature called "cross wiring" or "auto cross wiring", which allows the Simple Injector container to resolve services from the built-in MS.DI container to inject them into your Simple Injector-resolved application components. This feature is implemented using Simple Injector's unresolved type resolution. As there is no such feature in MS.DI, it's not possible to let MS.DI automatically callback into Simple Injector to fetch missing dependencies. You can always do this on a per-service basis by making an explicit registration to MS.DI though, e.g. services.AddTransient<IService>(_ => container.GetInstance<IService>()).

    In the majority of cases, however, you shouldn't need to be able to 'cross wire' back from MS.DI to Simple Injector, because the dependency would typically go from application component towards framework (or third-party) abstraction; not the other way around.

    But the fact that Simple Injector doesn't replace the built-in DI infrastructure (but run side-by-side) does have the consequence that adding registrations to the IServiceCollection doesn't add them to Simple Injector. And this means that if you have a third-party library (or self-built NuGet package) that integrated with IServiceCollection, this won't run as part of Simple Injector.

    Under normal conditions this wouldn't be a problem, because its okay for framework and third-party components to be composed by MS.DI, especially because Simple Injector makes it easy to pull them in. You, as an application developer, would typically don't see the difference (although it's important to understand what to register where).

    But your case is different, because you are building a reusable library with application-specific abstractions (and implementations) while you want to simplify registration for the application developers using these abstractions. Adding a AddMyReusableLibrary(this IServiceCollection) method to that library will work when the end application is completely relying on MS.DI or Autofac, but won't help when the application uses Simple Injector.

    There are a few options here:

    1. You build an extra NuGet package, say MyReusableLibrary.SimpleInjector that depends on both MyReusableLibrary and SimpleInjector, and adds an AddMyReusableLibrary(this SimpleInjector.Container) method, or
    2. You simply document what registrations need to be made to Simple Injector. It will typically be a handful of registrations. This gives the application developer full control and knowledge about the made registrations, although, admittingly, does cause some more code in the application's Composition Root, and doesn't allow new features in vNext of your MyReusableLibrary to get automatically added.
    3. You design your reusable library according to Mark Seemann's advice on DI-friendly libraries. Although, in your case that unlikely helps because your reusable library actually contains application abstractions. It is a reusable library as in -it is a NuGet package-, but in fact contains parts of the application's core design and domain.