Integrating Managed Extensibility Framework with the .NET Service Provider

Introduction

It seems I’m in the mood for Managed Extensibility Framework: second post in a week about it! This time, I’m going to talk about how we can integrate it with the .NET Core’s service provider/dependency injection (DI) library (Microsoft.Extensions.DependencyInjection).

Mind you, this will apply to both ASP.NET Core and .NET Core console apps.

Locating Services

We’ve seen before how we can find all types that match a given interface:

public static class ContainerConfigurationExtensions     {         public static ContainerConfiguration WithAssembliesInPath(this ContainerConfiguration configuration, string path, SearchOption searchOption = SearchOption.TopDirectoryOnly)         {             return WithAssembliesInPath(configuration, path, null, searchOption);         }         public static ContainerConfiguration WithAssembliesInPath(this ContainerConfiguration configuration, string path, AttributedModelProvider conventions, SearchOption searchOption = SearchOption.TopDirectoryOnly)         {             var assemblyFiles = Directory                 .GetFiles(path, "*.dll", searchOption);             var assemblies = assemblyFiles                 .Select(AssemblyLoadContext.Default.LoadFromAssemblyPath);             configuration = configuration.WithAssemblies(assemblies, conventions);             return configuration;         }     }

Service Registration

The next step is picking up all of the found types and registering them with the DI:

public static class ServiceCollectionExtensions     {         public static IServiceCollection AddFromAssembliesInPath<T>(this IServiceCollection services, ServiceLifetime lifetime, string path = null) where T : class         {             var factory = new ExportFactory<T, object>(() => new Tuple<T, Action>(Activator.CreateInstance<T>(), () => { }), new object());             var conventions = new ConventionBuilder();             var builder = conventions                 .ForTypesDerivedFrom<T>()                 .Export<T>();             if (lifetime == ServiceLifetime.Singleton)             {                 builder = builder.Shared();             }             path = path ?? AppContext.BaseDirectory;             var configuration = new ContainerConfiguration()                 .WithAssembliesInPath(path, conventions);             using (var container = configuration.CreateContainer())             {                 var svcs = container.GetExports<Lazy<T>>();                 foreach (var svc in svcs)                 {                     services.Add(new ServiceDescriptor(typeof(T), sp => svc.Value, lifetime));                 }             }             return services;         }         public static IServiceCollection AddSingletonFromAssembliesInPath<T>(this IServiceCollection services, string path = null) where T : class         {             return AddFromAssembliesInPath<T>(services, ServiceLifetime.Singleton, path);         }         public static IServiceCollection AddScopedFromAssembliesInPath<T>(this IServiceCollection services, string path = null) where T : class         {             return AddFromAssembliesInPath<T>(services, ServiceLifetime.Scoped, path);         }         public static IServiceCollection AddTransientFromAssembliesInPath<T>(this IServiceCollection services, string path = null) where T : class         {             return AddFromAssembliesInPath<T>(services, ServiceLifetime.Transient, path);         }     }

The AddFromAssembliesInPath extension method is what does all the work; it leverages the previous WithAssembliesInPath method to locate all types that match a given interface, in the assemblies inside a specific folder (which can be the current one). AddSingletonFromAssembliesInPath, AddScopedFromAssembliesInPath and AddTransientFromAssembliesInPath are merely here to make your life a (little bit) easier. Although MEF only supports singletons (Shared) and transient (Non-shared) lifetimes, with this approach

Notice how MEF let’s us resolve Lazy<T> instances besides T. This is pretty cool, as we can delay object instantiation to a later stage, when the object is actually needed. A word of caution: the instantiation will actually be done by MEF, not by the .NET Core DI, so you won’t have constructor injection.

Putting it all Together

So, armed with these two extension methods, we can add this to the ConfigureServices method of your ASP.NET Core app (or wherever you populate your service provider):

services.AddTransientFromAssembliesInPath<IPlugin>();

Here IPlugin is just some interface, nothing to do with the one described in the previous post. After this, you should be able to inject all of the actual implementations:

public class HomeController : Controller

{

public HomeController(IEnumerable<IPlugin> plugins) { … }

}

                             

3 Comments

  • Good support, thanks for the sharing

  • Nice topic but horrible one-liner excel-like code formatting on the page.

  • Your Extension Method didn´t work.

    Do you have a solution?

    Thanks
    Dan


    [Test]
    public void TransientServiceTest()
    {
    Compose();

    IServiceProvider serviceProvider = ConfigureServices();

    //Act
    var serviceBInstance1 = serviceProvider.GetRequiredService<IServiceB>();
    var ServiceBInstance2 = serviceProvider.GetRequiredService<IServiceB>();

    //Assert
    Assert.AreEqual(serviceBMef.GetName(), serviceBInstance1.GetName());
    Assert.AreNotEqual(serviceBInstance1.GetId(), ServiceBInstance2.GetId()); //------------Error, same instance

    }

    private void Compose()
    {
    try
    {
    var catalog = new AggregateCatalog();
    catalog.Catalogs.Add(new AssemblyCatalog(typeof(ServiceCollectionExtensionTest).Assembly));

    _container = new CompositionContainer(catalog);
    _container.ComposeParts(this);
    }
    catch (CompositionException compositionException)
    {
    Console.WriteLine(compositionException.ToString());
    }
    }

    private IServiceProvider ConfigureServices()
    {
    var services = new ServiceCollection();

    services.AddSingletonFromAssembliesInPath<IServiceA>();
    services.AddTransientFromAssembliesInPath<IServiceB>();

    return services.BuildServiceProvider();
    }

    [Export(typeof(IServiceB))]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    internal class ServiceB : IServiceB
    {
    private Guid _id;

    public ServiceB()
    {
    _id = Guid.NewGuid();
    }

    public Guid GetId()
    {
    return _id;
    }

    public string GetName()
    {
    return nameof(ServiceA);
    }
    }

Add a Comment

As it will appear on the website

Not displayed

Your website