Making Your Library Friendly for Dependency Injection in C#

There's an agnostic method to make it easier for DI users without making DI itself mandatory.

Making Your Library Friendly for Dependency Injection in C#

I love making C# class libraries and distributing them, both via Github as an open source repo and on NuGet as an easy to use, readily available package. As the complexity of a library increases, the complexity of using said library can also increase. The end user might be utilizing dependency injection (or DI) to keep this complexity in check for their own application.

One pitfall with dependency injection when using external libraries is, the user must also register all the classes they plan to use. Some libraries allow the user to register all types in a particular assembly, but this can lead to problems when multiple implementations for the same interface exist. This also makes it the user's responsibility to know how those types are supposed to be instantiated.

Why not just depend on a DI library in your own library? For one, it limits the user to using the same DI library. Another drawback is the need to include the DI library in the user's binary distribution, just to use your library in the first place. If everybody did this, we'd have an explosion of dependencies.

The user might not even be using DI. It's not good practice to force this on them. Surely, there's a better and agnostic method.

DI Infrastructure

All it takes to make a library DI friendly is a few classes that facilitate the process of finding out how services map to interfaces.

Patterns

The following strategy assumes a particular pattern is being used for the library:

  • Each service only has other service interfaces in the constructor
  • Each interface is implemented only by a single service, and interfaces are the primary method of access by the user
  • In cases where either of the above two conditions can't be met, a factory is created which accepts the necessary parameters, and is responsible for creating the services necessary

Runtime dependencies (command line args, config files) are best wrapped into services of their own or passed in as arguments to functions, not used as constructor arguments when we use the following strategy. In the end, these aren't concrete rules; use your best judgement above all else.

Programmatically Identify Services

We need some way to easily identify types in our library without introducing a whole lot of boilerplate. Attributes are an excellent way to do just that. I start by creating ServiceAttribute, which will adorn all the services I plan for the user to use.

// ServiceAttribute.cs

using System;

namespace MyProject
{
    /// <summary>
    ///     Marks a particular class as a service.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    internal class ServiceAttribute : Attribute
    {
        public ServiceAttribute(bool singleInstance = true)
        {
            SingleInstance = singleInstance;
        }

        public bool SingleInstance { get; }
    }
}

I like to default to using single instance services because it makes me think more data driven. There isn't any mechanism to enforce these services to only have a single instance - that's just a suggestion to the user.

Let the User Know in a Meaningful Way

We need some way to get this information back to the user. I create a ServiceMapping structure that contains all the pertinent information.

// ServiceMapping.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace MyProject
{
    /// <summary>
    /// A type map for a single service.
    /// </summary>
    public struct ServiceMapping
    {
        internal ServiceMapping(Type implementation, IEnumerable<Type> services, bool singleInstance)
        {
            Services = services.ToArray();
            Implementation = implementation;
            SingleInstance = singleInstance;

            if (Services.Any(s => !s.IsInterface))
                throw new Exception($"Services must all be interfaces. (Implementation: {Implementation})");
        }

        /// <summary>
        /// Implementation of a service.
        /// </summary>
        public Type Implementation { get; }

        /// <summary>
        /// Interfaces that a service should implement. These should all be interfaces.
        /// </summary>
        public Type[] Services { get; }
        
        /// <summary>
        /// If true, this service should only have a single instance in the container.
        /// </summary>
        public bool SingleInstance { get; }
    }
}

Along with this, I also create a static type catalog. This is what will actually do the work of finding out what services implement what interfaces. This is the single place the user will find everything their DI library will need.

// ServiceTypes.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace MyProject
{
    /// <summary>
    /// Dependency locator for this library.
    /// </summary>
    public static class ServiceTypes
    {
        /// <summary>
        /// Get the complete type map for this library.
        /// </summary>
        public static IEnumerable<ServiceMapping> GetMappings()
        {
            return typeof(ServiceTypes)
                .Assembly
                .ExportedTypes
                .Where(t => t.IsClass && !t.IsAbstract && t.GetCustomAttributes<ServiceAttribute>().Any())
                .Select(t => new ServiceMapping(
                    t,
                    t.GetInterfaces().Where(i => i != typeof(IDisposable)),
                    t.GetCustomAttributes<ServiceAttribute>().First().SingleInstance));
        }
    }
}

IDisposable is excluded as an interface because it has special meaning within .NET. Many DI libraries will automatically call Dispose() when the root container is disposed. We don't want to cause issues by attempting to implement that interface with a service.

Using the Type Catalog

Once the infrastructure above is in place, services are marked with the [Service] attribute, and have appropriate interfaces, everything is in place for the user to use the specified service.

With AutoFac

The following can be used to register types with a ContainerBuilder.

foreach (var mapping in ServiceTypes.GetMappings())
{
	var registration = builder.RegisterType(mapping.Implementation);
	foreach (var service in mapping.Services)
		registration.As(service);
	if (mapping.SingleInstance)
		registration.SingleInstance();
	else
		registration.InstancePerDependency();
}

Conclusion

DI is a great pattern for large or complex projects. I've used several different libraries throughout the course of personal and professional programming.

This strategy can be found in the upcoming RhythmCodex, and will be found in many other future projects.