Dependency Inject (Di) "Friendly" Library

Dependency Inject (DI) friendly library

This is actually simple to do once you understand that DI is about patterns and principles, not technology.

To design the API in a DI Container-agnostic way, follow these general principles:

Program to an interface, not an implementation

This principle is actually a quote (from memory though) from Design Patterns, but it should always be your real goal. DI is just a means to achieve that end.

Apply the Hollywood Principle

The Hollywood Principle in DI terms says: Don't call the DI Container, it'll call you.

Never directly ask for a dependency by calling a container from within your code. Ask for it implicitly by using Constructor Injection.

Use Constructor Injection

When you need a dependency, ask for it statically through the constructor:

public class Service : IService
{
private readonly ISomeDependency dep;

public Service(ISomeDependency dep)
{
if (dep == null)
{
throw new ArgumentNullException("dep");
}

this.dep = dep;
}

public ISomeDependency Dependency
{
get { return this.dep; }
}
}

Notice how the Service class guarantees its invariants. Once an instance is created, the dependency is guaranteed to be available because of the combination of the Guard Clause and the readonly keyword.

Use Abstract Factory if you need a short-lived object

Dependencies injected with Constructor Injection tend to be long-lived, but sometimes you need a short-lived object, or to construct the dependency based on a value known only at run-time.

See this for more information.

Compose only at the Last Responsible Moment

Keep objects decoupled until the very end. Normally, you can wait and wire everything up in the application's entry point. This is called the Composition Root.

More details here:

  • Where should I do Injection with Ninject 2+ (and how do I arrange my Modules?)
  • Design - Where should objects be registered when using Windsor

Simplify using a Facade

If you feel that the resulting API becomes too complex for novice users, you can always provide a few Facade classes that encapsulate common dependency combinations.

To provide a flexible Facade with a high degree of discoverability, you could consider providing Fluent Builders. Something like this:

public class MyFacade
{
private IMyDependency dep;

public MyFacade()
{
this.dep = new DefaultDependency();
}

public MyFacade WithDependency(IMyDependency dependency)
{
this.dep = dependency;
return this;
}

public Foo CreateFoo()
{
return new Foo(this.dep);
}
}

This would allow a user to create a default Foo by writing

var foo = new MyFacade().CreateFoo();

It would, however, be very discoverable that it's possible to supply a custom dependency, and you could write

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

If you imagine that the MyFacade class encapsulates a lot of different dependencies, I hope it's clear how it would provide proper defaults while still making extensibility discoverable.


FWIW, long after writing this answer, I expanded upon the concepts herein and wrote a longer blog post about DI-Friendly Libraries, and a companion post about DI-Friendly Frameworks.

Convenience methods for a DI-friendly library

You can use extension methods to allow users of your package to easily register specific services and their dependencies. The IServiceCollection interface will work across various DI containers. Here's a quick example from a small side project I did a while back. The example is an extension method that allows easy registration of a Azure Storage service.

public static class AzureBlobStorageServiceCollectionExtensions
{
public static IServiceCollection AddAzureBlobFileStorageService(this IServiceCollection services,
Action<AzureBlobStorageServiceConfigOptions> options)
{
var configOptions = new AzureBlobStorageServiceConfigOptions();
options(configOptions);

services.AddScoped<IFileStorageService>(sp =>
{
var logger = sp.GetRequiredService<ILogger<AzureBlobStorageService>>();
var connectionString = configOptions.ConnectionString ??
throw new ArgumentNullException(nameof(configOptions.ConnectionString));
var containerName = configOptions.ContainerName ??
throw new ArgumentNullException(nameof(configOptions.ContainerName));
return new AzureBlobStorageService(logger, connectionString, containerName);
});

return services;
}
}

You can then register the service in an application like this

services.AddAzureBlobFileStorageService(options =>
{
options.ConnectionString = Configuration["Storage:ConnectionString"];
options.ContainerName = Configuration["Storage:ContainerName"];
});

So it provides a simple way to register the service and all it's dependencies and configuration data. You can use the service normally by requesting the IFileStorageService.

Not sure if this is what you were asking about but hopefully it helps. You can look up Service Collection Extension methods in the docs and read more about the specifics.

Best practices for Inversion of Control in libraries?

You should not be using ServiceLocator or any other DI-framework-specific pieces in your core library. That couples your code to the specific DI framework, and makes it so anyone consuming your library must also add that DI framework as a dependency for their product. Instead, use constructor injection and the various techniques mentioned by Mark Seeman in his excellent answer here.

Then, to help users of your library to get bootstrapped more easily, you can provide separate DI-Container-specific libraries with some basic utility classes that handle most of the standard DI bindings you're expecting people to use, so in many cases they can get started with a single line of code.

The problem of missing dependencies until run-time is a common one when using DI frameworks. The only real way around it is to use "poor-man's DI", where you have a class that actually defines how each type of object gets constructed, so you get a compile-time error if a dependency is introduced that you haven't dealt with.

However, you can often mitigate the problem by checking things as early in the runtime as possible. For example, SimpleInjector has a Validate() method that you can call after all the bindings have been set up, and it'll throw an exception if it can tell that it won't know how to construct dependencies for any of the types that have been registered to it. I also like adding a simple integration test to my testing suite that sets up all the bindings and then tries to construct all of the top-level types in my application (Web API Controllers, e.g.), and fails if it can't do so.

Writing a library that is dependency-injection enabled

A good DI implementation should enable DI on any object, regardless of the latter being DI-agnostic or not.

Prism is a bad example, as the last time I used it (2 years ago) it required objects to be DI-agnostic by enforcing use of the [Injection] attribute. A good non-DI-agnostic example is Spring Framework (extremely popular DI framework for Java, has a .NET port called Spring.NET), which allows enabling DI via so-called context files - these are xml files that describe dependencies. The latter need not be part of your library, leaving it as a completely independent dll file.

The example of Spring can tell you that you should not have any specific configuration, prerequisites or patterns to follow in order to make an object injectable, or allow objects to be injected to it, besides the programming to interfaces paradigm, and allowing programmatic access to suitable constructors and property setters.

This does not mean that any DI framework should support manipulation of plain CLR (.NET) objects, a.k.a. POCO-s. Some frameworks rely only on their specific mechanisms and may not be suitable to use with DI-independent code. Usually, they would require direct dependency on the DI framework to the library, which I think you want to (and probably should) avoid.



Related Topics



Leave a reply



Submit