The scope of this article is to showcase the possibilities of the Command pattern applied for web APIs, in order to abstract infrastructural aspects of the hosting platform while allowing the development team to focus on extending the API with new functionality without the need to carry platform specifics during the development of the API. In order to make sure the implementation is platform agnostic, both Azure Functions and .Net Minimal API are used to host the same API code, with minimal differences in terms of setup.
The complete source code is available in my git repository with examples for Azure functions and Minimal Api.
In one of my earlier articles I already discussed the importance and perspectives of applying the Command pattern as an abstraction model for business flows and how it allows extending the system with cross cutting concerts. This article focuses more on implementation details rather than the high level architectural perspective.
Objectives
Let’s define some clear and perhaps ambitious objectives for this proof of concept:
1. Be able to build extensible APIs without repetitive infrastructure components just by defining commands and command handlers.
2. The same solution should work in different hosting platforms without changing the business domain.
3. The API must expose an Open API definition for every endpoint (swagger) in order to make the API discoverabe.
4. Both azure and local environments should use the same infrastructure implementations.
5. Implementation for cross cutting concerns such as logging, exception handling authorization should be realized without mixing with the business domain.
The above objectives are key aspects to design a flexible and scalable solution which can be understood without deep understanding of the underlying infrastructure, where application infrastructure and business domain is highly decoupled allowing them to evolve at a different pace during the lifecycle of the project.
Now let’s take the steps needed to achieve the above goals, starting with the first one.
Command and command handlers
Commands, besides representing a domain model, also contain in their definition an authorization resource in the form of a string which is understood by the authorization service. Commands always have only one handler and one or more command validators. Special commands are queries which don’t require validation, as they don’t apply any state change (Http Get).
Below is an example of commands and command handlers:
public class CreateItemCommand : ICommand
{
public string Permission => "Demo.Items.Create";
public Item Item { get; set; }
}
public class GetItemsByNameQuery : IQuery
{
public string Permission => "Demo.Items.Read";
public string Name { get; set; }
}
public class ItemCommandHandlers :
ICommandHandler<CreateItemCommand, CreateItemCommandResponse>,
ICommandHandler<UpdateItemCommand>,
ICommandHandler<DeleteItemCommand>
{
public Task<CreateItemCommandResponse> Handle(CreateItemCommand command)
{
//create item here
return Task.FromResult(new CreateItemCommandResponse { ItemId = Guid.NewGuid() });
}
...............
}
public class ItemQueryHandlers : IQueryHandler<GetItemsByNameQuery, List<Item>>
{
public Task<List<Item>> Handle(GetItemsByNameQuery query)
{
return Task.FromResult(new List<Item> { new Item { Id = Guid.NewGuid(), Name = "N1" }, new Item { Id = Guid.NewGuid(), Name = "N2" } });
}
..................
}
Our goal is to build APIs of any dimension just defining the command and their command handlers and expose them as Http endpoints without further configuration or additional code.
Towards our goals, the next step is to implement the command dispatcher.
Command dispatcher
The command dispatcher is responsible to resolve and execute command validators and command handlers for a given command or query, in my case using the service locator pattern.
Below is a fragment of the command dispatcher’s implementation, complete code is in the CommandDispatcher class.
[Dependency(typeof(ICommandDispatcher))]
public class CommandDispatcher : ICommandDispatcher
{
private readonly IServiceProvider _serviceProvider;
public CommandDispatcher(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task<CommandResponse> DispatchCommand<TCommand, TResponse>(TCommand command) where TCommand : ICommand
{
var validationErrors = await ValidateCommand(command);
if (validationErrors.Any())
return new CommandResponse { IsValid = false, ValidationErrors = validationErrors };
var commandHandler = _serviceProvider.GetRequiredService<ICommandHandler<TCommand,TResponse>>();
return new CommandResponse<TResponse> { Response = await commandHandler.Handle(command) };
}
}
Above at line 13 the DispatchCommand method validates the command by using data annotation validator or ICommandValidators<> if any defined. In case the validation passes, the dispatcher resolves the command handler (line 18) for the given command and returns its execution result (line 20).
As the next step we have to make sure whenever a command handler or validator is implemented it is registered in the IOC container, so the dispatcher can resolve those.
Convention based registration of handlers
In order to automatically register all command handlers and validators, the following code fragment has to be executed in the application startup, both for Azure Functions and Minimal API, in order to register the handlers in the ServiceCollection.
For more details, check my article about convention based dependency resolution.
builder.Services.AddConventionalDependencies("TNArch.Microservices",
c => c.WithConfiguration(builder.Configuration)
.ImplementsOpenGenericInterface(typeof(ICommandHandler<>))
.ImplementsOpenGenericInterface(typeof(ICommandHandler<,>))
.ImplementsOpenGenericInterface(typeof(IQueryHandler<,>))
.ImplementsOpenGenericInterface(typeof(ICommandValidator<>))
.DecoratedWith<DependencyAttribute>()
.DecoratedWith<DecorateDependencyAttribute>()
.DecoratedWith<ReplaceDependencyAttribute>()
.DecoratedWithConfigurationDescriptor()
.UseCommandAsApiEndpoint("Minimal Api Demo Microservice"));
Above the lines 3-6 are the conventions that register command handlers, query handlers and command validators in the IOC container. You can find complete code in following links for Azure Functions and Minimal API.
Now that we have a command dispatcher, we have to make sure all the requests that are made to our API are mapped to a command/query and are calling the command dispatcher.
Mapping requests to commands
In order to map commands and queries to Http requests, we need to implement a conventions which maps Http verbs and request urls to commands and queries. In my case the are the following:
- Query names are mapped to endpoint names except the Command/Query suffixes.
- Queries are always mapping to http Get.
- Commands are mapped to HttpVerbs based on the prefix of their names, driven by the domain language used in the project. I use a dictionary for this mapping for simplicity:
new Dictionary<string, OperationType>()
{
{ "Create", OperationType.Post },
{ "Add", OperationType.Post },
{ "Update", OperationType.Put },
{ "Change", OperationType.Put },
{ "Edit", OperationType.Put },
{ "Delete", OperationType.Delete },
{ "Remove", OperationType.Delete },
{ "Deactivate", OperationType.Delete },
};
Now that we have a conceptual definition of our request mapping let’s see the implementation.
In order to improve the performance of the request mapping the solution builds a collection of request map objects in a singleton service called CommandToApiMapper used to identify commands and queries based on the Url of the Http request and the Http verb, but it will also serve the generation of Open API json using Swagger in a later stage.
Here is the code snippet for creating the request map:
private HandlerRequestMap GetHandlerRequestMap(Type serviceType)
{
var genericArguments = serviceType.GetGenericArguments();
var requestType = genericArguments[0];
var isQuery = typeof(IQuery).IsAssignableFrom(requestType);
var hasReturnType = genericArguments.Length > 1;
var responseType = typeof(CommandResponse);
var dispatcherInvoker = typeof(ICommandDispatcher).GetGenericMethod(nameof(ICommandDispatcher.DispatchCommand), genericArguments);
if (isQuery)
{
dispatcherInvoker = typeof(ICommandDispatcher).GetGenericMethod(nameof(ICommandDispatcher.DispatchQuery), genericArguments);
responseType = typeof(QueryResult<>).MakeGenericType(genericArguments.Last());
}
else if (hasReturnType)
responseType = typeof(CommandResponse<>).MakeGenericType(genericArguments.Last());
return new HandlerRequestMap
{
RequestName = GetRequestName(requestType),
OperationType = GetOperationType(requestType),
HandlerType = serviceType,
RequestType = requestType,
ResponseType = responseType,
DispatcherInvoker = dispatcherInvoker,
};
}
private string GetRequestName(Type requestType)
{
var requestName = requestType.Name;
if (requestName.EndsWith("Query"))
requestName = requestName.Replace("Query", string.Empty);
if (requestName.EndsWith("Command"))
requestName = requestName.Replace("Command", string.Empty);
return char.ToLowerInvariant(requestName[0]) + requestName[1..];
}
The GetHandlerRequestMap method is executed for all the command and query handlers present in the referenced assemblies, and creates a HandlerRequestMap object for each handler with the details needed for invoking the Command Dispatcher and generating the Open API document.
In order to make all our command/query handlers to respond to requests we have to invoke the command dispatcher from our function (Azure Function) or route map (Minimal Api)
Invoking the command dispatcher
We already have A command dispatcher and conventions to map requests to command, we only have to bridge the gap in between with a generic implementation called CommandDispatcherInvoker
[Dependency(typeof(ICommandDispatcherInvoker))]
public async Task<object> DispatchRequest(string commandName, HttpRequest req)
{
var handlerMap = _operationToApiMapper.GetHandlerMap(commandName, req.Method);
object requestObject;
if (req.QueryString.HasValue)
requestObject = JsonSerializer.Deserialize(req.Query.QueryStringToJson(), handlerMap.RequestType, _options);
else
requestObject = await JsonSerializer.DeserializeAsync(req.Body, handlerMap.RequestType, _options);
using var scope = _serviceProvider.CreateScope();
scope.ServiceProvider.GetRequiredService<IIdentityService>().SetHttpContext(req);
var commandDispatcher = scope.ServiceProvider.GetRequiredService<ICommandDispatcher>();
return await handlerMap.DispatcherInvoker.InvokeAsync(commandDispatcher, requestObject);
}
With all the above in place we only need to define a function or a route (minimal ApI) to handle all the requests to our API:
//Minimal API
var app = builder.Build();
app.Map("/api/{command}", async (string command, ICommandDispatcherInvoker dispatcherInvocer, HttpRequest req) => Results.Json(await dispatcherInvocer.DispatchRequest(command, req)));
app.Run();
//Azure function
[FunctionName(nameof(RequestReceived))]
public async Task<IActionResult> RequestReceived([HttpTrigger(AuthorizationLevel.Function, "get", "post", "put", "delete", Route = "{*commandName}")] HttpRequest req, string commandName)
{
return new JsonResult(await _invoker.DispatchRequest(commandName, req));
}
Let’s see our first query handler in action
public class GetItemsByNameQuery : IQuery
{
public string Permission => "Demo.Items.Read";
public string Name { get; set; }
}
public class ItemQueryHandlers : IQueryHandler<GetItemsByNameQuery, List<Item>>
{
public Task<List<Item>> Handle(GetItemsByNameQuery query)
{
var items = new[] { new Item { Id = Guid.NewGuid(), Name = "N1" }, new Item { Id = Guid.NewGuid(), Name = "N2" } };
return Task.FromResult(items.Where(i=> i.Name == query.Name).ToList());
}
}
For many use cases this might be enough, but in order for the API to be discoverable an Open API definition should also be generated which then can be used to import in Azure API Management, Postman or generate client code.
Last but not least, we should have some cross cutting concerns implemented to showcase how the Command pattern combined with the Decorator pattern can decouple the application infrastructure and business domain. In the second part of this sequel I’ll address the remaining objectives.