Print

Dependency Injection in ASP.NET Web Forms with the Common Service Locator

This article describes how to create and configure a custom PageHandlerFactory class that enables automatic constructor injection for System.Web.UI.Page classes. This keeps your application design clean and allows you to keep the application’s dependency to the IoC library to a minimum.

When working with IoC frameworks, one should always try to minimize the amount of application code that takes a dependency on that framework. In an ideal world, there would only be a single place in the application were the container is queried for dependencies. ASP.NET Web Forms however, was never designed with these concepts in mind. It therefore is tempting to directly request for dependencies in the Page classes instead of looking for a workaround. This is what a Page could look like without such a workaround:

public partial class _Default : System.Web.UI.Page
{
    private IUserService userService;

    public _Default()
    {
        this.userService = ServiceLocator.Current.GetInstance<IUserService>();
    }
}

While this doesn’t look that bad, it creates a dependency on an particular implementation and even when your calling an abstraction (as I do with the Common Service Locator in the example) you might want to prevent this, because you’ve still got a dependency and a bit of plumbing in each and every page.

The way to intercept the creation of Page types in ASP.NET Web Forms, is by replacing the default PageHandlerFactory implementation. While some think that automatic constructor injection is not possible with Web Forms, I will show you otherwise.

The code below shows my CommonServiceLocatorPageHandlerFactory. This is a PageHandlerFactory that uses automatic constructor injection to create new Page types by using the Common Service Locator (CSL) interface. I deliberately use the CSL for this, because my Simple Service Locator library depends on that interface. If you're not using the CSL, changing the code to work with your IoC library is can be done by changing a single line, as you will see below.

When using this custom PageHandlerFactory the previously shown Page class can be changed to the following:

public partial class _Default : System.Web.UI.Page
{
    private IUserService userService;

    protected _Default()
    {
    }

    public _Default(IUserService userService)
    {
        this.userService = userService;
    }
}

Please note that the page must contain the default constructor. The code compilation model that ASP.NET uses behind the covers, creates a new type based on the defined _Default type. ASP.NET does this to allow the creation of the control hierarchy as it is defined in the markup. Because of this inheriting strategy, every Page class in your application must have a default constructor, although it doesn’t have to be public.

Registration of the CommonServiceLocatorPageHandlerFactory can be done in the web.config in the following way:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*" path="*.aspx"
        type="CSL.CommonServiceLocatorPageHandlerFactory, CSL"/>
    </httpHandlers>
  </system.web>
  <system.webServer>
    <handlers>
      <add name="CSLPageHandler" verb="*" path="*.aspx"
        type="CSL.CommonServiceLocatorPageHandlerFactory, CSL"/>
    </handlers>
  </system.webServer>
</configuration>

Here is the code for the CommonServiceLocatorPageHandlerFactory:

public class SimpleInjectorPageHandlerFactory 
: PageHandlerFactory
{
private static object GetInstance(Type type)
{
// Change this line if you're not using the CSL,
// but a DI framework directly.
return Microsoft.Practices.ServiceLocation
.ServiceLocator.Current.GetInstance(type);
}

public override IHttpHandler GetHandler(HttpContext context,
string requestType, string virtualPath, string path)
{
var handler = base.GetHandler(context, requestType,
virtualPath, path);

if (handler != null)
{
InitializeInstance(handler);
HookChildControlInitialization(handler);
}

return handler;
}

private void HookChildControlInitialization(object handler)
{
Page page = handler as Page;

if (page != null)
{
// Child controls are not created at this point.
// They will be when PreInit fires.
page.PreInit += (s, e) =>
{
InitializeChildControls(page);
};
}
}

private static void InitializeChildControls(Control contrl)
{
var childControls = GetChildControls(contrl);

foreach (var childControl in childControls)
{
InitializeInstance(childControl);
InitializeChildControls(childControl);
}
}

private static Control[] GetChildControls(Control ctrl)
{
var flags =
BindingFlags.Instance | BindingFlags.NonPublic;

return (
from field in ctrl.GetType().GetFields(flags)
let type = field.FieldType
where typeof(UserControl).IsAssignableFrom(type)
let userControl = field.GetValue(ctrl) as Control
where userControl != null
select userControl).ToArray();
}

private static void InitializeInstance(object instance)
{
Type pageType = instance.GetType().BaseType;

var ctor = GetInjectableConstructor(pageType);

if (ctor != null)
{
try
{
var args = GetMethodArguments(ctor);

ctor.Invoke(instance, args);
}
catch (Exception ex)
{
var msg = string.Format("The type {0} " +
"could not be initialized. {1}", pageType,
ex.Message);

throw new InvalidOperationException(msg, ex);
}
}
}

private static ConstructorInfo GetInjectableConstructor(
Type type)
{
var overloadedPublicConstructors = (
from ctor in type.GetConstructors()
where ctor.GetParameters().Length > 0
select ctor).ToArray();

if (overloadedPublicConstructors.Length == 0)
{
return null;
}

if (overloadedPublicConstructors.Length == 1)
{
return overloadedPublicConstructors[0];
}

throw new ActivationException(string.Format(
"The type {0} has multiple public overloaded " +
"constructors and can't be initialized.", type));
}

private static object[] GetMethodArguments(MethodBase method)
{
return (
from parameter in method.GetParameters()
let parameterType = parameter.ParameterType
select GetInstance(parameterType)).ToArray();
}
}

