ASP.NET MVC2 Framework and Unity Dependency Injection Container

SOURCE CODE AVAILABLE HERE

As always the new Microsoft Technologies keep rolling out the door; each one making the prior obsolete because the new technologies keep making things easier and easier.

Having been bogged down by the CompositeWPF/PRISM and ISO-15926 (on the international development team) I had no room on even my backburner for MVC - at least until now :)

As with every technology, if I can't use Dependency Injection (more specifically Unity) right out of the box then this is the first thing I must do before moving forward - MVC was no different.  

Unfortunately the existing blogs, such as David Hadens (which I found instrumental in completing my objectives) were out of date.  MVC2 will pull the rug out from under these blogs because the IControllerFactory interface for CreateController no longer provides a "type" - it provides a "string" which will simply hold the controllerName; not as easy to work with.

public interface IControllerFactory
{
    IController CreateController(RequestContext requestContext, 
                                 string controllerName);
    void ReleaseController(IController controller);
}

Since the currently documented methods won't work and I'll want to ensure maximum backwards compatibility (now and in the future) I decided to take the Unity Interception route.    I will get the current factory (line 7/8) and register it as an instance (line 13) so that later when we Resolve IControllerFactory in the CustomControllerFactory (line 14) it will be wrapped as required for interception.   Notice that we also register a named IControllerFactory so that we can resolve it to specify it as the new ControllerFactory (line 31).

GlobalBase.cs in MVCContrib\Base
    1 /// <summary>
    2 /// Initializes the ControllerFactory interception.
    3 /// </summary>
    4 private void InitializeInterception()
    5 {
    6     // Get a reference to the current controller factory
    7     IControllerFactory factory =
    8         ControllerBuilder.Current.GetControllerFactory();
    9 
   10     // Configure Interception - note we register an instance of
   11     // the factory so that we can retrieve it in CustomerControllerFactory
   12     Container.AddNewExtension<Interception>()
   13         .RegisterInstance<IControllerFactory>(factory)
   14         .RegisterType<IControllerFactory,CustomControllerFactory>("Custom")
   15         .Configure<Interception>()
   16         .AddPolicy("ControllerPolicy")
   17         .AddMatchingRule(Container.Resolve<ControllerMatchingRule>())
   18         .AddCallHandler<ControllerCallHandler>();
   19 
   20     // Use Interface Interceptor
   21     Container.Configure<Interception>()
   22         .SetInterceptorFor<IControllerFactory>(new InterfaceInterceptor());
   23 
   24     // Now we'll need the factory reference (resolved by Unity) because 
   25     // it will be wrapped as required for interception
   26     IControllerFactory unityFactory = 
   27         Container.Resolve<IControllerFactory>("Custom");
   28 
   29     // Set the new ControllerFactory to the resolved factory 
   30     // which will be configured for interception
   31     ControllerBuilder.Current.SetControllerFactory(unityFactory);
  32 }

As noted above, in our CustomControllerFactory class we'll resolve the "Original" IControllerFactory (which will be wrapped for interception) and execute the CreateController(requestContext, controllerName) on it - we let MVC2 handle processing as designed.

CustomControllerFactory.cs in MVCContrib\Factories
    1 public IController CreateController(RequestContext requestContext, string controllerName)
    2 {
    3 
    4     Logger.Log("BEFORE CREATE:: CustomControllerFactory.CreateController()", 
    5         Category.Debug, Priority.None);
    6 
    7     // IControllerFactory without name will return the original 
    8     // ControllerFactory.  We're going going to let it do it's job
    9     // and get a reference to the controller.
   10     IController controller = Container.Resolve<IControllerFactory>()
   11         .CreateController(requestContext, controllerName);
   12 
   13     Logger.Log(string.Format("AFTER CREATE:: CustomControllerFactory.CreateController() CREATED [{0}]", 
   14         controller.GetType().Name), Category.Debug, Priority.None);
   15 
   16     return controller;
   17 }

