Command Query Separation for Azure Functions ⚡ ✔️ Provides generic function support for commands and queries with HTTPTriggers ✔️ Enables APIs based on HTTP POST and GET 📄 https://hlaueriksson.me/CommandQuery.AzureFunctions/
$ dotnet add package CommandQuery.AzureFunctionsCommand Query Separation for Azure Functions
POST and GETCommandQuery.AzureFunctions package from NuGet
PM> Install-Package CommandQuery.AzureFunctionsCommand and QueryICommand and ICommandHandler<in TCommand>ICommand<TResult> and ICommandHandler<in TCommand, TResult>IQuery<TResult> and IQueryHandler<in TQuery, TResult>Program.cs

Choose:
using CommandQuery.AzureFunctions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
namespace CommandQuery.Sample.AzureFunctions
{
public class Command(ICommandFunction commandFunction)
{
[Function(nameof(Command))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "command/{commandName}")] HttpRequest req,
FunctionContext context,
string commandName) =>
await commandFunction.HandleAsync(commandName, req, context.CancellationToken);
}
}
POST with the Content-Type application/json in the header.200.400 or 500.Commands with result:
200.using CommandQuery.AzureFunctions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
namespace CommandQuery.Sample.AzureFunctions
{
public class Query(IQueryFunction queryFunction)
{
[Function(nameof(Query))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "query/{queryName}")] HttpRequest req,
FunctionContext context,
string queryName) =>
await queryFunction.HandleAsync(queryName, req, context.CancellationToken);
}
}
POST with the Content-Type application/json in the header and the query itself as JSON in the bodyGET and the query itself as query string parameters in the URL200.400 or 500.Configuration in Program.cs:
using CommandQuery;
using CommandQuery.AzureFunctions;
using CommandQuery.Sample.Contracts.Commands;
using CommandQuery.Sample.Contracts.Queries;
using CommandQuery.Sample.Handlers;
using CommandQuery.Sample.Handlers.Commands;
using CommandQuery.Sample.Handlers.Queries;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices(ConfigureServices)
.Build();
// Validation
host.Services.GetService<ICommandProcessor>()!.AssertConfigurationIsValid();
host.Services.GetService<IQueryProcessor>()!.AssertConfigurationIsValid();
host.Run();
public static partial class Program
{
public static void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
services
//.AddSingleton(new JsonSerializerOptions(JsonSerializerDefaults.Web));
// Add commands and queries
.AddCommandFunction(typeof(FooCommandHandler).Assembly, typeof(FooCommand).Assembly)
.AddQueryFunction(typeof(BarQueryHandler).Assembly, typeof(BarQuery).Assembly)
// Add handler dependencies
.AddTransient<IDateTimeProxy, DateTimeProxy>()
.AddTransient<ICultureService, CultureService>();
}
}
The extension methods AddCommandFunction and AddQueryFunction will add functions and all command/query handlers in the given assemblies to the IoC container.
You can pass in a params array of Assembly arguments if your handlers are located in different projects.
If you only have one project you can use typeof(Program).Assembly as a single argument.
using System.Text;
using System.Text.Json;
using CommandQuery.AzureFunctions;
using CommandQuery.Sample.Contracts.Commands;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using NUnit.Framework;
namespace CommandQuery.Sample.AzureFunctions.Tests
{
public class CommandTests
{
[SetUp]
public void SetUp()
{
var serviceCollection = new ServiceCollection();
Program.ConfigureServices(serviceCollection);
var serviceProvider = serviceCollection.BuildServiceProvider();
Subject = new Command(serviceProvider.GetRequiredService<ICommandFunction>());
var context = new Mock<FunctionContext>();
context.SetupProperty(c => c.InstanceServices, serviceProvider);
Context = context.Object;
}
[Test]
public async Task should_handle_command()
{
var result = await Subject.Run(GetRequest(new FooCommand { Value = "Foo" }), Context, "FooCommand");
result.As<IStatusCodeActionResult>().StatusCode.Should().Be(200);
}
[Test]
public async Task should_handle_errors()
{
var result = await Subject.Run(GetRequest(new FooCommand()), Context, "FooCommand");
result.ShouldBeError("Value cannot be null or empty");
}
HttpRequest GetRequest(object body)
{
var request = new Mock<HttpRequest>();
request.Setup(r => r.Body).Returns(new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(body))));
return request.Object;
}
Command Subject = null!;
FunctionContext Context = null!;
}
}