ASP.NET Dependency Injection HTTP Module (MS Enter

2019-05-06 17:29发布

问题:

I've been following the steps in the 'Microsoft Enterprise Library 5.0' documentation to create a HTTP module to inject a reference to the Enterprise Library container into the pages of ASP.NET web application.

It contains the following code (which also appears online here):

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using Microsoft.Practices.Unity;

namespace Unity.Web
{
  public class UnityHttpModule : IHttpModule
  {
    public void Init(HttpApplication context)
    {
      context.PreRequestHandlerExecute += OnPreRequestHandlerExecute;
    }

    public void Dispose() { }

    private void OnPreRequestHandlerExecute(object sender, EventArgs e)
    {
      IHttpHandler currentHandler = HttpContext.Current.Handler;
      HttpContext.Current.Application.GetContainer().BuildUp(
                          currentHandler.GetType(), currentHandler);

      // User Controls are ready to be built up after page initialization is complete
      var currentPage = HttpContext.Current.Handler as Page;
      if (currentPage != null)
      {
        currentPage.InitComplete += OnPageInitComplete;
      }
    }

    // Build up each control in the page's control tree
    private void OnPageInitComplete(object sender, EventArgs e)
    {
      var currentPage = (Page)sender;
      IUnityContainer container = HttpContext.Current.Application.GetContainer();
      foreach (Control c in GetControlTree(currentPage))
      {
        container.BuildUp(c.GetType(), c);
      }
      context.PreRequestHandlerExecute -= OnPreRequestHandlerExecute;
    }

    // Get the controls in the page's control tree excluding the page itself
    private IEnumerable<Control> GetControlTree(Control root)
    {
      foreach (Control child in root.Controls)
      {
        yield return child;
        foreach (Control c in GetControlTree(child))
        {
          yield return c;
        }
      }
    }
  }
}

There are a number of problems with this code and the instructions that came with it.

1) The instructions don't mention where to place this code. Since it is a class, I placed it in the App_Code folder of my ASP.NET website project.

In fact, here is the instructions for this bit of code:

Create a new ASP.NET HTTP module class (named, for example, UnityHttpModule ) in your project that captures the PreRequestHandlerExecute event and executes code that walks the complete control tree of the current page request, applying the Unity BuildUp method to each control.

2) The HttpContext.Current.Application.GetContainer() method does not exist for me, even though I have the same DLL references used (I'm coding in .NET 4.0).

3) The OnPageInitComplete event references a 'context' variable... which doesn't seem to exist in this context.

Any ideas on what I'm missing here?

回答1:

Seems the documentation is badly organised.

In response to (2), what wasn't explained is that the HttpContext.Current.Application.GetContainer() method is actually an extension method, which is implemented like the code shown here.

To use this extension method, you simply have to import the 'Unity.Web' namespace.

Here is a copy of the extension method:

using System.Web;
using Microsoft.Practices.Unity;

namespace Unity.Web
{
  public static class HttpApplicationStateExtensions
  {
    private const string GlobalContainerKey = "EntLibContainer";

    public static IUnityContainer GetContainer(this HttpApplicationState appState)
    {
      appState.Lock();
      try
      {
        var myContainer = appState[GlobalContainerKey] as IUnityContainer;
        if (myContainer == null)
        {
          myContainer = new UnityContainer();
          appState[GlobalContainerKey] = myContainer;
        }
        return myContainer;
      }
      finally
      {
          appState.UnLock();
      }
    }
  }
}

With regards to the the dependency injection module code, I actually just used the basic method for getting an instance of the container, which as far as I'm concerned works just as well. The documentation says that the dependency injection HTTP module code improves 'testability' and 'discoverability', which is a little vague.

Anyway, here's the code for the basic approach:

protected void Application_Start(object sender, EventArgs e)
{
  Application.Lock();
  try
  {
    var myContainer = Application["EntLibContainer"] as IUnityContainer;
    if (myContainer == null)
    {
      myContainer = new UnityContainer();
      myContainer.AddExtension(new EnterpriseLibraryCoreExtension());
      // Add your own custom registrations and mappings here as required
      Application["EntLibContainer"] = myContainer;
    }
  }
  finally
  {
    Application.UnLock();
  }
}          

