【转】ASP.NET MVC 3 Service Location, Part 1: Introduction
Important Update
We've made significant changes to the IoC support in ASP.NET MVC 3 Beta. Please read Part 5 for more information.
Introduction
One of the new features in ASP.NET MVC 3 is the ability to register a service locator that will be used by the framework. Prior versions of the MVC framework have offered opportunities for introducing concepts like service location and dependency injection (DI); in MVC 3, we have formalized the process and opened up several new opportunities for developers.
This first post in the series will discuss the general strategy for service location in MVC 3. Later posts will discuss specific ways to perform service location and DI with existing and new features.
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.
General Strategy
The most important part of the strategy with service location is that it's going to be optional. This means that if you are not interested in working with a service locator, you won't be forced to. We will always offer a way to perform customization functions without requiring you to employ a service locator. We will also work to preserve backward compatibility as much as possible when introducing new service location capability into existing features of MVC.
When using the registered service locator, MVC will generally employ one of three strategies, depending on the kind of work it's trying to do.
1. Locating a singly registered service
There are several services which MVC uses today which have the ability to register a single instance of such a service. One example is the controller factory, which implements IControllerFactory. There is a single controller factory that's used for the entire application.
When MVC attempts to use a singly registered service, it will first ask the service locator whether it has an instance of that service available. If it does, then MVC will use that; if it does not, then it will fall back to the singleton registration that's available for non-service locator users.
The upside of this means that service locator users won't be required to fill their locator/container with all the existing default services used by MVC, because it will automatically fall back to these defaults when nothing exists in the locator. The potential downside of this means that it is theoretically possible to register a custom service in both places, but only the one in the locator will be used.
2. Locating a multiply registered service
There are several services which MVC uses today which have the ability to register multiple instances of such a service. One example of this is the view engine, which implements IViewEngine. Typically, MVC offers a registration point which acts like a list of such services, and also offers an API which acts as a facade and figures out the appropriate way to do the work. In the case of view engines, this facade (ViewEngines.Engines) takes the form of "go to each view engine on the list and ask it for a view until one of them can provide it". There are also multiply registered services where the facade uses all the services (ModelValidatorProviders.Providers) and aggregates all the responses together.
When MVC attempts to use a multiply registered service, it will continue to ask the collection facade to do the work. The collection facade will use both the statically registered instances of the service as well as any of the instances registered with the service locator, and combines them together in the way that is most appropriate for the facade. Where service order is important (as with view engines, for example), this usually means that the service locator instances will come before the statically registered instances.
Like the single registration strategy, the upside of this means that service locator users won't be required to fill their locator/container with the existing default service instances. The potential downside of this is that most containers don't offer a native ordering function for multi-registration of services, so where ordering is important, it may be necessary to use the non-service locator APIs. In practice, however, it probably won't be much of an issue, as most applications don't generally need to rely on ordering (for example, they will only primarily use a single view engine), especially since service locator services generally go before the existing pre-registered services.
3. Creating arbitrary objects
The final way that MVC may use a service locator is in the creation of arbitrary objects. (This is where we stray from the strictly service location functionality and use it more like a dependency injection container.) Where we've found it appropriate that an object we create may be in need of dependency injection of services, we will attempt to create that object through the service locator. One example is controller objects, which may want to take service dependencies to do their work.
When MVC attempts to create an arbitrary object in this way, it will ask the service locator to create the object on its behalf. If the service locator cannot fulfill this object creation, it will generally fall back to the existing behavior in prior versions of MVC 2; usually, this means calling Activator.CreateInstance.
IMvcServiceLocator and MvcServiceLocator
To enable service location in MVC, we've introduced a new interface (IMvcServiceLocator) and a new static singleton registration class (MvcServiceLocator). For Preview 1, we have also replicated the existing Common Service Locatorinterface (IServiceLocator) and exception class (ActivationException) in the System.Web.Mvc namespace.
public interface IServiceLocator : IServiceProvider { IEnumerable<TService> GetAllInstances<TService>(); IEnumerable<object> GetAllInstances(Type serviceType); TService GetInstance<TService>(); TService GetInstance<TService>(string key); object GetInstance(Type serviceType); object GetInstance(Type serviceType, string key); } public interface IMvcServiceLocator : IServiceLocator { void Release(object instance); } public static class MvcServiceLocator { public static IMvcServiceLocator Current { get; } public static IMvcServiceLocator Default { get; } public static void SetCurrent(IServiceLocator locator); }
The registration point is MvcServiceLocator.SetCurrent(), and only requires the service locator to support IServiceLocator. However, you'll notice that MvcServiceLocator.Current always returns an implementation of IMvcServiceLocator. We will automatically create the implementation of IMvcServiceLocator.Release() if one does not exist, which will call Dispose on the object if it implements IDisposable.
It's also worth noting that there will always be a service locator available, even if you've never registered one. Our default service locator is not a full fledged service locator, as it only calls Activator.CreateInstance and offers no registration system. It is not meant to be a replacement for a real service locator or dependency injection container.
No Configuration
MVC's usage of service location is limited to retrieving services. MVC has no need to configure the existing service locator. As such, we have made no effort to hide the configuration/registration process inherent to existing service locators or DI containers.
We feel that most people will choose the container they want to use based on its registration and configuration APIs, so attempting to hide them would be counter productive.
Complex Dependency Injection Needs
A common question might be why we chose to use Common Service Locator, when many container offer the ability to have extremely complex dependency injection systems (for example, nested containers for lifetime management tied to request lifetime).
Whenever possible, we will continue to offer factory-style services to allow complex dependency injection needs. For example, we will continue to offer IControllerFactory as a service, even though most users will be able to just registered their service locator without need to provide a custom controller factory any more. If you do need complex lifetime management, though, you can still opt to provide a controller factory which gives the flexibility to open up those complex creation options.
Open Question: To Common Service Locator Or Not?
For Preview 1, we decided not to ship the actual Common Service Locator (CSL) DLL. This decision was driven primarily by schedule.
We are currently debating internally as to whether or not to take the dependency on CSL. The advantage would be that container users would be able to leverage the existing implementations of CSL that most dependency injection containers come with (although they would need to add the implementation of IMvcServiceLocator, if desired). The disadvantage is that it creates a runtime dependency on an additional DLL that is not likely to be on the developer's machine or on the server, since CSL is not part of the .NET Framework. This slightly complicates the bin deployment story for MVC by adding a new DLL to remember to deploy, which would need to be in place whether the user uses a service locator or not.
Additionally, there is the matter of CSL's lack of the Release method, which we added to IMvcServiceLocator, and the matter of the mandate that CSL implementations throw exceptions rather than returning null when you cannot find an implementation of a specific service, which could cause real runtime performance issues.
What are your thoughts? Should we include a reference to the CSL, or is replicating the interface good enough? Should we attempt to work out a way to support both, thereby making the CSL reference optional and only necessary when the developer uses the CSL?
What's Next?
The first area of service location in MVC 3 that we'll cover is Controllers.