Using ASP.Net Webform Dependency Injection with .NET 4.7.2

Standard

ASP.Net logoStarting with .NET 4.7.2 (released April, 30st), Microsoft offers an endpoint to plug our favorite dependency injection container when developing ASP.Net Webforms applications, making possible to inject dependencies into UserControls, Pages and MasterPages.
In this article we are going to see how to build an adaptation layer to plug Autofac or the container used in ASP.Net Core.

Dependency Injection for Webforms

In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client’s state. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.

Wikipedia

In our example we would like to inject a dependency decoupled with the IDependency interface, into our Index page and our Master page.

using System;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
namespace Microsoft.Extensions.DependencyInjection.WebForms.Sample
{
[DebuggerDisplay("Dependency #{" + nameof(Id) + "}")]
public class Dependency : IDependency
{
private static int _id;
public int Id { get; }
public Dependency()
{
Id = Interlocked.Increment(ref _id);
}
public string GetFormattedTime() => DateTimeOffset.UtcNow.ToString("f", CultureInfo.InvariantCulture);
}
public interface IDependency
{
int Id { get; }
string GetFormattedTime();
}
}
view raw Dependency.cs hosted with ❤ by GitHub
<%@ Page Title="" Language="C#" MasterPageFile="~/Main.Master" CodeBehind="Index.aspx.cs" Inherits="Autofac.Integration.Web.Sample.Index" %>
<asp:Content ID="Content1" ContentPlaceHolderID="HeaderPlaceHolder" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder" runat="server">
<h1>Hi from Index</h1>
<div><%=Dependency.GetFormattedTime() %></div>
<div>Dependency #<%=Dependency.Id %></div>
</asp:Content>
view raw Index.aspx hosted with ❤ by GitHub
using System;
using System.Web.UI;
namespace Microsoft.Extensions.DependencyInjection.WebForms.Sample
{
public partial class Index : Page
{
protected IDependency Dependency { get; }
public Index(IDependency dependency)
{
Dependency = dependency;
}
}
}
view raw Index.aspx.cs hosted with ❤ by GitHub
<%@ Master Language="C#" CodeBehind="Main.master.cs" Inherits="Autofac.Integration.Web.Sample.Main" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title></title>
<asp:ContentPlaceHolder ID="HeaderPlaceHolder" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:ContentPlaceHolder ID="ContentPlaceHolder" runat="server">
</asp:ContentPlaceHolder>
</div>
<div><%=Dependency.GetFormattedTime() %></div>
<div>Dependency #<%=Dependency.Id %></div>
</form>
</body>
</html>
view raw Main.Master hosted with ❤ by GitHub
using System;
using System.Web.UI;
namespace Microsoft.Extensions.DependencyInjection.WebForms.Sample
{
public partial class Main : MasterPage
{
protected IDependency Dependency { get; }
public Main(IDependency dependency)
{
Dependency = dependency;
}
}
}
view raw Main.master.cs hosted with ❤ by GitHub

According to Microsoft, in the release note of the framework, the extension point is by implementing IServiceProvider and using it in the Init method of the Global.asax this way : HttpRuntime.WebObjectActivator = new MyProvider();

Plugging Autofac

When building an Autofac container, we end up with an object implementing the IContainer. So, we have to build an adapter that wraps the Autofac container and forwards
The first version is quite straightforward, we call Autofac if the type is registered else we rely on the Activator class :

using System;
using System.Reflection;
using System.Web;
using Autofac.Core.Lifetime;
namespace Autofac.Integration.Web
{
public class AutofacServiceProvider : IServiceProvider
{
private readonly ILifetimeScope _rootContainer;
public AutofacServiceProvider(ILifetimeScope rootContainer)
{
_rootContainer = rootContainer;
}
public object GetService(Type serviceType)
{
if (_rootContainer.IsRegistered(serviceType))
{
return _rootContainer.Resolve(serviceType);
}
return Activator.CreateInstance(serviceType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance, null, null, null);
}
}
}

However, it won’t work well. In fact, Webform subclass the Webform objects (Pages, UserControls, MasterPage) at runtime making them impossible to register in the ContainerBuilder. Therefore, for all those objects we are going to end up in the case with the Activator.

Hopefully, Autofac provides a way to dynamically declare registrations using the concept of RegistrationSource. By implementing one, we can then register at runtime our Webforms objects.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Autofac.Builder;
using Autofac.Core;
namespace Autofac.Integration.Web
{
public class WebFormRegistrationSource : IRegistrationSource
{
public IEnumerable<IComponentRegistration> RegistrationsFor(Service service, Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor)
{
if (service is IServiceWithType serviceWithType && serviceWithType.ServiceType.Namespace.StartsWith("ASP", true, CultureInfo.InvariantCulture))
{
return new[]
{
RegistrationBuilder.ForType(serviceWithType.ServiceType).CreateRegistration()
};
}
return Enumerable.Empty<IComponentRegistration>();
}
public bool IsAdapterForIndividualComponents => true;
}
}

Subclassed Webforms objects are by default declared in the ASP namespace, if we are asked a type in this namespace, we generate a registration else we let it go through.

Once we have this RegistrationSource, we can use it in our ContainerBuilder:

