Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify ServiceCollection integration for (non-ASP) .NET Core projects #639

Closed
dotnetjunkie opened this issue Dec 5, 2018 · 19 comments
Closed

Comments

@dotnetjunkie
Copy link
Collaborator

See: https://stackoverflow.com/questions/52111898/

Currently, cross wiring in Simple Injector is deeply integrated into ASP.NET Core. This means, for instance, that when you have a third-party component (such as Entity Framework Core) that is deeply integrated into IServiceCollection, it might be more involved to get this working in a simple .NET Core console application.

We should strive to streamline the experience for .NET Core users who don't use ASP.NET Core.

@dotnetjunkie
Copy link
Collaborator Author

Branch feature-639 created.

@dotnetjunkie
Copy link
Collaborator Author

dotnetjunkie commented Apr 22, 2019

I added a new SimpleInjector.Integration.ServiceCollection NuGet package that contains AddSimpleInjector(this IServiceCollection) and UseSimpleInjector(this IServiceProvider, Container) extension methods that simplify working with .NET Core (console) applications that make use of the IServiceCollection abstraction to get certain framework components.

Here's an example of the use, while configuring a pooled Entity Framework DbContext in the IServiceCollection:

public class MainService
{
    public MainService(AdventureWorksContext context) { }
}

public void Main()
{
    var container = new SimpleInjector.Container();
    
    var provider = new ServiceCollection()
        .AddDbContextPool<AdventureWorksContext>(options => { /*options */ })

        // New extension method. Sets up the basic configuration that allows Simple Injector to be
        // used in frameworks that require the use of IServiceCollection for registration of 
        // framework components.
        .AddSimpleInjector(container)

        .BuildServiceProvider(true);
    
    // New extension method. Finalizes the configuration of Simple Injector on top of
    // IServiceCollection. Ensures framework components can be injected into
    // Simple Injector-resolved components.
    provider.UseSimpleInjector(container);
    
    container.Register<MainService>();
    
    container.Verify();
    
    using (AsyncScopedLifestyle.BeginScope(container))
    {
        var service = container.GetInstance<MainService>();
        service.DoSomething();
    }
}

This new NuGet package is now in beta and I would like to urge anyone interested to give it a test run and send feedback about its working and its API. I am very interested to see different use cases and integration scenarios than the one given above.

@jakoss
Copy link

jakoss commented May 2, 2019

It seems to be working fine, i just have one question. In old code, using Microsoft.Extensions.DependencyInjection i used injected IServiceProvider to resolve some dependencies. Now I get the error that IServiceProvider is not registered. How can I handle such situation in Simple Injector? I can't inject my dependency by contructor so i need to use something like IServiceProvider. As far as i understand Simple Injector documentation i need to create new scope, but I need to pass container for that (and I don't think that's a good idea)

@jakoss
Copy link

jakoss commented May 2, 2019

Ok, i got it working by injecting Container, but i get why this is bad practise. Anyway - beta version is working great for me, thanks for that!

@jakoss
Copy link

jakoss commented May 2, 2019

I found a little issue. When I try to use HostedService the dependencies doesn't get injected.

I register HostedService like this:

.ConfigureServices((hostContext, services) =>
{
    services.AddHostedService<HostedService>();
    services.AddSimpleInjector(container);
})

And hosted service constructor looks like this:

internal class HostedService : IHostedService
{
    public HostedService(
        ILogger<HostedService> logger, ApplicationSettings settings,
        IFilesInfrastructure filesInfrastructure, WcfService wcfService,
        ThreadsManager threadsManager)
    {
        this.logger = logger;
        this.settings = settings;
        this.filesInfrastructure = filesInfrastructure;
        this.wcfService = wcfService;
        this.threadsManager = threadsManager;
    }
}

Stack Trace:

