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

AsImplementedInterfaces not Binding all Interfaces #1277

Closed
squadwuschel opened this issue May 26, 2021 · 13 comments
Closed

AsImplementedInterfaces not Binding all Interfaces #1277

squadwuschel opened this issue May 26, 2021 · 13 comments
Labels

Comments

@squadwuschel
Copy link

Describe the Bug

I am using autofac 6.2.0 in a .NET Core 3.1 web api Project.
I am using the AsImplementedInterfaces implementation to autobind all my Interfaces to my Classes.

The Problem is, that only the (2) ITodoItemManager binding ist working with the AsImplementedInterfaces but the (1) ITodoModelBuilder is not getting bindet and I get a error.

grafik

InvalidOperationException: Unable to resolve service for type 'Squad.Gui.Web.Models.Todo.Interfaces.ITodoModelBuilder' while attempting to activate 'Squad.Gui.Web.Controllers.HomeController'.

I've debugged it so far, that i can tell that all Assemblies are loaded where the interfaces and classes are included what I am trying to autobind.

grafik

Code for autobinding

    var applicationAssemblies = LoadApplicationAssemblies("squad.");
    builder.RegisterAssemblyTypes(applicationAssemblies.ToArray()).AsImplementedInterfaces();

when I manualy register the ITodoModeBuilder its working

  builder.RegisterType<TodoModelBuilder>().As<ITodoModelBuilder>()

Steps to Reproduce

I'Ve added my small demo Solution with the Problem to repoduce the issue.

WebTemplate.zip

Just set the webproject as start project and you should see the following error

grafik

Expected Behavior

All Interfaces should be autobinded with AsImplementedInterfaces when all assemblies are added right.

Exception with Stack Trace

InvalidOperationException: Unable to resolve service for type 'Squad.Gui.Web.Models.Todo.Interfaces.ITodoModelBuilder' while attempting to activate 'Squad.Gui.Web.Controllers.HomeController'.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, bool isDefaultParameterRequired)
lambda_method(Closure , IServiceProvider , object[] )
Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider+<>c__DisplayClass4_0.<CreateActivator>b__0(ControllerContext controllerContext)
Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider+<>c__DisplayClass5_0.<CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Dependency Versions

Autofac:

Additional Info

@tillig
Copy link
Member

tillig commented May 26, 2021

Running in a debug context or manually executing a registration causes the assembly binder to load some things that may not already be loaded. We silently ignore types we can't load.

Do something like this and see what you get:

using System.Diagnostics;
using System.Linq;
using Autofac.Util;

var applicationAssemblies = LoadApplicationAssemblies("squad.");
var loadable = applicationAssemblies.SelectMany(a => a.GetLoadableTypes()).ToArray();
Debug.WriteLine("LOADABLE TYPES:");
foreach (var t in loadable)
{
  Debug.WriteLine($"- {t.FullName}");
}

(Not writing this with a compiler, just trying to get help out fast. Point being, list the actual set of loadable types.)

Look in the list and see if both the type you want registered and the interface are in the list. My guess is that one or both are not, and that the debugger (or the compiler, when you manually register) is what's doing the loading transparently.

@squadwuschel
Copy link
Author

squadwuschel commented May 27, 2021

I've added this code snipped and the Type I want to bind is listed

