seesharper / LightInject.Microsoft.DependencyInjection

Implements Microsoft.Extensions.DependencyInjection.Abstraction
MIT License
47 stars 13 forks source link

DefaultServiceSelector behavior change in v3.7.0 #207

Open ArnaudB88 opened 11 months ago

ArnaudB88 commented 11 months ago

The DefaultServiceSelector value changed from (v3.6.3) options.DefaultServiceSelector = serviceNames => serviceNames.SingleOrDefault(string.IsNullOrWhiteSpace) ?? serviceNames.Last(); to (v3.7.0) options.DefaultServiceSelector = serviceNames => serviceNames.Last();

In the following scenario (dotnet7 asp.net core api project), the code to override registrations doesn't work anymore: Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Host
    .UseLightInject()
    .ConfigureContainer<ServiceContainer>(container =>
    {
        ConfigureContainer(container);
        ConfigureServicesWithContainer(container, builder.Services);//Add services to the container.
    });
...

void ConfigureContainer(IServiceContainer container)
{
    //Bootstrap container
    //https://www.lightinject.net/#assembly-scanning
     var dllFilePaths = Directory.GetFiles(AppContext.BaseDirectory, "MyNamespace.*.dll");
     foreach (var dllFilePath in dllFilePaths)
      {
           var assembly = Assembly.LoadFrom(dllFilePath);
           container.RegisterAssembly(assembly);
      }

    //Add/override registrations
    container.RegisterSingleton<AppSettings>(factory => AppSettingsFactory.Create());
}

The assembly scanning method will register the type 'AppSettings' with servicename "MyNamespace.AppSettings". The explicit singleton registration with factory afterwards, will register the type 'AppSettings' without a servicename.

When resolving the type, the following instances are returned;

This change in default behavior can't be used in all our projects since we rely on explicit overrides after assembly scanning.

Why was this change of behavior done? Is it possible that you changed it because you wanted registrations on the serviceCollection to be returned prior to registrations of the same type on the container (I say this based on the added unit tests). Can you change the behavior again so explicit registrations are returned prior to assembly scanned registrations?

ArnaudB88 commented 7 months ago

@seesharper Can you please take a look at this? It prevents us from using the latest version

ArnaudB88 commented 5 months ago

@seesharper Is the LightInject project abandoned?

seesharper commented 5 months ago

Sorry for the very late reply. The reason for this was that most users use LightInject as a drop-in replacement for the built-in container from Microsoft. Overriding services is in most cases done through the IServiceCollection which Microsoft provides. LightInject uses some tricks with the service names to make sure that we stay 100% compatible with MS.DI. This is not by any means ideal and now that MS.DI also supports named services, this becomes even worse. So in order to support MS.DI without the "hacks" , work has started to make some changes to LightInject that makes it possible to be compatible without using the service name as a way to hack this through. Would it be an acceptable workaround to override services via IServiceCollection rather than through IServiceRegistry?

ArnaudB88 commented 3 months ago

@seesharper To answer your question Would it be an acceptable workaround to override services via IServiceCollection rather than through IServiceRegistry?

That doesn't affect the result. The IServiceCollection doesn't contain any registrations from our application, it only contains ASP.NET Core class registrations. Also, we resolve services with the lightinject container, not the MS DI container. (fyi the IServiceCollection has 125 registrations, the lightinject container has 1500)

We have almost no option in how we could fix this in our code:

I see options to fix this in the LightInject code:

I understand your motivation for changes, but without a fix we simply can't upgrade anymore. It seems like other users have the same issue: #206

ArnaudB88 commented 5 days ago

@seesharper With the 4.0 update, I see that ContainerOptionsExtensions.WithMicrosoftSettings() doesnt set the ContainerOptions.DefaultServiceSelector anymore

With this change, it is possible to override assembly scanned registrations by registrations with different lifetimes again. If you share the same opinion, the new version fixes the bug described above.

But... a new bug arise with the new version: The web api project won't start the app and fails on the line var app = builder.Build();

The exception with callstack is

System.Collections.Generic.KeyNotFoundException
  HResult=0x80131577
  Message=The given key 'simple' was not present in the dictionary.
  Source=System.Collections.Concurrent
  StackTrace:
   at System.Collections.Concurrent.ConcurrentDictionary`2.ThrowKeyNotFoundException(TKey key)
   at System.Collections.Concurrent.ConcurrentDictionary`2.get_Item(TKey key)
   at Microsoft.Extensions.Logging.Console.ConsoleLoggerProvider.ReloadLoggerOptions(ConsoleLoggerOptions options)
   at Microsoft.Extensions.Logging.Console.ConsoleLoggerProvider..ctor(IOptionsMonitor`1 options, IEnumerable`1 formatters)
   at LightInject.Microsoft.DependencyInjection.PerRootScopeLifetime.GetInstance(GetInstanceDelegate createInstance, Scope scope, Object[] arguments)
   at LightInject.Microsoft.DependencyInjection.PerRootScopeLifetime.GetInstance(GetInstanceDelegate createInstance, Scope scope, Object[] arguments)
   at LightInject.Microsoft.DependencyInjection.PerRootScopeLifetime.GetInstance(GetInstanceDelegate createInstance, Scope scope, Object[] arguments)
   at LightInject.Microsoft.DependencyInjection.PerRootScopeLifetime.GetInstance(GetInstanceDelegate createInstance, Scope scope, Object[] arguments)
   at LightInject.ServiceContainer.GetInstance(Type serviceType, Scope scope)
   at LightInject.Scope.GetInstance(Type serviceType)
  //Type -> IHostApplicationLifetime
   at LightInject.Microsoft.DependencyInjection.LightInjectServiceProvider.GetRequiredService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.Extensions.Hosting.HostBuilder.<>c__DisplayClass35_0.<PopulateServiceCollection>b__2(IServiceProvider _)
   at LightInject.Microsoft.DependencyInjection.DependencyInjectionContainerExtensions.<>c__DisplayClass13_0`1.<CreateTypedFactoryDelegate>b__0(IServiceFactory serviceFactory)
   at LightInject.Microsoft.DependencyInjection.PerRootScopeLifetime.GetInstance(GetInstanceDelegate createInstance, Scope scope, Object[] arguments)
   at LightInject.ServiceContainer.GetInstance(Type serviceType, Scope scope)
   at LightInject.Scope.GetInstance(Type serviceType)
  //-> Type IHost
   at LightInject.Microsoft.DependencyInjection.LightInjectServiceProvider.GetRequiredService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.Extensions.Hosting.HostBuilder.ResolveHost(IServiceProvider serviceProvider, DiagnosticListener diagnosticListener)
   at Microsoft.Extensions.Hosting.HostApplicationBuilder.Build()
   at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
   at Program.<Main>$(String[] args) in C:\Projects\ClientXX\ProjectYY\ClientXX.ProjectYY.Web.Api\Program.cs:line 45

When I debug the ConsolleLoggerProvider, I see that the constructor parameter IEnumerable<ConsoleFormatter>? formatters only has one formatter (the json formatter and not the simple formatter). That explains why the key 'simple' was not found in the formatters directory.

ConsoleLoggerProvider constructor (Microsoft.Extensions.Logging v8 and v9 tested) With LightInject.Microsoft.Hosting v1.5.3 (in combination with LightInject v7) image

With LightInject.Microsoft.Hosting v2.1.0 (in combination with LightInject v7) image

All 3 instances (with v1.5.3 and v2.1.0) are registerd on the IServiceCollection without a service name and IsKeyed = false. I think the error is due to the resolving of a list of service types which only returns one instance with the v2.1.0 instead of all instances with the 1.5.3.