Self-Registering Libraries with Autofac 4 and vNex

2019-01-25 20:46发布

i'd like to create a Plugin Enviroment for my ASP.Net 5.0 / MVC 6 Application. I'm using Autofac as IOC Container and i like to load the Plugins (Class Libraries) from the build in DNX LibraryManager. The goal of using the Library Manager is, that i don't have to care about NuGet Packages and Frameworks.

The Problem i have is the LifeCycle, i have to build the IOC Container before the instance of the LibraryManager is available. Because the Autofac Container provides his own IServiceProvider Instance which i have to inject within the ConfigureService() Method call (AddAutofac).

Does anyone know how to get this working?

Update: I have fixed my problem with Davids help and updated the code to get it working with the release candidates. Also i have added support for configuration.

In my DNX Class Library i implemented a Class for Self-Registration:

public class AutofacModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.Register(c => new SimpleService())
               .As<IService>()
               .InstancePerLifetimeScope();
    }
}

In my MVC WebApplication i have added the Class Library as Dependency.

Startup.cs

public IConfiguration Configuration { get; set; }

public class Startup
{
    public Startup( IApplicationEnvironment applicationEnvironment )
    {
        IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
        configurationBuilder.SetBasePath( applicationEnvironment.ApplicationBasePath );

        configurationBuilder.AddJsonFile( "appsettings.json" );
        configurationBuilder.AddJsonFile( "autofac.json" );
        configurationBuilder.AddEnvironmentVariables();

        this.Configuration = configurationBuilder.Build();
    }

    public void ConfigureServices(IServiceCollection services)
    {                       
        services.AddMvc();                                     
        services.AddDependencies();    
    }

    public void Configure(IApplicationBuilder applicationBuilder, IHostingEnvironment hostingEnvironment)
    { 
        applicationBuilder.UseDependencies( this.Configuration );
        applicationBuilder.UseStaticFiles();      
        applicationBuilder.UseMvc();
    }
}     

I have created an DependencyResolver to keep the ContainerBuilder instance.

DependencyResolver.cs

public class DependencyResolver : IDependencyResolver
{
    private IContainer container;
    private readonly ContainerBuilder builder;     

    public DependencyResolver()
    {
        this.builder = new ContainerBuilder();   
    }

    public void RegisterModule( IModule module )
    {
        this.builder.RegisterModule( module );
    }

    public void RegisterModules( IEnumerable<Assembly> assemblies )
    {         
        this.builder.RegisterAssemblyModules(assemblies.ToArray());  
    }       

    public void Populate( IServiceCollection services)
    {
        this.builder.Populate( services );
    }

    public void Build()
    {
        this.container = this.builder.Build();
    }

    public T Resolve<T>() where T : class
    {                                                 
        return this.container?.Resolve<T>();              
    }      
}

IDependencyResolver.cs

public interface IDependencyResolver
{
    void RegisterModule( IModule module );
    void RegisterModules( IEnumerable<Assembly> assemblies );   
    void Populate(IServiceCollection services);
    void Build();
    T Resolve<T>() where T : class;
}

Last but not least i have created an Extension Class

DependencyResolverExtensions.cs

public static class DependencyResolverExtensions
{
    public static IServiceCollection AddDependencies( this IServiceCollection services )
    {
        DependencyResolver dependencyResolver = new DependencyResolver();
        dependencyResolver.Populate(services);

        ServiceDescriptor serviceDescriptor = new ServiceDescriptor(typeof ( IDependencyResolver ), dependencyResolver );
        services.TryAdd(serviceDescriptor);

        return services;
    }

    public static IApplicationBuilder UseDependencies(this IApplicationBuilder applicationBuilder, IConfiguration configuration)
    {
        IDependencyResolver dependencyResolver = applicationBuilder.GetService<IDependencyResolver>();
        if (dependencyResolver == null) return applicationBuilder;

        ILibraryManager libraryManager = applicationBuilder.GetService<ILibraryManager>();
        if (libraryManager == null) return applicationBuilder;

        IEnumerable<Assembly> assemblies = libraryManager.GetLoadableAssemblies();
        dependencyResolver.RegisterModules(assemblies);

        ConfigurationModule configurationModule = new ConfigurationModule( configuration );
        dependencyResolver.RegisterModule( configurationModule );

        dependencyResolver.Build();        

        IServiceProvider serviceProvider = dependencyResolver.Resolve<IServiceProvider>();
        applicationBuilder.ApplicationServices = serviceProvider;

        return applicationBuilder;
    }

    public static IEnumerable<Assembly> GetLoadableAssemblies(this ILibraryManager libraryManager)
    {
        List<Assembly> result = new List<Assembly>();    

        IEnumerable<Library> libraries = libraryManager.GetLibraries();    

        IEnumerable<AssemblyName> assemblyNames = libraries.SelectMany(e => e.Assemblies).Distinct();
        assemblyNames = Enumerable.Where(assemblyNames, e => e.Name.StartsWith("MyLib."));

        foreach (AssemblyName assemblyName in assemblyNames)
        {
            Assembly assembly = Assembly.Load(assemblyName);
            result.Add(assembly);
        }

        return result;
    }

