34

I am trying to work out a way to use dependency injection with ASP.NET Web Forms controls.

I have got lots of controls that create repositories directly, and use those to access and bind to data etc.

I am looking for a pattern where I can pass repositories to the controls externally (IoC), so my controls remain unaware of how repositories are constructed and where they come from etc.

I would prefer not to have a dependency on the IoC container from my controls, therefore I just want to be able to construct the controls with constructor or property injection.

(And just to complicate things, these controls are being constructed and placed on the page by a CMS at runtime!)

Any thoughts?

Jack Ukleja
  • 13,061
  • 11
  • 72
  • 113

6 Answers6

32

UPDATE 2019: With the introduction of Web Forms 4.7.2, there is now better support for DI. This invalidates the below. See: Wiring up Simple Injector in WebForms in .NET 4.7.2

You can use automatic constructor injection by replacing the default PageHandlerFactory with a custom one. This way you can use an overloaded constructor to load the dependencies. Your page might look like this:

public partial class HomePage : System.Web.UI.Page
{
    private readonly IDependency dependency;

    public HomePage(IDependency dependency)
    {
        this.dependency = dependency;
    }

    // Do note this protected ctor. You need it for this to work.
    protected HomePage () { }
}

Configuring that custom PageHandlerFactory can be done in the web.config as follows:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*" path="*.aspx"
        type="YourApp.CustomPageHandlerFactory, YourApp"/>
    </httpHandlers>
  </system.web>
</configuration>

Your CustomPageHandlerFactory can look like this:

public class CustomPageHandlerFactory : PageHandlerFactory
{
    private static object GetInstance(Type type)
    {
        // TODO: Get instance using your favorite DI library.
        // for instance using the Common Service Locator:
        return Microsoft.Practices.ServiceLocation
            .ServiceLocator.Current.GetInstance(type);
    }

    public override IHttpHandler GetHandler(HttpContext cxt, 
        string type, string vPath, string path)
    {
        var page = base.GetHandler(cxt, type, vPath, path);

        if (page != null)
        {
            // Magic happens here ;-)
            InjectDependencies(page);
        }

        return page;
    }

