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.