System.InvalidOperationException: Unable to resolve service for type 'MusicRecognition.Service.WorkerThreadManager.ApplicationSettings' while attempting to activate 'MusicRecognition.Service.WorkerThreadManager.HostedService'.
   w Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   w Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(Type serviceType, Type implementationType, CallSiteChain callSiteChain)
   w Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain)
   w Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateEnumerable(Type serviceType, CallSiteChain callSiteChain)
   w Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
   w Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.CreateServiceAccessor(Type serviceType)
   w System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   w Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   w Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
   w Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>d__9.MoveNext()
--- Koniec śladu stosu z poprzedniej lokalizacji, w której wystąpił wyjątek ---
   w System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   w System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   w Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Start(IHost host)
   w MusicRecognition.Service.WorkerThreadManager.Program.Start()

@dotnetjunkie
Copy link
Collaborator Author

but i get why this is bad practise.

Injecting Container is as bad as injecting IServiceProvider. It is fine when the consuming class is part of the Composition Root, but a problem when it's outside the Composition Root, because you would be applying the Service Locator anti-pattern.

@dotnetjunkie
Copy link
Collaborator Author

When i try to use HostedService the dependencies doesn't get injected.

See this:

container.RegisterSingleton<MyHostedService>();
services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService>(
    _ => container.GetInstance<MyHostedService>());

@matt-lethargic
Copy link

matt-lethargic commented May 2, 2019

I've been looking at this for a couple of days was making it way too hard for myself trying to follow everything that was said in all of the related issues etc. Got things mainly working except ILogger.

I'm using the Microsoft.Extensions.Logging.Abstractions.ILogger interface for logging, the catch is I'm using SeriLog. I've set it up using the Core IoC, but now don't know how to get this working with SI

.ConfigureServices((hostContext, services) =>
{
   var container = BuildContainer();

   services.AddSimpleInjector(container);
   services.AddSingleton<IHostedService>(container.GetInstance<QueueListenerService>());
   services.AddLogging(c => { c.AddSerilog(); });
})

the c.AddSerilog() does this (ILoggerProvider) new SerilogLoggerProvider(logger, dispose) which I tried replicating in SI container.RegisterInstance<ILoggerProvider>(new SerilogLoggerProvider(Log.Logger)); but this doesn't work either

Any help on this would be great, it's been a while since I've been this stuck / confused!!

@jakoss
Copy link

jakoss commented May 2, 2019

@matt-lethargic Use this instead https://github.com/serilog/serilog-extensions-hosting . Works for me just fine

@dotnetjunkie
Copy link
Collaborator Author

Thanks for your feedback. It's very important for me to know when you're confused. This helps me decide which information to add to the documentation. Logging should definately be one.

Got things mainly working except ILogger. [...] I've set it up using the Core IoC, but now don't know how to get this working with SI

There is Stack Overflow answer that describes how to integrate Microsoft.Extensions.Logging into Simple Injector, see here. This will probably work just as well with Serilog, but also take a look at this question, which is specifically about Serilog.

@matt-lethargic
Copy link

matt-lethargic commented May 2, 2019

@NekroMancer Funnily enough I was using that extension as well I had .UseSerilog(); in my code, but forgot to write it on my comment! Still doesn't work!

Here's most of my Program.cs

static async Task<int> Main(string[] args)
{
    Log.Logger = new LoggerConfiguration()
        .ReadFrom.Configuration(Configuration)
        .Enrich.FromLogContext()
        .CreateLogger();

    try
    {
        Log.Information("Starting");
        await CreateHostBuilder(args).Build().RunAsync();
        return 0;
    }
    catch (Exception ex)
    {
        Log.Fatal(ex, "host terminated unexpectedly");
        return 1;
    }
    finally
    {
        Log.CloseAndFlush();
    }
}

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return new HostBuilder()
        .ConfigureHostConfiguration(configHost =>
        {
            configHost.SetBasePath(Directory.GetCurrentDirectory());
            configHost.AddJsonFile("hostsettings.json", optional: true);
            configHost.AddEnvironmentVariables();
            configHost.AddCommandLine(args);
        })
        .ConfigureAppConfiguration(config =>
        {
            config.AddJsonFile("appsettings.json", optional: false);
            config.AddEnvironmentVariables();
            config.AddCommandLine(args);
        })
        .ConfigureServices((hostContext, services) =>
        {
            var container = BuildContainer();

            //services.Configure<AppConfig>(hostContext.Configuration.GetSection("AppConfig"));
            
            services.AddSimpleInjector(container);
            services.AddSingleton<IHostedService>(container.GetInstance<QueueListenerService>());
        })
        .ConfigureLogging(config =>
        {

        })
        .UseSerilog();
}