LOADABLE TYPES:

  • <>f__AnonymousType0`1
  • <>f__AnonymousType1`1
  • Squad.Gui.Web.Program
  • Squad.Gui.Web.Startup
  • Squad.Gui.Web.WebSkeleton.OperationResult`1
  • Squad.Gui.Web.WebSkeleton.ResultType
  • Squad.Gui.Web.Models.ErrorViewModel
  • Squad.Gui.Web.Models.IndexViewModel
  • Squad.Gui.Web.Models.Todo.TodoItem
  • Squad.Gui.Web.Models.Todo.TodoModelBuilder
  • Squad.Gui.Web.Models.Todo.Interfaces.ITodoModelBuilder
  • Squad.Gui.Web.Controllers.HomeController
  • Squad.Gui.Web.Controllers.TodoController
  • Squad.Gui.Web.Program+<>c
  • Squad.Gui.Web.Startup+<>c
  • Squad.Gui.Web.Models.Todo.TodoModelBuilder+<>c
    ...... some .net Core Assemblies ....
  • Squad.Logic.Implementation.TodoItemsManager
  • Squad.Logic.Implementation.TodoItemsManager+d__1
  • Squad.Logic.Interfaces.ITodoItemsManager
  • Squad.Models.DomainModel.TodoItemModel

the two bold ones I want to bind are not binded.

@tillig
Copy link
Member

tillig commented May 27, 2021

I should have read the exception closer. See this part in the stack?

Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider...

That means it's not going through Autofac at all to resolve stuff. Unless there's some inner exception with Autofac info that you've omitted, it's looking like the problem isn't that Autofac can't resolve the type; it's that you're not even going through Autofac to get there.

@squadwuschel
Copy link
Author

How can this happen? When I add it manually it works and when I don't add it its not using autofac for this interface to bind.
I am not shure what I've done wrong, because there is not much I've configured so far for Autofac its only one line code witrh the "AsImplementedInterfaces"

@alistairjevans
Copy link
Member

Can you provide your Program.cs/Startup.cs configuration? Or an example repo with a very minimal reproduction?

Just to check that you aren't somehow 'building' your container with Autofac, but then not using Autofac for the actual resolve operation.

@squadwuschel
Copy link
Author

squadwuschel commented May 27, 2021

I've added the zipfile with my small demo project in the initial post, is that enough?

  public class Startup
  {
    public ILifetimeScope AutofacContainer { get; private set; }
    public IConfiguration Configuration { get; }


    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }


    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions();
        services.AddControllersWithViews();
    }

    /// <summary>
    /// Autofac Bindings festlegen
    /// </summary>
    public void ConfigureContainer(ContainerBuilder builder)
    {
        // Register your own things directly with Autofac here. Don't call
        // builder.Populate(), that happens in AutofacServiceProviderFactory for you.

        //Standard Registrations für Interfaces und Klassen
        //builder.RegisterType<TodoItemsManager>().As<ITodoItemsManager>();
        //just for testing if this works 
        //builder.RegisterType<TodoModelBuilder>().As<ITodoModelBuilder>();

        //Autoregister Assembly Bindings, damit müssen wir nicht jeden Dienst für sich registrieren.
        var applicationAssemblies = LoadApplicationAssemblies("squad.");
        builder.RegisterAssemblyTypes(applicationAssemblies.ToArray()).AsImplementedInterfaces();
    }
    
    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        // If, for some reason, you need a reference to the built container, you
        // can use the convenience extension method GetAutofacRoot.
        this.AutofacContainer = app.ApplicationServices.GetAutofacRoot();

        app.UseStaticFiles();
        app.UseRouting();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });


    }


    /// <summary>
    /// Laden der Aktuellen Projektassemblies. Damit wir diese zum dynamischen Binden bei Autofac verwenden können.
    /// </summary>
    private List<Assembly> LoadApplicationAssemblies(string partOfassemblyNamespace)
    {
        var ass = System.IO.Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
        var assemblyList = new List<Assembly>();

        foreach (var file in Directory.GetFiles(ass, "*.dll"))
        {
            try
            {
                //wir fügen nur unsere Anwendungs Assemblies hinzu die von uns erstellt wurden.
                if (file.ToLower().Contains(partOfassemblyNamespace.ToLower()))
                {
                    assemblyList.Add(Assembly.LoadFile(file));
                }
            }
            catch (Exception)
            {
                // ignored
            }
        }

        return assemblyList;
    }
}




  public class Program
  {
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            //Autofac als Default Provider festlegen!
            .UseServiceProviderFactory(new AutofacServiceProviderFactory())
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

@tillig
Copy link
Member

tillig commented May 27, 2021

Aside: The zip file might be easier to work with if it was more of a minimal repro thing. It's got the whole git repo, all the .vs folder with user-specific files, build output, UI assets, the whole shebang. Trying to figure this stuff out really is about "remove all the stuff until all that's left is the bit that shows the issue." That's why we look for a unit test we can run or a repo with a super minimal repro in it rather than a zip file.

@tillig
Copy link
Member

tillig commented May 27, 2021

The project as provided in the zip file doesn't build. On build I see:

...\20_Gui\WebTemplate\Controllers\HomeController.cs(12,17): error CS0246: The type or namespace name 'ITodoModelBuilder' could not be found (are you missing a using directive or an assembly reference?) [...\20_Gui\WebTemplate\Gui.Web.csproj]

Looking at HomeController I see that there's a Squad.Logic.Interfaces namespace included but the ITodoModelBuilder isn't found.

ITodoModelBuilder not found

The TodoController shows the same error.

ITodoModelBuilder looks to be in the Squad.Gui.Web.Models.Todo.Interfaces namespace, which is not included by the controller.

The ITodoModelBuilder interface

I further see that the TodoModelBuilder has a problem because the interface it's supposed to implement is not there. Again, it appears the wrong namespace is included here.

TodoModelBuilder class

I'm not going to spend any more time debugging this. My current guess is that there are stale versions of libraries being used and/or cached and the problem is that while the controller is looking for ITodoModelBuilder from one namespace, the implementation is against a different namespace.

I'm not sure if other contributors have additional time to spend here, but I won't personally be able to come back until this is:

  • Trimmed down to only a minimal repro - stick it in a repo rather than a zip file, remove everything that doesn't have to do with the issue - all .vs stuff, all extra controllers, views that have nothing to do with the repro, everything. Super minimal repro that gets right to the heart of it. Why? Because a lot of the time it turns out as the minimal repro is being created there's an 'aha!' moment where the issue becomes clear; and because it reduces the amount of stuff we have to poke through.
  • Code that compiles - I kinda feel like that's a minimum for a repro.

I legitimately do apologize for having to kick it back to you like this. I do want to help, but while we'll do our best to troubleshoot stuff that has to do with Autofac, we're not a free consulting and debugging service. You'll have to do some work, too, if you need help.

@squadwuschel
Copy link
Author

squadwuschel commented May 28, 2021

thanks for your time, I will do my best to break this down into minimal repo i could host on github to reproduce the issue.
It was my fault that I don't think so far that I've just zipped my "small" demo solution which was working on my lokal Environment sorry for that. I will be back when I got a real small reproducable demo.

@squadwuschel
Copy link
Author

I've created the smales example possible and got the same problem I've also checked the Loadable Types example from above and there are all types included.

Here is the link to the git Repo

https://github.com/squadwuschel/autofacTest.git

its an .NET Core 3.1 WebApi project. Just hitting F5 in VS 2019 should compile it and starting browser and showing the error like above.

@tillig
Copy link
Member

tillig commented May 29, 2021

The problem is that the types you're registering aren't the same as the types the controller is referring to.

This seems counter-intuitive, but it has to do with .NET assembly load context. In a nutshell, there are some assemblies already loaded into the application by the .NET assembly loader. Later, your code is trying to load the same assemblies but using Assembly.LoadFile. This results in a duplicate but different set of assemblies being loaded, such that the application has two copies of the same assembly loaded but in different contexts.

Here's some code you can put in ConfigureContainer in your app that shows the differences.

// There are some assemblies already loaded.
var appDomainAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName.Contains("squad", StringComparison.OrdinalIgnoreCase));
Debug.WriteLine("ASSEMBLIES ALREADY LOADED:");
foreach (var a in appDomainAssemblies)
{
    Debug.WriteLine($"- {a.FullName}");
}

// Grab the assembly already loaded in the AppDomain.
var alreadyLoadedAssembly = appDomainAssemblies.First(a => a.GetName().Name.Equals("Squad.AutofacTest", StringComparison.OrdinalIgnoreCase));
Debug.WriteLine($"ALREADY LOADED: {alreadyLoadedAssembly.FullName} | {alreadyLoadedAssembly.CodeBase}");

// Some assemblies get loaded manually.
var applicationAssemblies = LoadApplicationAssemblies("squad.");
Debug.WriteLine("ASSEMBLIES LOADED BY LoadApplicationAssemblies:");
foreach (var a in applicationAssemblies)
{
    Debug.WriteLine($"- {a.FullName}");
}

// Grab the assembly just now loaded in a different context using Assembly.LoadFile.
var justLoadedAssembly = applicationAssemblies.First(a => a.GetName().Name.Equals("Squad.AutofacTest", StringComparison.OrdinalIgnoreCase));
Debug.WriteLine($"JUST LOADED: {justLoadedAssembly.FullName} | {justLoadedAssembly.CodeBase}");

// These two assemblies are NOT EQUAL.
Debug.WriteLine($"LOADED EQUAL? {alreadyLoadedAssembly == justLoadedAssembly}");

// Let's compare types.
var alreadyLoadedType = alreadyLoadedAssembly.GetTypes().First(t => t.Name.Equals("ITodoModelBuilder", StringComparison.OrdinalIgnoreCase));
var justLoadedType = justLoadedAssembly.GetTypes().First(t => t.Name.Equals("ITodoModelBuilder", StringComparison.OrdinalIgnoreCase));
Debug.WriteLine($"ALREADY LOADED: {alreadyLoadedType.FullName}");
Debug.WriteLine($"JUST LOADED: {justLoadedType.FullName}");
Debug.WriteLine($"ARE THOSE THE SAME TYPE? {alreadyLoadedType == justLoadedType}");

The output of this looks like:

ASSEMBLIES ALREADY LOADED:
- Squad.AutofacTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ALREADY LOADED: Squad.AutofacTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null | file:///C:/dev/autofacTest/AutofacTest/bin/Debug/netcoreapp3.1/Squad.AutofacTest.dll
ASSEMBLIES LOADED BY LoadApplicationAssemblies:
- Squad.AutofacTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
JUST LOADED: Squad.AutofacTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null | file:///C:/dev/autofacTest/AutofacTest/bin/Debug/netcoreapp3.1/Squad.AutofacTest.dll
LOADED EQUAL? False
ALREADY LOADED: Squad.AutofacTest.Models.Interfaces.ITodoModelBuilder
JUST LOADED: Squad.AutofacTest.Models.Interfaces.ITodoModelBuilder
ARE THOSE THE SAME TYPE? False

This is why the manual registration works - when you have the manual registration...

builder.RegisterType<TodoModelBuilder>().As<ITodoModelBuilder>();

...the compiler is binding this to the same type as the one in the constructor for the controller - they're all in the same assembly.

When you do the assembly loading and registering, the registration is literally a different type, at least according to the runtime type loader. It's a "ghost" sort of, where it has the same name but it's not literally the same bytes loaded into memory so it's not the same type.

You can further prove this is the case by changing the assembly scanning registration to look like this:

var applicationAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName.Contains("squad", StringComparison.OrdinalIgnoreCase));
builder.RegisterAssemblyTypes(applicationAssemblies.ToArray()).AsImplementedInterfaces();

That is, if you use the stuff that's already loaded instead of using the Assembly.LoadFile assemblies, everything actually works.

Assembly loading problems are hard. They're even harder in .NET Core where dependencies don't actually always come from the folder where the application is compiled/running. We have an issue about trying to write some docs to help a little with this but assembly loading isn't an Autofac concern; it's .NET and the app developer.

I would recommend:

  • If you're not writing plugins, just use the assemblies loaded in the current domain. If you're using IIS, look into using the BuildManager to get these to make sure you don't have any surprises.
  • If you are writing plugins, you'll need to start diving into proper handling of assembly loading in .NET Core. I'd guess that you'll also have to get into how to load those assemblies' dependencies because, again, it's not necessarily the easiest thing in the world.

However, at least for this issue, Autofac is functioning as designed so I'm going to close it.

@tillig tillig closed this as completed May 29, 2021
@tillig tillig added the invalid label May 29, 2021
@squadwuschel
Copy link
Author

Thx for your detailed explanation. I got it working. It's not straight forward but it works. The problem was, that I don't only have the webprojekt I got some more project DLLs with managers to autobind.

and the code for

var applicationAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName.Contains("squad", StringComparison.OrdinalIgnoreCase));

grafik

only loads the web assembly and not my other project assemblies.
And the compiled web project creates the following DLLs
grafik

  • squad.gui.web.dll
  • squuad.gui.web.views.dll

and this seems not to be the same "assembly" as the "AppDomain.CurrentDomain.GetAssemblies()..." loads, because with this load I only got one assembly loaded and this is the right Assembly for the autobinding.

So to get my WebProject Assembly and all my other Project dependend assemblies I am using this function

    private List<Assembly> LoadApplicationAssemblies(string partOfassemblyNamespace)
    {
        var ass = System.IO.Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
        var assemblyList = new List<Assembly>();

        foreach (var file in Directory.GetFiles(ass, "*.dll"))
        {
            try
            {
                //exclude also the gui.web, because we load them below
                if (file.ToLower().Contains(partOfassemblyNamespace.ToLower()) && !file.ToLower().Contains("gui.web"))
                {
                    assemblyList.Add(Assembly.LoadFile(file));
                }
            }
            catch (Exception)
            {
                // ignored
            }
        }

        var applicationAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName.Contains("squad", StringComparison.OrdinalIgnoreCase));

        foreach (var applicationAssembly in applicationAssemblies)
        {
            if (assemblyList.All(x => x.FullName != applicationAssembly.FullName))
            {
                assemblyList.Add(applicationAssembly);

            }
        }

        return assemblyList;
    }

like you mentioned it has nothing todo with autofac it seems to be a problem with the assembly loading.

@alsami
Copy link
Member

alsami commented May 31, 2021

In general as a recommendation, don't use AppDomain.CurrentDomain.GetAssemblies.

Instead maintain a list of assemblies you want to load types from, something like this:

var types = new Assembly[] { typeof(MyClass).Assembly, typeof(MyOtherClass).Assembly };

This requires you to maintain the list, yes, but it also makes sure that you only load what you want. Sometimes explicitness works just better than implicitness.

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

No branches or pull requests

4 participants