Now we can let our configured ControllerCallHandler Intercept the CreateController() method - we let it execute and upon return have a reference to our controller (line 19).   With the reference we set the Container property (to a child container)

    1 public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
    2 {
    3     IMethodReturn msg;
    4     try
    5     {
    6         Logger.Log(string.Format("BEFORE::ControllerCallHandler.Invoke for {0}",
    7             input.Target.GetType().Name), 
    8             Category.Debug, Priority.None);
    9 
   10         msg = getNext()(input, getNext);
   11 
   12         // Create child container for each
   13         IUnityContainer childContainer = Container.CreateChildContainer();
   14 
   15         // If the controller is being handled by MVCContrib - implements IControllerBase
   16         // then we'll set the Container.  Attempts to do a BuildUp here do not work as
   17         // expected so the Container Setter is expected to perform a Container.Buildup(this)
   18         // See Base\ControllerBase Container setter for details
   19         if (msg.ReturnValue is IControllerBase)
   20             ((IControllerBase)msg.ReturnValue).Container = childContainer;
   21 
   22         Logger.Log(string.Format("AFTER::ControllerCallHandler.Invoke for {0}",
   23             input.Target.GetType().Name),
   24             Category.Debug, Priority.None);
   25 
   26     }
   27     catch (Exception ex)
   28     {
   29         msg = input.CreateExceptionMethodReturn(ex);
   30     }
   31 
   32     return msg;
   33 }

The ControllerBase (which controllers will derive from) will wire-up the controller - the Container setter code follows:

public IUnityContainer Container
{
    get { return _container; }
    set
    {
        // Don't set if already set
        if (_container != null && _container.GetType().ToString() == value.GetType().ToString())
            return;
 
        _container = value;
 
        // Buildup of MVCControllerBase so that we can
        // have logger and future types
        value.BuildUp(this);
 
        // We'll have a Logger now that this is built up.
        Logger.Log("ControllerBase::Container (setter) -- Performing Buildup of " + GetType().Name,
            Category.Debug, Priority.None);
 
        // The type will be that of the derived class.
        // BuildUp of the parent controller
        value.BuildUp(GetType(), this);
 
        // Notify controller
        OnContainerSet();
    }
}

With the internals covered I'll move on to my primary objective - Extensibility. I created my MVCContrib project with the notion that minimal code changes will be required to implement Unity in my ASP.NET MVC2 projects.  

Starting with the Global.ascx.cs file you'll find we derive from GlobalBase.  We'll override the virtual methods (letting the base handle some wire-up) and for the most part keep the Global.ascx.cs file intact.  Note how we subscribe to the OnApplicationStart event. 

namespace MvcApplication2
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801
 
    /// <summary>
    /// MVC Application
    /// </summary>
    public class MvcApplication : GlobalBase 
    {
        public static bool IsLoaded { get; set; }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="MvcApplication"/> class.
        /// </summary>
        public MvcApplication()
        {
            if (!IsLoaded)
            {   // Ensure we only do this once
                this.OnApplicationStart += 
                    new EventHandler<EventArgs>(MvcApplication_OnApplicationStart);
                IsLoaded = true;
            }
        }
 
        /// <summary>
        /// Handles the OnApplicationStart event of the MvcApplication control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing 
        /// the event data.</param>
        void MvcApplication_OnApplicationStart(object sender, EventArgs e)
        {
            Logger.Log("OnApplicationStart", Category.Debug, Priority.None);
 
            RegisterRoutes(RouteTable.Routes);
        }
 
        /// <summary>
        /// Registers the routes.
        /// </summary>
        /// <param name="routes">The routes.</param>
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute(
                "Default",                                             
                "{controller}/{action}/{id}",                          
                new { controller = "Home", action = "Index", id = "" } 
            );
 
        }
 
        /// <summary>
        /// Hook into InitializeContainer
        /// </summary>
        protected override void OnInitializeContainer()
        {
            Logger.Log("OnInitalizeContainer ", Category.Debug, Priority.None);
 
            // Configures container in class external to Global.asax.cs
            // (see Container folder)
            ContainerInitialize initContainer = 
                Container.Resolve<ContainerInitialize>();
            initContainer.InitializeContainer();
        }
    }
}

In my HomeController.cs you'll find that all I really needed to do is derive from MVVCControllerBase.  When I click on the HOME link the logger reveals the following processing - note how the EventAggregator (published by service) was handled (in bold): 