private static Container BuildContainer()
{
    var container = new Container();

    container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
    container.RegisterInstance(typeof(IServiceProvider), container);
    
    container.RegisterSingleton<QueueListenerService>();
    container.Register<ICommandDispatcher, CommandDispatcher>();
    container.Register<IQueueListener, ServiceBusQueueListener>();
    container.Register<IMessageProcessor, MessageProcessor>();

    return container;
}

and for context

QueueListenerService.cs ctor

using Microsoft.Extensions.Logging;
...
public QueueListenerService(ILogger logger, IQueueListener queueListener)
{
    _queueListener = queueListener ?? throw new ArgumentNullException(nameof(queueListener));
    _logger = _logger ?? throw new ArgumentNullException(nameof(_logger));
}

I get exception SimpleInjector.ActivationException: The constructor of type QueueListenerService contains the parameter with name 'logger' and type ILogger that is not registered.

@dotnetjunkie I've read those SO post and you seem to point towards writing a ILogger interface that belongs to the app and hides the chosen framework, this is what I was trying to do by using the MS ILogger interface from their Microsoft.Extensions.Logging.Abstraction nuget package (that does seem to adhere to SIP).

I think the two bits I'm struggling with are how cross-wiring works, are things registerd in .net IoC available to SI and the other way around and if UseSerilog() is registering itself properly with .net IoC then wouldn't it be avabile in SI?

@jakoss
Copy link

jakoss commented May 3, 2019

First of all - you don't do any cross-wiring in your sample code. You need to call UseSimpleInjector(container) on IServiceProvider. I'm doing this like:

host = new HostBuilder()
    .UseSerilog()
    .ConfigureServices((hostContext, services) =>
    {
        services.AddSimpleInjector(container);
        // register my dependencies
        [...]
    })
    .Build();

host.Services.UseSimpleInjector(container);
container.Verify();
host.Start();

And it works fine. You need to call UseSimpleInjector on builded provider.

Second - apparently you can't inject non-generic ILogger itself (source: https://stackoverflow.com/a/51394689/939133). You have to inject ILogger<QueryListenerService> and then you can assign it to private ILogger field. Or, you can inject ILoggerFactory.

@dotnetjunkie
Copy link
Collaborator Author

@matt-lethargic

I've read those SO post and you seem to point towards writing an ILogger interface that belongs to the app and hides the chosen framework, this is what I was trying to do by using the MS ILogger interface

The MS ILogger interface is a framework abstraction, not an application-specific abstraction. In that post I urge to define the latter

by using the MS ILogger interface from their Microsoft.Extensions.Logging.Abstraction nuget package (that does seem to adhere to SIP).

The problem is that Microsoft.Extensions.Logging.Abstraction.ILogger does not adhere to the Interface Segregation Principle.

But either way, the post also describes how to override Simple Injector's injection behavior to allow using (the non-generic) MS ILogger as a dependency, while injecting Logger<T> implementations into it.

@NekroMancer

apparently you can't inject non-generic ILogger itself (source: https://stackoverflow.com/a/51394689/939133). You have to inject ILogger<QueryListenerService> and then you can assign it to private ILogger field. Or, you can inject ILoggerFactory.