using System;
using System.Web;
namespace Autofac.Integration.Web.Sample
{
public class Global : HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
var builder = new ContainerBuilder();
builder.RegisterType<Dependency>().As<IDependency>().InstancePerRequest();
builder.RegisterSource(new WebFormRegistrationSource());
var container = builder.Build();
var provider = new AutofacServiceProvider(container);
HttpRuntime.WebObjectActivator = provider;
}
}
}
view raw Global.asax.cs hosted with ❤ by GitHub

Please note that in this case, we never register the Index page or Master page.

Allowing “per request” lifetime

It would be interesting to make the “per request” lifetime available. This way, all the objects of the request (the page, the handler, the master page, etc.) can share the same instance of the dependencies. It is a kind of singleton but only per HTTP request, making it safe to use (unlike a simple singleton).

To provide this, Autofac usually creates a LifetimeScope, uses it and stores it in a per request bag (located in the current HttpContext, in the Items property).
We are going to do the same in our AutofacServiceProvider : try to retrieve an existing instance of the LifetimeScope, creating it and storing it if needed and when the requests end, disposing it. If there is no HttpContext, we end up with the root scope, the container itself.

using System;
using System.Reflection;
using System.Web;
using Autofac.Core.Lifetime;
namespace Autofac.Integration.Web
{
public class AutofacServiceProvider : IServiceProvider
{
private readonly ILifetimeScope _rootContainer;
public AutofacServiceProvider(ILifetimeScope rootContainer)
{
_rootContainer = rootContainer;
}
public object GetService(Type serviceType)
{
ILifetimeScope lifetimeScope;
var currentHttpContext = HttpContext.Current;
if (currentHttpContext != null)
{
lifetimeScope = (ILifetimeScope)currentHttpContext.Items[typeof(ILifetimeScope)];
if (lifetimeScope == null)
{
void CleanScope(object sender, EventArgs args)
{
if (sender is HttpApplication application)
{
application.RequestCompleted -= CleanScope;
lifetimeScope.Dispose();
}
}
lifetimeScope = _rootContainer.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag);
currentHttpContext.Items.Add(typeof(ILifetimeScope), lifetimeScope);
currentHttpContext.ApplicationInstance.RequestCompleted += CleanScope;
}
}
else
{
lifetimeScope = _rootContainer;
}
if (lifetimeScope.IsRegistered(serviceType))
{
return lifetimeScope.Resolve(serviceType);
}
return Activator.CreateInstance(serviceType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance, null, null, null);
}
}
}

Now, when registering a dependency with InstancePerRequest() method, it makes only one instance per HTTP request.

Using Microsoft Dependency Injection container

We can use the same technique to use the container from Microsoft. Although the container instance implements the IServiceProvider, we have to wrap it anyway. In fact, we need to do this to handle the “per request” scope.

using System;
using System.Web;
namespace Microsoft.Extensions.DependencyInjection.WebForms.Sample
{
public class Global : HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
var collection = new ServiceCollection();
collection.AddScoped<IDependency, Dependency>();
var provider = new ServiceProvider(collection.BuildServiceProvider());
HttpRuntime.WebObjectActivator = provider;
}
}
}
view raw Global.asax.cs hosted with ❤ by GitHub
using System;
using System.Reflection;
using System.Web;
namespace Microsoft.Extensions.DependencyInjection.WebForms
{
public class ServiceProvider : IServiceProvider
{
private readonly IServiceProvider _serviceProvider;
public ServiceProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public object GetService(Type serviceType)
{
try
{
IServiceScope lifetimeScope;
var currentHttpContext = HttpContext.Current;
if (currentHttpContext != null)
{
lifetimeScope = (IServiceScope)currentHttpContext.Items[typeof(IServiceScope)];
if (lifetimeScope == null)
{
void CleanScope(object sender, EventArgs args)
{
if (sender is HttpApplication application)
{
application.RequestCompleted -= CleanScope;
lifetimeScope.Dispose();
}
}
lifetimeScope = _serviceProvider.CreateScope();
currentHttpContext.Items.Add(typeof(IServiceScope), lifetimeScope);
currentHttpContext.ApplicationInstance.RequestCompleted += CleanScope;
}
}
else
{
lifetimeScope = _serviceProvider.CreateScope();
}
return ActivatorUtilities.GetServiceOrCreateInstance(lifetimeScope.ServiceProvider, serviceType);
}
catch (InvalidOperationException)
{
//No public ctor available, revert to a private/internal one
return Activator.CreateInstance(serviceType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance, null, null, null);
}
}
}
}

The main difference with Autofac is that there’s no RegistrationSource as this concept only exists in Autofac. However, there is a helper method ActivatorUtilities.GetServiceOrCreateInstance which allows to create an instance of an unregistered component passing registered dependencies to the constructor. Therefore, we can use this to create our instances.

Final word

We’ve seen how to create wrappers around famous dependency injection containers to provide dependency injection for ASP.Net Webforms thanks to the new extension point available from .Net 4.7.2.
It is now possible to make clean dependency injection in Pages and prepare our legacy apps to transition to ASP.Net Core.

You can find the full samples on my GitHub :

6 thoughts on “Using ASP.Net Webform Dependency Injection with .NET 4.7.2

    • As explained in the article :

      According to Microsoft, in the release note of the framework, the extension point is by implementing IServiceProvider and using it in the Init method of the Global.asax this way : HttpRuntime.WebObjectActivator = new MyProvider();

  1. Andrew Rands

    Great article. I think you should mention changes to web.config (at least for MS ) – I kept getting errors until I ensured this line was present:

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.