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 Unity Application Block information can be found at the Unity Application Block site. |
The design patterns such as Inversion of Control, Dependency Injection, Factory, and Lifetime (described in the "Common Scenarios" section of 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:
- It can provide the capability to substitute one component for another using a pluggable architecture.
- It can provide the capability to centralize and abstract common features and to manage crosscutting concerns such as logging, authentication, caching, and validation.
- It can provide increased configuration flexibility.
- It can provide the capability to locate and instantiate services and components, including singleton instances of these services and components.
- It can provide simplified testability for individual components and sections of the application.
- It can provide simplified overall design, with faster and less error-prone development.
- It can provide 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 automatically injected, using techniques such as constructor injection, property (setter) injection, and method call injection that inject dependent objects immediately when 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 Crosscutting Concerns
- Service and Component Location
Pluggable Architectures
By designing applications to use a pluggable architecture, developers and users can 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 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.
By using dependency injection mechanisms, such as the Unity Application Block container, developers can map different components that implement a specific interface (such as a provider interface) or that 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 is required. The following sections of this topic, Managing Crosscutting 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 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 automatically instantiate and inject instances of these components into a component, based on either configuration settings in the container or on code within the application.
For more information about 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 Crosscutting 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 "crosscutting concerns." Most large applications require services such as logging, caching, validation, and authorization, and these are crosscutting 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. By using dependency injection mechanisms, such as the Unity Application Block container, developers can register components that implement crosscutting concerns and then obtain a reference to the component at run time, without having to specify exactly which implementation of the component is required.
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 the following 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. Alternatively, you can 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 the following 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
Frequenly, applications 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 having to specify where to look for the specific service or whether it is a singleton-based or an instance-based service. By using dependency injection mechanisms, such as the Unity Application Block container, developers can 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 is required 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 obtains a reference to 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 the following 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 every request for the service returns the same instance, as shown in the following code. Alternatively, you could 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 more information about using lifetime managers to control the creation, lifetime, and disposal of objects, see Using Lifetime Managers. For more information about 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 creates 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 it 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 component 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 file. If the value with the key CustomerService contains "CustomerDataService", the code returns an instance of the CustomerData class. If it contains the value "CustomerFileService", the code returns 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)