10

In MVC 6 RC1 we used the IAssemlbyProvider interface to register assemblies that were discovered at runtime and inject additional controller types, in a similar fashion to this post.. Now with RC2 release the IAssemblyProvider has been removed and has changed to (see reference).

Our framework version is currently net46.

Since the upgrade our controllers in external assemblies (not referenced) are returning a 404 status.

We have tried manually add the controller to the registered controllers via the ApplicationPartManager.

var mvcBuilder = services.AddMvc();
var controllerFeature = new ControllerFeature();
mvcBuilder.PartManager.PopulateFeature(controllerFeature);

var moduleControllers = ModulesManager.GetControllers();
foreach (var c in moduleControllers)
    controllerFeature.Controllers.Add(c);

mvcBuilder.PartManager.PopulateFeature(controllerFeature);

and...

services.AddMvc().ConfigureApplicationPartManager(app =>
{
    var controllerFeature = new ControllerFeature();
    app.PopulateFeature(controllerFeature);

    var moduleControllers = ModulesManager.GetControllers();
    foreach (var c in moduleControllers)
        controllerFeature.Controllers.Add(c);

    app.PopulateFeature(controllerFeature);
});

Now the assemblies are definitely loaded into the AppDomain as our dependency injection system is finding and populating services for other items in the external assemblies.

With our previous implementations this worked nicely using the IAssemblyProvider.

public class ModuleAwareAssemblyProvider : IAssemblyProvider
{
    private readonly DefaultAssemblyProvider _defaultProvider;

    public ModuleAwareAssemblyProvider(DefaultAssemblyProvider defaultProvider)
    {
        _defaultProvider = defaultProvider;
    }

    public IEnumerable<Assembly> CandidateAssemblies
    {
        get
        {
            return _defaultProvider.CandidateAssemblies.Concat(ModulesManager.Assemblies).Distinct();
        }
    }
}

I understand RC2 is still relatively new but if anyone has any experience registering additional controllers at start-up would be helpful.

Cheers, Nico

afnpires
  • 601
  • 1
  • 8
  • 24
Nico
  • 12,493
  • 5
  • 42
  • 62

3 Answers3

14

After some time spending working with the ControllerFeature directly with no result it was time to go back to basics.

Basically at start up of the application controllers are registered into the controller feature container not from the controller feature. This is key, as you need to get the controllers registered.

I was browsing the GitHub repository for RC2 and came across the ControllerFeatureProvider. As stated.

Discovers controllers from a list of <see cref="ApplicationPart"/>

And then has a method further down to PopulateFeature where we can see it grabs all the parts registered to the application and extracts the controller interfaces (the IsController() method is worth a review).

/// <inheritdoc />
public void PopulateFeature(
    IEnumerable<ApplicationPart> parts,
    ControllerFeature feature)
{
    foreach (var part in parts.OfType<IApplicationPartTypeProvider>())
    {
        foreach (var type in part.Types)
        {
            if (IsController(type) && !feature.Controllers.Contains(type))
            {
                feature.Controllers.Add(type);
            }
        }
    }
}

So now we know how the controllers are found, they come from an ApplicationPart registered to the application. Next question was how do we create an application part.

After some review and trying to use dependency injection, manually adding the part to the application to get my parts registered I came across another concept.

The interface IMvcBuilder has the extension method AddApplicationPart which adds an Assembly to the application parts. This is done by wrapping the assembly in an AssemblyPart application part. On review of the AssemblyPart this part returns all of the types found in the assembly to the calling part system (in our case the ControllerFeatureProvider).

/// <inheritdoc />
public IEnumerable<TypeInfo> Types => Assembly.DefinedTypes;

Now something interesting with the AssemblyPart is the method GetReferencePaths()

/// <inheritdoc />
public IEnumerable<string> GetReferencePaths()
{
    var dependencyContext = DependencyContext.Load(Assembly);
    if (dependencyContext != null)
    {
        return dependencyContext.CompileLibraries.SelectMany(library => library.ResolveReferencePaths());
    }

    // If an application has been compiled without preserveCompilationContext, return the path to the assembly
    // as a reference. For runtime compilation, this will allow the compilation to succeed as long as it least
    // one application part has been compiled with preserveCompilationContext and contains a super set of types
    // required for the compilation to succeed.
    return new[] { Assembly.Location };
}

It appears that the final piece of the puzzle is to enable preserveCompilationContext within the modules (or external assembly's) project.json file.

"preserveCompilationContext": {
    "type": "boolean",
    "description": "Set this option to preserve reference assemblies and other context data to allow for runtime compilation.",
    "default": false
}

Finally the implementation and resolution for this became quite simple. Each of our external assemblies (or modules) are loaded through our ModuleManager class. This has a list of all referenced module assemblies. So in the ConfigureServices method in the Startup.cs file where the MVC is registered we simply call the extension method AddApplicationPart for each module assembly as.

var mvcBuilder = services.AddMvc();
foreach(var module in ModulesManager.ReferencedModules)
{
    mvcBuilder.AddApplicationPart(module.ReferencedAssembly);
}

Once making these small changes my external controllers stopped returning a 404.

Nico
  • 12,493
  • 5
  • 42
  • 62
  • 1
    I'm a bit confused about your `ModuleManager` class. Is it `ModuleManager` or `ModulesManager`? Either way I can't seem to find a reference to it in any documentation or source code. How do I go about getting it and stepping through the `ReferencedModules`? – DrewB Jun 14 '16 at 11:24
  • The `ModulesManager` is a custom class (our own class) used to find modules based on specific paths and referenced versions. You should be able to easily implement your own version as you see fit. – Nico Jun 14 '16 at 22:39
  • Thanks for the good explanation. `preserveCompilationContext` did the trick for me. I cannot believe how badly documented the whole asp.net core is. I understand its new but a bit more advanced documentation would be very welcome. – DarkUrse Oct 04 '16 at 09:55
  • Thank you for this research, it's exactly what I needed to know for a similar situation. – banana Oct 30 '16 at 06:48
1

Just add in Startup -> ConfigureServices

services.AddMvc()
.AddApplicationPart(typeof(OtherAssemblyController).GetTypeInfo().Assembly)
.AddControllersAsServices();
Oswaldo Alvarez
  • 4,772
  • 1
  • 22
  • 20
0

I used Application Part using following builder extension method to implement Plugins functionality. I hope it would be useful for somebody.

ASP.NET CORE 1.1

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.IO;
using System.Runtime.Loader;

namespace AppPartTest1.Web.Helpers.Features
{
    public static class FeaturesBuilderExtensions
    {
        //
        // Summary:
        //     Adds Features supports to the Application.
        //
        // Parameters:
        //   builder:
        //     The Microsoft.Extensions.DependencyInjection.IMvcBuilder.
        public static IMvcBuilder AddFeaturesSupport(this IMvcBuilder builder, IConfigurationRoot Configuration, IHostingEnvironment environment)
        {

            var fileNames = Directory.GetFiles("Features", "*.dll");

            foreach (string fileName in fileNames)
            {
                builder.AddApplicationPart(AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(environment.ContentRootPath, fileName)));
            }

            return builder;
        }
    }
}

Call

    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();

        environment = env;
    }

    public IHostingEnvironment environment { get; set; }
    public IConfigurationRoot Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        var builder = services.AddMvc()
            .AddFeaturesSupport(this.Configuration, this.environment);
    }
SamJackSon
  • 1,071
  • 14
  • 19