Command Query Separation for Google Cloud Functions ⚡ ✔️ Provides generic function support for commands and queries with HTTP functions ✔️ Enables APIs based on HTTP POST and GET 📄 https://hlaueriksson.me/CommandQuery.GoogleCloudFunctions/
$ dotnet add package CommandQuery.GoogleCloudFunctionsCommand Query Separation for Google Cloud Functions
POST and GETCommandQuery.GoogleCloudFunctions package from NuGet
PM> Install-Package CommandQuery.GoogleCloudFunctionsCommand and QueryICommand and ICommandHandler<in TCommand>ICommand<TResult> and ICommandHandler<in TCommand, TResult>IQuery<TResult> and IQueryHandler<in TQuery, TResult>Startup.csusing CommandQuery.GoogleCloudFunctions;
using Google.Cloud.Functions.Framework;
using Google.Cloud.Functions.Hosting;
using Microsoft.AspNetCore.Http;
namespace CommandQuery.Sample.GoogleCloudFunctions
{
[FunctionsStartup(typeof(Startup))]
public class Command(ICommandFunction commandFunction) : IHttpFunction
{
public async Task HandleAsync(HttpContext context)
{
var commandName = context.Request.Path.Value!.Substring("/api/command/".Length);
await commandFunction.HandleAsync(commandName, context, context.RequestAborted);
}
}
}
POST with the Content-Type application/json in the header.200.400 or 500.Commands with result:
200.using CommandQuery.GoogleCloudFunctions;
using Google.Cloud.Functions.Framework;
using Google.Cloud.Functions.Hosting;
using Microsoft.AspNetCore.Http;
namespace CommandQuery.Sample.GoogleCloudFunctions
{
[FunctionsStartup(typeof(Startup))]
public class Query(IQueryFunction queryFunction) : IHttpFunction
{
public async Task HandleAsync(HttpContext context)
{
var queryName = context.Request.Path.Value!.Substring("/api/query/".Length);
await queryFunction.HandleAsync(queryName, context, context.RequestAborted);
}
}
}
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 Startup.cs:
using CommandQuery.GoogleCloudFunctions;
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 Google.Cloud.Functions.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace CommandQuery.Sample.GoogleCloudFunctions
{
public class Startup : FunctionsStartup
{
public override void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) =>
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>();
public override void Configure(WebHostBuilderContext context, IApplicationBuilder app)
{
// Validation
app.ApplicationServices.GetService<ICommandProcessor>()!.AssertConfigurationIsValid();
app.ApplicationServices.GetService<IQueryProcessor>()!.AssertConfigurationIsValid();
}
}
}
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(Startup).Assembly as a single argument.
You can integration test your functions with the Google.Cloud.Functions.Testing package.
using System.Net;
using System.Net.Http.Json;
using CommandQuery.Sample.Contracts.Commands;
using FluentAssertions;
using Google.Cloud.Functions.Testing;
using NUnit.Framework;
namespace CommandQuery.Sample.GoogleCloudFunctions.Tests
{
public class CommandTests
{
[SetUp]
public void SetUp()
{
Server = new FunctionTestServer<Command>();
Client = Server.CreateClient();
}
[TearDown]
public void TearDown()
{
Client.Dispose();
Server.Dispose();
}
[Test]
public async Task should_handle_command()
{
var response = await Client.PostAsJsonAsync("/api/command/FooCommand", new FooCommand { Value = "Foo" });
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Test]
public async Task should_handle_errors()
{
var response = await Client.PostAsJsonAsync("/api/command/FooCommand", new FooCommand());
await response.ShouldBeErrorAsync("Value cannot be null or empty");
}
FunctionTestServer<Command> Server = null!;
HttpClient Client = null!;
}
}