This implementation does one sneaky thing to achieve it’s goal. It is nearly impossible to instantiate the type our self, because that would mean that we need to rewrite the complete compilation engine of ASP.NET. Instead we delegate the creation to the PageHandlerFactory base class. After the creation of this type (created using the default constructor) we search for an overloaded constructor on its base type (remember that ASP.NET creates a sub type), find out what arguments this constructor has, and load those dependencies by calling the Common Service Locator. After that we invoke that overloaded constructor. I repeat: we call an overloaded constructor on an already initialized class.

This is very sneaky, but works like hell. Besides initializing the Page class itself, it will initializes all UserControls recursively.

A few side notes: Keep in mind that this will fail in partially trusted environments. When doing this in partial trust, there is no good feasible workaround. In partial trust there is not much else we can do than seeing the Page as a Composition Root and calling the container from within the default constructor. Second note: This will only work for .aspx pages. For intercepting the creation of .ashx HTTP Handlers we will need to create a custom IHttpHandlerFactory, which is new since ASP.NET 2.0.

Happy injecting!

- .NET General, C#, Dependency injection, Simple Service Locator - five comments / No trackbacks - §

The code samples on my weblog are colorized using javascript, but you disabled javascript (for my website) on your browser. If you're interested in viewing the posted code snippets in color, please enable javascript.

five comments:

Great article.

Depending on your DI framework though, it may be simpler. For example, we use the Microsoft Patterns & Practices "Unity" framework for DI that exposes a "BuildUp" method that can inject into properties on already constructed instances. Here is our (simplified, no error handling) code for our situation:

public override IHttpHandler GetHandler(HttpContext context,
    string requestType, string virtualPath, string path)
{
    var page = base.GetHandler(context, requestType, virtualPath, path);
    if (page != null) page = InjectDependencies(context, page);
    return page;
}

private static IHttpHandler InjectDependencies(HttpContext context, IHttpHandler page)
{
    return SomeHelperClass.GetContainer(context).BuildUp(page.GetType(), page);
}

Then the code-behind file is just like any other System.Web.UI.Page class except for the following addition property that is of a type already registered with our container so it will get filled by the DI container:

[Microsoft.Practices.Unity.Dependency]
public IMyDao myDao { get; set; }
Mufasa - 04 01 11 - 18:47

You are correct that most DI frameworks have a construct for this. To be able to use such a feature of your DI framework, you often need framework specific attributes, as Unity’s DependencyAttribute. Using such attributes makes it harder to swap frameworks later on, so if you wish to minimize the dependencies on your DI framework (and ideally only have a dependency in your application’s Composition Root), you would end up with something as described in this article.

The Simple Service Locator DI container for instance, doesn't allow users to specify attributes to ensure it to be replaced later on with an other DI implementation. I wrote this article especially with the Simple Service Locator in mind.
Steven (URL) - 05 01 11 - 11:01

Thanks very much for this -- worked pretty much straight out the box for me. I was able to hook this up to NInject very easily.

Great work!
John - 11 10 11 - 12:50

Welcome! Nike Air Max Shoes Pas Cher .FR,Store Online, "Health & Lifestyle" . Sale All kinds of Nike Air Max shoes, with nike air max 90, nike air max 95,nike air max 2012,nike air max 2011,nike air max pas cher etc. http://www.nikeairmaxot.com.
Nike Air Max Shoes (URL) - 18 04 12 - 09:57

Excellent post. Does the GetChildControls method work? In my test scenario with a Master Page, a Content Page and a User Control referenced from the Content Page, the GetChildControls method always returns an empty array. It returns null on "field.GetValue(ctrl) as Control". Note that it's "field.GetValue(ctrl)" that returns null. I've adapted the code to use Ninject and it works fine injecting dependencies into pages. Now that I have a requirement to do User Controls within Content Pages, I encountered this issue.
Ian - 22 07 13 - 10:59


No trackbacks:

Trackback link:

Please enable javascript to generate a trackback url


  
Remember personal info?

/

Before sending a comment, you have to answer correctly a simple question everyone knows the answer to. This completely baffles automated spam bots.
 

  (Register your username / Log in)

Notify:
Hide email:

Small print: All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.