5
\$\begingroup\$

I was playing around with ASP.NET Core 3.1 and tried to implement Dependency Injection flavor called Pure DI, i.e. without dependency container (even the built in one) to instantiate Controllers. You can see entire code here, but here's the rundown: I know I have to replace the default IControllerActivator but the main issue concerns singleton IDisposable dependencies, which have to be held by the Composition Root for entire application lifetime and disposed when the application shuts down (I know it's not strictly necessary, but it's a good practice - I've seen some funky implementations of database bulk-inserts which flush data in the Dispose method :S). My idea then is based on implementing a custom ControllerActivator (the Composition Root) with a Singleton pattern, so I can access it and dispose of it when the application shuts down. Here's the Program.cs (I trimmed some comments and namespace imports to save space):

    public class Program
    {
        public static async Task Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();
            try
            {
                await host.RunAsync();
            }
            finally
            {
                /* Dispose singletons held in ControllerActivator when application shuts down. */
                ControllerActivator.Singleton.Dispose();
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            return Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });

        }
    }

And here's the ControllerActivator:

    public sealed class ControllerActivator
        : IControllerActivator, IDisposable
    {
        internal static readonly ControllerActivator Singleton = new ControllerActivator();

        private readonly List<IDisposable> _singletonDisposables = new List<IDisposable>();

        /// <summary>
        ///     An example of a singleton, disposable object used in controller's dependency graph.
        /// </summary>
        private readonly DisposableDependency _singletonDisposableDependency;

        public ControllerActivator()
        {
            _singletonDisposableDependency = RegisterSingletonForDispose(new DisposableDependency());
        }

        object IControllerActivator.Create(ControllerContext context)
        {
            if (GetControllerType(context) == typeof(HelloController))
            {
                var scopedDependency = RegisterForDispose(context, new DisposableDependency());
                return new HelloController(
                    _singletonDisposableDependency,
                    scopedDependency);
            }

            throw new InvalidOperationException("Unknown Controller Type");
        }

        void IControllerActivator.Release(ControllerContext context, object controller)
        {
        }

        public void Dispose()
        {
            foreach (var disposable in _singletonDisposables)
            {
                disposable.Dispose();
            }
        }

        private T RegisterSingletonForDispose<T>(T disposableSingleton)
            where T : IDisposable
        {
            _singletonDisposables.Add(disposableSingleton);
            return disposableSingleton;
        }

        private Type GetControllerType(ControllerContext context)
        {
            return context.ActionDescriptor.ControllerTypeInfo.AsType();
        }

        private T RegisterForDispose<T>(ActionContext context, T scopedDisposable)
            where T : IDisposable
        {
            context.HttpContext.Response.RegisterForDispose(scopedDisposable);
            return scopedDisposable;
        }
    }

Please take a look in the linked GitHub repo to get the full picture.

Now to my questions: Can you see any potential problems with such implementation? Is there a better, more "standard" way to do this?

Note: plugging into IApplicationLifetime in Startup is deprecated in ASP.NET Core 3.1, that's why I utilized Program's finally clause to dispose ControllerActivator.

TIA

\$\endgroup\$
0

2 Answers 2

4
\$\begingroup\$

You might want to check out the code samples repository of the book Dependency Injection Principles, Practices, and Patterns. I co-authored this book. It contains a Pure DI example for an ASP.NET Core web application.

This example implements a custom IControllerActivator that acts as Composition Root. This activator implements IDisposable to implement deterministic cleanup of Singleton dependencies on application shutdown. Its Dispose method is called by the ASP.NET Core framework on shutdown. This is achieved by adding the customer activator to ASP.NET Core's infrastructure.

Section 7.3.2 of the book contains an example that adds custom middleware using Pure DI. This example is reflected in the code samples here.

For creation of middlewhere that shares the same Singleton, you can add extra methods (e.g. CreateMyMiddleware()) on your custom controller activator (which is different than the example in section 7.3.2) and make sure they are added to the ASP.NET Core pipeline during startup.

This, however, will not immediately solve the problem of sharing scoped dependencies between controllers and middleware, but since middleware is resolved from the same class, this shouldn't be that hard to implement.

\$\endgroup\$
1
  • 3
    \$\begingroup\$ Hi Steven. I've read your and Mark's book (it's beyond great! :)) - I'll take another look at the examples and try to see if I can resolve the shared scoped dependencies issue somehow. Thank you! :) \$\endgroup\$
    – Piotr
    Nov 15, 2020 at 16:44
0
\$\begingroup\$

After some time I realized the main problem with usage of Singleton pattern for this purpose: it is the inability to replace dependencies with test doubles in tests. It is because the CompositionRoot instance, which implements both IMiddlewareFactory and IControllerActivator, is static. Maybe it would be possible to set specific dependencies in tests using Ambient Context [anti]pattern but that brings its own set of problems.

I updated the linked repository with alternative (I think better) solution.

(Note that I expanded the example to cover resolving middlewares as well as controllers, which was not mentioned in the original question - this also demonstrates an unresolved flaw in this solution, i.e. that request scoped dependencies are not shared between middlewares and controllers.)

It basically boils down to utilization of the built-in DI container (i.e. the IServiceCollection) in Startup.cs a bit more, like so:

        public void ConfigureServices(
            IServiceCollection services)
        {
            services
                .AddSingleton<CompositionRoot>()
                .AddSingleton<IMiddlewareFactory>(s => s.GetRequiredService<CompositionRoot>())
                .AddSingleton<IControllerActivator>(s => s.GetRequiredService<CompositionRoot>())
                .AddControllers();
        }

This leaves open a possibility to replace CompositionRoot registration in test code.

Usage of factory method in IMiddlewareFactory and IControllerActivator registrations (instead of e.g. services.AddSingleton<IMiddlewareFactory, CompositionRoot>) is necessary to avoid creating two separate instances of CompositionRoot - we resolve an instance from the first registration as both IMiddlewareFactory and IControllerActivator.

Program.cs can then be reduced to its standard form:

    public static class Program
    {
        public static async Task Main(string[] args)
        {
            using var host = CreateHostBuilder(args).Build();
            await host.RunAsync();
        }

        private static IHostBuilder CreateHostBuilder(string[] args)
        {
            return Host
                .CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>());
        }
    }

This way the CompositionRoot gets disposed along with IServiceCollection when application shuts down, and that gives us a chance to dispose singleton dependencies it created.

In summary I have to say it would be quite cumbersome to follow this pattern in a large code-base - ASP.NET Core seems to really push for usage of the built-in container. But it is possible.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.