Skip to content

Latest commit

 

History

History
187 lines (154 loc) · 7.22 KB

CommandQuery.GoogleCloudFunctions.md

File metadata and controls

187 lines (154 loc) · 7.22 KB

CommandQuery.GoogleCloudFunctions ⚡

build CodeFactor

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

Get Started

  1. Install Google.Cloud.Functions.Templates
  2. Create a new gcf-http project
  3. Install the CommandQuery.GoogleCloudFunctions package from NuGet
    • PM> Install-Package CommandQuery.GoogleCloudFunctions
  4. Create functions
    • Preferably named Command and Query
  5. Create commands and command handlers
    • Implement ICommand and ICommandHandler<in TCommand>
    • Or ICommand<TResult> and ICommandHandler<in TCommand, TResult>
  6. Create queries and query handlers
    • Implement IQuery<TResult> and IQueryHandler<in TQuery, TResult>
  7. Configure services in Startup.cs

Commands

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 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);
        }
    }
}
  • The function is requested via HTTP POST with the Content-Type application/json in the header.
  • The name of the command is the slug of the URL.
  • The command itself is provided as JSON in the body.
  • If the command succeeds; the response is empty with the HTTP status code 200.
  • If the command fails; the response is an error message with the HTTP status code 400 or 500.

Commands with result:

  • If the command succeeds; the response is the result as JSON with the HTTP status code 200.

Queries

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);
        }
    }
}
  • The function is requested via:
    • HTTP POST with the Content-Type application/json in the header and the query itself as JSON in the body
    • HTTP GET and the query itself as query string parameters in the URL
  • The name of the query is the slug of the URL.
  • If the query succeeds; the response is the result as JSON with the HTTP status code 200.
  • If the query fails; the response is an error message with the HTTP status code 400 or 500.

Configuration

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.

Testing

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!;
    }
}

Samples