Starting 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.
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(); | |
} | |
} |
<%@ 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> |
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; | |
} | |
} | |
} |
<%@ 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> |
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; | |
} | |
} | |
} |
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; | |
} | |
} | |
} |
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; | |
} | |
} | |
} |
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 :
- Sample using Autofac
- Sample using Microsoft Dependency Injection container
How do get asp.net to use my WebObjectActivator to instantiate HttpModules ?
As explained in the article :
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:
What line?
Great article, that make me finally understand it, appreciate it.