Search code examples
asp.net-mvcasp.net-mvc-4mef

How can we support modular and testable patterns with ASP.NET MVC 4 and MEF 2?


We're trying to use MEF 2 with ASP.NET MVC 4 to support an extensible application. There are really 2 parts to this question (hope that's okay SO gods):

  1. How do we use Microsoft.Composition and the MVC container code (MEF/MVC demo source) to replace Ninject as our DI for ICoreService, ICoreRepository, IUnitOfWork, and IDbContext?
    It looks like we can't use both Ninject and the MVC container at the same time (I'm sure many are saying "duh"), so we'd like to go with MEF, if possible. I tried removing Ninject and setting [Export] attributes on each of the relevant implementations, spanning two assemblies in addition to the web project, but Save() failed to persist with no errors. I interpreted that as a singleton issue, but could not figure out how to sort it out (incl. [Shared]).

  2. How do we load multiple assemblies dynamically at runtime?
    I understand how to use CompositionContainer.AddAssemblies() to load specific DLLs, but for our application to be properly extensible, we require something more akin to how I (vaguely) understand catalogs in "full" MEF, which have been stripped out from the Microsoft.Composition package (I think?); to allow us to load all IPluggable (or whatever) assemblies, which will include their own UI, service, and repository layers and tie in to the Core service/repo too.

EDIT 1
A little more reading solved the first problem which was, indeed, a singleton issue. Attaching [Shared(Boundaries.HttpRequest)] to the CoreDbContext solved the persistence problem. When I tried simply [Shared], it expanded the 'singletonization' to the Application level (cross-request) and threw an exception saying that the edited object was already in the EF cache.

EDIT 2
I used the iterative assembly loading "meat" from Nick Blumhardt's answer below to update my Global.asax.cs code. The standard MEF 2 container from his code did not work in mine, probably because I'm using the MEF 2(?) MVC container. Summary: the code listed below now works as desired.


CoreDbContext.cs (Data.csproj)

[Export(typeof(IDbContext))]
[Shared(Boundaries.HttpRequest)]
public class CoreDbContext : IDbContext { ... }

CoreRepository.cs (Data.csproj)

[Export(typeof(IUnitOfWork))]
[Export(typeof(ICoreRepository))]
public class CoreRepository : ICoreRepository, IUnitOfWork
{
    [ImportingConstructor]
    public CoreRepository(IInsightDbContext context)
    {
        _context = context;
    }

    ... 
}

CoreService.cs (Services.csproj)

[Export(typeof(ICoreService))]
public class CoreService : ICoreService
{
    [ImportingConstructor]
    public CoreService(ICoreRepository repository, IUnitOfWork unitOfWork)
    {
        _repository = repository;
        _unitOfWork = unitOfWork;
    }

    ... 
}

UserController.cs (Web.csproj)

public class UsersController : Controller
{
    [ImportingConstructor]
    public UsersController(ICoreService service)
    {
        _service = service;
    }

    ... 
}

Global.asax.cs (Web.csproj)

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        CompositionProvider.AddAssemblies(
            typeof(ICoreRepository).Assembly,
            typeof(ICoreService).Assembly,
        );

        // EDIT 2 -- 
        // updated code to answer my 2nd question based on Nick Blumhardt's answer
        foreach (var file in System.IO.Directory.GetFiles(Server.MapPath("Plugins"), "*.dll"))
        {
            try
            {
                var name = System.Reflection.AssemblyName.GetAssemblyName(file);
                var assembly = System.Reflection.Assembly.Load(name);
                CompositionProvider.AddAssembly(assembly);
            }
            catch
            {
                // You'll need to craft exception handling to
                // your specific scenario.
            }
        }
    }
}

Solution

  • If I understand you correctly, you're looking for code that will load all assemblies from a directory and load them into the container; here's a skeleton for doing that:

    var config = new ContainerConfiguration();
    foreach (var file in Directory.GetFiles(@".\Plugins", "*.dll"))
    {
       try
       {
           var name = AssemblyName.GetAssemblyName(file);
           var assembly = Assembly.Load(name);
           config.WithAssembly(assembly);
       }
       catch
       {
           // You'll need to craft exception handling to
           // your specific scenario.
       }
    }
    var container = config.CreateContainer();
    // ...
    

    Hammett discusses this scenario and shows a more complete version in F# here: http://hammett.castleproject.org/index.php/2011/12/a-decent-directorycatalog-implementation/

    Note, this won't detect assemblies added to the directory after the application launches - Microsoft.Composition isn't intended for that kind of use, so if the set of plug-ins changes your best bet is to detect that with a directory watcher and prompt the user to restart the app. HTH!