AspNet - How To Use Dependency Injection with Web Forms

Intro

With the advent of v4.7.2 it became possible to have Dependency Injection in Asp.Net Web Forms!

This guide is focused on Unity IoC but at the end I will share a NuGet package that has all of these pre-built for other providers.

Breaks down into 4 key components.

ContainerServiceProvider

I cleaned up the Microsoft code a bit and fixed a typo here and there but for the most part this is very straightforward. You don't have to fully understand it but this is the code that uses UnityContainer to resolve a Service when requested.

using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Web.Hosting;
using Unity;

namespace WebFormsUnityExample
{
    public class ContainerServiceProvider : IServiceProvider, IRegisteredObject
    {
        private const int TypesCannotResolveCacheCap = 100000;
        public IUnityContainer Container { get; internal set; } = new UnityContainer();
        internal IServiceProvider NextServiceProvider { get; }
        internal ConcurrentDictionary<Type, bool> TypeCannotResolveDictionary { get; set; } = new ConcurrentDictionary<Type, bool>();

        public ContainerServiceProvider(IServiceProvider next)
        {
            NextServiceProvider = next;
            HostingEnvironment.RegisterObject(this);
        }

        /// <summary>
        /// Implementation of IServiceProvider. Asp.net will call this method to
        /// create the instances of Page/UserControl/HttpModule etc.
        /// </summary>
        /// <param name="serviceType"></param>
        /// <returns></returns>
        public object GetService(Type serviceType)
        {
            // Try unresolvable types
            if (TypeCannotResolveDictionary.ContainsKey(serviceType))
            {
                return DefaultCreateInstance(serviceType);
            }

            // Try the container
            object result = null;

            try
            {
                result = Container.Resolve(serviceType);
            }
            catch (ResolutionFailedException) { } // Swallow

            // Try the next provider
            if (result == null)
            {
                result = NextServiceProvider?.GetService(serviceType);
            }

            // Default activation
            if (result == null)
            {
                if ((result = DefaultCreateInstance(serviceType)) != null)
                {
                    // Cache it
                    if (TypeCannotResolveDictionary.Count < TypesCannotResolveCacheCap)
                    {
                        TypeCannotResolveDictionary.TryAdd(serviceType, true);
                    }
                }
            }

            return result;
        }

        // Can be overloaded for culture specifics.
        protected virtual object DefaultCreateInstance(Type type)
        {
            return Activator.CreateInstance(
                type,
                BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.CreateInstance,
                null,
                null,
                null);
        }

        public void Stop(bool immediate)
        {
            HostingEnvironment.UnregisterObject(this);

            Container.Dispose();
        }
    }
}

HttpApplicationExtensions

I don't remember anything changing vs. Microsoft's repository here... we are extending HttpApplication to create an AddUnity call and to GetUnityContainer functionality at the Application level.

using System;
using System.Web;
using Unity;

namespace WebFormsUnityExample
{
    public static class HttpApplicationExtensions
    {
        public static IUnityContainer AddUnity(this HttpApplication application)
        {
            if (application == null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            return UnityAdapter.AddUnity();
        }

        public static IUnityContainer GetUnityContainer(this HttpApplication application)
        {
            return UnityAdapter.GetContainer();
        }
    }
}

UnityAdapter

Nothing changes here. We are wiring the HttpRuntime WebObjectActivator to our ContainerServiceProvider. Summaries from Microsoft explains it all.

using System.Web;
using Unity;

namespace WebFormsUnityExample
{
    public static class UnityAdapter
    {
        private static readonly object _lock = new object();

        /// <summary>
        /// Add a new Unity container in asp.net application. If there is WebObjectActivator already registered,
        /// it will be chained up. When the new WebObjectActivator can't resolve the type, the previous WebObjectActivator
        /// will be used. If the previous WebObjectActivator can't resolve it either, DefaultCreateInstance will be used
        /// which creates instance through none public default constructor based on reflection.
        /// </summary>
        /// <returns></returns>
        public static IUnityContainer AddUnity()
        {
            lock (_lock)
            {
                HttpRuntime.WebObjectActivator = new ContainerServiceProvider(HttpRuntime.WebObjectActivator);

                return GetContainer();
            }
        }

        /// <summary>
        /// Get most recent added Unity container
        /// </summary>
        /// <returns></returns>
        public static IUnityContainer GetContainer()
        {
            return (HttpRuntime.WebObjectActivator as ContainerServiceProvider)?.Container;
        }
    }
}

UnityConfig

This class should be familiar to most Unity experienced developers but newbie devs need to know this is where you actually add your services to your IoC (Inversion of Control) container. In this example, we are putting services in our UnityContainer.

using Unity;

namespace WebFormsUnityExample
{
    public static class UnityConfig
    {
        public static void RegisterTypes(this IUnityContainer container)
        {
            // TODO: Register Instances (optionally with container lifetimes) or Registration By Convention.
            // Ex.)
            //container.RegisterInstance<ILogger>(
            //    new Logger());
        }
    }
}

Mitch All Together

Putting everything together, we now modify Application_Start in Global.asax.cs. All that work to call two lines! You don't have to have the container private but its recommended to keep a copy in memory at the top (or bottom if you prefer stackwise thinking) of the application.

using System;
using Unity;

namespace WebFormsUnityExample
{
    public class Global : System.Web.HttpApplication
    {
        private static IUnityContainer _unityContainer { get; set; }

        protected void Application_Start(object sender, EventArgs e)
        {
            _unityContainer = this.AddUnity();
            _unityContainer.RegisterTypes();
        }
    }
}

Okay... so how do I use it?! Great question me. Here is a super simple parameter injection for a Page example.

public partial class Login : Page
{
    private static ILogger _logger { get; set; } // our service

    public Login() { } // required to get rid of an Aspx error on missing empty constructors

    public Login(ILogger logger) : base() // a single parameter injected!
    {
        _logger = logger;
    }
}

At this point, you are free to use that Logger like a total baus!

Other Providers (including Unity)

The main repo can be found on GitHub under AspNet.

It's maintained by Microsoft. It also supports Unity but an older version. The code above is borrowed and tweaked from the same repo but by putting it in your project manually, you won't have to wait forever for Microsoft updates to not depend on obsolete versions of Unity (or any flavor of Dependency Injection for that matter). I currently have a PR to update its Unity version dependency but its been over 3 weeks and the related issue has been there for months. I guarantee there is no rush from Microsoft so this falls in line with a DIY.