So with the extension code in place, and code in my global.asax file to create an instance of the Enterprise Library container, the only thing left to do is write the code to get an instance of the container as needed. So when I want to get an instance of the LogWriter class, I would write this:

using Unity.Web;

public LogWriter getLogWriter()
{
    var container = HttpContext.Current.Application.GetContainer();
    return container.Resolve<LogWriter>();
}

The Unity.Web namespace is needed to allow us to invoke the GetContainer() extension method.



回答2:

The code provided by the MSDN article is pretty shocking actually. Firstly, it won't compile, as you'll get an undeclared variable error at this line:

context.PreRequestHandlerExecute -= OnPreRequestHandlerExecute;

As context is passed into the Init method and not stored anywhere. If you do capture that parameter and store it in a field, then you'll get a runtime exception:

Event handlers can only be bound to HttpApplication events during IHttpModule initialization.

So, the following appears to work:

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using Microsoft.Practices.Unity;

namespace Unity.Web
{
    /// <summary>
    /// An <see cref="IHttpModule" /> that automatically injects dependencies into ASP.NET WebForms pages.
    /// </summary>
    /// <remarks>
    /// Since the pages have already been constructed by the time the module is called, constructor injection cannot be used. However,
    /// property injection can be used instead.
    /// </remarks>
    public class UnityHttpModule : IHttpModule
    {
        private HttpApplication _context;
        private bool _disposed;

        /// <summary>
        /// Initializes a module and prepares it to handle requests.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpApplication"/> that provides access to the methods, properties, and events common to all application objects within an ASP.NET application </param>
        public void Init(HttpApplication context)
        {
            _context = context;
            _context.PreRequestHandlerExecute += OnPreRequestHandlerExecute;
        }

        /// <summary>
        /// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"/>.
        /// </summary>
        public void Dispose()
        {
            GC.SuppressFinalize(this);
            Dispose(true);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }

        if (disposing)
        {
            if (_context != null)
            {
                _context.PreRequestHandlerExecute -= OnPreRequestHandlerExecute;
            }
        }

        _disposed = true;
    }

    /// <summary>
    /// Handles the <see cref="E:PreRequestHandlerExecute" /> event.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="eventArgs">The <see cref="EventArgs"/> instance containing the event data.</param>
    private void OnPreRequestHandlerExecute(object sender, EventArgs eventArgs)
    {
        var currentHandler = HttpContext.Current.Handler;
        if (currentHandler != null)
        {
            HttpContext.Current.Application.GetContainer().BuildUp(currentHandler.GetType(), currentHandler);
        }

        // User Controls are ready to be built up after page initialization is complete
        var currentPage = HttpContext.Current.Handler as Page;
        if (currentPage != null)
        {
            currentPage.InitComplete += OnPageInitComplete;
        }
    }

    /// <summary>
    /// Handles the <see cref="E:PageInitComplete" /> event.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
    private void OnPageInitComplete(object sender, EventArgs e)
    {
        var currentPage = (Page)sender;
        var container = HttpContext.Current.Application.GetContainer();
        foreach (var c in GetControlTree(currentPage))
        {
            container.BuildUp(c.GetType(), c);
        }
    }

    /// <summary>
    /// Gets the controls in the page's control tree, excluding the page itself.
    /// </summary>
    /// <param name="root">The root control.</param>
    /// <returns>The child controls of the <paramref name="root" /> control.</returns>
    private static IEnumerable<Control> GetControlTree(Control root)
    {
        foreach (Control child in root.Controls)
        {
            yield return child;
            foreach (var control in GetControlTree(child))
            {
                yield return control;
            }
        }
    }
}

You'll need the rest of the infrastructure code that @CiaranGallagher refers to in his answer to complete the plumbing, although I prefer to use project injectors, so in his example the code would be:

using Unity.Web;

[Dependency]
public LogWriter Writer { get; set; }

You can't use constructor injection with WebForms as the module uses BuildUp on already existing controls, but property injection works fine, as does method injection.