I consider the injection of ILogger<T> into application components to be a bad thing. Compared to injecting ILogger, ILogger<T> gives more noise, is error prone, and complicates testing. Instead, you should want to let the infrastructure ensure that always the right logger is injected according to a convention. With ILogger the most obvious convention is to always inject a Logger<T> where the T equals the consuming type. So HomeController gets injected with a Logger<HomeController> while it merely depends on the non-generic ILogger.

Your source, however, is correct. There is no way to do that with the built-in DI Container. There is, however, a reason you choose to use Simple Injector over the simplistic MS.DI container. That's because Simple Injector does allow you to make your code more maintainable and prevents you from having to fallback to code smells and hacks because of the limitation of the used DI Container.

So as my SO answer shows, you can (and arguably should) inject a non-generic ILogger into your application components.

@matt-lethargic
Copy link

So I'm just doing what would of been easier in the first place, I've ripped out MS ILogger, written my own logging interface that belongs to the core of the solution and implementing it at the Composition Root and injecting that with SI leaving MS.DI alone as much as possible.

@dotnetjunkie
Copy link
Collaborator Author

dotnetjunkie commented May 7, 2019

After taking a close look at both your responses, and taking a good look again at the .NET Generic Host documentation, I came to the conclusion that it might be good if another integration package gets added; one specific for working with Generic Hosts.

The new SimpleInjector.Integration.ServiceCollection package simplifies cross wiring on top of third-party libraries that integrate with IServiceCollection. This, however, does not simplify working with Generic Hosts, especially integration with Hosted Services—even if integration is just a few lines of code; troubles can be prevented if the Simple Injector integration package adds helper methods for this.

To give you an idea of the integration model I'm thinking of right now, here is a visual representation of this model:

stacked net core integration model

This diagram shows the model that is already in place with the SimpleInjector.Integration.ServiceCollection package added in v4.6.0-beta1. The green dashed parts, however, are things that I think should be added. This means that a new SimpleInjector.Integration.GenericHost package should be added that sits in between Integration.ServiceCollection and Integration.AspNetCore.

Here's a code-centric overview of what I think the end result should be:

Short-running .NET Core Console with only Simple Injector

Example:

public void Main()
{
    var container = new Container();
    
    container.Register<MainService>();
    
    container.Verify();
    
    var service = container.GetInstance<MainService>();
    service.DoAwesomeStuff();
}

Dependencies:

  • SimpleInjector.dll

Explanation:

For a true console application, integration with IServiceCollection is not always required. In many cases, you can do just fine with a pure Simple Injector approach.

Short-running .NET Core Console with only Simple Injector using scoping

Example:

public void Main()
{
    var container = new Container();
    container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
    
    container.Register<MainService>(Lifestyle.Scoped);
    
    container.Verify();
    
    using (AsyncScopedLifestyle.BeginScope(container))
    {
        var service = container.GetInstance<MainService>();
        service.DoAwesomeStuff();
    }
}

Dependencies:

  • SimpleInjector.dll

Explanation:

This example shows a short-running console application, that doesn't need to integrate with IServiceCollection where only Simple Injector is required.

.NET Core Console with Simple Injector integrated with .NET Core framework components

Example:

public void Main()
{
    var container = new Container();
    
    var services = new ServiceCollection()
        // Add all framework components you wish to inject into app components, for instance:
        .AddDbContextPool<AdventureWorksContext>(options => { /*options */ })
        .AddLogging()
        .AddOptions()
        
        // Integrate with Simple Injector
        .AddSimpleInjector(container);
        
    services
        .BuildServiceProvider(true)
        .UseSimpleInjector(container, options => // enables auto cross wiring.
        {
            // Allow application components to depend on Ms.Extensions.Logging.ILogger
            options.UseLogging();
        });
        
    container.Verify();
    
    using (AsyncScopedLifestyle.BeginScope(container))
    {
        var service = container.GetInstance<MainService>();
        service.DoAwesomeStuff();
    }
}

