Unity and ASP.Net MVC

Aug 7, 2009 at 5:49 PM

I'd like to nail down how to wire up Unity container to ASP.Net Mvc.  Now I do not need ServiceLocator because keep-it-simple principle applies in most web apps.  However, it's not super trivial on how to Integrate. 

The issue that i have is that i am not quiet sure how to register with Unity all available controllerTypes to be Per Web Request life cycle. 

Thoughts: If two outstanding requests are done for the same controller, i want to ensure that each uses their own instance.  In high level terms, do I need to iterate over all IController implementing types in the current assembly and then register it with the container with LifeTimeManager that saves resolved/instantiated objects in the HttpContext.Current?

This is what i have so far:

public interface IContainerAccessor
    {
        /// <summary> Unity container.
        /// </summary>
        IUnityContainer Container { get; }
    }

public class UnityControllerFactory : DefaultControllerFactory
    {
        private IUnityContainer _container;

        /// <summary> Contructor that instantiates the container, taking configuration from web.config </summary>
        public UnityControllerFactory(IUnityContainer container)
        {
            this._container = container;
        }

        /// <summary>
        /// </summary>
        /// <param name="controllerType"> The type of controller to instantiate. </param>
        /// <returns>An instance of the Controller </returns>
        /// <exception cref="ArgumentNullException">This exception is thrown when controllerType is null.</exception>
        /// <exception cref="ArgumentException">This exception is thrown when controllerType type does not implement IController.</exception>
        protected override IController GetControllerInstance(Type controllerType)
        {
            if (controllerType == null)
            {
                throw new ArgumentNullException("controllerType");
            }

            if (!typeof(IController).IsAssignableFrom(controllerType))
            {
                throw new ArgumentException(
                    string.Format("Type requested is not a controller: {0}", controllerType.Name), "controllerType");
            }

            return this._container.Resolve(controllerType) as IController;
        }
    }

In Global.asax.cs

/// <summary> Unity container property. </summary>
        public static IUnityContainer Container
        {
            get
            {
                return _container;
            }
        }

        /// <summary> read-only Container property
        /// </summary>
        IUnityContainer IContainerAccessor.Container
        {
            get
            {
                return Container;
            }
        }

protected void Application_Start()
        {
            ContainerInit();
            RegisterRoutes(RouteTable.Routes);
            ControllerBuilder.Current.SetControllerFactory(new UnityControllerFactory(Container));
        }

        protected void Application_End(object sender, EventArgs e)
        {
            ContainerCleanup();
        }

        /// <summary> Clean
        /// </summary>
        private static void ContainerInit()
        {
            if (_container == null)
            {
                _container = new UnityContainer();
            }
        }

        private static void ContainerCleanup()
        {
            if (Container != null)
            {
                Container.Dispose();
            }
        }

Aug 8, 2009 at 4:36 PM

Hi rroman81,

My UnityContribution (EventAggregator) got stomped on back on changeset 18055 without any warning so I since been lone-wolfing it.  I prefer the EventAggregator (pulled from the CompositeWPF) because I use PRISM and Unity quite heavily - code is consistent.    I took a different approach to injecting MVC2 with Unity; I use interface interception - if you have time for code-review I would be interested in your feedback.  

You'll find in the source that I have an MVCContrib  which utilizes UnityContrib (EventAggregator and Logging).   The wire-up magic is done in the MVCContrib.Base.GlobalBase.cs file.   You'll find that minimal code changes are required to implement MVCContrib - Global.asax.cs has minor changes and controllers need only derive from MVCControllerBase.

I use Interface Interception - the code that handles IControllerFactory interception follows  (source code available HERE)

ControllerCallHandler.cs

/// <summary>
/// Implement this method to execute your handler processing.
/// </summary>
/// <param name="input">Inputs to the current call to the target.</param>
/// <param name="getNext">Delegate to execute to get the next delegate in the handler
/// chain.</param>
/// <returns>Return value from the target.</returns>
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
    IMethodReturn msg;
    try
    {
        Logger.Log(string.Format("BEFORE::ControllerCallHandler.Invoke for {0}",
            input.Target.GetType().Name), 
            Category.Debug, Priority.None);

        msg = getNext()(input, getNext);

        // Create child container for each
        IUnityContainer childContainer = Container.CreateChildContainer();

        // If the controller is being handled by MVCContrib - implements IControllerBase
        // then we'll set the Container.  Attempts to do a BuildUp here do not work as
        // expected so the Container Setter is expected to perform a Container.Buildup(this)
        // See Base\ControllerBase Container setter for details
        if (msg.ReturnValue is IControllerBase)
            ((IControllerBase)msg.ReturnValue).Container = childContainer;

        Logger.Log(string.Format("AFTER::ControllerCallHandler.Invoke for {0}",
            input.Target.GetType().Name),
            Category.Debug, Priority.None);
    
    }
    catch (Exception ex)
    {
        msg = input.CreateExceptionMethodReturn(ex);
    }

    return msg;
}

I've found that Unity's Buildup has issue with dynamically generated instances so my ControllerBase.cs (implementing IControllerBase) contains the following:

/// <summary>
/// Gets or sets the container.  Set by the CustomControllerFactory
/// </summary>
/// <value>The container.</value>
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();
    }
}

My HomeController looks as follows:

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();
    }
}

With the logging generating the following output on "Home" click:

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_18300eb12f6c4846875ab28ce152b54f
DEBUG: HomeController:CTOR
Debug(None): ControllerBase::Container (setter) -- Performing Buildup of HomeController
Debug(None): HomeController::OnContainerSet()
Debug(None): AFTER::ControllerCallHandler.Invoke for Wrapped_IControllerFactory_18300eb12f6c4846875ab28ce152b54f
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_18300eb12f6c4846875ab28ce152b54f
Debug(None): AFTER::ControllerCallHandler.Invoke for Wrapped_IControllerFactory_18300eb12f6c4846875ab28ce152b54f
Debug(None): CustomControllerFactory.ReleaseController() RELEASED [HomeController]