ASP.NET Core automatic type registration

A little bit of syntactic sugar for you this Friday!

Let’s say we have an application that uses a command pattern to keep the controllers slim. Maybe we have a base command class that looks a bit like:

public abstract class CommandBase<TModel> where TModel : class
{
  protected CommandBase(MyDbContext db)
  {
    Db = db;
  }

  protected MyDbContext Db { get; }

  Task<CommandResult> ExecuteAsync(TModel model);
}

Using a pattern like this means that we can have very slim controller actions where the logic is moved into business objects:

public async Task<IActionResult> Post(
  [FromServices] MyCommand command,
  [FromBody] MyCommandModel model)
{
  if (!ModelState.IsValid)
    return BadRequest(ModelState);
  var result = await command.ExecuteAsync(model);
  return HandleResultSomehow(result);
}

We could slim this down further using a validation filter, but this is good enough for now. Note that we’re injecting our command model in the action parameters, which makes our actions very easy to test if we want to.

The problem here is that, unless we register all of our command classes with DI, this won’t work, and you’ll see an `Unable to resolve service for type` error. Registering the types is easy, but it’s also easy to forget to do, and leads to a bloated startup class. Instead, we can ensure that any commands which are named appropriately are automatically added to our DI pipeline by writing an extension method:

public static void AddAllCommands(this IServiceCollection services)
{
  const string NamespacePrefix = "Example.App.Commands";
  const string NameSuffix = "Command";

  var commandTypes = typeof(Startup)
    .Assembly
    .GetTypes()
    .Where(t =>
      t.IsClass &&
      t.Namespace?.StartsWith(NamespacePrefix, StringComparison.OrdinalIgnoreCase) == true &&
      t.Name?.EndsWith(NameSuffix, StringComparison.OrdinalIgnoreCase) == true);

  foreach (var type in commandTypes)
  {
    services.AddTransient(type);
  }
}

Using this, we can use naming conventions to ensure that all of our command classes are automatically registered and made available to our controllers.

2 thoughts on “ASP.NET Core automatic type registration

  1. A great article.

    I use the following to register interfaces and their implementations (i.e. Dependency Inversion):

    internal static class ServiceCollectionExtensions
    {
    private static readonly string Prefix = typeof(ServiceCollectionExtensions).Namespace.Split(‘.’)[0]; // Uppermost namespace

    private static IEnumerable Mappings { get; }

    static ServiceCollectionExtensions()
    {
    var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase));
    var exportedTypes = assemblies.SelectMany(a => a.GetExportedTypes());

    Mappings = exportedTypes.Where(IsClass).Select(InterfacesForClass).SelectMany(t => t);
    }

    public static void AddFromDomain(this IServiceCollection services)
    {
    foreach ((Type Service, Type Implementation) mapping in Mappings)
    {
    services.AddTransient(mapping.Service, mapping.Implementation);
    }
    }

    private static bool IsClass(Type type)
    {
    return type.IsClass
    && !(
    type.IsAbstract
    || type.IsGenericType
    || type.IsSubclassOf(typeof(Attribute))
    || type.GetInterfaces().Length == 0
    );
    }

    private static IEnumerable InterfacesForClass(Type type)
    {
    return from i in type.GetInterfaces()
    let name = type.Name
    where i.Name == “I” + name // name prefixed with I, eg IUnitOfWork matches UnitOfWork
    || i.IsGenericType // the interface is generic; and
    && name.StartsWith(i.GetGenericArguments()[0].Name) // the name begins with the interface’s generic parameter type name; and
    && name.EndsWith(Regex.Replace(i.Name, @”(^I)|(`\d$)”, string.Empty)) // the name ends with the interface’s name
    // eg IRepository matches UserRepository
    select (i, type);
    }
    }

    Usage:

    public void ConfigureServices(IServiceCollection services)
    {
    services.AddFromDomain();
    }

Leave a reply to stevenbey Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.