Dependencies:

  • SimpleInjector.dll
  • SimpleInjector.Integration.ServiceCollection.dll

Explanation:

In this example, the application requires working with framework components that can't easily be configured without IServiceCollection (most notably pooled DbContext). To achieve this, the SimpleInjector.Integration.ServiceCollection package can be used.

The AddLogging() method allows Simple Injector-resolved application components to be injected with the (non-generic) Microsoft.Extensions.Logging.ILogger interface, while ensuring a contextual generic Logger<T> implementation is injected.

.NET Core Generic Host with Simple Injector integration

Example:

public static async Task Main(string[] args)
{
    var container = new Container();

    IHost host = new HostBuilder()
        .ConfigureHostConfiguration(configHost => { ... })
        .ConfigureAppConfiguration((hostContext, configApp) => { ... })
        .ConfigureServices((hostContext, services) =>
        {
            services.AddSimpleInjector(container, options =>
            {
                // Hooks hosted services into the Generic Host pipeline
                // while resolving them through Simple Injector
                options.AddHostedService<TimedHostedService>();
            });
        })
        .ConfigureLogging((hostContext, configLogging) => { ... })
        .UseConsoleLifetime()
        .Build()
        .UseSimpleInjector(container, options =>
        {
            options.UseLogging();
        });

    container.Verify();

    await host.RunAsync();
}

Dependencies:

  • SimpleInjector.dll
  • SimpleInjector.Integration.GenericHost.dll (references SimpleInjector.Integration.ServiceCollection)

Explanation:

The .NET Generic Host model is tightly coupled with the IServiceCollection abstraction and it automatically adds many framework components to the IServiceCollection. AddHostedService<T>() ensures a hosted service is registered in Simple Injector and added to the Generic Host pipline.

ASP.NET Core (MVC) with Simple Injector integration

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddSimpleInjector(container, options =>
        {           
            options.AddHostedService<TimedHostedService>();
            
            options.AddAspNetCore() // Adds request scoping
                .AddControllerActivation()      
                .AddViewComponentActivation()   
                .AddPageModelActivation()
                .AddTagHelperActivation();
        
        });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseSimpleInjector(container, options =>
    {
        options.UseLogging();

        // Middleware integration
        options.UseMiddleware<CustomMiddleware1>(app);
    });
}

Dependencies:

  • SimpleInjector.dll
  • SimpleInjector.Integration.GenericHost.dll (references SimpleInjector.Integration.ServiceCollection)
  • SimpleInjector.Integration.AspNetCore.dll

Explanation:

This allows using the integration extensions inside ASP.NET Core's Startup class, instead of inside the HostBuilder.

dotnetjunkie added a commit that referenced this issue May 8, 2019
@dotnetjunkie
Copy link
Collaborator Author

I pushed Simple Injector v4.6.0-beta2 to NuGet. This beta adds:

  • a new SimpleInjector.Integration.GenericHost package that simplifies integration with Generic Hosts (as shown in the ".NET Core Generic Host with Simple Injector integration" example above)
  • a new .UseLogging() extension method that simplifies injecting ILogger dependencies into application components.

This beta release also contains the other v4.6 features that are currently marked as closed.

I would urge everyone reading this to check this out and supply me with feedback.

@matt-lethargic
Copy link

Just tried this out with a Generic Host and it works perfectly so far, even when using the Microsoft ILogger (including generic version) and then adding Serilog into the mix. I'll let you know if I do find anything. Thank you this is just what I was looking for.

@jakoss
Copy link

jakoss commented May 9, 2019

Works great for me too. Thanks :)

@dotnetjunkie
Copy link
Collaborator Author

dotnetjunkie commented May 10, 2019

Thank you for your feedback. Much appreciated. This gives me confidence about the usefulness and correctness of the new API.

In the meantime, I've been working on the documentation for this new API. Preview versions of these new integration guides can be found here:

Your feedback on these pages is very welcome.

Thanks in advance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants