Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0606668

Browse files
committedJul 28, 2021
📚
1 parent e1ed611 commit 0606668

11 files changed

+728
-543
lines changed
 

‎CommandQuery.AWSLambda.md

Lines changed: 131 additions & 120 deletions
Large diffs are not rendered by default.

‎CommandQuery.AspNetCore.md

Lines changed: 116 additions & 136 deletions
Large diffs are not rendered by default.

‎CommandQuery.AzureFunctions.md

Lines changed: 138 additions & 188 deletions
Large diffs are not rendered by default.

‎CommandQuery.Client.md

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,36 @@
1-
# CommandQuery.Client
1+
### CommandQuery.Client 🧰
22

3-
> Clients for CommandQuery
4-
5-
## Installation
6-
7-
| NuGet | | [![CommandQuery.Client][1]][2] |
8-
| :--------------- | ----: | :------------------------------------------------------------------- |
9-
| Package Manager | `PM>` | `Install-Package CommandQuery.Client -Version 1.0.0` |
10-
| .NET CLI | `>` | `dotnet add package CommandQuery.Client --version 1.0.0` |
11-
| PackageReference | | `<PackageReference Include="CommandQuery.Client" Version="1.0.0" />` |
12-
| Paket CLI | `>` | `paket add CommandQuery.Client --version 1.0.0` |
13-
14-
[1]: https://img.shields.io/nuget/v/CommandQuery.Client.svg?label=CommandQuery.Client
15-
[2]: https://www.nuget.org/packages/CommandQuery.Client
16-
17-
## Sample Code
3+
[![build](https://github.com/hlaueriksson/CommandQuery/actions/workflows/build.yml/badge.svg)](https://github.com/hlaueriksson/CommandQuery/actions/workflows/build.yml) [![CodeFactor](https://codefactor.io/repository/github/hlaueriksson/commandquery/badge)](https://codefactor.io/repository/github/hlaueriksson/commandquery)
184

19-
[`CommandQuery.Sample.Client`](/samples/CommandQuery.Sample.Client)
5+
> Clients for CommandQuery
206
21-
## Commands
7+
#### Commands
228

23-
Create a `CommandClient` and invoke `Post`:
9+
Create a `CommandClient` and invoke `PostAsync`:
2410

25-
```csharp
26-
var commandClient = new CommandClient("https://commandquery-sample-azurefunctions-vs2.azurewebsites.net/api/command/");
11+
```cs
12+
var commandClient = new CommandClient("https://commandquery-sample-aspnetcore-v5.azurewebsites.net/api/command/");
2713

28-
commandClient.Post(new FooCommand { Value = "sv-SE" });
2914
await commandClient.PostAsync(new FooCommand { Value = "en-GB" });
3015
```
3116

3217
Commands with result:
3318

34-
```csharp
35-
var result = commandClient.Post(new BazCommand { Value = "sv-SE" });
36-
result = await commandClient.PostAsync(new BazCommand { Value = "en-GB" });
19+
```cs
20+
var result = await commandClient.PostAsync(new BazCommand { Value = "en-GB" });
3721
```
3822

39-
## Queries
40-
41-
Create a `QueryClient` and invoke `Post` or `Get`:
23+
#### Queries
4224

43-
```csharp
44-
var queryClient = new QueryClient("https://commandquery-sample-azurefunctions-vs2.azurewebsites.net/api/query/");
25+
Create a `QueryClient` and invoke `PostAsync` or `GetAsync`:
4526

46-
var result = queryClient.Post(new BarQuery { Id = 1 });
47-
result = await queryClient.PostAsync(new BarQuery { Id = 1 });
27+
```cs
28+
var queryClient = new QueryClient("https://commandquery-sample-aspnetcore-v5.azurewebsites.net/api/query/");
4829

49-
result = queryClient.Get(new BarQuery { Id = 1 });
30+
var result = await queryClient.PostAsync(new BarQuery { Id = 1 });
5031
result = await queryClient.GetAsync(new BarQuery { Id = 1 });
5132
```
33+
34+
#### Samples
35+
36+
* [CommandQuery.Sample.Client](https://github.com/hlaueriksson/CommandQuery/tree/master/samples/CommandQuery.Sample.Client)

‎CommandQuery.GoogleCloudFunctions.md

Lines changed: 279 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,279 @@
1-
# CommandQuery.GoogleCloudFunctions
1+
### CommandQuery.GoogleCloudFunctions ⚡
2+
3+
[![build](https://github.com/hlaueriksson/CommandQuery/actions/workflows/build.yml/badge.svg)](https://github.com/hlaueriksson/CommandQuery/actions/workflows/build.yml) [![CodeFactor](https://codefactor.io/repository/github/hlaueriksson/commandquery/badge)](https://codefactor.io/repository/github/hlaueriksson/commandquery)
4+
5+
> Command Query Separation for Google Cloud Functions
6+
7+
* Provides generic function support for commands and queries with *HTTP functions*
8+
* Enables APIs based on HTTP `POST` and `GET`
9+
10+
#### Get Started
11+
12+
0. Install **Google.Cloud.Functions.Templates**
13+
* [https://www.nuget.org/packages/Google.Cloud.Functions.Templates/](https://www.nuget.org/packages/Google.Cloud.Functions.Templates/)
14+
1. Create a new **gcf-http** project
15+
* [Tutorial](https://github.com/GoogleCloudPlatform/functions-framework-dotnet#quickstarts)
16+
2. Install the `CommandQuery.GoogleCloudFunctions` package from [NuGet](https://www.nuget.org/packages/CommandQuery.GoogleCloudFunctions)
17+
* `PM>` `Install-Package CommandQuery.GoogleCloudFunctions`
18+
3. Create functions
19+
* Preferably named `Command` and `Query`
20+
4. Create commands and command handlers
21+
* Implement `ICommand` and `ICommandHandler<in TCommand>`
22+
* Or `ICommand<TResult>` and `ICommandHandler<in TCommand, TResult>`
23+
5. Create queries and query handlers
24+
* Implement `IQuery<TResult>` and `IQueryHandler<in TQuery, TResult>`
25+
6. Configure services in `Startup.cs`
26+
27+
#### Commands
28+
29+
```cs
30+
using System.Threading.Tasks;
31+
using CommandQuery.GoogleCloudFunctions;
32+
using Google.Cloud.Functions.Framework;
33+
using Google.Cloud.Functions.Hosting;
34+
using Microsoft.AspNetCore.Http;
35+
using Microsoft.Extensions.Logging;
36+
37+
namespace CommandQuery.Sample.GoogleCloudFunctions
38+
{
39+
[FunctionsStartup(typeof(Startup))]
40+
public class Command : IHttpFunction
41+
{
42+
private readonly ILogger _logger;
43+
private readonly ICommandFunction _commandFunction;
44+
45+
public Command(ILogger<Command> logger, ICommandFunction commandFunction)
46+
{
47+
_logger = logger;
48+
_commandFunction = commandFunction;
49+
}
50+
51+
public async Task HandleAsync(HttpContext context)
52+
{
53+
var commandName = context.Request.Path.Value.Substring("/api/command/".Length);
54+
55+
await _commandFunction.HandleAsync(commandName, context, _logger, context.RequestAborted);
56+
}
57+
}
58+
}
59+
```
60+
61+
* The function is requested via HTTP `POST` with the Content-Type `application/json` in the header.
62+
* The name of the command is the slug of the URL.
63+
* The command itself is provided as JSON in the body.
64+
* If the command succeeds; the response is empty with the HTTP status code `200`.
65+
* If the command fails; the response is an error message with the HTTP status code `400` or `500`.
66+
67+
Commands with result:
68+
69+
* If the command succeeds; the response is the result as JSON with the HTTP status code `200`.
70+
71+
#### Queries
72+
73+
```cs
74+
using System.Threading.Tasks;
75+
using CommandQuery.GoogleCloudFunctions;
76+
using Google.Cloud.Functions.Framework;
77+
using Google.Cloud.Functions.Hosting;
78+
using Microsoft.AspNetCore.Http;
79+
using Microsoft.Extensions.Logging;
80+
81+
namespace CommandQuery.Sample.GoogleCloudFunctions
82+
{
83+
[FunctionsStartup(typeof(Startup))]
84+
public class Query : IHttpFunction
85+
{
86+
private readonly ILogger _logger;
87+
private readonly IQueryFunction _queryFunction;
88+
89+
public Query(ILogger<Query> logger, IQueryFunction queryFunction)
90+
{
91+
_logger = logger;
92+
_queryFunction = queryFunction;
93+
}
94+
95+
public async Task HandleAsync(HttpContext context)
96+
{
97+
var queryName = context.Request.Path.Value.Substring("/api/query/".Length);
98+
99+
await _queryFunction.HandleAsync(queryName, context, _logger, context.RequestAborted);
100+
}
101+
}
102+
}
103+
```
104+
105+
* The function is requested via:
106+
* HTTP `POST` with the Content-Type `application/json` in the header and the query itself as JSON in the body
107+
* HTTP `GET` and the query itself as query string parameters in the URL
108+
* The name of the query is the slug of the URL.
109+
* If the query succeeds; the response is the result as JSON with the HTTP status code `200`.
110+
* If the query fails; the response is an error message with the HTTP status code `400` or `500`.
111+
112+
#### Configuration
113+
114+
Configuration in `Startup.cs`:
115+
116+
```cs
117+
using System.Text.Json;
118+
using CommandQuery.GoogleCloudFunctions;
119+
using CommandQuery.Sample.Contracts.Commands;
120+
using CommandQuery.Sample.Contracts.Queries;
121+
using CommandQuery.Sample.Handlers;
122+
using CommandQuery.Sample.Handlers.Commands;
123+
using CommandQuery.Sample.Handlers.Queries;
124+
using Google.Cloud.Functions.Hosting;
125+
using Microsoft.AspNetCore.Builder;
126+
using Microsoft.AspNetCore.Hosting;
127+
using Microsoft.Extensions.DependencyInjection;
128+
129+
namespace CommandQuery.Sample.GoogleCloudFunctions
130+
{
131+
public class Startup : FunctionsStartup
132+
{
133+
public override void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) =>
134+
services
135+
//.AddSingleton(new JsonSerializerOptions(JsonSerializerDefaults.Web))
136+
137+
// Add commands and queries
138+
.AddCommandFunction(typeof(FooCommandHandler).Assembly, typeof(FooCommand).Assembly)
139+
.AddQueryFunction(typeof(BarQueryHandler).Assembly, typeof(BarQuery).Assembly)
140+
141+
// Add handler dependencies
142+
.AddTransient<IDateTimeProxy, DateTimeProxy>()
143+
.AddTransient<ICultureService, CultureService>();
144+
145+
public override void Configure(WebHostBuilderContext context, IApplicationBuilder app)
146+
{
147+
// Validation
148+
app.ApplicationServices.GetService<ICommandProcessor>().AssertConfigurationIsValid();
149+
app.ApplicationServices.GetService<IQueryProcessor>().AssertConfigurationIsValid();
150+
}
151+
}
152+
}
153+
```
154+
155+
The extension methods `AddCommandFunction` and `AddQueryFunction` will add functions and all command/query handlers in the given assemblies to the IoC container.
156+
You can pass in a `params` array of `Assembly` arguments if your handlers are located in different projects.
157+
If you only have one project you can use `typeof(Startup).Assembly` as a single argument.
158+
159+
#### Testing
160+
161+
```cs
162+
using System.Collections.Generic;
163+
using System.IO;
164+
using System.Text;
165+
using System.Threading.Tasks;
166+
using CommandQuery.GoogleCloudFunctions;
167+
using CommandQuery.Sample.Contracts.Queries;
168+
using FluentAssertions;
169+
using Microsoft.AspNetCore.Http;
170+
using Microsoft.AspNetCore.WebUtilities;
171+
using Microsoft.Extensions.DependencyInjection;
172+
using NUnit.Framework;
173+
174+
namespace CommandQuery.Sample.GoogleCloudFunctions.Tests
175+
{
176+
public class QueryTests
177+
{
178+
public class when_using_the_real_function_via_Post
179+
{
180+
[SetUp]
181+
public void SetUp()
182+
{
183+
var serviceCollection = new ServiceCollection();
184+
new Startup().ConfigureServices(null, serviceCollection);
185+
var serviceProvider = serviceCollection.BuildServiceProvider();
186+
187+
Subject = new Query(null, serviceProvider.GetService<IQueryFunction>());
188+
}
189+
190+
[Test]
191+
public async Task should_work()
192+
{
193+
var context = GetHttpContext("BarQuery", "POST", content: "{ \"Id\": 1 }");
194+
195+
await Subject.HandleAsync(context);
196+
var value = await context.Response.AsAsync<Bar>();
197+
198+
value.Id.Should().Be(1);
199+
value.Value.Should().NotBeEmpty();
200+
}
201+
202+
[Test]
203+
public async Task should_handle_errors()
204+
{
205+
var context = GetHttpContext("FailQuery", "POST", content: "{ \"Id\": 1 }");
206+
207+
await Subject.HandleAsync(context);
208+
209+
await context.Response.ShouldBeErrorAsync("The query type 'FailQuery' could not be found");
210+
}
211+
212+
Query Subject;
213+
}
214+
215+
public class when_using_the_real_function_via_Get
216+
{
217+
[SetUp]
218+
public void SetUp()
219+
{
220+
var serviceCollection = new ServiceCollection();
221+
new Startup().ConfigureServices(null, serviceCollection);
222+
var serviceProvider = serviceCollection.BuildServiceProvider();
223+
224+
Subject = new Query(null, serviceProvider.GetService<IQueryFunction>());
225+
}
226+
227+
[Test]
228+
public async Task should_work()
229+
{
230+
var context = GetHttpContext("BarQuery", "GET", query: new Dictionary<string, string> { { "Id", "1" } });
231+
232+
await Subject.HandleAsync(context);
233+
var value = await context.Response.AsAsync<Bar>();
234+
235+
value.Id.Should().Be(1);
236+
value.Value.Should().NotBeEmpty();
237+
}
238+
239+
[Test]
240+
public async Task should_handle_errors()
241+
{
242+
var context = GetHttpContext("FailQuery", "GET", query: new Dictionary<string, string> { { "Id", "1" } });
243+
244+
await Subject.HandleAsync(context);
245+
246+
await context.Response.ShouldBeErrorAsync("The query type 'FailQuery' could not be found");
247+
}
248+
249+
Query Subject;
250+
}
251+
252+
static HttpContext GetHttpContext(string queryName, string method, string content = null, Dictionary<string, string> query = null)
253+
{
254+
var context = new DefaultHttpContext();
255+
context.Request.Path = new PathString("/api/query/" + queryName);
256+
context.Request.Method = method;
257+
258+
if (content != null)
259+
{
260+
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(content));
261+
}
262+
263+
if (query != null)
264+
{
265+
context.Request.QueryString = new QueryString(QueryHelpers.AddQueryString("", query));
266+
}
267+
268+
context.Response.Body = new MemoryStream();
269+
270+
return context;
271+
}
272+
}
273+
}
274+
```
275+
276+
#### Samples
277+
278+
* [CommandQuery.Sample.GoogleCloudFunctions](https://github.com/hlaueriksson/CommandQuery/tree/master/samples/CommandQuery.Sample.GoogleCloudFunctions)
279+
* [CommandQuery.Sample.GoogleCloudFunctions.Tests](https://github.com/hlaueriksson/CommandQuery/tree/master/samples/CommandQuery.Sample.GoogleCloudFunctions.Tests)
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Please sign in to comment.