Dependency Injection in ASP.NET WebForms with StructureMap

Ahhh, the beautiful world of Dependency Injection (DI) trying to find it's way back in ASP.NET WebForms. What is it with me and ASP.NET WebForms lately? Just when I think I left all this behind, more and more work is thrown my way using this technology.

ASP.NET forms has been around for a while and will also be an integral part of "One ASP.NET", so it is not a lost skill after all. I'm currently working on a major migration project bringin a 1.1 ASP.NET website to the 21st century-ish. All the way to .NET 4.0 - no 4.5 I'm afraid due to underlying user infrastructure restrictions. Still, better than nothing!

One of the first tasks was to re-architecture the site and introduce some DI love to make development a bit more robust and certainly more testable - VERY IMPORTANT! Since StructureMap is used on a number of other projects, I was asked to use this for my DI implementation. There are a lot of options when it comes to choose your DI but for this scenario we will focus on StructureMap.

Some initial thoughts on StructureMap: The framework is powerful and actively maintained. The documentation is adequate but the site is bad to navigate, still remeniscent of early 2000 and the majority of the code samples need to be updated to the latest version. However, with a bit of digging, google/bing you get there eventually...

In this post we will see how to implement DI in your .aspx page and .ascx controls using StructureMap.

Let's start by creating a new ASP.NET website in Visual Studio. Make sure you select the ASP.NET WebForm template and let VS do it's magic.

select project type

Next, add the StructureMap though NuGet. For a change, we will use the NuGet command line, but you feel free to use the GUI. They both perform the same task:

With StructureMap installed, make sure you test that the site is running before continuing to the next step. Tested? Good, now lets create a simple interface and a concrete class so that we can give StructureMap something to do. Fist the interface:

public interface IEmailService  
{
    bool SendEmail(string recepient, string emailBody);
}

Then, the concrete class that implements the IEmailService interface:

public class EmailService : IEmailService  
{
    private readonly string connectionString;

    public EmailService(string connectionString)
    {
        // not sure why email service needs a connection to the DB
        // but for our sample, it will have to do :)
        this.connectionString = connectionString;
    }

    public bool SendEmail(string recepient, string emailBody)
    {
        // do so work here
        return true;
    }
}

I decided to have a constructor with a primitive parameter, only for the purpose of this example and to show you how to configure StructureMap accordingly. With our services in place, we can now define the StructureMap container configuration and create the IoC class that will perform the actual dependency resolution.
You can configure StructureMap using either xml or the fluent API. I am much in favor of the fluent API so the configuration should look like this:

public class ScanningRegistry : Registry  
    {
        public ScanningRegistry()
        {
            // Usually this value should be taken from web.config, but it's hardcoded here for simplicity
            var databaseConnectionString = "testconnectionstring";

            this.For<IEmailService>().Use<EmailService>().Ctor<string>("connectionString").Is(databaseConnectionString);

            this.Policies.SetAllProperties(y => y.WithAnyTypeFromNamespaceContainingType<EmailService>());
        }
    }

The ScanningRegistry class inherits from Registry and accomplishes the following:

  • retrieve/create a primitive value for the constructor of the concrete object
  • maps the concrete object to the interface
  • configurs the container to resolve all properties in any class with concrete objects found in the same assembly as our EmailService

The last part is very important as it will be the foundation for our .aspx and .ascx dependency injection. This is because .net webforms and user controls don't have a constructor. As such, the only way to automatically resolve objects is through properties, as we will see pretty soon.

With the configuration out of the way, we need a class to create the IoC container and resolve our objects.

public static class IoC  
{
  private static readonly IContainer Container;

  static IoC()
  {
      Container = new Container(new ScanningRegistry());
  }

  public static T Resolve<T>()
  {
      return Container.GetInstance<T>();
  }

  public static T Resolve<T>(string name)
  {
      return Container.GetInstance<T>(name);
  }

  public static void BuildUp(object target)
  {
      Container.BuildUp(target);
  }

  // helper method that shows what's in the container
  public static string WhatDoIHave()
  {
      return Container.WhatDoIHave();
  }
}

In this class, we instantiate a container and we provide a couple of methods for resolving objects. Of special interest are the two last methods: BuildUp() and WhatDoIHave(). An explanation of BuildUp() as taken directly from the StructureMap documentation:

Many times you simply cannot control when an object is going to be created (ASP.Net WebForms), but you may still want to inject dependencies and even primitive values into an already constructed object. To fill this gap, StructureMap 2.5.2+ introduces the "BuildUp()" method on Container and ObjectFactory. BuildUp() works by finding the default Instance for the concrete type passed into the BuildUp() method (or create a new Instance if one does not already exist), then applying any setters from that Instance configuration. At this time, StructureMap does not apply interception inside of BuildUp().

WhatDoIHave() is a simple helper method that return a string with the contents (resolved objects) in the container. Really helpful when troubleshooting

Finally we need to initialise StructureMap during the application startup. Open Global.asax, find the void Application_Start(object sender, EventArgs e) and add the following line:
IoC.IoC.BuildUp(this);

At this stage, you should check that your project builds successfully. If the build is good, we are now at a stage where we can configure our pages with StructureMap. An easy way to do this, is to create a base page that all your pages will derive from. The base page will be responsible for resolving all your dependencies and your pages will remain clean and DRY.

Create a new class and name it BasePageWithIoC. Make the class derive from Page and override the OnInit() method as per below:

public class BasePageWithIoC : Page  
{
  protected override void OnInit(EventArgs e)
  {
      base.OnInit(e);
      IoC.BuildUp(this);
  }
}

Do the same for the user controls, if you have any. Create a class that derives from UserControl and override the OnInit() method as per below:

public class BaseUserControlWithIoC : UserControl  
{
  protected override void OnLoad(EventArgs e)
  {
      IoC.BuildUp(this);
      base.OnLoad(e);
  }
}

Let's put all this to practice. Open your Default.aspx.cs class and change the class to inherit from BasePageWithIoC instead of Page. Then add an IEmailService property. Within the Page_Load() method call the SendEmail() method. Optionally, output the results to screen or use the debugger to examine the output. When navigating to this page on your browser, you should get a success result(true) from the SendEmail() method. The page code is attached below:

public partial class _Default : BasePageWithIoC  
{
    public IEmailService EmailService { get; set; }

    protected void Page_Load(object sender, EventArgs e)
    {
        var result = EmailService.SendEmail("test@test.com", "hello world");
        lblEmailResult.InnerText = "The email result was :" + (result ? "success" : "failure");
    }
}

The sample project can be found on Github, so if you cannot get it to work using the steps above or wish to see the code in action, feel free to jump on Github and have a look.

I hope you've found this helpful and I managed to save you some time/pain. Feel free to leave comments or recommendations on how to make this better.

P.S Make sure you follow me on Twitter @christosmatskas for more up-to-date news, articles and tips.


  • Share this post on
comments powered by Disqus