on
6-minute read
Dependency Injection Code Smell: Injecting runtime data into components
Injecting runtime data into your application components is a code smell. Runtime data should flow through the method calls of already-constructed object graphs.
A recurring theme when it comes to questions about dependency injection is how to wire up and resolve components a.k.a. injectables (the classes that contain the application’s behavior) that require runtime data during construction. My answer to this is always the same:
Here’s an example of a MoveCustomerCommand
component that gets constructed with runtime data—the CustomerId
and DestinationAddress
.
interface ICommand
{
void Execute();
}
class MoveCustomerCommand : ICommand
{
private readonly ICustomerRepository repository;
public MoveCustomerCommand(ICustomerRepository repository)
{
this.repository = repository;
}
public int CustomerId { get; set; } Runtime data
public Address DestinationAddress { get; set; } Runtime data
public void Execute()
{
Use repository, CustomerId and Address to handle the operation
}
}
In the code snippet, the construction of the component requires both the ICustomerRepository
dependency in its constructor and the runtime data values for the customer ID and address through its public fields. The runtime values are specific to one particular request.
This implementation is problematic because you need request-specific information to correctly initialize this component. To be able to create a new MoveCustomerCommand
, the consuming code must either create the component itself, delegate its creation to a factory, or call back into the container passing the runtime data—all of which cause problems of their own:
- Creating the component in code is a Dependency Inversion Principle violation and makes it impossible to decorate, intercept or replace the component without making sweeping changes throughout the code base.
- A factory will add a pointless extra layer of abstraction to the application, increasing complexity and decreasing maintainability. Complexity is increased because the consumer now has to deal with an extra abstraction (the factory). Maintainability is decreased, because for each component, a factory method must be created and maintained that will handwire the component with its dependencies.
- Calling back into the container directly leads to the Service Locator anti-pattern.
Both the factory and Service Locator approach cause the creation of this part of the object graph to be delayed until runtime. Although delaying the creation of the object graph until runtime isn’t a bad thing per se, it makes it harder to verify your configuration because resolving the root object will only test some of the object graph.
The solution to these issues is actually quite simple: remove the injection of runtime data out of the construction phase of the component and pass it on using method calls after construction. Not surprisingly, the following design solves these problems:
interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
class MoveCustomer Runtime data — no behavior
{
public int CustomerId { get; set; }
public Address DestinationAddress { get; set; }
}
class MoveCustomerHandler : ICommandHandler<MoveCustomer>
{
private readonly ICustomerRepository repository;
public MoveCustomerHandler(ICustomerRepository repository)
{
this.repository = repository;
}
public void Handle(MoveCustomer command)Accepts runtime data
{
Use repository and command to handle the operation
}
}
The command has now become a behaviorless Parameter Object that can be passed on to the new command handler component. This change solves the problems with the original design:
- The creation of object graphs can now be verified with a single automated test.
- No callbacks to a Service Locator are needed.
- No factory is needed; code can depend directly on
ICommandHandler<MoveCustomer>
. - Creation of the object graph is not needlessly delayed until runtime.
The general fix here is to change the public API to expose the runtime data through its contract so that the request-specific information can be passed through. This allows the component to become stateless.
But not all violations can be solved like this. Sometimes you don’t want to change the public API of your abstractions, especially when the runtime data is an implementation detail. To visualize this point let’s take a look at the following example:
class CustomerRepository : ICustomerRepository
{
private readonly IUnitOfWork uow;
private readonly int currentUserId;
private readonly DateTime now;
public CustomerRepository( Constructor injected with runtime data
IUnitOfWork uow,
int currentUserId, Runtime data
DateTime now) Runtime data
{
if (currentUserId <= 0) throw new ArgumentException();
if (now.Year < 2015) throw new ArgumentException();
this.uow = uow;
this.currentUserId = currentUserId;
this.now = now;
}
public void Save(Customer entity)
{
entity.CreatedBy = this.currentUserId;
entity.CreatedOn = this.now;
this.uow.Save(entity);
}
}
The example shows a CustomerRepository
that in addition to depending on an IUnitOfWork
, also requires the current user id and the current system time. The current user id is the Id
of the logged in user on whose behalf the operation is executed. This Id
and current time are both used to update the Customer
entity before it is persisted to the database.
Just as in the previous example, this use of runtime data is problematic. In this component there is some ambiguity in the constructor because when examining the type, it is unclear what is needed to inject. What DateTime
value should be injected? Should it be the Now
, Today
, yesterday? In other words, it would be very easy to create the CustomerRepository
with incorrect values, and the only way to verify whether the configuration is correct is through manual testing or a rather awkward integration test.
In this example, however, you don’t want to make the runtime data into input parameters of the CustomerRepository
's Save
method because that would mean the Save
method gets two extra parameters. The addition of these parameters to the Save
method will ripple through the system because the direct and indirect consumers of the ICustomerRepository
abstraction will need to add these parameters to their API as well—all the way up the chain. Not only would this pollute the API, it would also force you to make sweeping changes throughout the code base for each and every piece of runtime data that some implementation requires in the future.
When a component requires runtime state in its constructor, it becomes impossible to verify the configuration in a maintainable way. A unit test must be written for each component that verifies whether that particular object can be created, while supplied with fake—but valid—runtime data needed for the component to initialize.
The current user id and current time are runtime values but they are implementation details and consumers of the repository should not be concerned with such details. You should place these runtime values behind clearly defined abstractions, removing the ambiguity in their definition and allowing the runtime data to flow through the system with the method calls, as shown in the following listing:
class CustomerRepository : ICustomerRepository
{
private readonly IUnitOfWork uow;
private readonly IUserContext userContext;
private readonly ITimeProvider timeProvider;
public CustomerRepository(
IUnitOfWork uow,
IUserContext userContext, Runtime data hidden behind abstractions
ITimeProvider timeProvider) Abstraction
{
this.uow = uow;
this.userContext = userContext;
this.timeProvider = timeProvider;
}
public void Save(Customer entity)
{
entity.CreatedBy = this.userContext.CurrentUserId; Using abstraction
entity.CreatedOn = this.timeProvider.Now; Using abstraction
this.uow.Save(entity);
}
}
Creating implementations for the two newly defined abstractions could be as simple as the following:
class TimeProvider : ITimeProvider
{
public DateTime Now => DateTime.Now;
}
class HttpSessionUserContext : IUserContext
{
public int CurrentUserId => (int)HttpContext.Current.Session["userId"];
}
These two implementations are adapters; they adapt your application-specific abstractions to a specific technology, tool, or system component that you wish to hide from your application components. These adapters are part of the Composition Root.
Do note though that primitive values (such as int
and string
) are not runtime data per definition. Configuration values, such as connection strings, are primitives, but they are usually known at application startup, and don’t change during the lifetime of the application. Those ‘static’ values can safely be injected into the constructor. Still, if you find yourself injecting the same configuration value into many different components you are missing an abstraction, but that’s a discussion for another day.
To summarize, the solution to the problem of injecting runtime data into components is to let runtime data flow through method calls on an initialized object graph by either:
- passing runtime data through method calls of the API or
- retrieving runtime data from specific abstractions that allow resolving runtime data.
Happy injecting.
Comments
Dennis van der Stelt - 09 November 15
Great article! Welcome back to blogging! :)
Jan Hartmann - 09 November 15
As Dennis stated; long awaited blog post from you and as always, on point. :-)
Hoping to see more in the future.
Nazaret - 17 November 15
Awesome! Welcome back! :)
Yacoub Massad - 22 November 15
Great article. But I think there is something that is still missing. Consider the example of a UI application that allows people to type in an FTP server and some other connectivity settings at runtime, and then connects to such server to do some work (like allowing the user to upload or download files). Wouldn’t you in this case create an abstract factory that creates some IFtpServer
implementation given the connection settings? Consuming classes of IFtpServer
wouldn’t want to know anything about the connection settings, so you can’t put these settings as method parameters. Also you cannot obtain such information from some context interface as you did with the ITimeProvider
for example. So it seems that the need for abstract factories still exists for some cases
Steven - 14 August 16
Yacoub,
In my latest article I go into more details about why I think Abstract Factories are a design smell and should be avoided in most cases.
Luke Briner - 15 February 18
Thanks so much for this. Helping me to understand some of the more subtle issues with DI and in a very concise way while not skipping the “why not to do it this way”!
Wish to comment?
Found a typo?
Buy my book
Dependency Injection Principles, Practices, and Patterns. If you're interested to learn more about DI and software design in general, consider reading my book. Besides English, the book is available in Chinese, Italian, Polish, Russian, and Japanese.
I coauthored the book