System Design When Using a Dependency Injection Container
Retired Content |
---|
This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. |
The latest Enterprise Library information can be found at the Enterprise Library site. |
The design patterns such as Inversion of Control, Dependency Injection, Factory, and Lifetime (described in the "Common Scenarios" section of the topic Introduction to the Unity Application Block) provide several major advantages when building applications that consist of many individual classes and components. Designing applications that conform to these patterns can provide the following:
- The capability to substitute one component for another using a pluggable architecture
- The capability to centralize and abstract common features and to manage cross cutting concerns such as logging, authentication, caching, and validation
- Increased configuration flexibility
- The capability to locate and instantiate services and components, including singleton instances of these services and components
- Simplified testability for individual components and sections of the application
- Simplified overall design, with faster and less error-prone development
- Ease of reuse for common components within other applications
Of course, implementing these patterns can initially make the design and development process more complex, but the advantages easily justify this extra complexity. In addition, the use of a comprehensive dependency injection mechanism can actually make the task of designing and developing best-practice applications much easier.
Fundamentally, there are two approaches to using a dependency injection mechanism:
- You can arrange to have dependent objects injected automatically, using techniques such as constructor injection, property (setter) injection, and method call injection that inject dependent objects as soon as you instantiate the parent object. This approach is generally most appropriate for applications that require a pluggable architecture or where you want to manage cross cutting concerns.
- You can have objects injected only on demand, by calling the Resolve method of the container only when you need to retrieve a reference to a specific object. This approach, which is actually closest to the Inversion of Control pattern, is generally most appropriate for service and component location.
The Unity Application Block provides a comprehensive dependency injection mechanism and is easy to incorporate into your applications. However, it does change the way that you design these applications. The following sections of this topic describe the three significant areas where dependency injection is useful:
- Pluggable Architectures
- Managing Cross Cutting Concerns
- Service and Component Location
Pluggable Architectures
Designing applications to use a pluggable architecture allows developers and users to add and modify the functionality of an application without changing the core code or processes. ASP.NET is an example of a pluggable architecture, where you can add and remove providers for features such as authentication, caching, and session management. The Unity Application Block itself also facilitates a pluggable architecture that allows you to substitute the container for one of your own design and add extensions through the container extensions architecture.
Dependency injection mechanisms such as the Unity Application Block container allow developers to map different components that implement a specific interface (such as a provider interface) or which inherit from a specific base class to the appropriate concrete implementations. Developers can then obtain a reference to the appropriate provider component at run time, without having to specify exactly which implementation of the component they require. The following sections of this topic, Managing Cross Cutting Concerns and Service and Component Location, show how you can use dependency injection to create instances of a specific class based on a request for a more general class (such as an interface or base class definition).
In addition, the providers may themselves require instances of other components. For example, a caching provider may need to use the services of a cryptography component or service. When components require the services of other components, you can use dependency injection to instantiate and inject instances of these components into a component automatically, based on either configuration settings in the container or on code within the application.
For more details of how you can use dependency injection in this way, see the following scenarios in this guidance:
- Annotating Objects for Constructor Injection
- Annotating Objects for Property (Setter) Injection
- Annotating Objects for Method Call Injection
Managing Cross Cutting Concerns
The features and tasks implemented in applications are often referred to as "concerns." The tasks specific to the application are "core concerns." The tasks that are common across many parts of the application, and even across different applications, are "cross cutting concerns." Most large applications require services such as logging, caching, validation, and authorization, and these are cross cutting concerns.
The simplest way to centralize these features is to build separate components that implement each of the required features, and then use these components wherever the management of that concern is required—in one or more applications. Dependency injection mechanisms such as the Unity Application Block container allow developers to register components that implement cross cutting concerns and then obtain a reference to the component at run time, without having to specify exactly which implementation of the component they require.
For example, if you have a component named FileLogger that performs logging tasks, your application can instantiate this component using the "new" operator, as shown in this code.
FileLogger myLogger = new FileLogger();
'Usage
Dim myLogger As New FileLogger()
If you then want to change the application to use the new component FastFileLogger, you must change the application code. However, if you have an interface ILogger that all implementations of the logging components use, you might instead write the code like the following.
ILogger myLogger = new FileLogger();
'Usage
Dim myLogger As ILogger = New FileLogger()
However, this does not solve the problem. Instead, you can use a dependency injection mechanism to map the ILogger interface to a specific concrete instance of the logging component, or even to multiple implementations so that the application can specify which one it requires. The following code uses the Unity Application Block to register a mapping for both a default type of logging component and a named type with the ILogger interface. You could alternatively specify these mappings using the Unity configuration file.
// Create container and register types
IUnityContainer myContainer = new UnityContainer();
myContainer.RegisterType<ILogger, FileLogger>(); // default instance
myContainer.RegisterType<ILogger, FastFileLogger>("FastLogger");
'Usage
' Create container and register types
Dim myContainer As IUnityContainer = New UnityContainer()
myContainer.RegisterType(Of ILogger, FileLogger)() ' default instance
myContainer.RegisterType(Of ILogger, FastFileLogger)("FastLogger")
The application can then obtain a reference to the default logging component using the following code.
ILogger myLogger = myContainer.Resolve<ILogger>();
'Usage
Dim myLogger As ILogger = myContainer.Resolve(Of ILogger)()
In addition, the application can use a variable (perhaps set in configuration or at run time) to specify a different implementation of the logging component interface as required, as shown in this code.
// Retrieve logger type name from configuration
String loggerName = ConfigurationManager.AppSettings["LoggerName"].ToString();
ILogger myLogger = myContainer.Resolve<ILogger>(loggerName);
'Usage
' Retrieve logger type name from configuration
Dim loggerName As String = ConfigurationManager.AppSettings("LoggerName").ToString()
Dim myLogger As ILogger = myContainer.Resolve(Of ILogger)(loggerName)
Service and Component Location
Applications often require the use of "services" or components that are specific to the application; examples are business logic components, data access components, interface components, and process controllers. In some cases, these services may be instance-based, so that each section of the application or each task requires a separate individual instance of the service. However, it is also common for services to be singleton-based, so that every user of the service references the same single instance of the service.
A service location facility makes it easy for an application to obtain a reference to a service or component, without needing to specify where to look for the specific service, or whether it is a singleton-based or an instance-based service. Dependency injection mechanisms such as the Unity Application Block container allow developers to register services in the appropriate way and then obtain a reference to the service at run time, without having to specify exactly which implementation of the service they require or what type of instance it actually is.
For example, if you have a singleton service class named CustomerData that you interact with to read and update information for any customer, your application will obtain a reference this service usually by calling a static GetInstance method of the service (which ensures that it is a singleton and that only one instance can exist), as shown in this code.
CustomerData cData = CustomerData.GetInstance();
'Usage
Dim cData As CustomerData = CustomerData.GetInstance()
Instead, you can use the Unity container to set the CustomerData class type with a specific lifetime that ensures it behaves as a singleton so that that every request for the service will return the same instance, as shown in the following code. You could alternatively specify these mappings using the Unity configuration file.
// Create container and register type as a singleton instance
IUnityContainer myContainer = new UnityContainer();
myContainer.RegisterType<CustomerData>(new ContainerControlledLifetimeManager());
'Usage
' Create container and register type as a singleton instance
Dim myContainer As IUnityContainer = New UnityContainer()
myContainer.RegisterType(Of CustomerData)(New ContainerControlledLifetimeManager())
Note
For information about using lifetime managers to control the creation, lifetime, and disposal of objects, see Using Lifetime Managers. For details of how to create custom lifetime managers, see Creating Lifetime Managers.
The application can then obtain a reference to the single instance of the CustomerData service using the following code. If the instance does not yet exist, the container will create it.
CustomerData cData = myContainer.Resolve<CustomerData>();
'Usage
Dim cData As CustomerData = myContainer.Resolve(Of CustomerData)()
In addition, perhaps a new CustomerFile component you decide to use in your application inherits the same base class named CustomerAccessBase as the CustomerData service, but is not a singleton—instead, it requires that your application instantiate an instance for each customer. In this case, you can specify mapping names when you register one component as a singleton type and one as an instance type (with the default transient lifetime), and then use the same application code to retrieve the required instance.
For example, the following code shows how you can register two named mappings for objects that inherit from the same base class, then—at run time—collect a string value from elsewhere in the application configuration that specifies which of the mappings to use. In this case, the value comes from the AppSettings section of the configuration. If the value with the key CustomerService contains "CustomerDataService", the code will return an instance of the CustomerData class. If it contains the value "CustomerFileService", the code will return an instance of the CustomerFile class.
IUnityContainer myContainer = new UnityContainer();
// Register CustomerData type as a singleton instance
myContainer.RegisterType<CustomerAccessBase, CustomerData>("CustomerDataService",
new ContainerControlledLifetimeManager());
// Register CustomerFile type with the default transient lifetime
myContainer.RegisterType<CustomerAccessBase, CustomerFile>("CustomerFileService");
...
String serviceName = ConfigurationManager.AppSettings["CustomerService"].ToString();
CustomerAccessBase cData
= (CustomerAccessBase)myContainer.Resolve<CustomerAccessBase>(serviceName);
'Usage
Dim myContainer As IUnityContainer = New UnityContainer()
' Register CustomerData type as a singleton instance
myContainer.RegisterType(Of CustomerAccessBase, CustomerData)("CustomerDataService", _
New ContainerControlledLifetimeManager())
' Register CustomerFile type with the default transient lifetime
myContainer.RegisterType(Of CustomerAccessBase, CustomerFile)("CustomerFileService")
...
Dim serviceName As String = ConfigurationManager.AppSettings("CustomerService").ToString()
Dim cData As CustomerAccessBase = myContainer.Resolve(Of CustomerAccessBase)(serviceName)