    private static void InjectDependencies(object page)
    {
        Type pageType = page.GetType().BaseType;

        var ctor = GetInjectableCtor(pageType);

        if (ctor != null)
        {
            object[] arguments = (
                from parameter in ctor.GetParameters()
                select GetInstance(parameter.ParameterType)
                .ToArray();

            ctor.Invoke(page, arguments);
        }
    }

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

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

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

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

Downside is that this only works when running your side in Full Trust. You can read more about it here. But do note that developing ASP.NET applications in partial trust seems a lost cause.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • Hi Steven, I have something like this implemented in my project and it works really good. But I'm facing a problem now. It's described here "http://stackoverflow.com/questions/15692499/page-routing-in-asp-net-4-0-extensionless-url-versus-pagehandlerfactory-asp". Could you please take a look and maybe share some opinion? – Ondrej Janacek Mar 29 '13 at 16:42
  • Castle Windsor. Nevermind, I solved it by Resolving from Boostrapper in pages. It's not that cool, but hey, it's working and still looks good. – Ondrej Janacek Mar 30 '13 at 09:19
  • I found this excellent article http://www.codemag.com/Article/1210031 (I think linked from another SO answer, but now I can't find which one) which includes more example code related to the above solution, and also, interestingly, shows how Microsoft Managed Extensibility Framework (MEF) can help you to solve this and similar dependency injection problems in a very useful and slightly non-standard way. – MikeBeaton Mar 02 '17 at 11:49
  • 1
    @steven Starting from .NET 4.7.2 (what's new), it is now easy for developers to use Dependency Injection in WebForms applications. [info](https://stackoverflow.com/a/51516403/2803565). – S.Serpooshan Jul 25 '18 at 10:23
7

Autofac supports fairly unobtrusive dependency injection in ASP.NET WebForms. My understanding is it just hooks into the ASP.NET page lifecycle using an http module and does property injection. The only catch is that for controls I don't think this happens until after the Init event.

Frank Bryce
  • 8,076
  • 4
  • 38
  • 56
Roman
  • 19,581
  • 6
  • 68
  • 84
7

Starting from .NET 4.7.2 (what's new), it is now easy for developers to use Dependency Injection in WebForms applications. With the UnityAdapter, you can add it to your existing WebForms application in 4 simple steps. See this blog.

S.Serpooshan
  • 7,608
  • 4
  • 33
  • 61
  • Does it support only Unity or can it use other IoC containers? – Endy Tjahjono Sep 04 '18 at 08:29
  • 2
    The `AspNet.WebFormsDependencyInjection.Unity` nuget package currently supports only Unity, but using [the source code](https://github.com/aspnet/AspNetWebFormsDependencyInjection) as an example, you can use other IoC containers. Here is how I [customized it to use SimpleInjector](https://gist.github.com/ndc/7334d459c3eb0be594aad3973a488cad). – Endy Tjahjono Sep 05 '18 at 08:37
3

The best way is to have a base class for the controls like:

public class PartialView : UserControl
{
    protected override void OnInit(System.EventArgs e)
    {
        ObjectFactory.BuildUp(this);
        base.OnInit(e);
    }
}

That will inject any control that inherits from that base class (uses structuremap). Combining that with a property based config, you will be able to have controls like:

public partial class AdminHeader : PartialView
{
   IMyRepository Repository{get;set;}
}

Update 1: If you can't have the controls inherit, perhaps the CMS has a hook right after creating the controls, in there you can call the BuildUp. Also if the CMS allows you to hook something to fetch the instance you could use constructor based injection, but I prefer BuildUp on this specific scenario as asp.net doesn't have a hook for this.

eglasius
  • 35,831
  • 5
  • 65
  • 110
  • Thanks for the response. The perfectionist side of me would like the controls not to have a dependency on the ObjectFactory framework i.e. pure dependency injection. Obviously this implies something external, creating the controls. – Jack Ukleja Feb 26 '09 at 09:07
  • Re: Update 1. I will have a poke around in the CMS and see if I can find anything. I guess one problem with constructor based injection in ASP.NET is that the controls become 'undesignable' at that point. Unless the designer know how to build them up. – Jack Ukleja Feb 26 '09 at 09:09
1

You could also create some singleton instances in the Application_Start global.asax event and have them available as public static readonly properties.

Andrei Rînea
  • 20,288
  • 17
  • 117
  • 166
-1

This is a solution I recently used to avoid hooking into the pipeline (I find that confuses everyone that looks at my code in the future, but yes, I see its benefits as well):

public static class TemplateControlExtensions
{
    static readonly PerRequestObjectManager perRequestObjectManager = new PerRequestObjectManager();

    private static WIIIPDataContext GetDataContext(this TemplateControl templateControl)
    {
        var dataContext = (WIIIPDataContext) perRequestObjectManager.GetValue("DataContext");

        if (dataContext == null) 
        {
           dataContext = new WIIIPDataContext();
           perRequestObjectManager.SetValue("DataContext", dataContext);   
        }

        return dataContext;
    }

    public static IMailer GetMailer(this TemplateControl templateControl)
    {
        return (IMailer)IoC.Container.Resolve(typeof(IMailer));
    }

    public static T Query<T>(this TemplateControl templateControl, Query<T> query)
    {
        query.DataContext = GetDataContext(templateControl);
        return query.GetQuery();
    }

    public static void ExecuteCommand(this TemplateControl templateControl, Command command)
    {
        command.DataContext = GetDataContext(templateControl);
        command.Execute();
    }

    private class PerRequestObjectManager
    {
        public object GetValue(string key)
        {
            if (HttpContext.Current != null && HttpContext.Current.Items.Contains(key))
                return HttpContext.Current.Items[key];
            else
                return null;
        }

        public void SetValue(string key, object newValue)
        {
            if (HttpContext.Current != null)
                HttpContext.Current.Items[key] = newValue;
        }
    }
}

This shows how you can create your own life time manager pretty easily as well as hook into an IoC container if you so desire. Oh, and I am also using a query/command structure which is sort of unrelated, but more on the reasoning behind that can be found here:

Limit your abstractions: Refactoring toward reduced abstractions

Merritt
  • 2,333
  • 21
  • 23