Inheritance is a powerful tool that can save time and enforce program structure, but it’s also one that developers frequently abuse. In particular, it can increase maintenance costs when used with dependency injection.
The problem is if you are using constructor injection and adding another dependency to your base class. Then you need to extend all subclasses as well. In this post, I will explain some alternatives when you need to do such a task.
The alternative ways to use dependency injection with inheritance in C# are:
- Property Injection
- Service Aggregator pattern
- Decorator pattern
Let’s go through some examples to see how to achieve this in code.
Class inheritance and Dependency Injection
Modern programming languages focus on object-oriented design, which allows programmers to factor common functionality into reusable classes. One of the popular approaches to code reuse is inheritance, which allows developers to create new classes that build on existing classes by adding new functionality or overriding existing functionality.
One criticism of inheritance is that it tightly couples parent class with child class. It is harder to reuse the code and write unit tests. That’s why most developers prefer dependency injection as a way to reuse code.
Dependency injection is a way to inject dependencies into a class for use in its methods. With dependency injection, the class becomes loosely coupled from the dependencies, making it easier to reuse code and to maintain a class in the future.
This post aims to answer the question of how to combine dependency injection with class inheritance.
What’s the problem we are trying to solve?
Let’s say we have the following code:
interface IBookController
{
List<string> GetBookNames();
}
class BookController: IBookController
{
private readonly IBookService _bookService;
private readonly IOrderService _orderService;
public BookController(IBookService bookService, IOrderService orderService)
{
_bookService = bookService;
_orderService = orderService;
}
public List<string> GetBookNames()
{
//some code that retrieves and returns list of book names.
return new List<string>();
}
//other code here...
}
class NonFictionBookController : BookController
{
public NonFictionBookController(IBookService bookService, IOrderService orderService)
: base(bookService, orderService)
{
}
}
The code contains a base class, BookController, and a derived class NonFictionBookController. The BookController class has two dependencies, IBookService and IOrderService. The derived class NonFictionBookController then has to have a constructor that takes the same arguments and passes them to the base class constructor.
We have a requirement to add another dependency, for example, ISessionService. This service initializes a user session before BookController’s methods are called.
One option is to pass it as a constructor parameter. In that case, we have to change the base class constructor and the child class constructor. In case there are multiple child classes, then the code becomes harder to manage. Not to mention if the base class has multiple constructors and need to pass the new dependency to each of them.
Let’s see what possible solutions to adding a new dependency through the constructor are.
Property Injection
The first solution I show to you is the Property Injection approach. As you know, there are three types of dependency injection:
- Constructor injection – Constructor injection is a form of dependency injection in which one or more of the application’s dependencies (or services) are supplied to the class through its constructor. Every dependency is one constructor argument. Constructor injection is often considered superior to setter injection because the constructor is often the most tightly coupled to the class itself, making it the ideal location for dependency injection. It also provides a clear distinction between the initialization code (that executes when the class is instantiated) and the class’s regular methods and fields.
- Method injection – Method injection is a type of dependency injection which allows a program to defer instantiation of a dependency to the time when it is needed. This is achieved by passing one or more dependencies at run-time as parameters of a method.
- Property injection – also known as setter injection. This is a way of injecting dependencies of a class through its properties. That allows us to create loosely coupled objects. This process also allows us to transfer the instantiation of a class to somewhere else in our code. One big benefit of property injection is that it makes code easier to test.
Solution without IoC container
The first thing we need to do is to define a new property in the BookController:
public ISessionService SessionService { get; set; }
Now it’s easy to inject a new dependency by initializing this property.
BookController bookController =
new BookController(new BookService(), new OrderService());
bookController.SessionService = new SessionService();
As you could see, NonFictionBookController didn’t change, which is exactly what we wanted.
Solution with IoC container
In case I want to use an Inversion of Control container (also known as DI container) such as Autofac, the first thing you have to do is to register all necessary components:
//create builder object that will register all classes
var builder = new ContainerBuilder();
//register book controllers
builder.RegisterType<BookController>()
.As<IBookController>().AsSelf().PropertiesAutowired();
builder.RegisterType<NonFictionBookController>();
//register book dependencies
builder.RegisterType<BookService>().As<IBookService>().AsSelf();
builder.RegisterType<OrderService>().As<IOrderService>().AsSelf();
builder.RegisterType<SessionService>().As<ISessionService>().AsSelf();
//build the container can create, wire and manage all dependencies
_container = builder.Build();
We use RegisterType method to register dependencies. BookController registration has additional step, we call the PropertiesAutowired method to auto resolve all properties on it.
The code to resolve BookController is as follows:
var bookController = _container.Resolve<IBookController>();
Service Aggregator pattern
The service aggregator pattern is a software design pattern that can be used to realize a set of services from a single point of access. In other words, it is a mechanism that allows users to access multiple services through a single, unified interface.
You may use the pattern in situations where a software architecture has been developed in an evolutionary way or where different groups within a software project have been created to focus on different aspects of the overall architecture.
Let’s see how to use it in this case.
Solution without IoC container
Start by defining a new interface IAggregateService:
interface IAggregateService
{
public IBookService BookService { get; set; }
public IOrderService OrderService { get; set; }
public ISessionService SessionService { get; set; }
}
This interface contains all necessary dependencies. The implementation of it is fairly simple:
class AggregateService : IAggregateService
{
public AggregateService(IBookService bookService,
IOrderService orderService,
ISessionService sessionService)
{
BookService = bookService;
OrderService = orderService;
SessionService = sessionService;
}
public IBookService BookService { get; set; }
public IOrderService OrderService { get; set; }
public ISessionService SessionService { get; set; }
}
Now the BookController takes only one dependency:
class BookController: IBookController
{
private readonly IBookService _bookService;
private readonly IOrderService _orderService;
private readonly ISessionService _sessionService;
public BookController(IAggregateService aggregateService)
{
_bookService = aggregateService.BookService;
_orderService = aggregateService.OrderService;
_sessionService = aggregateService.SessionService;
}
public List<string> GetBookNames()
{
//some code that retrieves and returns list of book names.
return new List<string>();
}
//other code here...
}
And the NonFictionBookController subclass needs to be changed as well to reflect the change of the dependencies.
class NonFictionBookController : BookController
{
public NonFictionBookController(
IAggregateService aggregateService): base(aggregateService)
{
}
}
One big advantage of this approach is that adding new dependencies doesn’t change the constructor signature of BookController and NonFictionBookController.
One extra step is added in the initialization. We need to initialize the AggregateService.
var aggregateService = new AggregateService
(
new BookService(),
new OrderService(),
new SessionService()
);
var bookController =
new BookController(aggregateService);
Solution with IoC container
One small change in the registration process is adding registration step for the new service:
builder.RegisterType<AggregateService>()
.As<IAggregateService>().AsSelf();
And that’s it, the resolve part stays the same:
var bookController = _container.Resolve<IBookController>();
Decorator Pattern
The Decorator pattern is a structural design pattern. The concept is to add ‘new’ functionality to an existing class without changing its source code. This is achieved by wrapping a class with another one, which will add new features to it. The wrapping class is called Decorator. The decorator wraps the original class and adds extra functionality.
Solution without IoC container
The first step is to define a decorator class.
class SessionBookController : IBookController
{
private readonly ISessionService _sessionService;
private readonly IBookController _bookController;
public SessionBookController(ISessionService sessionService,
IBookController bookController)
{
_sessionService = sessionService;
_bookController = bookController;
}
public List<string> GetBookNames()
{
_sessionService.InitializeSession();
return _bookController.GetBookNames();
}
}
This class implements IBookController and takes another IBookController instance as a constructor argument. This way, the functionality of the BookController can be extended without actually changing the BookController. SessionBookController will initialize the session and return the output of the wrapped GetBookNames method.
We need to make the change in the usage now. We will use an instance of SessionBookController in the code, but pass the BookController instance to it.
var bookController =
new BookController(new BookService(), new OrderService());
//we use this instance now
var sessionBookController =
new SessionBookController(bookController, new SessionService());
Solution with IoC container
We need to add one change to the way we register BookController.
//register book controllers
builder.RegisterType<BookController>()
.As<IBookController>().AsSelf().PropertiesAutowired();
builder.RegisterType<NonFictionBookController>();
//register decorator
builder.RegisterDecorator<SessionBookController, IBookController>();
The last line is an addition to the registration process. RegisterDecorator will register SessionBookController as a wrapper to the instance of the IBookController.
//this returns instance of SessionBookController
var bookController = _container.Resolve<IBookController>();
Conclusion
The first step in mastering any new technology is to know what you can do with it. In today’s developer’s world, the number of new technologies available is exploding. But it’s always good to know the common principles and best software practices.
Many of these principles, like Dependency Injection and other SOLID principles, are difficult to master but are incredibly powerful in helping you create reusable and decoupled code.