ASP.NET MVC 3 Service Location, Part 2: Controllers

Important Update

We've made significant changes to the IoC support in ASP.NET MVC 3 Beta. Please read Part 5 for more information.

Controller Creation

The most common form of service location today in ASP.NET MVC is for controller creation. In MVC 1.0, we created an interface named IControllerFactory which is responsible for the location and creation of controllers. This interface was introduced with the explicit desire to support dependency injection of controllers.

Update: (31 July 2010) I've added the source code for UnityMvcServiceLocator to the end of this post.

Disclaimer

This blog post talks about ASP.NET MVC 3 Preview 1, which is a pre-release version. Specific technical details may change before the final release of MVC 3. This release is designed to elicit feedback on features with enough time to make meaningful changes before MVC 3 ships, so please comment on this blog post or contact me if you have comments.

Location: IControllerFactory

This is a "singly registered" style service introduced in MVC 1.0. The static registration point for this service is atControllerBuilder.Current.SetControllerFactory for non-DI users.

The logic in ControllerBuilder was updated to attempt to find IControllerFactory first by calling MvcServiceLocator.Current.GetInstance<IControllerFactory>(). If the service does not exist in the service locator, then it falls back to the static registration point. The default IControllerFactory remains DefaultControllerFactory.

The following is an example implementation of a controller factory, using Microsoft Unity as the dependency injection container:

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

public class UnityControllerFactory : IControllerFactory {
    private IUnityContainer _container;
    private IControllerFactory _innerFactory;

    public UnityControllerFactory(IUnityContainer container)
        : this(container, new DefaultControllerFactory()) {
    }

    protected UnityControllerFactory(IUnityContainer container,
                                     IControllerFactory innerFactory) {
        _container = container;
        _innerFactory = innerFactory;
    }

    public IController CreateController(RequestContext requestContext,
                                        string controllerName) {
        try {
            return _container.Resolve<IController>(controllerName.ToLowerInvariant());
        }
        catch (Exception) {
            return _innerFactory.CreateController(requestContext, controllerName);
        }
    }

    public void ReleaseController(IController controller) {
        _container.Teardown(controller);
    }
}

It delegates creation of controllers to an inner controller factory when the container does not contain the controller in question (in this case, it uses the DefaultControllerFactory as its default inner controller factory). This allows us to register controllers by name, rather than matching the controller string to a type name:

CustomNamed.cs

public class CustomNamed : Controller {
    public ActionResult Index() {
        return View();
    }
}

Global.asax.cs

public class MvcApplication : HttpApplication {
    protected void Application_Start() {
        // ...

        var container = new UnityContainer();
        container.RegisterType<IController, CustomNamed>("admin");

        var factory = new UnityControllerFactory(container);
        ControllerBuilder.Current.SetControllerFactory(factory);

        // ...
    }
}

Because of our custom controller factory, now any time the URL has "admin" for its controller, we'll end up using an instance of the CustomNamed class. Because we have our own custom controller factory, we don't need to follow the default MVC conventions for controller classes.

In the example above, we used the existing static registration point for controller factories. We could also have written Application_Start() like this:

Global.asax.cs

public class MvcApplication : HttpApplication {
    protected void Application_Start() {
        // ...

        var container = new UnityContainer();
        var factory = new UnityControllerFactory(container);
        container.RegisterInstance<IControllerFactory>(factory);
        container.RegisterType<IController, CustomNamed>("admin");

        MvcServiceLocator.SetCurrent(new UnityMvcServiceLocator(container));

        // ...
    }
}

Now MVC is getting the controller factory from the service locator rather than the static registration point. Although we're still creating the factory by hand in the above example, we open up the possibility for letting the container create the controller factory for us, including getting dependency injection during creation for any services it might need to use.

Location: Controller instances

This is a new feature for MVC 3. The MVC framework (specifically, the DefaultControllerFactory class) has been updated to attempt to create all controller instances with the registered service locator. If the creation fails, then it will fall back to the pre-MVC 3 behavior of using Activator.CreateInstance.

Again using Unity as our example container, let's presume we have the following code:

IMathService.cs

public interface IMathService {
    int Add(int left, int right);
}

MathService.cs

public class MathService : IMathService {
    public int Add(int left, int right) {
        return left + right;
    }
}

HomeController.cs

using System.Web.Mvc;

public class HomeController : Controller {
    IMathService _mathService;

    public HomeController(IMathService mathService) {
        _mathService = mathService;
    }

    public ActionResult Index() {
        return View(_mathService.Add(3, 6));
    }
}

What we're created is a service interface, an implementation of that interface that we want to use at runtime, and a controller which consumes it.

The controller takes the dependency for IMathService on its constructor. It doesn't know what instance of IMathService it's going to get, and it doesn't really care. This kind of dependency injection is called "constructor injection", and it's a fairly common way to do DI.

Because we've cut the dependency between HomeController and MathService, we've made it easier to swap out which service we'll use, as well as making it easier to test HomeController now. During unit testing we can provide a mock of IMathService to the controller.

To complete the registration process so that everything gets wired up automatically at runtime, we'll create the Unity container and tell it "whenever anything asks for IMathService, give it an instance of MathService":

Global.asax.cs

protected void Application_Start() {
    // ...

    var container = new UnityContainer();
    container.RegisterType<IMathService, MathService>();
    MvcServiceLocator.SetCurrent(new UnityMvcServiceLocator(container));

    // ...
}

Like many DI frameworks, Unity doesn't require explicit configuration to make arbitrary objects. Constructor injection is automatically supported. We don't have a single line of configuration in the container which knows anything about HomeController. Unity will look at the request for building HomeController, see that its constructor requires an IMathService, and recursively determine how to build that. If, for instance, the MathService class itself required some service, Unity would continue to recursively resolve all the dependent services until it has satisfied them all.

What's Next?

The next area of service location in MVC 3 that we'll cover is View Engines and View Pages.

UnityMvcServiceLocator.cs

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

public class UnityMvcServiceLocator : IMvcServiceLocator {
    IUnityContainer _container;

    public UnityMvcServiceLocator(IUnityContainer container) {
        _container = container;
    }

    public IEnumerable<TService> GetAllInstances<TService>() {
        return _container.ResolveAll<TService>();
    }

    public IEnumerable<object> GetAllInstances(Type serviceType) {
        return _container.ResolveAll(serviceType);
    }

    public TService GetInstance<TService>() {
        return (TService)Resolve(typeof(TService));
    }

    public TService GetInstance<TService>(string key) {
        return (TService)Resolve(typeof(TService), key);
    }

    public object GetInstance(Type serviceType) {
        return Resolve(serviceType);
    }

    public object GetInstance(Type serviceType, string key) {
        return Resolve(serviceType, key);
    }

    public object GetService(Type serviceType) {
        return Resolve(serviceType);
    }

    public void Release(object instance) {
        _container.Teardown(instance);
    }

    private object Resolve(Type serviceType, string key = null) {
        try {
            return _container.Resolve(serviceType, key);
        }
        catch (Exception ex) {
            throw new ActivationException(ex.Message, ex);
        }
    }
}

你可能感兴趣的:(controller)