Mike Christensen
Mike Christensen

Reputation: 91608

IServiceCollection cannot resolve type when loaded from an outside assembly

I'm running into a strange issue using dependency injection, adding in a Singleton for a type that comes from an outside assembly. This is using the Azure Function framework, but I'm not sure if that has anything to do with it or if this would repro with ASP.NET Core as well. My actual "real world" implementation is far, far too complicated to outline here, but I've managed to come up with a minimal repro.

BugRepro.dll (This is a Azure Function project)

This has two files.

Test.cs:

public class Test
{
    private readonly AppConfig config;

    public Test(AppConfig config)
    {
        this.config = config;
    }

    [FunctionName("Test")]
    public async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
    {
        return new OkObjectResult(config.SampleSetting);
    }
}

Startup.cs:

namespace BugRepro
{
    public class AppConfig
    {
        public string SampleSetting { get; set; } = "Test";
    }

    public static class Startup
    {
        public static void Configure(IServiceCollection services)
        {
            services.AddSingleton(new AppConfig());
        }
    }
}

This assembly has a reference to TestDll.dll.

TestDll.dll (This is a normal .NET Core library)

Startup.cs:

[assembly: FunctionsStartup(typeof(TestDll.Startup))]
namespace TestDll
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var asm = Assembly.LoadFile(Path.Combine(builder.GetContext().ApplicationRootPath, @"bin\BugRepro.dll"));
            var type = asm.GetType("BugRepro.Startup");
            var method = type.GetMethod("Configure");
            method.Invoke(null, new object[] {builder.Services});
        }
    }
}

When the Azure Function runs, the TestDll.Startup.Configure method is automatically called by the framework. This method loads the other assembly, BugRepro.dll using reflection and calls the BugRepro.Startup.Configure static method, passing the IServiceCollection.

The BugRepro.Startup.Configure static method adds a single instance of AppConfig to the service collection. I can verify the instance has successfully been added to the service collection, and stepping all the way into the code, the right ServiceDescriptor and everything has been created. Everything appears perfect.

However, when I call the /Test endpoint, I get the error:

[2021-02-04T06:39:10.502Z] Executed 'Test' (Failed, Id=22cfb587-3ba9-401f-b0a5-8688aed7bc9d, Duration=386ms)
[2021-02-04T06:39:10.506Z] Microsoft.Extensions.DependencyInjection.Abstractions: Unable to resolve service for type 'BugRepro.AppConfig' while attempting to activate 'BugRepro.Test'.

Basically, it acts like the Singleton has never been registered and it cannot resolve that type.

Ways to fix:

So, if I move the AppConfig class from the BugRepro.dll to TestDll.dll (basically the AppConfig type is in the same DLL as my FunctionsStartup class), the code works as expected.

Another way to fix it is to use an interface, which is defined in TestDll:

public interface IConfig
{
}

Then make AppConfig implement that interface, then register the Singleton using its interface:

services.AddSingleton<IConfig>(new AppConfig());

However, then I have to inject IConfig into the Test constructor rather than AppConfig which I do not want to do.

My Question

Is there a way to register a Singleton for a type that lives in an external assembly? To me, this seems like a bug in the .NET Core DI framework. Thanks!

Upvotes: 0

Views: 1171

Answers (1)

Steven
Steven

Reputation: 172646

This problem is caused by your use of Assembly.LoadFile. The LoadFile method can cause the same assembly to be loaded twice in such way that the framework sees this as a totally different assembly, with different types.

The solution is to use Asssembly.Load instead:

var assembly = Assembly.Load(AssemblyName.GetAssemblyName("c:\..."));

See for instance this related topic (for Simple Injector, but the problem is identical) for more information.

Upvotes: 3

Related Questions