Skip to content

Latest commit

History

History
306 lines (258 loc) 路 10.3 KB

CommandQuery.AWSLambda.md

File metadata and controls

306 lines (258 loc) 路 10.3 KB

CommandQuery.AWSLambda 鈿

build CodeFactor

Command Query Separation for AWS Lambda

  • Provides generic function support for commands and queries with Amazon API Gateway
  • Enables APIs based on HTTP POST and GET

Get Started

  1. Install AWS Toolkit for Visual Studio
  2. Create a new AWS Serverless Application (.NET Core) project
  3. Install the CommandQuery.AWSLambda package from NuGet
    • PM> Install-Package CommandQuery.AWSLambda
  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 the serverless template

New Project - AWS Serverless Application (.NET Core - C#)

New AWS Serverless Application - Empty Serverless Application

Choose:

  • Empty Serverless Application

Commands

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using CommandQuery.AWSLambda;
using CommandQuery.Sample.Contracts.Commands;
using CommandQuery.Sample.Handlers;
using CommandQuery.Sample.Handlers.Commands;
using Microsoft.Extensions.DependencyInjection;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace CommandQuery.Sample.AWSLambda;

public class Command
{
    private readonly ICommandFunction _commandFunction;

    public Command()
    {
        var services = new ServiceCollection();
        //services.AddSingleton(new JsonSerializerOptions(JsonSerializerDefaults.Web));
        services.AddCommandFunction(typeof(FooCommandHandler).Assembly, typeof(FooCommand).Assembly);
        // Add handler dependencies
        services.AddTransient<ICultureService, CultureService>();

        var serviceProvider = services.BuildServiceProvider();
        serviceProvider.GetService<ICommandProcessor>().AssertConfigurationIsValid(); // Validation
        _commandFunction = serviceProvider.GetService<ICommandFunction>();
    }

    public async Task<APIGatewayProxyResponse> Handle(APIGatewayProxyRequest request, ILambdaContext context) =>
        await _commandFunction.HandleAsync(request.PathParameters["commandName"], request, context.Logger);
}
  • 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 Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using CommandQuery.AWSLambda;
using CommandQuery.Sample.Contracts.Queries;
using CommandQuery.Sample.Handlers;
using CommandQuery.Sample.Handlers.Queries;
using Microsoft.Extensions.DependencyInjection;

namespace CommandQuery.Sample.AWSLambda
{
    public class Query
    {
        private readonly IQueryFunction _queryFunction;

        public Query()
        {
            var services = new ServiceCollection();
            //services.AddSingleton(new JsonSerializerOptions(JsonSerializerDefaults.Web));
            services.AddQueryFunction(typeof(BarQueryHandler).Assembly, typeof(BarQuery).Assembly);
            // Add handler dependencies
            services.AddTransient<IDateTimeProxy, DateTimeProxy>();

            var serviceProvider = services.BuildServiceProvider();
            serviceProvider.GetService<IQueryProcessor>().AssertConfigurationIsValid(); // Validation
            _queryFunction = serviceProvider.GetService<IQueryFunction>();
        }

        public async Task<APIGatewayProxyResponse> Handle(APIGatewayProxyRequest request, ILambdaContext context) =>
            await _queryFunction.HandleAsync(request.PathParameters["queryName"], request, context.Logger);
    }
}
  • 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 serverless.template:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Transform": "AWS::Serverless-2016-10-31",
  "Description": "An AWS Serverless Application.",
  "Resources": {
    "Command": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Architectures": [
          "x86_64"
        ],
        "Handler": "CommandQuery.Sample.AWSLambda::CommandQuery.Sample.AWSLambda.Command::Handle",
        "Runtime": "dotnet6",
        "CodeUri": "",
        "MemorySize": 256,
        "Timeout": 30,
        "Role": null,
        "Policies": [
          "AWSLambdaBasicExecutionRole"
        ],
        "Events": {
          "PostResource": {
            "Type": "Api",
            "Properties": {
              "Path": "/command/{commandName}",
              "Method": "POST"
            }
          }
        }
      }
    },
    "Query": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Architectures": [
          "x86_64"
        ],
        "Handler": "CommandQuery.Sample.AWSLambda::CommandQuery.Sample.AWSLambda.Query::Handle",
        "Runtime": "dotnet6",
        "CodeUri": "",
        "MemorySize": 256,
        "Timeout": 30,
        "Role": null,
        "Policies": [
          "AWSLambdaBasicExecutionRole"
        ],
        "Events": {
          "GetResource": {
            "Type": "Api",
            "Properties": {
              "Path": "/query/{queryName}",
              "Method": "GET"
            }
          },
          "PostResource": {
            "Type": "Api",
            "Properties": {
              "Path": "/query/{queryName}",
              "Method": "POST"
            }
          }
        }
      }
    }
  },
  "Outputs": {
    "ApiURL": {
      "Description": "API endpoint URL for Prod environment",
      "Value": {
        "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
      }
    }
  }
}

Testing

using System.Collections.Generic;
using System.Threading.Tasks;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Amazon.Lambda.TestUtilities;
using CommandQuery.Sample.Contracts.Queries;
using FluentAssertions;
using NUnit.Framework;

namespace CommandQuery.Sample.AWSLambda.Tests
{
    public class QueryTests
    {
        public class when_using_the_real_function_via_Post
        {
            [SetUp]
            public void SetUp()
            {
                Subject = new Query();
                Request = GetRequest("POST", content: "{ \"Id\": 1 }");
                Context = new TestLambdaContext();
            }

            [Test]
            public async Task should_work()
            {
                var result = await Subject.Handle(Request.QueryName("BarQuery"), Context);
                var value = result.As<Bar>();

                value.Id.Should().Be(1);
                value.Value.Should().NotBeEmpty();
            }

            [Test]
            public async Task should_handle_errors()
            {
                var result = await Subject.Handle(Request.QueryName("FailQuery"), Context);

                result.ShouldBeError("The query type 'FailQuery' could not be found");
            }

            Query Subject;
            APIGatewayProxyRequest Request;
            ILambdaContext Context;
        }

        public class when_using_the_real_function_via_Get
        {
            [SetUp]
            public void SetUp()
            {
                Subject = new Query();
                Request = GetRequest("GET", query: new Dictionary<string, IList<string>> { { "Id", new List<string> { "1" } } });
                Context = new TestLambdaContext();
            }

            [Test]
            public async Task should_work()
            {
                var result = await Subject.Handle(Request.QueryName("BarQuery"), Context);
                var value = result.As<Bar>();

                value.Id.Should().Be(1);
                value.Value.Should().NotBeEmpty();
            }

            [Test]
            public async Task should_handle_errors()
            {
                var result = await Subject.Handle(Request.QueryName("FailQuery"), Context);

                result.ShouldBeError("The query type 'FailQuery' could not be found");
            }

            Query Subject;
            APIGatewayProxyRequest Request;
            ILambdaContext Context;
        }

        static APIGatewayProxyRequest GetRequest(string method, string content = null, Dictionary<string, IList<string>> query = null)
        {
            var request = new APIGatewayProxyRequest
            {
                HttpMethod = method,
                Body = content,
                MultiValueQueryStringParameters = query
            };

            return request;
        }
    }
}

Samples