Debug(None): BEFORE CREATE:: CustomControllerFactory.CreateController()
Debug(None): ControllerMatchingRule.Matches(member=[System.Web.Mvc.IControllerFactory])
Debug(None): ControllerMatchingRule.Matches(member=[System.Web.Mvc.IControllerFactory])
Debug(None): BEFORE::ControllerCallHandler.Invoke for Wrapped_IControllerFactory_0fb5a472127c4b4b932425e809665c62
DEBUG: HomeController:CTOR
Debug(None): ControllerBase::Container (setter) -- Performing Buildup of HomeController
Debug(None): HomeController::OnContainerSet()
Debug(None): AFTER::ControllerCallHandler.Invoke for Wrapped_IControllerFactory_0fb5a472127c4b4b932425e809665c62
Debug(None): AFTER CREATE:: CustomControllerFactory.CreateController() CREATED [HomeController]
Debug(High): CONTROLLER:HomeController  -- Index() 
 
Debug(None): HomeController.DataServiceHandler STATUS = [3 Records sent]
 
Debug(None): PresentationModel.Clients updated with 3 records
Debug(None): ControllerMatchingRule.Matches(member=[System.Web.Mvc.IControllerFactory])
Debug(None): ControllerMatchingRule.Matches(member=[System.Web.Mvc.IControllerFactory])
Debug(None): BEFORE::ControllerCallHandler.Invoke for Wrapped_IControllerFactory_0fb5a472127c4b4b932425e809665c62
Debug(None): AFTER::ControllerCallHandler.Invoke for Wrapped_IControllerFactory_0fb5a472127c4b4b932425e809665c62
Debug(None): CustomControllerFactory.ReleaseController() RELEASED [HomeController]
 
 
HomeController.cs in MVCApplication2\Controllers
 
namespace MvcApplication2.Controllers
{
[HandleError]
public class HomeController : MVCControllerBase
{
    public HomeController()
    {
        Debug.WriteLine("HomeController:CTOR", "DEBUG");
    }
 
    [Dependency]
    public IDataService service { get; set; }
 
    [Dependency]
    public IPresentationModel model { get; set; }
 
    [Dependency]
    public IEventAggregator Aggregator { get; set; }
 
    [Dependency]
    public IAggregatorEventTokens Tokens { get; set; }
 
    /// <summary>
    /// We don't have constructor injection since we're using CustomControllerFactory
    /// so we'll rely on notification when the container is set.  This will be processed
    /// everytime a page is hit and our EventAggregator is a singleton so we'll have to
    /// ensure we only subscribe one time.
    /// </summary>
    protected override void OnContainerSet()
    {
 
        Logger.Log("HomeController::OnContainerSet()", Category.Debug, Priority.None);
 
        // GetTokens will only return null if key is not already set
        if (Tokens.GetToken(GetType().FullName) == null)
        {
            Logger.Log("HomeController::Subscribed to DataServiceEvent -- EVENT HANDLER",
                Category.Debug, Priority.None);
 
            // Subscribe to the DataServiceEvent
            SubscriptionToken token = Aggregator.GetEvent<DataServiceEvent>()
                .Subscribe(DataServiceHandler, ThreadOption.PublisherThread, true);
 
            // Set the token so we don't subscribe more than once
            Tokens.SetToken(GetType().FullName, token);
        }
    }
 
    public void DataServiceHandler(DataServiceEventArgs e)
    {
        Logger.Log(string.Format(
            "HomeController.DataServiceHandler STATUS = [{0}]", e.Status), 
            Category.Debug, Priority.None);
    }
 
    public ActionResult Index()
    {
        ViewData["Message"] = "Welcome to ASP.NET MVC!";
        Logger.Log("CONTROLLER:HomeController  -- Index() ", 
            Category.Debug, Priority.High);
 
        // Call the data service to get client list
        model.Clients = service.GetClients();
        return View();
    }
 
    public ActionResult About()
    {
        Logger.Log("CONTROLLER:HomeController  -- About()", 
            Category.Debug, Priority.High);
 
        ViewData["ModelClients"] = string.Format("There are {0} clients loaded!", 
            model.Clients.Count);
        return View();
    }
}
}

SOURCE CODE AVAILABLE HERE


Tags: , , ,
Categories:


Actions: E-mail | Permalink |  Grammar/Typo/Better way? Please let me know