Updated 23rd January 2019:
The GitHub repository is here and the NuGet package is here
This is still pretty raw and needs developing further. Contributors welcome!

Introduction

I'm working on a pretty large Asp.Net Core web application and a while back I got fed up with maintaining all the service registrations in dependency injection. So, if you have more than just a few lines of services.AddScoped<ISomething, Something>() this post may be of interest to you.
I figured there had to be a nicer approach so I took a look at an attribute based approach to register my services. I'm really happy with what I ended up with, so it's time to share!

Usage

I'll start by showing how I'd like to use the attribute based registration, then I'll show how it's implemeted. For the purposes of this post I'm going to show only the Asp.Net Core web hosting usage, but this can easily be applied to the generic hosting model as well. If you want to know about that, let me know in the comments.

Extension Method

I want it to be easy to add my auto registration into my application, so I'll need to create an extension method for IWebHostBuilder to do that. Here's what I want my Program.cs to look like:

    public class Program {

        public static void Main(string[] args) {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args)
            => WebHost
                .CreateDefaultBuilder(args)
                .AutoRegisterServices()
                .UseStartup<Startup>();
    }

Notice the addition of .AutoRegisterServices() to the standard template. That's where the magic will happen.

Attributes

Here's what the attribute usage should look like:

Registering with no interface

This is the simplest case. This will add MyClass into dependency injection without using an interface, which is equivalent to services.AddScoped<MyClass>().

[AutoRegister(ServiceLifetime.Scoped)]
public class MyClass {
}

Registering with an interface

Next I want to add a class that implements an interface, but I don't want to have to specify the interface in my attribute. The auto registration should be able to detect the interface and use it. This is equivalent to services.AddScoped<IMyClass, MyClass>().

public interface IMyClass {
}

[AutoRegister(ServiceLifetime.Scoped)]
public class MyClass: IMyClass {
}

Registering multiple classes with an interface

For my purposes, I want to have to explicitly state that multiple registrations of the same interface is OK. So I have an additional attribute here that I can use on the interface for this purposes. In my implementaion I throw an error if this is missing and more than one registration for the same interface is attempted.

[AutoRegisterUsage(AllowMultiple = true)]
public interface IMyClass {
}

[AutoRegister(ServiceLifetime.Scoped)]
public class MyClass: IMyClass {
}

[AutoRegister(ServiceLifetime.Scoped)]
public class MyOtherClass: IMyClass {
}

This would be equivalent to the following lines:

services.AddScoped<IMyClass, MyClass>();
services.AddScoped<IMyClass, MyOtherClass>();

Handling multiple interfaces

I want to handle classes that implement multiple interfaces. Take the following for example:

public interface IServiceA {
}

public interface IServiceB {
}

[AutoRegister(ServiceLifetime.Scoped)]
public class MyService : IServiceA, IServiceB {
}

This would be the equivalent of the following lines:

services.AddScoped<IServiceA, MyService>();
services.AddScoped<IServiceB, MyService>();

Now, that may not be what you need. What if you want to share the same instance of MyService for IServiceA and IServiceB? I may come back to that depending on how long this post gets :)

Specifying interfaces to register

When handling classes that implement multiple interfaces, sometimes I may wish to only add certain interfaces into dependency injection:

public interface IServiceA {
}

public interface IServiceB {
}

[AutoRegister(ServiceLifetime.Scoped, typeof(IServiceA))]
public class MyService : IServiceA, IServiceB {
}

This would be the equivalent of just services.AddScoped<IServiceA, MyService>().

Implementation

I've been through a few iterations of the implementation for this over several projects and it seems to get simpler each time.

Extension Methods

IWebHostBuilder extension method

public static IWebHostBuilder AutoRegisterServices(this IWebHostBuilder webHostBuilder)
    => AutoRegisterServices(
        webHostBuilder,
        AppDomain.CurrentDomain.GetAssemblies()
        );

public static IWebHostBuilder AutoRegisterServices(this IWebHostBuilder webHostBuilder, params Assembly[] assemblies) {

    return webHostBuilder.ConfigureServices((context, services) => {
        services.AutoRegisterServices(assemblies);
    });

}

The first method here will use all the assemblies in the curren AppDomain when searching for classes decorated with the AutoRegister attribute. If you wanted to restrict the assemblies to look in, then you could use the second method and supply specific assemblies. But remember, this only runs once at start up so it has a very minimal impact to scan everything in the AppDomain.

IServiceCollection extension method

The code above simply passes through to another extension method on the IServiceCollection interface. This is to make it easy to support the generic host model by keeping the code on the IWebHostBuilder/IHostBuidler as light as possible.

First up, the extension method used above:

public static void AutoRegisterServices(this IServiceCollection services, Assembly[] assemblies) {

    var items = GetTypesToRegister(assemblies);

    foreach (var item in items)
        services.RegisterType(item.typeInfo, item.attribute);

}

To get the types that should be registered I just scan for the custom attribute using reflection:

public static IEnumerable<(TypeInfo typeInfo, AutoRegisterAttribute attribute)> GetTypesToRegister(Assembly[] assemblies)
    => from assembly in assemblies
       from type in assembly.DefinedTypes
       let attribute = type.GetCustomAttribute<AutoRegisterAttribute>()
       where attribute != null
       select (type, attribute);

This returns a tuple of the decorated type and the attribute itself.
Next is the actual registering of the types in DI. For brevity I have omitted the code that checks for multiple registrations of the same service type.

public static void RegisterType(this IServiceCollection services, TypeInfo implementationType, AutoRegisterAttribute attribute) {

    var interfaces = GetInterfaces(implementationType, attribute);

    foreach (var serviceType in interfaces)
        services.Add(new ServiceDescriptor(
            serviceType,
            implementationType,
            attribute.ServiceLifetime
            ));

}

public static Type[] GetInterfaces(TypeInfo implementationType, AutoRegisterAttribute attribute) {

    var allInterfaces = implementationType.GetInterfaces();

    return allInterfaces.Count() == 0
        ? new[] { implementationType }
        : allInterfaces
        ;

}

In GetInterfaces(), if the implementation type does not implement any interfaces, then I can use the implementation type as the service type.
There's not much more to it than that.

Conclusion

This approach has massively improved the way that I work with large applications, so I hope it may prove useful to others.
I'm in the process of putting the source for this in GitHub and also creating a NuGet package containing all the functionality. I'll update this post once that's all done.