Dependency Injection (DI) is a design pattern used in software development to achieve Inversion of Control (IoC) between classes and their dependencies. It allows a system to follow the Single Responsibility Principle more closely by decoupling components and making them more modular and testable. In C#, one of the popular libraries for implementing DI is Unity, which is a lightweight container provided by Microsoft. This article will delve into the details of using Dependency Injection in C# with Unity, covering concepts, setup, configuration, and practical examples.
Understanding Dependency Injection
Before diving into Unity specifics, it’s crucial to grasp what Dependency Injection is and how it fits into the larger concept of Inversion of Control (IoC).
What is Dependency Injection?
Dependency Injection is a technique to achieve Inversion of Control (IoC) by providing an object with its dependencies rather than the object creating them itself. This results in a more modular, testable, and maintainable codebase.
There are three common types of Dependency Injection:
- Constructor Injection: Dependencies are provided through the class constructor.
- Property Injection: Dependencies are provided through public properties of the class.
- Method Injection: Dependencies are provided through methods of the class.
Benefits of Dependency Injection
- Decoupling: Classes are not tightly bound to their dependencies, which makes it easier to swap out implementations.
- Testability: By injecting dependencies, you can easily mock or stub them in unit tests.
- Maintainability: Changes in dependencies do not require modifications to the dependent classes.
- Flexibility: It allows for dynamic configuration of dependencies, which can be particularly useful in complex applications.
Introducing Unity
Unity is a Dependency Injection (DI) container developed by Microsoft. It provides a straightforward way to manage object lifetimes and resolve dependencies in .NET applications. Unity supports various features such as constructor injection, property injection, and method injection.
Key Features of Unity
- Flexible Registration: You can register types and instances in multiple ways.
- Lifetime Management: Unity supports different lifetime managers, such as singleton and transient.
- Interception: Unity allows for aspect-oriented programming through interception.
- Configuration: Unity can be configured via code or configuration files.
Setting Up Unity
To get started with Unity, you need to add the Unity NuGet package to your project. Here’s how you can do that:
- Install Unity via NuGet Open your project in Visual Studio, and navigate to the NuGet Package Manager Console. Execute the following command to install the Unity container:
Install-Package Unity
- Configure Unity Container You need to create and configure a
UnityContainer
instance to manage dependencies. This is typically done in the composition root of your application, which is where the container is set up.
using Unity;
public class Program
{
static void Main(string[] args)
{
var container = new UnityContainer();
RegisterTypes(container);
var myService = container.Resolve<IMyService>();
myService.DoWork();
}
private static void RegisterTypes(IUnityContainer container)
{
container.RegisterType<IMyService, MyService>();
}
}
Configuring Dependencies
Unity supports various ways to register and resolve dependencies. Let’s explore some of the common scenarios.
Constructor Injection
Constructor Injection is the most common method used for injecting dependencies. Here’s how you can configure it with Unity:
- Define the Interfaces and Implementations
public interface IMyService
{
void DoWork();
}
public class MyService : IMyService
{
private readonly IRepository _repository;
public MyService(IRepository repository)
{
_repository = repository;
}
public void DoWork()
{
_repository.Save();
}
}
public interface IRepository
{
void Save();
}
public class Repository : IRepository
{
public void Save()
{
// Save logic
}
}
- Register Types with Unity
private static void RegisterTypes(IUnityContainer container)
{
container.RegisterType<IRepository, Repository>();
container.RegisterType<IMyService, MyService>();
}
- Resolve and Use the Dependencies
var myService = container.Resolve<IMyService>();
myService.DoWork();
Property Injection
Property Injection involves setting dependencies through properties. It’s less common but can be useful in scenarios where constructor injection is not feasible.
- Define the Class with a Property
public class MyService
{
[Dependency]
public IRepository Repository { get; set; }
public void DoWork()
{
Repository.Save();
}
}
- Register the Type
private static void RegisterTypes(IUnityContainer container)
{
container.RegisterType<IRepository, Repository>();
container.RegisterType<MyService>();
}
- Resolve and Use the Service
var myService = container.Resolve<MyService>();
myService.DoWork();
Method Injection
Method Injection provides dependencies via method parameters. It’s less common but can be used in specific scenarios.
- Define the Class with a Method
public class MyService
{
public void DoWork(IRepository repository)
{
repository.Save();
}
}
- Register the Type
private static void RegisterTypes(IUnityContainer container)
{
container.RegisterType<IRepository, Repository>();
container.RegisterType<MyService>();
}
- Resolve and Use the Service
var myService = container.Resolve<MyService>();
myService.DoWork(container.Resolve<IRepository>());
Lifetime Management
Unity provides several lifetime managers to control the lifespan of objects. Here are some common ones:
Singleton Lifetime Manager
A Singleton Lifetime Manager ensures that only one instance of a type is created and shared throughout the application.
container.RegisterType<IMyService, MyService>(new ContainerControlledLifetimeManager());
Transient Lifetime Manager
A Transient Lifetime Manager creates a new instance of the type every time it is requested.
container.RegisterType<IMyService, MyService>();
Per Resolve Lifetime Manager
A Per Resolve Lifetime Manager creates a new instance of the type per each resolve call, but will reuse the same instance within a single resolve operation.
container.RegisterType<IMyService, MyService>(new PerResolveLifetimeManager());
Interception and Aspect-Oriented Programming
Unity supports interception, which allows you to add cross-cutting concerns such as logging or transaction management to your application without modifying the core logic.
- Install the Unity Interception Package
Install-Package Unity.Interception
- Configure Interception Define an interface and implementation for the interceptor:
public interface ILoggingInterceptor : ICallHandler
{
}
public class LoggingInterceptor : ILoggingInterceptor
{
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
// Log before invocation
var result = getNext()(input, getNext);
// Log after invocation
return result;
}
}
- Register Interception
private static void RegisterTypes(IUnityContainer container)
{
container.AddNewExtension<Interception>();
container.RegisterType<IMyService, MyService>()
.Configure<Interception>()
.SetInterceptorFor<IMyService>(new LoggingInterceptor());
}
Practical Example: Building a Web Application
Let’s put everything together with a practical example of a simple web application.
- Define the Services and Repositories
public interface IUserService
{
User GetUser(int id);
}
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public User GetUser(int id)
{
return _userRepository.GetById(id);
}
}
public interface IUserRepository
{
User GetById(int id);
}
public class UserRepository : IUserRepository
{
public User GetById(int id)
{
// Retrieve user from database
return new User { Id = id, Name = "John Doe" };
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
- Configure Unity in ASP.NET Core In an ASP.NET Core application, you configure Unity in the
Startup
class.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var container = new UnityContainer();
RegisterTypes(container);
services.AddSingleton<IUnityContainer>(container);
}
private static void RegisterTypes(IUnityContainer container)
{
container.RegisterType<IUserRepository, UserRepository>();
container.RegisterType<IUserService, UserService>();
}
}
- Inject and Use Services in Controllers
public class UserController : Controller
{
private readonly IUserService _userService
;
public UserController(IUserService userService)
{
_userService = userService;
}
public IActionResult Index(int id)
{
var user = _userService.GetUser(id);
return View(user);
}
}
Conclusion
Dependency Injection with Unity in C# provides a robust and flexible way to manage dependencies, enhance testability, and improve code maintainability. By understanding the core concepts of DI, exploring Unity’s features, and applying best practices, you can create more modular and scalable applications. Unity’s support for various injection methods, lifetime management, and interception allows you to tailor the DI setup to fit the needs of your application, whether it’s a simple console app or a complex web application.
By leveraging Unity and the principles of Dependency Injection, you can ensure that your C# applications are well-structured and prepared for future growth and changes.