Containerless Dependency Injection in C#

Rasmus Schultz - Feb 21 '22 - - Dev Community

🧡 Dependency injection is at the heart of every good piece of software.

In most typical projects, dependency injection is implemented by configuring some sort of dependency injection (DI) container.

If you don't have a DI container available, if you don't understand how your DI container works, or if you just don't like the idea of having a complex facility for something that is ultimately pretty simple, in this article, I will demonstrate a really simple DI pattern that does not require any container library or framework.

In this example, we'll be bootstrapping a few simple mock components - to keep the discussion focused, these don't have any actual implementations, just constructor arguments.

class Connection
{
  public Connection(string connectionString)
  {
  }
}

class UserService
{
  public UserService(Connection c)
  {
  }
}

class WebRequest
{
  public WebRequest()
  {
  }
}

class ListUsersController
{
  public ListUsersController(UserService u, WebRequest r)
  {
  }
}
Enter fullscreen mode Exit fullscreen mode

So we have:

  • a Connection representing a database connection object,
  • a UserService that requires a Connection,
  • a WebRequest representing an incoming HTTP request (which would be generated by an HTTP server component of some sort) and
  • a ListUsersController which depends on the UserService and an incoming WebRequest.

Note that the Connection class in this example has a dependency on a connectionString - we will use this to illustrate the difference between dependencies on something from the external environment, versus dependencies within the internal application: an external dependency such as this has to come from somewhere outside the application, such as a configuration file or environment variable.

Contexts

Instances of our components have to be created and held somewhere, for the duration of their lifetime - personally, I like to refer to this "somewhere" as a context.

We could say, for example:

"a controller exists within the context of a web request"

For our example, we need two contexts:

  • An ApplicationContext for components that exist for the entire lifetime of the running application, and
  • A RequestContext for components that exist only within the context of a specific web-request.

So you can see why the distinction is important: while the application context is going to exist for as long as your application is running, request contexts are going to come and go with individual web requests, as they are received and routes to different controllers with different dependencies.

In practice, the implementation of a context is just a class with fields to hold the instances of our components - for each component we need:

  1. A private field to hold the component instance. To make sure we don't create our components before (or unless) they're needed, we store every component instance in a System.Lazy<Y> instance.
  2. An initialization in the constructor. This is where we define the factory functions for each of our components, by injecting an anonymous function (a closure) into the appropriate Lazy<T>.
  3. A public accessor. This is how we retrieve component instance from the context.

For the ApplicationContext, this looks like the following:

class ApplicationContext
{
  private Lazy<Connection> _connection;
  private Lazy<UserService> _userService;

  public ApplicationContext(string connectionString)
  {
    _connection = new(() => new Connection(connectionString));
    _userService = new(() => new UserService(Connection));
  }

  public Connection Connection { get => _connection.Value; }
  public UserService UserService { get => _userService.Value; }
}
Enter fullscreen mode Exit fullscreen mode

Just to give you a feel for the "rhythm" of this pattern, I included a public accessor for Connection - if a given component is only used internally within the context, exposing it via an accessor might not be necessary; you should decide what you need or want to expose.

Let's go ahead and define our RequestContext as well:

class RequestContext
{
  private Lazy<ListUsersController> _listUsersController;

  public RequestContext(ApplicationContext app, WebRequest request)
  {
    _listUsersController = new(() => new ListUsersController(app.UserService, request));
  }

  public ListUsersController ListUsersController { get => _listUsersController.Value; }
}
Enter fullscreen mode Exit fullscreen mode

Notice that a RequestContext requires an ApplicationContext. Recall how, in the beginning of the article, we described the connectionString required for an ApplicationContext as an "external dependency"?

In the same sense, the ApplicationContext is "external" to the RequestContext: from within the boundary of a RequestContext, an ApplicationContext is the external world - an environment with it's own distinct life-cycle.

As you can see, our model now consists of long-lived, shared components in one context - as well as more short-lived, request-specific components in a different context.

Finally, let's briefly illustrate how to put these together:

var app = new ApplicationContext("user=blah;password=meh");

var request = new WebRequest();

var requestContext = new RequestContext(app, request);

var controller = requestContext.ListUsersController;
Enter fullscreen mode Exit fullscreen mode

Now, in an automated test, you might actually write code in a linear fashion like this - and being able to wire up real dependencies for integration testing is certainly a benefit of this pattern.

In a real project, some kind of long-lived server component (in the application context) would provide a proper WebRequest for a handler of some sort, which would generate a RequestContext, apply some pattern matching, pick the right controller, and so on...

Building an actual application is outside the scope of this article - the point of this example is to illustrate the fact that components exist in different contexts because they have different life-cycles.

Here is a running playground for you to try out - I encourage you to play around with the dependencies, perhaps mock out an example with mock components from a project you're currently working on, and get a feel for it.

Conclusions

The beauty of this pattern, is that it scales beautifully to complex projects - while a dependency graph can get very large, this simple pattern allows you to deal with the creation of each component individually, while the larger graph of dependencies simply unfolds and resolves itself through the use of Lazy<T> and simple accessors.

The arguable downside of not using a DI container, is that you do need to manually define the creation of every component - personally, I don't find this pattern too verbose, and I actually like the fact that a breaking change to a constructor requires me to consider the impact on other components.

Lastly, someone asked me, what would happen if we accidentally created a circular dependency - would the program go into an infinite loop, terminating with a stack overflow? Well, see for yourself - but no, Lazy<T> actually throws an exception in this situation, which is good news. Arguably, the error message might not be as helpful as it might be with a DI container - but if you make this mistake, at least you won't be setting any servers on fire. 😆🔥

I hope this article gave you an idea of just how simple dependency injection can actually be. Happy coding! 🙋‍♂️

. . . . . . . . . . . . . . . . . .