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

Registering new ASP.NET Controllers at runtime #663

Open
rafaelvascc opened this Issue Feb 13, 2019 · 4 comments

Comments

Projects
None yet
2 participants
@rafaelvascc
Copy link

rafaelvascc commented Feb 13, 2019

Hi, i have an ASP.NET Core web app that has a hosted service that continously monitor a folder for new dll files and tries to load the controllers found on those files into the web app using the new ASP.NET Core Part Manager. I have no problem when i read those files at the application startup. The Controllers are loaded correcly and i can call their actions.

The problem is when i add a new dll file when the application is running, i can load the controllers using the Part Manager, but i get an error from Simple Injector when trying to call the controller action:

For the SimpleInjectorControllerActivator to function properly, it requires all controllers to be registered explicitly in Simple Injector, but a registration for PersonController is missing. To ensure all controllers are
registered properly, call the RegisterMvcControllers extension method on the Container from within your Startup.Configure method while supplying the IApplicationBuilder instance, e.g. "this.container.RegisterMvcControllers(app);". Full controller name: Testing.PersonController.

Ok, so in theory i just need to register the new controller using Simple Injetor. The problem is i get this error when trying to register the controller:

The container can't b e changed after the first call to GetInstance, GetAllInstances, Verify, and some calls of GetRegistration. Please see https://simpleinjector.org/locked to understand why the container is locked. The following stack trace describes the location where the container was locked:

This is the Code of the class the produces the error:

internal class UserDefinedControllerWatcher : IHostedService, IDisposable
{
    private readonly AspNetCoreCompilerOptions _options;
    private FileSystemWatcher _watcher;
    private readonly ControllerFeatureProvider _provider;
    private readonly ApplicationPartManager _partManager;
    private readonly Container _container;

    public UserDefinedControllerWatcher(AspNetCoreCompilerOptions options, 
        Container container,
        ApplicationPartManager partManager, 
        IEnumerable<IActionDescriptorChangeProvider> changeProviders)
    {
        _container = container;
        _options = options;
        _partManager = partManager;
        _provider = (ControllerFeatureProvider)partManager.FeatureProviders
            .Where(p => p is ControllerFeatureProvider).FirstOrDefault();

        _watcher = new FileSystemWatcher
        {
            Path = _options.GeneratedAssembliesLocation,
            NotifyFilter = NotifyFilters.LastAccess
                                 | NotifyFilters.LastWrite
                                 | NotifyFilters.FileName
                                 | NotifyFilters.DirectoryName,
            Filter = "*.dll",
            EnableRaisingEvents = false
        };

        _watcher.Created += OnNewAssemblyDetected;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _watcher.EnableRaisingEvents = true;
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop watching.
        _watcher.EnableRaisingEvents = false;
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _watcher.EnableRaisingEvents = false;
        _watcher.Dispose();
    }

    private void OnNewAssemblyDetected(object source, FileSystemEventArgs e)
    {
        LoadControllersFromAssembly(e.FullPath);
    }

    private void LoadControllersFromAssembly(string assemblyPath)
    {
        var assembly = Assembly.LoadFrom(assemblyPath);
        var controllers = assembly.GetExportedTypes()
            .Where(t => typeof(ControllerBase).IsAssignableFrom(t));

        if (controllers.Any())
        {
            if (!_partManager.ApplicationParts.Any(p => p is AssemblyPart 
                && p.Name == assembly.FullName.Split(',')[0]))
            {
                _partManager.ApplicationParts.Add(new AssemblyPart(assembly));
            }

            var controllerFeature = new ControllerFeature();
            foreach (var controller in controllers)
            {
                // This call fails because the container is already locked
                _container.AddRegistration(controller, Lifestyle.Transient
                    .CreateRegistration(controller, _container));
                var controllerTypeInfo = controller.GetTypeInfo();
                if (!UserDefinedControllerProvider.ControllerFeatureAcessor.Controllers
                    .Any(c => c.FullName == controllerTypeInfo.FullName))//TODO: Add logging
                {
                    UserDefinedControllerProvider
                        .ControllerFeatureAcessor.Controllers.Add(controllerTypeInfo);
                }
            }

            UserDefinedControllersActionDescriptorChangeProvider.Instance.HasChanged = true;
            UserDefinedControllersActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
        }
    }
}

Is there any way i can register those controllers at runtime using simple injector?
Thanks.

@dotnetjunkie

This comment has been minimized.

Copy link
Collaborator

dotnetjunkie commented Feb 13, 2019

Is there any way i can register those controllers at runtime using simple injector?

Forgetting to register controllers is a common source of errors. That is why the SimpleInjectorControllerActivator protects against this by forcing the user to register all controllers. This is typically a good solution, but it prevents you from easily resolving dynamically loaded controller types.

To work around this, you will have to create a custom IControllerActivator and add that instead of adding the SimpleInjectorControllerActivator. Creating your custom IControllerActivator is simple and can be done as follows:

public sealed class MyCustomControllerActivator : IControllerActivator
{
    private readonly Container container;
    public MyCustomControllerActivator(Container container) => this.container = container;

    public object Create(ControllerContext context) =>
        this.container.GetInstance(context.ActionDescriptor.ControllerTypeInfo.AsType());

    public void Release(ControllerContext context, object controller) { }
}

Now replace this line:

services.AddSingleton<IControllerActivator>(new SimpleInjectorControllerActivator(container));

with the following:

services.AddSingleton<IControllerActivator>(new MyCustomControllerActivator(container));
@rafaelvascc

This comment has been minimized.

Copy link
Author

rafaelvascc commented Feb 13, 2019

@dotnetjunkie Thanks it worked pertctly for the controllers! :)
But now i have another problem i was not forseeing... I need to register the Mediatr request handlers and other generated service classes too. :(
Is there any way to register a new class during runtime?
Thanks!

@dotnetjunkie

This comment has been minimized.

Copy link
Collaborator

dotnetjunkie commented Feb 13, 2019

Is there any way to register a new class during runtime?

Absolutely. You can achieve this by hooking onto the the container's ResolveUnregisteredType event as described in the API reference library.

@rafaelvascc

This comment has been minimized.

Copy link
Author

rafaelvascc commented Feb 14, 2019

@dotnetjunkie Thank you very much for your help Steven, now everything is working like a charm. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.