    public static T GetService<T>(this IApplicationBuilder applicationBuilder) where T : class
    {
        return applicationBuilder.ApplicationServices.GetService(typeof (T)) as T;
    }
}

If you need to switch between different implementations, like mock and real data you can use the Autofac Configuration.

autofac.json

{
    "components": [
        {
            "type": "MyLib.Data.EF.EntitiesData, MyLib.Data.EF",
            "services": [
                {
                    "type": "MyLib.Abstractions.IDataRepository, MyLib.Abstractions"
                }
            ]
        }
    ]
}

2条回答
冷血范
2楼-- · 2019-01-25 21:06

I've come up with a solution that uses part of this, but also uses a ComponentContainer that addresses the potential memory leaks in the DependencyResolver. This also works with RC1. Not sure yet about RC2 as it's not complete enough for me to test.

The ComponentContainer looks like this:

    public static class ComponentContainer {
    static IContainer _container;
    static ContainerBuilder _containerBuilder;

    public static ContainerBuilder Builder {
        get {
            if (_containerBuilder == null)
                _containerBuilder = new ContainerBuilder();
            return _containerBuilder;
        }
    }

    public static IServiceProvider ServiceProvider {
        get {
            if (_container == null)
                _container = _containerBuilder.Build();
            return _container.Resolve<IServiceProvider>();
        }
    }

    public static ComponentFactory<TObject> Component<TObject>() => new ComponentFactory<TObject>(_container);

    public static void RegisterAssembly(Assembly assembly) {
        if (assembly == null) return;

        foreach (var obj in assembly.GetTypes().Where(t => t.GetCustomAttribute<ExportAttribute>() != null)) {
            ExportAttribute att = obj.GetCustomAttribute<ExportAttribute>();
            if (att.ContractType != null) {
                _containerBuilder.RegisterType(obj).As(att.ContractType);
            } else {
                foreach (var intf in obj.GetInterfaces())
                    _containerBuilder.RegisterType(obj).As(intf);
            }
        }
    }
}

public class ComponentFactory<TObject> : IDisposable {
    protected TObject CurrentObject;
    protected ILifetimeScope CurrentScope;
    public TObject Current => (TObject)CurrentObject;
    public ComponentFactory(IContainer container) {
        CurrentScope = container.BeginLifetimeScope();
        CurrentObject = CurrentScope.Resolve<TObject>();
    }

    public TObject Component => CurrentObject;

    public void Dispose() {
        (CurrentObject as IDisposable)?.Dispose();
        CurrentScope.Dispose();
    }
}

Then in Startup.cs I do the following:

    public virtual IServiceProvider ConfigureServices(IServiceCollection services) {
        services.AddMvc();
        services.AddOptions();
        services.AddSession();
        services.AddCaching();

        var assemblyLoadContextAccessor = services.FirstOrDefault(s => s.ServiceType == typeof(IAssemblyLoadContextAccessor)).ImplementationInstance as IAssemblyLoadContextAccessor;
        var libraryManager = services.FirstOrDefault(s => s.ServiceType == typeof(ILibraryManager)).ImplementationInstance as ILibraryManager;

        var loadContext = assemblyLoadContextAccessor.Default;

        foreach(var library in libraryManager.GetLibraries()) {
            var assembly = loadContext.Load(library.Name);

            if(assembly != null) {
                var module = assembly.GetTypes().FirstOrDefault(t => t == typeof(IModule));

                if(module != null)
                    ComponentContainer.Builder.RegisterAssemblyModules(assembly);
                else 
                    ComponentContainer.RegisterAssembly(assembly);                          
            }
        }
        ComponentContainer.Builder.Populate(services);

        return ComponentContainer.ServiceProvider;
    }

To export modules within an assembly, I either mark them with an ExportAttribute or add a class to the assembly that implements Autofac's IModule. The code in ConfigureServices will enumerate through the application's modules and feed them to the static Builder in ComponentContainer. Once the container has been built, you can either resolve modules through injection in a constructor or you can request a specific type by:

(using var myComponentFactory = ComponentContainer.Component<IMyModule>()) {
    //You can now access your component through myComponentFactory.Component
    //Once it passes out of scope of using, it will be properly disposed of 
    //along with the scope from which it was created.
}

Edit: With the release of RC2, this code is no longer valid as the enumeration of assemblies and classes will fail. I haven't come up with a good solution yet. If anyone else has any suggestions for enumerating assemblies in RC2, please let me know.

查看更多
爱情/是我丢掉的垃圾
3楼-- · 2019-01-25 21:20

It's a shame that ConfigureServices is not injectable, that would make this a lot easier.

Looking at the code you should be safe to replace the IServiceProvider inside Configure(...) instead of inside ConfigureServices(...) and get the intended behavior. ApplicationServices is setable.

In your UseAutofac method you should be able to do something like:

public static IApplicationBuilder UseAutofac( [NotNull] this IApplicationBuilder applicationBuilder )
{
    IAutofacResolver autofacResolver = applicationBuilder.GetService<IAutofacResolver>();
    ILibraryManager libraryManager = applicationBuilder.GetService<ILibraryManager>();

    autofacResolver.RegisterLibraryModules( libraryManager);
    applicationBuilder.ApplicationServices = autofacResolver.Resolve();

    return applicationBuilder;
}
查看更多
登录 后发表回答