LightInject.Microsoft.DependencyInjection icon indicating copy to clipboard operation
LightInject.Microsoft.DependencyInjection copied to clipboard

DefaultServiceSelector behavior change in v3.7.0

Open ArnaudB88 opened this issue 1 year ago • 4 comments

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;

  • v3.6.3: instance of registered type without a servicename (so the last 'override 'registration, lifetime PerContainer)
  • v3.7.0: instance of registered type with a namespace (so the original registration from the assembly scanning, lifetime null)

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 avatar Dec 14 '23 12:12 ArnaudB88

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

ArnaudB88 avatar Apr 17 '24 09:04 ArnaudB88

@seesharper Is the LightInject project abandoned?

ArnaudB88 avatar Jun 10 '24 12:06 ArnaudB88

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?

seesharper avatar Jun 10 '24 22:06 seesharper

@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:

  • Default registrations through assembly scanning get a service name. We can specify a custom naming provider, but that won't make a difference since 'any service name' is chosen prior to 'no service name'. A naming provider who leaves the name empty seems prone to exception because of possible duplicates. Also, we can't specify a naming provider without specifying the lifetime (which is unwanted).
  • Explicit registrations after assembly scanning (eg. overrides) get no service name by default. We could specify a name here. But many registrations make this dirty, a lot of work and prone to duplicates. We would also need to specify a name which is sorted as last (eg. prefix with ZZZ). Feels like a dirty fix.
  • Resolving a registration will take the last matching registration (sorted by service name). We could specify another DefaultServiceSelector (like in v3.6.3), but this option is the reason I created this ticket.

I see options to fix this in the LightInject code:

  • use the DefaultServiceSelector implementation of v3.6.3 again
  • explicit registrations should also have a default service name. It can use the same nameprovider as assembly scanning. This can possibly fix the issue as overrides would/should get a service name which is sorted as last.

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 avatar Aug 08 '24 13:08 ArnaudB88