diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 0cf7b57..cd92afb 100755
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,10 +1,7 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0.100-jammy
+FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy
# Install make
-RUN apt update && apt install -y make
+# RUN apt-get update &&
+# RUN apt-get update && apt-get install -y make
-## https://github.com/dotnet/aspire/blob/main/docs/using-latest-daily.md
-RUN dotnet workload update --skip-sign-check --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json \
- && dotnet workload install aspire --skip-sign-check --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json
-
-# RUN dotnet workload install aspire
+RUN dotnet workload install aspire
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index d3a250f..410124e 100755
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -17,6 +17,8 @@
"ghcr.io/devcontainers/features/azure-cli:1": {},
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {},
"ghcr.io/rio/features/k3d:1": {},
+ "ghcr.io/azure/azure-dev/azd:0": { "version": "latest" },
+ "ghcr.io/prom3theu5/aspirational-manifests/aspirate:latest": {},
"ghcr.io/devcontainers/features/common-utils:2": {
"configureZshAsDefaultShell": true
}
@@ -37,6 +39,7 @@
"ms-dotnettools.csdevkit",
"ms-azuretools.vscode-docker",
"dunn.redis",
+ "ms-azuretools.vscode-bicep",
"GitHub.copilot"
]
}
diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh
index c754d99..244021a 100755
--- a/.devcontainer/post-create.sh
+++ b/.devcontainer/post-create.sh
@@ -5,10 +5,21 @@ while (! kubectl cluster-info ); do
# Docker takes a few seconds to initialize
echo "Waiting for Docker to launch..."
k3d cluster delete
- k3d cluster create -p '8081:80@loadbalancer' --k3s-arg '--disable=traefik@server:0'
+ k3d registry create myregistry.localhost --port 12345
+ k3d cluster create -p '8081:80@loadbalancer' --k3s-arg '--disable=traefik@server:0' --registry-use k3d-myregistry.localhost:12345
sleep 1
done
+# docker pull docker.io/library/postgres:16.2
+# docker pull docker.io/library/rabbitmq:3.13-management
+# docker pull docker.io/library/redis:7.2
+
+# docker tag docker.io/library/postgres:16.2 k3d-myregistry.localhost:12345/postgres:16.2
+# docker tag docker.io/library/rabbitmq:3.13-management k3d-myregistry.localhost:12345/rabbitmq:3.13-management
+# docker tag docker.io/library/redis:7.2 k3d-myregistry.localhost:12345/redis:7.2
+
+# docker tag product-api k3d-myregistry.localhost:12345/product-api:latest
+
## dotnet
dotnet restore
diff --git a/.editorconfig b/.editorconfig
index 4a9952c..276e6b0 100755
--- a/.editorconfig
+++ b/.editorconfig
@@ -2,7 +2,7 @@ root = true
# All files
[*]
-indent_style = space
+indent_style = tab
# Xml files
[*.xml]
@@ -170,6 +170,23 @@ csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
+csharp_style_namespace_declarations = block_scoped:silent
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
+csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
+csharp_style_prefer_extended_property_pattern = true:suggestion
#### Naming styles ####
[*.{cs,vb}]
@@ -361,4 +378,11 @@ dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case
+tab_width = 4
+indent_size = 4
+end_of_line = crlf
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_style_allow_multiple_blank_lines_experimental = true:silent
+dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
diff --git a/.github/workflows/dotnet-code-coverage.yaml b/.github/workflows/dotnet-code-coverage.yaml
new file mode 100644
index 0000000..377c8ea
--- /dev/null
+++ b/.github/workflows/dotnet-code-coverage.yaml
@@ -0,0 +1,46 @@
+name: .NET Coverage
+
+on:
+ push:
+ branches:
+ - main
+ - feat/*
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Install .NET Aspire workload
+ run: dotnet workload install aspire
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore
+
+ - name: Test
+ run: dotnet test --no-build --settings tests.runsettings --results-directory ./coverage
+
+ - name: Publish coverage
+ uses: irongut/CodeCoverageSummary@v1.3.0
+ with:
+ filename: coverage/**/coverage.cobertura.xml
+ badge: true
+ format: markdown
+
+ - name: Add Coverage PR Comment
+ uses: marocchino/sticky-pull-request-comment@v2
+ if: github.event_name == 'pull_request'
+ with:
+ recreate: true
+ path: code-coverage-results.md
diff --git a/.gitignore b/.gitignore
index 5069b04..59c2d03 100755
--- a/.gitignore
+++ b/.gitignore
@@ -350,4 +350,5 @@ MigrationBackup/
.ionide/
.idea/
tmp/
-.env
\ No newline at end of file
+.env
+coverage/
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index fce9cd1..cf78083 100755
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,45 +1,77 @@
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ true
+ true
+ 8.0.5
+ 8.4.0
+ 8.0.4
+ 8.0.1
+ 0.0.4
+ 8.0.1
+ 8.1.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NuGet.config b/NuGet.config
index 54af6e8..0e585fb 100755
--- a/NuGet.config
+++ b/NuGet.config
@@ -2,7 +2,6 @@
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 03bc0d8..0e7f420 100755
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
The coffeeshop apps on .NET Aspire
+![Counter API-Code Coverage](https://img.shields.io/badge/Code%20Coverage-73%25-yellow?style=flat)
+
## Prerequisites
If you run on `Windows 11`:
@@ -19,6 +21,21 @@ If you run on `Windows 11`:
# http://localhost:5019
```
+## Generate manifest file (powershell)
+
+```sh
+dotnet run --project app-host\CoffeeShop.AppHost.csproj `
+ -- `
+ --publisher manifest `
+ --output-path ../aspire-manifest.json
+```
+
+## Deploy to Kubernetes
+
+```sh
+dotnet tool install -g aspirate --prerelease
+```
+
## Run with Justfile (cross-platform)
```sh
@@ -34,3 +51,8 @@ On Windows 11 - WSL2 Ubuntu 22 integrated, we can use `Podman Desktop` to replac
> make run
# http://localhost:5019
```
+
+```sh
+dotnet publish "/workspaces/coffeeshop-aspire/app-host/../product-api/CoffeeShop.ProductApi.csproj" -p:PublishProfile="DefaultContainer" -p:PublishSingleFile="true"
+-p:PublishTrimmed="false" --self-contained "true" --verbosity "quiet" --nologo -r "linux-x64" -p:ContainerRegistry="k3d-myregistry.localhost:12345" -p:ContainerRepository="product-api" -p:ContainerImageTag="latest"
+```
diff --git a/app-host/CoffeeShop.AppHost.csproj b/app-host/CoffeeShop.AppHost.csproj
new file mode 100644
index 0000000..1a988c0
--- /dev/null
+++ b/app-host/CoffeeShop.AppHost.csproj
@@ -0,0 +1,37 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app-host/HealthCheckExtensions.cs b/app-host/HealthCheckExtensions.cs
new file mode 100644
index 0000000..fd0bc88
--- /dev/null
+++ b/app-host/HealthCheckExtensions.cs
@@ -0,0 +1,65 @@
+using Aspirant.Hosting;
+
+using HealthChecks.NpgSql;
+using HealthChecks.RabbitMQ;
+using HealthChecks.Redis;
+using HealthChecks.Uris;
+
+namespace CoffeeShop.AppHost;
+
+///
+/// Ref: https://github.com/davidfowl/WaitForDependenciesAspire/tree/main/WaitForDependencies.Aspire.Hosting
+///
+public static class Extensions
+{
+ public static IResourceBuilder WithHealthCheck(this IResourceBuilder builder)
+ {
+ return builder.WithAnnotation(HealthCheckAnnotation.Create(cs => new RabbitMQHealthCheck(new RabbitMQHealthCheckOptions { ConnectionUri = new(cs) })));
+ }
+
+ public static IResourceBuilder WithHealthCheck(this IResourceBuilder builder)
+ {
+ return builder.WithAnnotation(HealthCheckAnnotation.Create(cs => new RedisHealthCheck(cs)));
+ }
+
+ public static IResourceBuilder WithHealthCheck(this IResourceBuilder builder)
+ {
+ return builder.WithAnnotation(HealthCheckAnnotation.Create(cs => new NpgSqlHealthCheck(new NpgSqlHealthCheckOptions(cs))));
+ }
+
+ public static IResourceBuilder WithHealthCheck(
+ this IResourceBuilder builder,
+ string? endpointName = null,
+ string path = "health",
+ Action? configure = null)
+ where T : IResourceWithEndpoints
+ {
+ return builder.WithAnnotation(new HealthCheckAnnotation(async (resource, ct) =>
+ {
+ if (resource is not IResourceWithEndpoints resourceWithEndpoints)
+ {
+ return null;
+ }
+
+ var endpoint = endpointName is null
+ ? resourceWithEndpoints.GetEndpoints().FirstOrDefault(e => e.Scheme is "http" or "https")
+ : resourceWithEndpoints.GetEndpoint(endpointName);
+
+ var url = endpoint?.Url;
+
+ if (url is null)
+ {
+ return null;
+ }
+
+ var options = new UriHealthCheckOptions();
+
+ options.AddUri(new(new(url), path));
+
+ configure?.Invoke(options);
+
+ var client = new HttpClient();
+ return new UriHealthCheck(options, () => client);
+ }));
+ }
+}
\ No newline at end of file
diff --git a/app-host/Program.cs b/app-host/Program.cs
index f67b5e3..6be3f8d 100755
--- a/app-host/Program.cs
+++ b/app-host/Program.cs
@@ -1,20 +1,47 @@
+using Aspirant.Hosting;
+
+using CoffeeShop.AppHost;
+
var builder = DistributedApplication.CreateBuilder(args);
-var rabbitmq = builder.AddRabbitMQContainer("rabbitmq");
+var postgresQL = builder.AddPostgres("postgresQL").WithHealthCheck().WithPgAdmin();
+var postgres = postgresQL.AddDatabase("postgres");
+
+var redis = builder.AddRedis("redis").WithHealthCheck();
+var rabbitmq = builder.AddRabbitMQ("rabbitmq").WithHealthCheck().WithManagementPlugin();
+
+var productApi = builder.AddProject("product-api")
+ .WithSwaggerUI();
+
+var counterApi = builder.AddProject("counter-api")
+ .WithReference(productApi)
+ .WithReference(rabbitmq)
+ .WaitFor(rabbitmq)
+ .WithSwaggerUI();
+
+builder.AddProject("barista-api")
+ .WithReference(rabbitmq)
+ .WaitFor(rabbitmq);
-var productApi = builder.AddProject("productapi")
- .WithReplicas(2);
+builder.AddProject("kitchen-api")
+ .WithReference(rabbitmq)
+ .WaitFor(rabbitmq);
-builder.AddProject("counterapi")
- .WithReference(productApi)
- .WithReference(rabbitmq);
+var orderSummaryApi = builder.AddProject("order-summary")
+ .WithReference(postgres)
+ .WithReference(rabbitmq)
+ .WaitFor(postgres)
+ .WaitFor(rabbitmq)
+ .WithSwaggerUI();
-builder.AddProject("baristaapi")
- .WithReference(rabbitmq)
- .WithReplicas(2);
+var isHttps = builder.Configuration["DOTNET_LAUNCH_PROFILE"] == "https";
+var ingressPort = int.TryParse(builder.Configuration["Ingress:Port"], out var port) ? port : (int?)null;
-builder.AddProject("kitchenapi")
- .WithReference(rabbitmq)
- .WithReplicas(2);
+builder.AddYarp("ingress")
+ .WithEndpoint(scheme: isHttps ? "https" : "http", port: ingressPort)
+ .WithReference(productApi)
+ .WithReference(counterApi)
+ .WithReference(orderSummaryApi)
+ .LoadFromConfiguration("ReverseProxy");
builder.Build().Run();
diff --git a/app-host/Properties/launchSettings.json b/app-host/Properties/launchSettings.json
index 420fa28..e0f80eb 100755
--- a/app-host/Properties/launchSettings.json
+++ b/app-host/Properties/launchSettings.json
@@ -1,41 +1,30 @@
{
- "$schema": "http://json.schemastore.org/launchsettings.json",
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true,
- "iisExpress": {
- "applicationUrl": "http://localhost:5284",
- "sslPort": 44331
- }
- },
- "profiles": {
- "http": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- "launchUrl": "swagger",
- "applicationUrl": "http://localhost:5019",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "https": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- "launchUrl": "swagger",
- "applicationUrl": "https://localhost:7256;http://localhost:5019",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
- "launchUrl": "swagger",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- }
- }
-}
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17092;http://localhost:15158",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21011",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22250"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15158",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19041",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20248",
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-host/SwaggerUi/OpenApiRouteBuilderExtensions.cs b/app-host/SwaggerUi/OpenApiRouteBuilderExtensions.cs
new file mode 100644
index 0000000..5cb89b8
--- /dev/null
+++ b/app-host/SwaggerUi/OpenApiRouteBuilderExtensions.cs
@@ -0,0 +1,47 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+internal static class OpenApiRouteBuilderExtensions
+{
+ ///
+ /// Helper method to render Swagger UI view for testing.
+ ///
+ public static IEndpointConventionBuilder MapSwaggerUI(this IEndpointRouteBuilder endpoints)
+ {
+ return endpoints.MapGet("/swagger/{resourceName}/{documentName}", (string resourceName, string documentName) => Results.Content($$"""
+
+
+
+ OpenAPI -{{resourceName}}- {{documentName}}
+
+
+
+
+
+
+
+
+
+
+
+ """, "text/html")).ExcludeFromDescription();
+ }
+}
diff --git a/app-host/SwaggerUi/README.md b/app-host/SwaggerUi/README.md
new file mode 100644
index 0000000..bb2eb80
--- /dev/null
+++ b/app-host/SwaggerUi/README.md
@@ -0,0 +1 @@
+Ref: https://github.com/davidfowl/AspireSwaggerUI
\ No newline at end of file
diff --git a/app-host/SwaggerUi/SwaggerUi.Aspire.Hosting.csproj b/app-host/SwaggerUi/SwaggerUi.Aspire.Hosting.csproj
new file mode 100644
index 0000000..387eaf8
--- /dev/null
+++ b/app-host/SwaggerUi/SwaggerUi.Aspire.Hosting.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/app-host/SwaggerUi/SwaggerUiAnnotation.cs b/app-host/SwaggerUi/SwaggerUiAnnotation.cs
new file mode 100644
index 0000000..8fa590f
--- /dev/null
+++ b/app-host/SwaggerUi/SwaggerUiAnnotation.cs
@@ -0,0 +1,8 @@
+using Aspire.Hosting.ApplicationModel;
+
+public class SwaggerUIAnnotation(string[] documentNames, string path, EndpointReference endpointReference) : IResourceAnnotation
+{
+ public string[] DocumentNames { get; } = documentNames;
+ public string Path { get; } = path;
+ public EndpointReference EndpointReference { get; } = endpointReference;
+}
diff --git a/app-host/SwaggerUi/SwaggerUiExtensions.cs b/app-host/SwaggerUi/SwaggerUiExtensions.cs
new file mode 100644
index 0000000..f3d1b47
--- /dev/null
+++ b/app-host/SwaggerUi/SwaggerUiExtensions.cs
@@ -0,0 +1,181 @@
+using System.Collections.Immutable;
+using Aspire.Hosting;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Lifecycle;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Yarp.ReverseProxy.Forwarder;
+
+public static class SwaggerUIExtensions
+{
+ ///
+ /// Maps the swagger ui endpoint to the application.
+ ///
+ /// The resource builder.
+ /// The list of open api documents. Defaults to "v1" if null.
+ /// The path to the open api document.
+ /// The endpoint name
+ public static IResourceBuilder WithSwaggerUI(this IResourceBuilder builder,
+ string[]? documentNames = null, string path = "swagger/v1/swagger.json", string endpointName = "http")
+ {
+ if (!builder.ApplicationBuilder.Resources.OfType().Any())
+ {
+ // Add the swagger ui code hook and resource
+ builder.ApplicationBuilder.Services.TryAddLifecycleHook();
+ builder.ApplicationBuilder.AddResource(new SwaggerUIResource("swagger-ui"))
+ .WithInitialState(new CustomResourceSnapshot
+ {
+ ResourceType = "swagger-ui",
+ Properties = [],
+ State = "Starting"
+ })
+ .ExcludeFromManifest();
+ }
+
+ return builder.WithAnnotation(new SwaggerUIAnnotation(documentNames ?? ["v1"], path, builder.GetEndpoint(endpointName)));
+ }
+
+ class SwaggerUiHook(ResourceNotificationService notificationService,
+ ResourceLoggerService resourceLoggerService) : IDistributedApplicationLifecycleHook
+ {
+ public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
+ {
+ var openApiResource = appModel.Resources.OfType().SingleOrDefault();
+
+ if (openApiResource is null)
+ {
+ return;
+ }
+
+ // We host a single webserver that will manage the swagger ui endpoints for all resources
+ var builder = WebApplication.CreateSlimBuilder();
+
+ builder.Services.AddHttpForwarder();
+ builder.Logging.ClearProviders();
+
+ builder.Logging.AddProvider(new ResourceLoggerProvider(resourceLoggerService.GetLogger(openApiResource.Name)));
+
+ var app = builder.Build();
+
+ // openapi/resourcename/documentname.json
+ app.MapSwaggerUI();
+
+ var resourceToEndpoint = new Dictionary();
+ var portToResourceMap = new Dictionary)>();
+
+ foreach (var r in appModel.Resources)
+ {
+ if (!r.TryGetLastAnnotation(out var annotation))
+ {
+ continue;
+ }
+
+ // We store the url and path for each resource so we can hit the open api endpoint
+ resourceToEndpoint[r.Name] = (annotation.EndpointReference.Url, annotation.Path);
+
+ var paths = new List();
+ // To avoid cors issues, we expose URLs that send requests to the apphost and then forward them to the actual resource
+ foreach (var documentName in annotation.DocumentNames)
+ {
+ paths.Add($"swagger/{r.Name}/{documentName}");
+ }
+
+ // We store the URL for the resource on the host so we can map it back to the actual address once they are allocated
+ portToResourceMap[app.Urls.Count] = (annotation.EndpointReference.Url, paths);
+
+ // We add a new URL for each resource that has a swagger ui annotation
+ // This is because swagger ui takes over the entire url space
+ app.Urls.Add("http://127.0.0.1:0");
+ }
+
+ var client = new HttpMessageInvoker(new SocketsHttpHandler());
+
+ // Swagger UI will make requests to the apphost so we can avoid doing any CORS configuration.
+ app.Map("/openapi/{resourceName}/{documentName}.json",
+ async (string resourceName, string documentName, IHttpForwarder forwarder, HttpContext context) =>
+ {
+ var (endpoint, path) = resourceToEndpoint[resourceName];
+
+ await forwarder.SendAsync(context, endpoint, client, (c, r) =>
+ {
+ r.RequestUri = new($"{endpoint}/{path}");
+ return ValueTask.CompletedTask;
+ });
+ });
+
+ app.Map("{*path}", async (HttpContext context, IHttpForwarder forwarder, string? path) =>
+ {
+ var (endpoint, _) = portToResourceMap[context.Connection.LocalPort];
+
+ await forwarder.SendAsync(context, endpoint, client, (c, r) =>
+ {
+ r.RequestUri = path is null ? new(endpoint) : new($"{endpoint}/{path}");
+ return ValueTask.CompletedTask;
+ });
+ });
+
+ await app.StartAsync(cancellationToken);
+
+ var addresses = app.Services.GetRequiredService().Features.GetRequiredFeature().Addresses;
+
+ var urls = ImmutableArray.CreateBuilder();
+
+ // Map our index back to the actual address
+ var index = 0;
+ foreach (var rawAddress in addresses)
+ {
+ var address = BindingAddress.Parse(rawAddress);
+
+ // We map the bound port to the resource URL. This lets us forward requests to the correct resource
+ var (_, paths) = portToResourceMap[address.Port] = portToResourceMap[index++];
+
+ // We add the swagger ui endpoint for each resource
+ foreach (var p in paths)
+ {
+ urls.Add(new UrlSnapshot(rawAddress, $"{rawAddress}/{p}", IsInternal: false));
+ }
+ }
+
+ await notificationService.PublishUpdateAsync(openApiResource, s => s with
+ {
+ State = "Running",
+ Urls = urls.ToImmutableArray()
+ });
+ }
+ }
+
+ private class ResourceLoggerProvider(ILogger logger) : ILoggerProvider
+ {
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new ResourceLogger(logger);
+ }
+
+ public void Dispose()
+ {
+ }
+
+ private class ResourceLogger(ILogger logger) : ILogger
+ {
+ public IDisposable? BeginScope(TState state) where TState : notnull
+ {
+ return logger.BeginScope(state);
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return logger.IsEnabled(logLevel);
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ logger.Log(logLevel, eventId, state, exception, formatter);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-host/SwaggerUi/SwaggerUiResource.cs b/app-host/SwaggerUi/SwaggerUiResource.cs
new file mode 100644
index 0000000..51c02f4
--- /dev/null
+++ b/app-host/SwaggerUi/SwaggerUiResource.cs
@@ -0,0 +1,6 @@
+using Aspire.Hosting.ApplicationModel;
+
+public class SwaggerUIResource(string name) : Resource(name)
+{
+
+}
diff --git a/app-host/app-host.csproj b/app-host/app-host.csproj
deleted file mode 100755
index 3eeffa6..0000000
--- a/app-host/app-host.csproj
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- Exe
- net8.0
- enable
- enable
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app-host/app1.http b/app-host/app1.http
new file mode 100644
index 0000000..08abbf0
--- /dev/null
+++ b/app-host/app1.http
@@ -0,0 +1,44 @@
+# For more info on HTTP files go to https://aka.ms/vs/httpfile
+@hostname=localhost:5000
+
+POST https://{{hostname}}/c/api/v1/orders
+Content-Type: application/json
+
+{
+ "orderId": "{{$guid}}",
+ "commandType": 0,
+ "orderSource": 0,
+ "location": 0,
+ "loyaltyMemberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "baristaItems": [
+ {
+ "itemType": {{$randomInt 0 5}}
+ },
+ {
+ "itemType": {{$randomInt 0 5}}
+ }
+ ],
+ "kitchenItems": [
+ {
+ "itemType": {{$randomInt 6 9}}
+ }
+ ],
+ "timestamp": "{{$datetime iso8601}}"
+}
+
+###
+GET https://{{hostname}}/c/api/v1/fulfillment-orders
+content-type: application/json
+
+###
+GET https://{{hostname}}/p/api/v1/item-types
+content-type: application/json
+
+###
+GET https://{{hostname}}/p/api/v1/items-by-types/1,2,3
+content-type: application/json
+
+###
+@orderId = 8cf20000-8d12-00ff-acd0-08dc7cc27ccd
+GET https://{{hostname}}/audit/api/v1/summary?orderId={{orderId}}
+content-type: application/json
\ No newline at end of file
diff --git a/app-host/appsettings.Development.json b/app-host/appsettings.Development.json
index ff66ba6..76ad0df 100755
--- a/app-host/appsettings.Development.json
+++ b/app-host/appsettings.Development.json
@@ -1,8 +1,11 @@
{
"Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Information",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "Aspire.Hosting": "Information",
+ "Aspire.Hosting.Dcp": "Information"
+ }
}
}
diff --git a/app-host/appsettings.json b/app-host/appsettings.json
index b844787..e088c3f 100755
--- a/app-host/appsettings.json
+++ b/app-host/appsettings.json
@@ -1,10 +1,72 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning",
- "Aspire.Hosting.Dcp": "Warning",
- "Microsoft.AspNetCore.DataProtection": "Information"
- }
- }
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "Ingress": {
+ "Port": 5000
+ },
+ "ReverseProxy": {
+ "Routes": {
+ "productapi": {
+ "ClusterId": "productapi",
+ "Match": {
+ "Path": "/p/{**remainder}"
+ },
+ "Transforms": [
+ { "PathRemovePrefix": "/p" },
+ { "PathPrefix": "/" },
+ { "RequestHeaderOriginalHost": "true" }
+ ]
+ },
+ "counterApi": {
+ "ClusterId": "counterApi",
+ "Match": {
+ "Path": "/c/{**remainder}"
+ },
+ "Transforms": [
+ { "PathRemovePrefix": "/c" },
+ { "PathPrefix": "/" },
+ { "RequestHeaderOriginalHost": "true" }
+ ]
+ },
+ "orderSummaryApi": {
+ "ClusterId": "orderSummaryApi",
+ "Match": {
+ "Path": "/audit/{**remainder}"
+ },
+ "Transforms": [
+ { "PathRemovePrefix": "/audit" },
+ { "PathPrefix": "/" },
+ { "RequestHeaderOriginalHost": "true" }
+ ]
+ }
+ },
+ "Clusters": {
+ "productapi": {
+ "Destinations": {
+ "base_destination": {
+ "Address": "http://product-api"
+ }
+ }
+ },
+ "counterApi": {
+ "Destinations": {
+ "base_destination": {
+ "Address": "http://counter-api"
+ }
+ }
+ },
+ "orderSummaryApi": {
+ "Destinations": {
+ "base_destination": {
+ "Address": "http://order-summary"
+ }
+ }
+ }
+ }
+ }
}
diff --git a/app-host/aspirate-output/docker-compose.yaml b/app-host/aspirate-output/docker-compose.yaml
new file mode 100644
index 0000000..d017657
--- /dev/null
+++ b/app-host/aspirate-output/docker-compose.yaml
@@ -0,0 +1,108 @@
+services:
+ postgresQL:
+ container_name: "postgresQL"
+ image: "docker.io/library/postgres:16.2"
+ environment:
+ POSTGRES_HOST_AUTH_METHOD: "scram-sha-256"
+ POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256"
+ POSTGRES_USER: "postgres"
+ POSTGRES_PASSWORD: "2lUmFKentK!fuyjdGIK4ka"
+ ports:
+ - target: 5432
+ published: 5432
+ restart: unless-stopped
+ redis:
+ container_name: "redis"
+ image: "docker.io/library/redis:7.2"
+ ports:
+ - target: 6379
+ published: 6379
+ restart: unless-stopped
+ rabbitmq:
+ container_name: "rabbitmq"
+ image: "docker.io/library/rabbitmq:3.13-management"
+ environment:
+ RABBITMQ_DEFAULT_USER: "guest"
+ RABBITMQ_DEFAULT_PASS: "m7YZc!8nqV6bmTb8VKM318"
+ ports:
+ - target: 5672
+ published: 5672
+ - target: 15672
+ published: 15672
+ restart: unless-stopped
+ product-api:
+ container_name: "product-api"
+ image: "product-api:latest"
+ environment:
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory"
+ ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"
+ ports:
+ - target: 8080
+ published: 10000
+ - target: 8443
+ published: 10001
+ restart: unless-stopped
+ counter-api:
+ container_name: "counter-api"
+ image: "counter-api:latest"
+ environment:
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory"
+ ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"
+ services__product-api__http__0: "http://product-api:8080"
+ ConnectionStrings__rabbitmq: "amqp://guest:m7YZc!8nqV6bmTb8VKM318@rabbitmq:5672"
+ ports:
+ - target: 8080
+ published: 10002
+ - target: 8443
+ published: 10003
+ restart: unless-stopped
+ barista-api:
+ container_name: "barista-api"
+ image: "barista-api:latest"
+ environment:
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory"
+ ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"
+ ConnectionStrings__rabbitmq: "amqp://guest:m7YZc!8nqV6bmTb8VKM318@rabbitmq:5672"
+ ports:
+ - target: 8080
+ published: 10004
+ - target: 8443
+ published: 10005
+ restart: unless-stopped
+ kitchen-api:
+ container_name: "kitchen-api"
+ image: "kitchen-api:latest"
+ environment:
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory"
+ ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"
+ ConnectionStrings__rabbitmq: "amqp://guest:m7YZc!8nqV6bmTb8VKM318@rabbitmq:5672"
+ ports:
+ - target: 8080
+ published: 10006
+ - target: 8443
+ published: 10007
+ restart: unless-stopped
+ order-summary:
+ container_name: "order-summary"
+ image: "order-summary:latest"
+ environment:
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true"
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory"
+ ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"
+ ConnectionStrings__postgres: "Host=postgresQL;Port=5432;Username=postgres;Password=2lUmFKentK!fuyjdGIK4ka;Database=postgres"
+ ConnectionStrings__rabbitmq: "amqp://guest:m7YZc!8nqV6bmTb8VKM318@rabbitmq:5672"
+ ports:
+ - target: 8080
+ published: 10008
+ - target: 8443
+ published: 10009
+ restart: unless-stopped
diff --git a/app-host/aspirate-state.json b/app-host/aspirate-state.json
new file mode 100644
index 0000000..b414403
--- /dev/null
+++ b/app-host/aspirate-state.json
@@ -0,0 +1,48 @@
+{
+ "projectPath": ".",
+ "namespace": "coffeeshop",
+ "containerImageTags": [
+ "latest"
+ ],
+ "imagePullPolicy": "IfNotPresent",
+ "containerBuilder": "docker",
+ "kubeContext": "k3d-k3s-default",
+ "outputFormat": "kustomize",
+ "privateRegistryEmail": "aspir8@aka.ms",
+ "includeDashboard": true,
+ "useCustomNamespace": true,
+ "secrets": {
+ "salt": "EBQWuonzWR6ciGDa",
+ "hash": "qC2Cj\u002Bdvmez\u002BREtX9lWAaHcX1/fCDhy71t/5URBY23k=",
+ "secrets": {
+ "postgresQL-password": {
+ "value": "EBQWuonzWR6ciGDaz\u002BVw/NaSBujWMsclpH2c9tNxYjYh7xom2bblDrRVn6hfrPF9ja0="
+ },
+ "rabbitmq-password": {
+ "value": "EBQWuonzWR6ciGDa\u002BxNAFVcRzLbDrscmCJ0eZNA3eQ90zD4G17fxPph8qItzveRZtsU="
+ },
+ "postgresQL": {
+ "POSTGRES_PASSWORD": "EBQWuonzWR6ciGDaz\u002BVw/NaSBujWMsclpH2c9tNxYjYh7xom2bblDrRVn6hfrPF9ja0="
+ },
+ "postgres": {},
+ "redis": {},
+ "rabbitmq": {},
+ "product-api": {},
+ "counter-api": {
+ "ConnectionStrings__rabbitmq": "EBQWuonzWR6ciGDatn7KBVD9q6vYrfaZnAJGPPArfjItrHQuy6DMA8lTjJB8uMRfuv0bfVAkppcrnPSChcBdetazdRv7YX4VaUHLGM4="
+ },
+ "barista-api": {
+ "ConnectionStrings__rabbitmq": "EBQWuonzWR6ciGDatn7KBVD9q6vYrfaZnAJGPPArfjItrHQuy6DMA8lTjJB8uMRfuv0bfVAkppcrnPSChcBdetazdRv7YX4VaUHLGM4="
+ },
+ "kitchen-api": {
+ "ConnectionStrings__rabbitmq": "EBQWuonzWR6ciGDatn7KBVD9q6vYrfaZnAJGPPArfjItrHQuy6DMA8lTjJB8uMRfuv0bfVAkppcrnPSChcBdetazdRv7YX4VaUHLGM4="
+ },
+ "order-summary": {
+ "ConnectionStrings__postgres": "EBQWuonzWR6ciGDaqxc3s7Lbk\u002BUObOfAKkEQtNkpfDYq8zQ6yqLNEoBDsd1htPlOyKFdACt0nbEjrPyMi\u002BYxSsuhYx7gcGBfAxWOXIviTVdgDiy2cWiIyi4J50NQtIXa9ReqyHbyGrZ0M4gBho0YXiPpfe8gyiPw5A==",
+ "ConnectionStrings__rabbitmq": "EBQWuonzWR6ciGDatn7KBVD9q6vYrfaZnAJGPPArfjItrHQuy6DMA8lTjJB8uMRfuv0bfVAkppcrnPSChcBdetazdRv7YX4VaUHLGM4="
+ }
+ }
+ },
+ "processAllComponents": true,
+ "isRunning": true
+}
\ No newline at end of file
diff --git a/app-host/manifest.json b/app-host/manifest.json
new file mode 100644
index 0000000..a6e2a41
--- /dev/null
+++ b/app-host/manifest.json
@@ -0,0 +1,211 @@
+{
+ "resources": {
+ "postgresQL": {
+ "type": "container.v0",
+ "connectionString": "Host={postgresQL.bindings.tcp.host};Port={postgresQL.bindings.tcp.port};Username=postgres;Password={postgresQL-password.value}",
+ "image": "docker.io/library/postgres:16.2",
+ "env": {
+ "POSTGRES_HOST_AUTH_METHOD": "scram-sha-256",
+ "POSTGRES_INITDB_ARGS": "--auth-host=scram-sha-256 --auth-local=scram-sha-256",
+ "POSTGRES_USER": "postgres",
+ "POSTGRES_PASSWORD": "{postgresQL-password.value}"
+ },
+ "bindings": {
+ "tcp": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "targetPort": 5432
+ }
+ }
+ },
+ "postgres": {
+ "type": "value.v0",
+ "connectionString": "{postgresQL.connectionString};Database=postgres"
+ },
+ "redis": {
+ "type": "container.v0",
+ "connectionString": "{redis.bindings.tcp.host}:{redis.bindings.tcp.port}",
+ "image": "docker.io/library/redis:7.2",
+ "bindings": {
+ "tcp": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "targetPort": 6379
+ }
+ }
+ },
+ "rabbitmq": {
+ "type": "container.v0",
+ "connectionString": "amqp://guest:{rabbitmq-password.value}@{rabbitmq.bindings.tcp.host}:{rabbitmq.bindings.tcp.port}",
+ "image": "docker.io/library/rabbitmq:3.13-management",
+ "env": {
+ "RABBITMQ_DEFAULT_USER": "guest",
+ "RABBITMQ_DEFAULT_PASS": "{rabbitmq-password.value}"
+ },
+ "bindings": {
+ "tcp": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "targetPort": 5672
+ },
+ "management": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "targetPort": 15672
+ }
+ }
+ },
+ "product-api": {
+ "type": "project.v0",
+ "path": "../product-api/CoffeeShop.ProductApi.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "counter-api": {
+ "type": "project.v0",
+ "path": "../counter-api/CoffeeShop.CounterApi.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "services__product-api__http__0": "{product-api.bindings.http.url}",
+ "services__product-api__https__0": "{product-api.bindings.https.url}",
+ "ConnectionStrings__rabbitmq": "{rabbitmq.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "barista-api": {
+ "type": "project.v0",
+ "path": "../barista-api/CoffeeShop.BaristaApi.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "ConnectionStrings__rabbitmq": "{rabbitmq.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "kitchen-api": {
+ "type": "project.v0",
+ "path": "../kitchen-api/CoffeeShop.KitchenApi.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "ConnectionStrings__rabbitmq": "{rabbitmq.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "order-summary": {
+ "type": "project.v0",
+ "path": "../order-summary/CoffeeShop.OrderSummary.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "ConnectionStrings__postgres": "{postgres.connectionString}",
+ "ConnectionStrings__rabbitmq": "{rabbitmq.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "postgresQL-password": {
+ "type": "parameter.v0",
+ "value": "{postgresQL-password.inputs.value}",
+ "inputs": {
+ "value": {
+ "type": "string",
+ "secret": true,
+ "default": {
+ "generate": {
+ "minLength": 22
+ }
+ }
+ }
+ }
+ },
+ "rabbitmq-password": {
+ "type": "parameter.v0",
+ "value": "{rabbitmq-password.inputs.value}",
+ "inputs": {
+ "value": {
+ "type": "string",
+ "secret": true,
+ "default": {
+ "generate": {
+ "minLength": 22,
+ "special": false
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/barista-api/CoffeeShop.BaristaApi.csproj b/barista-api/CoffeeShop.BaristaApi.csproj
new file mode 100644
index 0000000..e96a025
--- /dev/null
+++ b/barista-api/CoffeeShop.BaristaApi.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ BaristaApi
+ barista-api
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/barista-api/IntegrationEvents/EventHandlers/BaristaOrderedConsumer.cs b/barista-api/IntegrationEvents/EventHandlers/BaristaOrderedConsumer.cs
index 1ba8a0c..4010afb 100755
--- a/barista-api/IntegrationEvents/EventHandlers/BaristaOrderedConsumer.cs
+++ b/barista-api/IntegrationEvents/EventHandlers/BaristaOrderedConsumer.cs
@@ -1,59 +1,64 @@
using BaristaApi.Domain;
+
using CoffeeShop.MessageContracts;
+
using MassTransit;
namespace BaristaApi.IntegrationEvents.EventHandlers;
-internal class BaristaOrderedConsumer(IPublishEndpoint publisher, ILogger logger)
- : IConsumer
+internal class BaristaOrderedConsumer(IPublishEndpoint publisher, ILogger logger)
+ : IConsumer
{
- public async Task Consume(ConsumeContext context)
- {
- ArgumentNullException.ThrowIfNull(context);
- logger.LogInformation("Received an message {name}", nameof(context.Message));
-
- var message = context.Message;
-
- foreach(var item in message.ItemLines) {
- await Task.Delay(CalculateDelay(item.ItemType));
- }
-
- await publisher.Publish(new BaristaOrderUpdated{OrderId = message.OrderId, ItemLines = message.ItemLines});
- }
-
- private static TimeSpan CalculateDelay(ItemType itemType)
- {
- return itemType switch
- {
- ItemType.COFFEE_BLACK => TimeSpan.FromSeconds(5),
- ItemType.COFFEE_WITH_ROOM => TimeSpan.FromSeconds(5),
- ItemType.ESPRESSO => TimeSpan.FromSeconds(7),
- ItemType.ESPRESSO_DOUBLE => TimeSpan.FromSeconds(7),
- ItemType.CAPPUCCINO => TimeSpan.FromSeconds(10),
- _ => TimeSpan.FromSeconds(3)
- };
- }
+ public async Task Consume(ConsumeContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ logger.LogInformation("Received an message {name}", nameof(context.Message));
+
+ var message = context.Message;
+
+ // toto: processing and persist it
+
+ foreach (var item in message.ItemLines)
+ {
+ await Task.Delay(CalculateDelay(item.ItemType));
+ }
+
+ await publisher.Publish(new BaristaOrderUpdated { OrderId = message.OrderId, ItemLines = message.ItemLines });
+ }
+
+ private static TimeSpan CalculateDelay(ItemType itemType)
+ {
+ return itemType switch
+ {
+ ItemType.COFFEE_BLACK => TimeSpan.FromSeconds(5),
+ ItemType.COFFEE_WITH_ROOM => TimeSpan.FromSeconds(5),
+ ItemType.ESPRESSO => TimeSpan.FromSeconds(7),
+ ItemType.ESPRESSO_DOUBLE => TimeSpan.FromSeconds(7),
+ ItemType.CAPPUCCINO => TimeSpan.FromSeconds(10),
+ _ => TimeSpan.FromSeconds(3)
+ };
+ }
}
internal class BaristaOrderedConsumerDefinition : ConsumerDefinition
{
- public BaristaOrderedConsumerDefinition()
- {
- // override the default endpoint name
- EndpointName = "barista-service";
-
- // limit the number of messages consumed concurrently
- // this applies to the consumer only, not the endpoint
- ConcurrentMessageLimit = 8;
- }
-
- protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator,
- IConsumerConfigurator consumerConfigurator)
- {
- // configure message retry with millisecond intervals
- endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 200, 500, 800, 1000));
-
- // use the outbox to prevent duplicate events from being published
- endpointConfigurator.UseInMemoryOutbox();
- }
+ public BaristaOrderedConsumerDefinition()
+ {
+ // override the default endpoint name
+ EndpointName = "barista-service";
+
+ // limit the number of messages consumed concurrently
+ // this applies to the consumer only, not the endpoint
+ ConcurrentMessageLimit = 8;
+ }
+
+ protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator,
+ IConsumerConfigurator consumerConfigurator)
+ {
+ // configure message retry with millisecond intervals
+ endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 200, 500, 800, 1000));
+
+ // use the outbox to prevent duplicate events from being published
+ endpointConfigurator.UseInMemoryOutbox();
+ }
}
\ No newline at end of file
diff --git a/barista-api/IntegrationEvents/Events/BaristaOrderUpdated.cs b/barista-api/IntegrationEvents/Events/BaristaOrderUpdated.cs
index afa6e3b..13c6d19 100755
--- a/barista-api/IntegrationEvents/Events/BaristaOrderUpdated.cs
+++ b/barista-api/IntegrationEvents/Events/BaristaOrderUpdated.cs
@@ -5,5 +5,5 @@ namespace CoffeeShop.MessageContracts;
public record BaristaOrderUpdated
{
public Guid OrderId { get; init; }
- public List ItemLines { get; init; } = new();
+ public List ItemLines { get; init; } = [];
}
\ No newline at end of file
diff --git a/barista-api/IntegrationEvents/Events/BaristaPlaced.cs b/barista-api/IntegrationEvents/Events/BaristaPlaced.cs
index c9182e6..109b210 100755
--- a/barista-api/IntegrationEvents/Events/BaristaPlaced.cs
+++ b/barista-api/IntegrationEvents/Events/BaristaPlaced.cs
@@ -5,5 +5,5 @@ namespace CoffeeShop.MessageContracts;
public record BaristaOrderPlaced
{
public Guid OrderId { get; init; }
- public List ItemLines { get; init; } = new();
+ public List ItemLines { get; init; } = [];
}
\ No newline at end of file
diff --git a/barista-api/Program.cs b/barista-api/Program.cs
index f0d8bf5..5e98faa 100755
--- a/barista-api/Program.cs
+++ b/barista-api/Program.cs
@@ -1,50 +1,53 @@
using FluentValidation;
using MassTransit;
using BaristaApi.IntegrationEvents.EventHandlers;
+using CoffeeShop.Shared.OpenTelemetry;
+using CoffeeShop.Shared.Exceptions;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
+builder.Services.AddExceptionHandler();
+builder.Services.AddExceptionHandler();
builder.Services.AddProblemDetails();
builder.Services.AddHttpContextAccessor();
-builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining());
-builder.Services.AddValidatorsFromAssemblyContaining();
+builder.Services.AddMediatR(cfg => {
+ cfg.RegisterServicesFromAssemblyContaining();
+ cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
+ cfg.AddOpenBehavior(typeof(HandlerBehavior<,>));
+});
+builder.Services.AddValidatorsFromAssemblyContaining(includeInternalTypes: true);
builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddSwaggerGen();
builder.Services.AddMassTransit(x =>
{
- x.AddConsumer(typeof(BaristaOrderedConsumerDefinition));
+ x.AddConsumer(typeof(BaristaOrderedConsumerDefinition));
- x.SetKebabCaseEndpointNameFormatter();
+ x.SetKebabCaseEndpointNameFormatter();
- x.UsingRabbitMq((context, cfg) =>
- {
- // cfg.Host(builder.Configuration.GetValue("RabbitMqUrl")!);
- cfg.Host(builder.Configuration.GetConnectionString("rabbitmq")!);
- cfg.ConfigureEndpoints(context);
- });
+ x.UsingRabbitMq((context, cfg) =>
+ {
+ cfg.Host(builder.Configuration.GetConnectionString("rabbitmq")!);
+ cfg.ConfigureEndpoints(context);
+ });
});
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
var app = builder.Build();
app.UseExceptionHandler();
-if (app.Environment.IsDevelopment())
-{
- app.UseSwagger();
- app.UseSwaggerUI();
-}
-
app.UseRouting();
app.MapDefaultEndpoints();
-app.Map("/", () => Results.Redirect("/swagger"));
+app.Run();
-// _ = app.MapOrderUpApiRoutes();
+public partial class Program;
-app.Run();
diff --git a/barista-api/appsettings.Development.json b/barista-api/appsettings.Development.json
index a7dbfd2..ff66ba6 100755
--- a/barista-api/appsettings.Development.json
+++ b/barista-api/appsettings.Development.json
@@ -4,6 +4,5 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
- },
- "RabbitMqUrl": "localhost"
+ }
}
diff --git a/barista-api/appsettings.json b/barista-api/appsettings.json
index 4d56694..430039c 100755
--- a/barista-api/appsettings.json
+++ b/barista-api/appsettings.json
@@ -1,9 +1,13 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
- "AllowedHosts": "*"
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "postgres": "Server=localhost;Port=5432;Database=postgres;",
+ "rabbitmq": "amqp://localhost"
+ }
}
diff --git a/client.local.http b/client.local.http
index 5322fad..53b6efb 100755
--- a/client.local.http
+++ b/client.local.http
@@ -1,5 +1,6 @@
@product_host = http://localhost:5001
@host = http://localhost:5002
+@order_host = http://localhost:5005
###
GET {{product_host}}/v1/api/item-types HTTP/1.1
@@ -36,4 +37,9 @@ content-type: application/json
###
GET {{host}}/v1/api/fulfillment-orders HTTP/1.1
-content-type: application/json
\ No newline at end of file
+content-type: application/json
+
+###
+@orderId = f0c60000-8d12-00ff-509a-08dc78db471b
+GET {{order_host}}/summary?orderId={{orderId}}
+content-type: application/json
diff --git a/coffeeshop-aspire.sln b/coffeeshop-aspire.sln
index 63f1ed4..0d475c2 100755
--- a/coffeeshop-aspire.sln
+++ b/coffeeshop-aspire.sln
@@ -3,17 +3,33 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "barista-api", "barista-api\barista-api.csproj", "{1E1B0C85-DB1F-485E-8B5A-CE03E9977029}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.BaristaApi", "barista-api\CoffeeShop.BaristaApi.csproj", "{1E1B0C85-DB1F-485E-8B5A-CE03E9977029}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "service-defaults", "service-defaults\service-defaults.csproj", "{5E32B73F-45E0-4E11-BBCD-397249DF3E82}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.ProductApi", "product-api\CoffeeShop.ProductApi.csproj", "{605A641D-F5E3-4B6F-9092-591F0A7DEE9D}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "product-api", "product-api\product-api.csproj", "{605A641D-F5E3-4B6F-9092-591F0A7DEE9D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.AppHost", "app-host\CoffeeShop.AppHost.csproj", "{EB61218A-CF80-4389-9C6A-D6AA5C205E95}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "app-host", "app-host\app-host.csproj", "{EB61218A-CF80-4389-9C6A-D6AA5C205E95}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.KitchenApi", "kitchen-api\CoffeeShop.KitchenApi.csproj", "{16CC8BE1-3E21-46FB-813D-CF37C7079EC4}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "kitchen-api", "kitchen-api\kitchen-api.csproj", "{16CC8BE1-3E21-46FB-813D-CF37C7079EC4}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.CounterApi", "counter-api\CoffeeShop.CounterApi.csproj", "{6A163558-FB94-449A-817A-8D8FD08D5E22}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "counter-api", "counter-api\counter-api.csproj", "{6A163558-FB94-449A-817A-8D8FD08D5E22}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{140D0842-3BBC-4339-9C75-331FD9CB50F8}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ .gitignore = .gitignore
+ aspire-manifest.json = aspire-manifest.json
+ Directory.Build.props = Directory.Build.props
+ Directory.Packages.props = Directory.Packages.props
+ global.json = global.json
+ NuGet.config = NuGet.config
+ README.md = README.md
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.Shared", "shared\CoffeeShop.Shared\CoffeeShop.Shared.csproj", "{46796FD1-9AC2-498D-8535-D9E57286AA2F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.OrderSummary", "order-summary\CoffeeShop.OrderSummary.csproj", "{FE457D2C-ABB3-464C-8C6D-16D09C77FFB0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeShop.CounterApi.IntegrationTests", "counter-api-tests\CoffeeShop.CounterApi.IntegrationTests.csproj", "{5DB71E8E-BF52-4918-895D-CD3BB373D6A0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -25,10 +41,6 @@ Global
{1E1B0C85-DB1F-485E-8B5A-CE03E9977029}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E1B0C85-DB1F-485E-8B5A-CE03E9977029}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E1B0C85-DB1F-485E-8B5A-CE03E9977029}.Release|Any CPU.Build.0 = Release|Any CPU
- {5E32B73F-45E0-4E11-BBCD-397249DF3E82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5E32B73F-45E0-4E11-BBCD-397249DF3E82}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5E32B73F-45E0-4E11-BBCD-397249DF3E82}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5E32B73F-45E0-4E11-BBCD-397249DF3E82}.Release|Any CPU.Build.0 = Release|Any CPU
{605A641D-F5E3-4B6F-9092-591F0A7DEE9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{605A641D-F5E3-4B6F-9092-591F0A7DEE9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{605A641D-F5E3-4B6F-9092-591F0A7DEE9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -45,6 +57,18 @@ Global
{6A163558-FB94-449A-817A-8D8FD08D5E22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A163558-FB94-449A-817A-8D8FD08D5E22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A163558-FB94-449A-817A-8D8FD08D5E22}.Release|Any CPU.Build.0 = Release|Any CPU
+ {46796FD1-9AC2-498D-8535-D9E57286AA2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {46796FD1-9AC2-498D-8535-D9E57286AA2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {46796FD1-9AC2-498D-8535-D9E57286AA2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {46796FD1-9AC2-498D-8535-D9E57286AA2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE457D2C-ABB3-464C-8C6D-16D09C77FFB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE457D2C-ABB3-464C-8C6D-16D09C77FFB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE457D2C-ABB3-464C-8C6D-16D09C77FFB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE457D2C-ABB3-464C-8C6D-16D09C77FFB0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5DB71E8E-BF52-4918-895D-CD3BB373D6A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5DB71E8E-BF52-4918-895D-CD3BB373D6A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5DB71E8E-BF52-4918-895D-CD3BB373D6A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5DB71E8E-BF52-4918-895D-CD3BB373D6A0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/counter-api-tests/CoffeeShop.CounterApi.IntegrationTests.csproj b/counter-api-tests/CoffeeShop.CounterApi.IntegrationTests.csproj
new file mode 100644
index 0000000..b1c931e
--- /dev/null
+++ b/counter-api-tests/CoffeeShop.CounterApi.IntegrationTests.csproj
@@ -0,0 +1,46 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/counter-api-tests/CounterApiFixture.cs b/counter-api-tests/CounterApiFixture.cs
new file mode 100644
index 0000000..cbae8bb
--- /dev/null
+++ b/counter-api-tests/CounterApiFixture.cs
@@ -0,0 +1,131 @@
+using CounterApi.Domain;
+using CounterApi.IntegrationEvents.EventHandlers;
+
+using MassTransit;
+
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+
+using WireMock.Client.Builders;
+
+using Xunit;
+
+namespace CoffeeShop.CounterApi.IntegrationTests;
+
+public sealed class CounterApiFixture : WebApplicationFactory, IAsyncLifetime
+{
+ private readonly IHost _app;
+
+ public IResourceBuilder Postgres { get; private set; }
+ private string _postgresConnectionString;
+
+ public IResourceBuilder RabbitMq { get; private set; }
+ private string _rabbitMqConnectionString;
+
+ public IResourceBuilder ProductApi { get; private set; }
+
+ public CounterApiFixture()
+ {
+ var options = new DistributedApplicationOptions { AssemblyName = typeof(CounterApiFixture).Assembly.FullName, DisableDashboard = true };
+ var appBuilder = DistributedApplication.CreateBuilder(options);
+
+ Postgres = appBuilder.AddPostgres("postgresQL");
+ RabbitMq = appBuilder.AddRabbitMQ("rabbitmq").WithHealthCheck();
+ ProductApi = appBuilder.AddWireMockNet("product-api")
+ .WithApiMappingBuilder(ProductApiMock.Build);
+
+ _app = appBuilder.Build();
+ }
+
+ protected override IHost CreateHost(IHostBuilder builder)
+ {
+ builder.ConfigureHostConfiguration(config =>
+ {
+ config.AddInMemoryCollection(new Dictionary
+ {
+ { $"ConnectionStrings:{Postgres.Resource.Name}", _postgresConnectionString },
+ { $"ConnectionStrings:{RabbitMq.Resource.Name}", _rabbitMqConnectionString },
+ { "ProductApiUrl", ProductApi.GetEndpoint("http").Url }
+ }!);
+ })
+ .ConfigureWebHost(builder =>
+ {
+ builder.UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.RemoveAll();
+ })
+ .ConfigureTestServices(services =>
+ {
+ services.AddMassTransitTestHarness(x =>
+ {
+ x.AddConsumer();
+ x.AddConsumer();
+ });
+ });
+ });
+
+ return base.CreateHost(builder);
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _app.StartAsync();
+
+ _postgresConnectionString = await Postgres.Resource.GetConnectionStringAsync();
+ _rabbitMqConnectionString = await RabbitMq.Resource.ConnectionStringExpression.GetValueAsync(default);
+
+ // if don't waiting then WireMock will be failed
+ await Task.Delay(TimeSpan.FromSeconds(5));
+ }
+
+ public new async Task DisposeAsync()
+ {
+ await base.DisposeAsync();
+ await _app.StopAsync();
+ if (_app is IAsyncDisposable asyncDisposable)
+ {
+ await asyncDisposable.DisposeAsync().ConfigureAwait(false);
+ }
+ else
+ {
+ _app.Dispose();
+ }
+ }
+}
+
+internal class ProductApiMock
+{
+ public static async Task Build(AdminApiMappingBuilder builder)
+ {
+ var itemTypes = new List {
+ new() {
+ ItemType = ItemType.CAKEPOP
+ },
+ new() {
+ ItemType = ItemType.CAPPUCCINO
+ }};
+
+ builder.Given(builder => builder
+ .WithRequest(request => request
+ .UsingGet()
+ .WithPath("/api/v1/item-types")
+ )
+ .WithResponse(response => response
+ .WithBodyAsJson(itemTypes)
+ )
+ );
+
+ await builder.BuildAndPostAsync();
+ }
+}
+
+public class ItemTypeDto
+{
+ public ItemType ItemType { get; set; }
+ public string Name { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/counter-api-tests/CounterApiTests.cs b/counter-api-tests/CounterApiTests.cs
new file mode 100644
index 0000000..1e89d60
--- /dev/null
+++ b/counter-api-tests/CounterApiTests.cs
@@ -0,0 +1,99 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+
+using Asp.Versioning;
+using Asp.Versioning.Http;
+
+using CoffeeShop.MessageContracts;
+using CoffeeShop.Shared.Helpers;
+
+using CounterApi.Domain;
+using CounterApi.Domain.Commands;
+using CounterApi.IntegrationEvents.EventHandlers;
+
+using MassTransit.Testing;
+
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+
+using Xunit;
+
+namespace CoffeeShop.CounterApi.IntegrationTests;
+
+internal class RetryHandler : DelegatingHandler
+{
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return base.SendAsync(request, cancellationToken);
+ }
+}
+
+public sealed class CounterApiTests : IClassFixture
+{
+ private readonly WebApplicationFactory _webApplicationFactory;
+ private readonly TestServer _host;
+ private readonly HttpClient _httpClient;
+ private readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web);
+
+ public CounterApiTests(CounterApiFixture fixture)
+ {
+ var handler = new ApiVersionHandler(new QueryStringApiVersionWriter(), new ApiVersion(1.0));
+ _webApplicationFactory = fixture;
+ _host = fixture.Server;
+ _httpClient = _webApplicationFactory.CreateDefaultClient(handler);
+ }
+
+ [Fact]
+ public async Task GetOrder()
+ {
+ // Act
+ var response = await _httpClient.GetAsync("/api/v1/fulfillment-orders");
+
+ // Assert
+ response.EnsureSuccessStatusCode();
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonSerializer.Deserialize>(body, _jsonSerializerOptions);
+
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public async Task SubmitOrder()
+ {
+ var json = new PlaceOrderCommand
+ {
+ OrderId = GuidHelper.NewGuid(),
+ CommandType = CommandType.PLACE_ORDER,
+ OrderSource = OrderSource.WEB,
+ Location = Location.ATLANTA,
+ LoyaltyMemberId = GuidHelper.NewGuid(),
+ Timestamp = DateTime.UtcNow,
+ BaristaItems = [new CommandItem { ItemType = ItemType.CAPPUCCINO }],
+ KitchenItems = [new CommandItem { ItemType = ItemType.CAKEPOP }],
+ };
+
+ var response = await _httpClient.PostAsJsonAsync("/api/v1/orders", json);
+
+ response.EnsureSuccessStatusCode();
+ var body = await response.Content.ReadAsStringAsync();
+
+ Assert.NotNull(body);
+
+ var testHarness = _host.Services?.GetService();
+ Assert.NotNull(testHarness);
+
+ await testHarness.Start();
+
+ var orderId = GuidHelper.NewGuid();
+
+ await testHarness.Bus.Publish(new BaristaOrderUpdated { OrderId = orderId });
+ await testHarness.Bus.Publish(new KitchenOrderUpdated { OrderId = orderId });
+
+ var consumer1 = testHarness.GetConsumerHarness();
+ var consumer2 = testHarness.GetConsumerHarness();
+
+ Assert.True(await consumer1.Consumed.Any(f => f.Context.Message.OrderId == orderId));
+ Assert.True(await consumer2.Consumed.Any(f => f.Context.Message.OrderId == orderId));
+ }
+}
diff --git a/counter-api-tests/HealthCheckExtensions.cs b/counter-api-tests/HealthCheckExtensions.cs
new file mode 100644
index 0000000..e3deedc
--- /dev/null
+++ b/counter-api-tests/HealthCheckExtensions.cs
@@ -0,0 +1,65 @@
+using Aspirant.Hosting;
+
+using HealthChecks.NpgSql;
+using HealthChecks.RabbitMQ;
+using HealthChecks.Redis;
+using HealthChecks.Uris;
+
+namespace CoffeeShop.CounterApi.IntegrationTests;
+
+///
+/// Ref: https://github.com/davidfowl/WaitForDependenciesAspire/tree/main/WaitForDependencies.Aspire.Hosting
+///
+public static class Extensions
+{
+ public static IResourceBuilder WithHealthCheck(this IResourceBuilder builder)
+ {
+ return builder.WithAnnotation(HealthCheckAnnotation.Create(cs => new RabbitMQHealthCheck(new RabbitMQHealthCheckOptions { ConnectionUri = new(cs) })));
+ }
+
+ public static IResourceBuilder WithHealthCheck(this IResourceBuilder builder)
+ {
+ return builder.WithAnnotation(HealthCheckAnnotation.Create(cs => new RedisHealthCheck(cs)));
+ }
+
+ public static IResourceBuilder WithHealthCheck(this IResourceBuilder builder)
+ {
+ return builder.WithAnnotation(HealthCheckAnnotation.Create(cs => new NpgSqlHealthCheck(new NpgSqlHealthCheckOptions(cs))));
+ }
+
+ public static IResourceBuilder WithHealthCheck(
+ this IResourceBuilder builder,
+ string? endpointName = null,
+ string path = "health",
+ Action? configure = null)
+ where T : IResourceWithEndpoints
+ {
+ return builder.WithAnnotation(new HealthCheckAnnotation(async (resource, ct) =>
+ {
+ if (resource is not IResourceWithEndpoints resourceWithEndpoints)
+ {
+ return null;
+ }
+
+ var endpoint = endpointName is null
+ ? resourceWithEndpoints.GetEndpoints().FirstOrDefault(e => e.Scheme is "http" or "https")
+ : resourceWithEndpoints.GetEndpoint(endpointName);
+
+ var url = endpoint?.Url;
+
+ if (url is null)
+ {
+ return null;
+ }
+
+ var options = new UriHealthCheckOptions();
+
+ options.AddUri(new(new(url), path));
+
+ configure?.Invoke(options);
+
+ var client = new HttpClient();
+ return new UriHealthCheck(options, () => client);
+ }));
+ }
+}
\ No newline at end of file
diff --git a/counter-api-tests/README.md b/counter-api-tests/README.md
new file mode 100644
index 0000000..6a2122b
--- /dev/null
+++ b/counter-api-tests/README.md
@@ -0,0 +1,29 @@
+# Get starting
+
+## Prerequisite
+
+```powershell
+dotnet tool install -g dotnet-reportgenerator-globaltool
+```
+
+## Actions
+
+```powershell
+dotnet test --settings tests.runsettings
+```
+
+```powershell
+reportgenerator `
+ -reports:".\**\TestResults\**\coverage.cobertura.xml" `
+ -targetdir:"coverage" `
+ -reporttypes:Html
+```
+
+```powershell
+.\coverage\index.htm
+```
+
+## Refs
+
+- https://github.com/thangchung/setup-dotnet-test-projects/tree/main/src/Services/PeopleService/DNP.PeopleService.Tests
+- https://knowyourtoolset.com/2024/01/coverage-reports/
diff --git a/counter-api/counter-api.csproj b/counter-api/CoffeeShop.CounterApi.csproj
old mode 100755
new mode 100644
similarity index 63%
rename from counter-api/counter-api.csproj
rename to counter-api/CoffeeShop.CounterApi.csproj
index 8ea20e8..f8fe349
--- a/counter-api/counter-api.csproj
+++ b/counter-api/CoffeeShop.CounterApi.csproj
@@ -1,24 +1,25 @@
-
-
-
- Exe
- CounterApi
- counter-api
- latest
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Exe
+ CounterApi
+ counter-api
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/counter-api/Domain/Commands/PlaceOrderCommand.cs b/counter-api/Domain/Commands/PlaceOrderCommand.cs
index 61eaf38..46101b5 100755
--- a/counter-api/Domain/Commands/PlaceOrderCommand.cs
+++ b/counter-api/Domain/Commands/PlaceOrderCommand.cs
@@ -1,25 +1,27 @@
+using CoffeeShop.Shared.Helpers;
+
using MediatR;
namespace CounterApi.Domain.Commands;
public class CommandItem
{
- public ItemType ItemType { get; set; }
+ public ItemType ItemType { get; set; }
}
public enum CommandType
{
- PLACE_ORDER
+ PLACE_ORDER
}
public class PlaceOrderCommand : IRequest
{
- public Guid OrderId { get; set; }
- public CommandType CommandType { get; set; } = CommandType.PLACE_ORDER;
- public OrderSource OrderSource { get; set; }
- public Location Location { get; set; }
- public Guid LoyaltyMemberId { get; set; }
- public List BaristaItems { get; set; } = new();
- public List KitchenItems { get; set; } = new();
- public DateTime Timestamp { get; set; } = DateTime.UtcNow;
+ public Guid OrderId { get; set; } = GuidHelper.NewGuid();
+ public CommandType CommandType { get; set; } = CommandType.PLACE_ORDER;
+ public OrderSource OrderSource { get; set; } = OrderSource.COUNTER;
+ public Location Location { get; set; } = Location.ATLANTA;
+ public Guid LoyaltyMemberId { get; set; } = GuidHelper.NewGuid();
+ public List BaristaItems { get; set; } = [];
+ public List KitchenItems { get; set; } = [];
+ public DateTime Timestamp { get; set; } = DateTimeHelper.NewDateTime();
}
\ No newline at end of file
diff --git a/counter-api/Domain/DomainEvents/OrderIn.cs b/counter-api/Domain/DomainEvents/OrderIn.cs
index 66cbead..16f86a6 100755
--- a/counter-api/Domain/DomainEvents/OrderIn.cs
+++ b/counter-api/Domain/DomainEvents/OrderIn.cs
@@ -1,8 +1,22 @@
-using CounterApi.Domain.SharedKernel;
+using CoffeeShop.Shared.Domain;
+
+using CounterApi.Domain.Dtos;
namespace CounterApi.Domain.DomainEvents;
-public class OrderIn(Guid orderId, Guid itemLineId, ItemType itemType) : IDomainEvent
+public class BaristaOrdersPlacedDomainEvent : EventBase
+{
+ public Guid? OrderId { get; set; }
+ public List ItemLines { get; init; } = [];
+}
+
+public class KitchenOrdersPlacedDomainEvent : EventBase
+{
+ public Guid? OrderId { get; set; }
+ public List ItemLines { get; init; } = [];
+}
+
+public class OrderIn(Guid orderId, Guid itemLineId, ItemType itemType) : EventBase
{
public Guid OrderId { get; set; } = orderId;
public Guid ItemLineId { get; set; } = itemLineId;
diff --git a/counter-api/Domain/DomainEvents/OrderUp.cs b/counter-api/Domain/DomainEvents/OrderUp.cs
index f6eba90..94752ea 100755
--- a/counter-api/Domain/DomainEvents/OrderUp.cs
+++ b/counter-api/Domain/DomainEvents/OrderUp.cs
@@ -1,10 +1,10 @@
-using CounterApi.Domain.SharedKernel;
+using CoffeeShop.Shared.Domain;
namespace CounterApi.Domain.DomainEvents;
-public class OrderUp(Guid itemLineId) : IDomainEvent
+public class OrderUp(Guid itemLineId) : EventBase
{
- public Guid ItemLineId => itemLineId;
+ public Guid ItemLineId => itemLineId;
}
public class BaristaOrderUp(Guid itemLineId) : OrderUp(itemLineId)
diff --git a/counter-api/Domain/DomainEvents/OrderUpdate.cs b/counter-api/Domain/DomainEvents/OrderUpdate.cs
index b2537ef..ef406b7 100755
--- a/counter-api/Domain/DomainEvents/OrderUpdate.cs
+++ b/counter-api/Domain/DomainEvents/OrderUpdate.cs
@@ -1,30 +1,30 @@
-using CounterApi.Domain.SharedKernel;
+//using CoffeeShop.Shared.Domain;
-namespace CounterApi.Domain.DomainEvents;
+//namespace CounterApi.Domain.DomainEvents;
-public class OrderUpdate : IDomainEvent
-{
- public Guid OrderId { get; }
- public Guid ItemLineId { get; }
- public ItemType ItemType { get; }
- public OrderStatus OrderStatus { get; }
- public string? MadeBy { get; }
+//public class OrderUpdate : EventBase
+//{
+// public Guid OrderId { get; }
+// public Guid ItemLineId { get; }
+// public ItemType ItemType { get; }
+// public OrderStatus OrderStatus { get; }
+// public string? MadeBy { get; }
- public OrderUpdate(Guid orderId, Guid itemLineId, ItemType itemType, OrderStatus orderStatus)
- {
- OrderId = orderId;
- ItemLineId = itemLineId;
- ItemType = itemType;
- OrderStatus = orderStatus;
- MadeBy = null;
- }
+// public OrderUpdate(Guid orderId, Guid itemLineId, ItemType itemType, OrderStatus orderStatus)
+// {
+// OrderId = orderId;
+// ItemLineId = itemLineId;
+// ItemType = itemType;
+// OrderStatus = orderStatus;
+// MadeBy = null;
+// }
- public OrderUpdate(Guid orderId, Guid itemLineId, ItemType itemType, OrderStatus orderStatus, string madeBy)
- {
- OrderId = orderId;
- ItemLineId = itemLineId;
- ItemType = itemType;
- OrderStatus = orderStatus;
- MadeBy = madeBy;
- }
-}
\ No newline at end of file
+// public OrderUpdate(Guid orderId, Guid itemLineId, ItemType itemType, OrderStatus orderStatus, string madeBy)
+// {
+// OrderId = orderId;
+// ItemLineId = itemLineId;
+// ItemType = itemType;
+// OrderStatus = orderStatus;
+// MadeBy = madeBy;
+// }
+//}
\ No newline at end of file
diff --git a/counter-api/Domain/Order.cs b/counter-api/Domain/Order.cs
index ef4a981..707c66b 100755
--- a/counter-api/Domain/Order.cs
+++ b/counter-api/Domain/Order.cs
@@ -1,178 +1,161 @@
-using System.Text.Json.Serialization;
+using CoffeeShop.Shared.Domain;
using CounterApi.Domain.Commands;
using CounterApi.Domain.DomainEvents;
using CounterApi.Domain.Dtos;
-using CounterApi.Domain.SharedKernel;
namespace CounterApi.Domain;
-public class Order
+public class Order(Guid id, OrderSource orderSource, Guid loyaltyMemberId, OrderStatus orderStatus, Location location)
+ : EntityRootBase(id)
{
- [JsonIgnore]
- public HashSet DomainEvents { get; private set; } = new HashSet();
-
- public Guid Id { get; set; } = Guid.NewGuid();
- public OrderSource OrderSource { get; set; }
- public Guid LoyaltyMemberId { get; set; }
- public OrderStatus OrderStatus { get; set; }
- public Location Location { get; set; }
- public List ItemLines { get; set; } = new();
- public DateTime Created { get; set; } = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc);
- public DateTime? Updated { get; set; }
-
- private Order(OrderSource orderSource, Guid loyaltyMemberId, OrderStatus orderStatus, Location location)
- : this(Guid.NewGuid(), orderSource, loyaltyMemberId, orderStatus, location)
- {
- }
-
- private Order(Guid id, OrderSource orderSource, Guid loyaltyMemberId, OrderStatus orderStatus, Location location)
- {
- Id = id;
- OrderSource = orderSource;
- LoyaltyMemberId = loyaltyMemberId;
- OrderStatus = orderStatus;
- Location = location;
- }
-
- public void AddDomainEvent(IDomainEvent eventItem)
- {
- DomainEvents ??= new HashSet();
- DomainEvents.Add(eventItem);
- }
-
- public void RemoveDomainEvent(IDomainEvent eventItem)
- {
- DomainEvents?.Remove(eventItem);
- }
-
- public static async Task From(PlaceOrderCommand placeOrderCommand, IItemGateway itemGateway)
- {
- var order = new Order(placeOrderCommand.OrderSource, placeOrderCommand.LoyaltyMemberId, OrderStatus.IN_PROGRESS, placeOrderCommand.Location)
- {
- Id = placeOrderCommand.OrderId
- };
-
- if (placeOrderCommand.BaristaItems.Count != 0)
- {
- var itemTypes = placeOrderCommand.BaristaItems.Select(x => x.ItemType);
- var items = await itemGateway.GetItemsByType(itemTypes.ToArray());
- foreach (var baristaItem in placeOrderCommand.BaristaItems)
- {
- var item = items.FirstOrDefault(x => x.ItemType == baristaItem.ItemType);
- var itemLine = new ItemLine(baristaItem.ItemType, item?.ItemType.ToString()!, (decimal)item?.Price!, ItemStatus.IN_PROGRESS, true);
-
- order.AddDomainEvent(new OrderUpdate(order.Id, itemLine.Id, itemLine.ItemType, OrderStatus.IN_PROGRESS));
- order.AddDomainEvent(new BaristaOrderIn(order.Id, itemLine.Id, itemLine.ItemType));
-
- order.ItemLines.Add(itemLine);
- }
- }
-
- if (placeOrderCommand.KitchenItems.Count != 0)
- {
- var itemTypes = placeOrderCommand.KitchenItems.Select(x => x.ItemType);
- var items = await itemGateway.GetItemsByType(itemTypes.ToArray());
- foreach (var kitchenItem in placeOrderCommand.KitchenItems)
- {
- var item = items.FirstOrDefault(x => x.ItemType == kitchenItem.ItemType);
- var itemLine = new ItemLine(kitchenItem.ItemType, item?.ItemType.ToString()!, (decimal)item?.Price!, ItemStatus.IN_PROGRESS, false);
-
- order.AddDomainEvent(new OrderUpdate(order.Id, itemLine.Id, itemLine.ItemType, OrderStatus.IN_PROGRESS));
- order.AddDomainEvent(new KitchenOrderIn(order.Id, itemLine.Id, itemLine.ItemType));
-
- order.ItemLines.Add(itemLine);
- }
- }
-
- return order;
- }
-
- public Order Apply(OrderUp orderUp)
- {
- if (ItemLines.Count == 0) return this;
-
- var item = ItemLines.FirstOrDefault(i => i.Id == orderUp.ItemLineId);
-
- if (item is not null)
- {
- item.ItemStatus = ItemStatus.FULFILLED;
- // AddDomainEvent(new OrderUpdate(Id, item.Id, item.ItemType, OrderStatus.FULFILLED, orderUp.MadeBy));
- }
-
- // if there are both barista and kitchen items is fulfilled then checking status and change order to Fulfilled
- if (ItemLines.All(i => i.ItemStatus == ItemStatus.FULFILLED))
- {
- OrderStatus = OrderStatus.FULFILLED;
- }
- return this;
- }
-
- public static OrderDto ToDto(Order order)
- {
- var dto = new OrderDto
- {
- Id = order.Id,
- OrderStatus = order.OrderStatus,
- Location = order.Location,
- OrderSource = order.OrderSource,
- LoyaltyMemberId = order.LoyaltyMemberId
- };
-
- foreach (var item in order.ItemLines)
- {
- dto.ItemLines.Add(new OrderItemLineDto(item.Id, item.ItemType, item.ItemStatus));
- }
-
- return dto;
- }
-
- public static async Task FromDto(OrderDto dto, IItemGateway itemGateway)
- {
- var order = new Order(dto.Id, dto.OrderSource, dto.LoyaltyMemberId, OrderStatus.IN_PROGRESS, dto.Location)
- {
- Id = dto.Id
- };
-
- var itemTypes = dto.ItemLines.Select(x => x.ItemType);
- var items = await itemGateway.GetItemsByType(itemTypes.ToArray());
-
- foreach (var itemLineDto in dto.ItemLines)
- {
- var item = items.FirstOrDefault(x => x.ItemType == itemLineDto.ItemType);
- var itemLine = new ItemLine(itemLineDto.ItemLineId, itemLineDto.ItemType, item?.ItemType.ToString()!, (decimal)item?.Price!, ItemStatus.IN_PROGRESS, true);
- order.ItemLines.Add(itemLine);
- }
-
- return order;
- }
+ public OrderSource OrderSource { get; set; } = orderSource;
+ public Guid LoyaltyMemberId { get; set; } = loyaltyMemberId;
+ public OrderStatus OrderStatus { get; set; } = orderStatus;
+ public Location Location { get; set; } = location;
+ public List ItemLines { get; set; } = [];
+
+ public static async Task From(PlaceOrderCommand placeOrderCommand, IItemGateway itemGateway)
+ {
+ var order = new Order(placeOrderCommand.OrderId, placeOrderCommand.OrderSource, placeOrderCommand.LoyaltyMemberId, OrderStatus.IN_PROGRESS, placeOrderCommand.Location);
+
+ if (placeOrderCommand.BaristaItems.Count != 0)
+ {
+ var itemTypes = placeOrderCommand.BaristaItems.Select(x => x.ItemType);
+ var items = await itemGateway.GetItemsByType(itemTypes.ToArray());
+ foreach (var baristaItem in placeOrderCommand.BaristaItems)
+ {
+ var item = items.FirstOrDefault(x => x.ItemType == baristaItem.ItemType);
+ var itemLine = new ItemLine(baristaItem.ItemType, item?.ItemType.ToString()!, (decimal)item?.Price!, ItemStatus.IN_PROGRESS, true);
+
+ // order.AddDomainEvent(new OrderUpdate(order.Id, itemLine.Id, itemLine.ItemType, OrderStatus.IN_PROGRESS));
+ order.AddDomainEvent(new BaristaOrderIn(order.Id, itemLine.Id, itemLine.ItemType));
+
+ order.ItemLines.Add(itemLine);
+ }
+ }
+
+ if (placeOrderCommand.KitchenItems.Count != 0)
+ {
+ var itemTypes = placeOrderCommand.KitchenItems.Select(x => x.ItemType);
+ var items = await itemGateway.GetItemsByType(itemTypes.ToArray());
+ foreach (var kitchenItem in placeOrderCommand.KitchenItems)
+ {
+ var item = items.FirstOrDefault(x => x.ItemType == kitchenItem.ItemType);
+ var itemLine = new ItemLine(kitchenItem.ItemType, item?.ItemType.ToString()!, (decimal)item?.Price!, ItemStatus.IN_PROGRESS, false);
+
+ // order.AddDomainEvent(new OrderUpdate(order.Id, itemLine.Id, itemLine.ItemType, OrderStatus.IN_PROGRESS));
+ order.AddDomainEvent(new KitchenOrderIn(order.Id, itemLine.Id, itemLine.ItemType));
+
+ order.ItemLines.Add(itemLine);
+ }
+ }
+
+ return order;
+ }
+
+ public Order Apply(OrderUp orderUp)
+ {
+ if (ItemLines.Count == 0) return this;
+
+ var item = ItemLines.FirstOrDefault(i => i.Id == orderUp.ItemLineId);
+
+ if (item is not null)
+ {
+ item.ItemStatus = ItemStatus.FULFILLED;
+ // AddDomainEvent(new OrderUpdate(Id, item.Id, item.ItemType, OrderStatus.FULFILLED, orderUp.MadeBy));
+ }
+
+ // if there are both barista and kitchen items is fulfilled then checking status and change order to Fulfilled
+ if (ItemLines.All(i => i.ItemStatus == ItemStatus.FULFILLED))
+ {
+ OrderStatus = OrderStatus.FULFILLED;
+ }
+ return this;
+ }
+
+ public Order DomainEventAggregation()
+ {
+ var baristaEvents = new BaristaOrdersPlacedDomainEvent();
+ var kitchenEvents = new KitchenOrdersPlacedDomainEvent();
+ foreach (var @event in DomainEvents)
+ {
+ switch (@event)
+ {
+ case BaristaOrderIn baristaOrderInEvent:
+ baristaEvents.OrderId ??= baristaOrderInEvent.OrderId;
+ baristaEvents.ItemLines.Add(
+ new OrderItemLineDto(
+ baristaOrderInEvent.ItemLineId,
+ baristaOrderInEvent.ItemType,
+ ItemStatus.IN_PROGRESS));
+ break;
+ case KitchenOrderIn kitchenOrderInEvent:
+ kitchenEvents.OrderId ??= kitchenOrderInEvent.OrderId;
+ kitchenEvents.ItemLines.Add(
+ new OrderItemLineDto(
+ kitchenOrderInEvent.ItemLineId,
+ kitchenOrderInEvent.ItemType,
+ ItemStatus.IN_PROGRESS));
+ break;
+ }
+ }
+
+ DomainEvents.Clear();
+ DomainEvents.Add(baristaEvents);
+ DomainEvents.Add(kitchenEvents);
+
+ return this;
+ }
+
+ public static OrderDto ToDto(Order order)
+ {
+ var dto = new OrderDto
+ {
+ Id = order.Id,
+ OrderStatus = order.OrderStatus,
+ Location = order.Location,
+ OrderSource = order.OrderSource,
+ LoyaltyMemberId = order.LoyaltyMemberId
+ };
+
+ foreach (var item in order.ItemLines)
+ {
+ dto.ItemLines.Add(new OrderItemLineDto(item.Id, item.ItemType, item.ItemStatus));
+ }
+
+ return dto;
+ }
+
+ public static async Task FromDto(OrderDto dto, IItemGateway itemGateway)
+ {
+ var order = new Order(dto.Id, dto.OrderSource, dto.LoyaltyMemberId, OrderStatus.IN_PROGRESS, dto.Location);
+
+ var itemTypes = dto.ItemLines.Select(x => x.ItemType);
+ var items = await itemGateway.GetItemsByType(itemTypes.ToArray());
+
+ foreach (var itemLineDto in dto.ItemLines)
+ {
+ var item = items.FirstOrDefault(x => x.ItemType == itemLineDto.ItemType);
+ var itemLine = new ItemLine(itemLineDto.ItemLineId, itemLineDto.ItemType, item?.ItemType.ToString()!, (decimal)item?.Price!, ItemStatus.IN_PROGRESS, true);
+ order.ItemLines.Add(itemLine);
+ }
+
+ return order;
+ }
}
-public class ItemLine
+public class ItemLine(Guid id, ItemType itemType, string name, decimal price, ItemStatus itemStatus, bool isBarista)
{
- public Guid Id { get; set; } = Guid.NewGuid();
- public ItemType ItemType { get; set; }
- public string Name { get; set; }
- public decimal Price { get; set; }
- public ItemStatus ItemStatus { get; set; }
- public bool IsBaristaOrder { get; set; }
-
- public ItemLine()
- {
- }
-
- public ItemLine(ItemType itemType, string name, decimal price, ItemStatus itemStatus, bool isBarista)
- : this(Guid.NewGuid(), itemType, name, price, itemStatus, isBarista)
- {
- }
-
- public ItemLine(Guid id, ItemType itemType, string name, decimal price, ItemStatus itemStatus, bool isBarista)
- {
- Id = id;
- ItemType = itemType;
- Name = name;
- Price = price;
- ItemStatus = itemStatus;
- IsBaristaOrder = isBarista;
- }
+ public Guid Id { get; set; } = id;
+ public ItemType ItemType { get; set; } = itemType;
+ public string Name { get; set; } = name;
+ public decimal Price { get; set; } = price;
+ public ItemStatus ItemStatus { get; set; } = itemStatus;
+ public bool IsBaristaOrder { get; set; } = isBarista;
+
+ public ItemLine(ItemType itemType, string name, decimal price, ItemStatus itemStatus, bool isBarista)
+ : this(Guid.NewGuid(), itemType, name, price, itemStatus, isBarista)
+ {
+ }
}
\ No newline at end of file
diff --git a/counter-api/Domain/SharedKernel/Events.cs b/counter-api/Domain/SharedKernel/Events.cs
deleted file mode 100755
index 327e9ba..0000000
--- a/counter-api/Domain/SharedKernel/Events.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using MediatR;
-
-namespace CounterApi.Domain.SharedKernel;
-
-public interface IDomainEvent : INotification
-{
-}
-
-public class EventWrapper(IDomainEvent @event) : INotification
-{
- public IDomainEvent Event => @event;
-}
\ No newline at end of file
diff --git a/counter-api/GlobalUsings.cs b/counter-api/GlobalUsings.cs
new file mode 100644
index 0000000..5fa9510
--- /dev/null
+++ b/counter-api/GlobalUsings.cs
@@ -0,0 +1,5 @@
+global using FluentValidation;
+global using MassTransit;
+global using MediatR;
+global using Asp.Versioning;
+global using System.Text.Json;
\ No newline at end of file
diff --git a/counter-api/Infrastructure/EventDispatcher.cs b/counter-api/Infrastructure/EventDispatcher.cs
new file mode 100644
index 0000000..122482b
--- /dev/null
+++ b/counter-api/Infrastructure/EventDispatcher.cs
@@ -0,0 +1,28 @@
+using CoffeeShop.MessageContracts;
+using CoffeeShop.Shared.Domain;
+using CounterApi.Domain.DomainEvents;
+
+namespace CounterApi.Infrastructure;
+
+public class EventDispatcher(IPublishEndpoint publisher) : INotificationHandler
+{
+ public virtual async Task Handle(EventWrapper @eventWrapper, CancellationToken cancellationToken)
+ {
+ switch (@eventWrapper.Event)
+ {
+ case BaristaOrdersPlacedDomainEvent @event:
+ await publisher.Publish(new {
+ @event.OrderId,
+ @event.ItemLines
+ }, cancellationToken);
+ break;
+ case KitchenOrdersPlacedDomainEvent @event:
+ await publisher.Publish(new
+ {
+ @event.OrderId,
+ @event.ItemLines
+ }, cancellationToken);
+ break;
+ }
+ }
+}
diff --git a/counter-api/Infrastructure/ItemHttpGateway.cs b/counter-api/Infrastructure/ItemHttpGateway.cs
index c9654f9..6fa92e2 100755
--- a/counter-api/Infrastructure/ItemHttpGateway.cs
+++ b/counter-api/Infrastructure/ItemHttpGateway.cs
@@ -1,4 +1,3 @@
-using System.Text.Json;
using CounterApi.Domain;
using CounterApi.Domain.Dtos;
@@ -13,7 +12,7 @@ public async Task> GetItemsByType(ItemType[] itemTypes)
var httpClient = httpClientFactory.CreateClient();
httpClient.BaseAddress = new Uri(config.GetValue("ProductApiUrl")!); //todo: need truly service discovery
- var httpResponseMessage = await httpClient.GetFromJsonAsync>(config.GetValue("GetItemTypesApiRoute", "/v1/api/item-types"));
+ var httpResponseMessage = await httpClient.GetFromJsonAsync>(config.GetValue("GetItemTypesApiRoute", "/api/v1/item-types"));
logger.LogInformation("Can get {Count} items", httpResponseMessage?.Count);
logger.LogInformation("JSON: {HttpResponseMessage}", JsonSerializer.Serialize(httpResponseMessage));
diff --git a/counter-api/IntegrationEvents/EventHandlers/BaristaOrderUpdatedConsumer.cs b/counter-api/IntegrationEvents/EventHandlers/BaristaOrderUpdatedConsumer.cs
index da6bc7e..a815c65 100755
--- a/counter-api/IntegrationEvents/EventHandlers/BaristaOrderUpdatedConsumer.cs
+++ b/counter-api/IntegrationEvents/EventHandlers/BaristaOrderUpdatedConsumer.cs
@@ -1,7 +1,5 @@
using CoffeeShop.MessageContracts;
-using MassTransit;
-
namespace CounterApi.IntegrationEvents.EventHandlers;
internal class BaristaOrderUpdatedConsumer(IPublishEndpoint publisher, ILogger logger)
diff --git a/counter-api/IntegrationEvents/EventHandlers/KitchenOrderUpdatedConsumer.cs b/counter-api/IntegrationEvents/EventHandlers/KitchenOrderUpdatedConsumer.cs
index 6d40df0..2eb3523 100755
--- a/counter-api/IntegrationEvents/EventHandlers/KitchenOrderUpdatedConsumer.cs
+++ b/counter-api/IntegrationEvents/EventHandlers/KitchenOrderUpdatedConsumer.cs
@@ -1,7 +1,5 @@
using CoffeeShop.MessageContracts;
-using MassTransit;
-
namespace CounterApi.IntegrationEvents.EventHandlers;
internal class KitchenOrderUpdatedConsumer(IPublishEndpoint publisher, ILogger logger)
diff --git a/counter-api/IntegrationEvents/Events/OrderPlaced.cs b/counter-api/IntegrationEvents/Events/OrderPlaced.cs
index 6ccd42b..09c15a8 100755
--- a/counter-api/IntegrationEvents/Events/OrderPlaced.cs
+++ b/counter-api/IntegrationEvents/Events/OrderPlaced.cs
@@ -5,11 +5,11 @@ namespace CoffeeShop.MessageContracts;
public record BaristaOrderPlaced
{
public Guid OrderId { get; init; }
- public List ItemLines { get; init; } = new();
+ public List ItemLines { get; init; } = [];
}
public record KitchenOrderPlaced
{
public Guid OrderId { get; init; }
- public List ItemLines { get; init; } = new();
+ public List ItemLines { get; init; } = [];
}
\ No newline at end of file
diff --git a/counter-api/IntegrationEvents/Events/OrderUpdated.cs b/counter-api/IntegrationEvents/Events/OrderUpdated.cs
index bf4de4f..3bba3b6 100755
--- a/counter-api/IntegrationEvents/Events/OrderUpdated.cs
+++ b/counter-api/IntegrationEvents/Events/OrderUpdated.cs
@@ -4,12 +4,12 @@ namespace CoffeeShop.MessageContracts;
public record BaristaOrderUpdated
{
- public Guid OrderId { get; init; }
- public List ItemLines { get; init; } = new();
+ public Guid OrderId { get; init; }
+ public List ItemLines { get; init; } = new();
}
public record KitchenOrderUpdated
{
- public Guid OrderId { get; init; }
- public List ItemLines { get; init; } = new();
+ public Guid OrderId { get; init; }
+ public List ItemLines { get; init; } = new();
}
\ No newline at end of file
diff --git a/counter-api/InternalsVisibleTo.cs b/counter-api/InternalsVisibleTo.cs
new file mode 100644
index 0000000..efd921e
--- /dev/null
+++ b/counter-api/InternalsVisibleTo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("CoffeeShop.CounterApi.IntegrationTests")]
\ No newline at end of file
diff --git a/counter-api/Program.cs b/counter-api/Program.cs
index 5840e90..faa3002 100755
--- a/counter-api/Program.cs
+++ b/counter-api/Program.cs
@@ -1,25 +1,46 @@
-using FluentValidation;
-using CounterApi.UseCases;
-using MassTransit;
using CounterApi.IntegrationEvents.EventHandlers;
using CounterApi.Infrastructure.Gateways;
using CounterApi.Domain;
+using CoffeeShop.Shared.Endpoint;
+using CoffeeShop.Shared.Exceptions;
+using CoffeeShop.Shared.OpenTelemetry;
+using CoffeeShop.Shared.OpenTelemetry.OtelMassTransit;
+using System.Diagnostics.CodeAnalysis;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
+builder.Services.AddExceptionHandler();
+builder.Services.AddExceptionHandler();
builder.Services.AddProblemDetails();
builder.Services.AddHttpContextAccessor();
-builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining());
-builder.Services.AddValidatorsFromAssemblyContaining();
+builder.Services.AddMediatR(cfg => {
+ cfg.RegisterServicesFromAssemblyContaining();
+ cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
+ cfg.AddOpenBehavior(typeof(HandlerBehavior<,>));
+});
+builder.Services.AddValidatorsFromAssemblyContaining(includeInternalTypes: true);
-builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
+builder.Services.AddEndpointsApiExplorer();
+
+builder.Services.AddApiVersioning(options =>
+{
+ options.DefaultApiVersion = new ApiVersion(1);
+ options.ApiVersionReader = new UrlSegmentApiVersionReader();
+}).AddApiExplorer(options =>
+{
+ options.GroupNameFormat = "'v'V";
+ options.SubstituteApiVersionInUrl = true;
+});
+
+builder.Services.AddEndpoints(typeof(Program).Assembly);
-// builder.Services.AddHttpClient(client =>
-// client.BaseAddress = new(builder.Configuration.GetValue("ProductApiUrl")!));
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
builder.Services.AddScoped();
builder.Services.AddMassTransit(x =>
@@ -31,36 +52,40 @@
x.UsingRabbitMq((context, cfg) =>
{
- // Console.WriteLine($"RabbitMQ Conn: {builder.Configuration.GetConnectionString("rabbitmq")}");
- // cfg.Host(new Uri(builder.Configuration.GetConnectionString("rabbitmq")!), h => {
- // h.Username("guest");
- // h.Password("guest");
- // });
-
cfg.Host(builder.Configuration.GetConnectionString("rabbitmq")!);
- cfg.ConfigureEndpoints(context);
+
+ cfg.UseSendFilter(typeof(OtelSendFilter<>), context);
+ cfg.UsePublishFilter(typeof(OtelPublishFilter<>), context);
+ cfg.UseConsumeFilter(typeof(OTelConsumeFilter<>), context);
+
+ cfg.ConfigureEndpoints(context);
});
});
var app = builder.Build();
+var apiVersionSet = app.NewApiVersionSet()
+ .HasApiVersion(new ApiVersion(1))
+ .ReportApiVersions()
+ .Build();
+
+var versionedGroup = app
+ .MapGroup("api/v{version:apiVersion}")
+ .WithApiVersionSet(apiVersionSet);
+
app.UseExceptionHandler();
-if (app.Environment.IsDevelopment())
+if(app.Environment.IsDevelopment())
{
- app.UseSwagger();
- app.UseSwaggerUI();
+ app.UseSwagger();
}
app.UseRouting();
app.MapDefaultEndpoints();
-
-app.Map("/", () => Results.Redirect("/swagger"));
-
-// todo
-_ = app.MapOrderInApiRoutes()
- // .MapOrderUpApiRoutes()
- .MapOrderFulfillmentApiRoutes();
+app.MapEndpoints(versionedGroup);
app.Run();
+
+[ExcludeFromCodeCoverage]
+public partial class Program;
diff --git a/counter-api/UseCases/OrderFulfillmentQuery.cs b/counter-api/UseCases/OrderFulfillmentQuery.cs
index 44016a5..27f991a 100755
--- a/counter-api/UseCases/OrderFulfillmentQuery.cs
+++ b/counter-api/UseCases/OrderFulfillmentQuery.cs
@@ -1,17 +1,13 @@
-using CounterApi.Domain.Dtos;
-
-using FluentValidation;
-using MediatR;
+using CoffeeShop.Shared.Endpoint;
namespace CounterApi.UseCases;
-internal static class OrderFulfillmentRouteMapper
+public class OrderFulfillmentEndpoint : IEndpoint
{
- public static IEndpointRouteBuilder MapOrderFulfillmentApiRoutes(this IEndpointRouteBuilder builder)
- {
- builder.MapGet("/v1/api/fulfillment-orders", async (ISender sender) => await sender.Send(new OrderFulfillmentQuery()));
- return builder;
- }
+ public void MapEndpoint(IEndpointRouteBuilder app)
+ {
+ app.MapGet("fulfillment-orders", async (ISender sender) => await sender.Send(new OrderFulfillmentQuery()));
+ }
}
public record OrderFulfillmentQuery : IRequest
@@ -20,33 +16,33 @@ public record OrderFulfillmentQuery : IRequest
internal class OrderFulfillmentValidator : AbstractValidator
{
- public OrderFulfillmentValidator()
- {
- }
+ public OrderFulfillmentValidator()
+ {
+ }
}
internal class OrderFulfillmentQueryHandler : IRequestHandler
{
- public async Task Handle(OrderFulfillmentQuery query, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(query);
+ public async Task Handle(OrderFulfillmentQuery query, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(query);
- var orderGuidProcceed = new List();
+ var orderGuidProcceed = new List();
- // todo
- // var orderGuidList = await daprClient.GetStateAsync>("statestore", "order-list", cancellationToken: cancellationToken);
- // if (orderGuidList != null && orderGuidList?.Count > 0)
- // {
- // foreach (var orderGuid in orderGuidList)
- // {
- // orderGuidProcceed.Add($"order-{orderGuid}");
- // }
+ // todo
+ // var orderGuidList = await daprClient.GetStateAsync>("statestore", "order-list", cancellationToken: cancellationToken);
+ // if (orderGuidList != null && orderGuidList?.Count > 0)
+ // {
+ // foreach (var orderGuid in orderGuidList)
+ // {
+ // orderGuidProcceed.Add($"order-{orderGuid}");
+ // }
- // var mulitpleStateResult = await daprClient.GetBulkStateAsync("statestore", orderGuidProcceed, parallelism: 1, cancellationToken: cancellationToken);
+ // var mulitpleStateResult = await daprClient.GetBulkStateAsync("statestore", orderGuidProcceed, parallelism: 1, cancellationToken: cancellationToken);
- // return Results.Ok(mulitpleStateResult.Select(x => JsonSerializer.Serialize(x.Value)).ToList());
- // }
+ // return Results.Ok(mulitpleStateResult.Select(x => JsonSerializer.Serialize(x.Value)).ToList());
+ // }
- return Results.Ok(orderGuidProcceed);
- }
+ return Results.Ok(orderGuidProcceed);
+ }
}
\ No newline at end of file
diff --git a/counter-api/UseCases/PlaceOrderCommand.cs b/counter-api/UseCases/PlaceOrderCommand.cs
index 0f945c9..15596b9 100755
--- a/counter-api/UseCases/PlaceOrderCommand.cs
+++ b/counter-api/UseCases/PlaceOrderCommand.cs
@@ -1,117 +1,45 @@
-using FluentValidation;
-using MediatR;
-using CounterApi.Domain.Commands;
+using CoffeeShop.Shared.Domain;
+using CoffeeShop.Shared.Endpoint;
+using CoffeeShop.Shared.OpenTelemetry;
+
using CounterApi.Domain;
-using System.Text.Json;
-using MassTransit;
-using CounterApi.Domain.SharedKernel;
-using CoffeeShop.MessageContracts;
-using CounterApi.Domain.DomainEvents;
-using CounterApi.Domain.Dtos;
+using CounterApi.Domain.Commands;
namespace CounterApi.UseCases;
-public static class OrderInRouteMapper
+public class OrderInEndpoint : IEndpoint
{
- public static IEndpointRouteBuilder MapOrderInApiRoutes(this IEndpointRouteBuilder builder)
- {
- builder.MapPost("/v1/api/orders", async (PlaceOrderCommand command, ISender sender) => await sender.Send(command));
- return builder;
- }
+ public void MapEndpoint(IEndpointRouteBuilder app)
+ {
+ app.MapPost("orders", async (PlaceOrderCommand command, ISender sender) => await sender.Send(command));
+ }
}
internal class OrderInValidator : AbstractValidator
{
+ public OrderInValidator()
+ {
+ RuleFor(command => command.OrderId)
+ .NotEmpty().WithMessage("The order identifier can't be empty.");
+ }
}
-internal class PlaceOrderHandler(IPublishEndpoint publisher, IItemGateway itemGateway, ILogger logger)
- : IRequestHandler
+// [IgnoreOTelOnHandler]
+internal class PlaceOrderHandler(IPublisher publisher, IItemGateway itemGateway, ILogger logger)
+ : IRequestHandler
{
- public async Task Handle(PlaceOrderCommand placeOrderCommand, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(placeOrderCommand);
-
- var itemTypes = new List { ItemType.ESPRESSO };
- var items = await itemGateway.GetItemsByType(itemTypes.ToArray());
- logger.LogInformation("[ProductAPI] Query: {JsonObject}", JsonSerializer.Serialize(items));
-
- var orderId = Guid.NewGuid().ToString();
-
- var order = await Order.From(placeOrderCommand, itemGateway);
- order.Id = new Guid(orderId);
-
- // map domain object to dto
- var dto = Order.ToDto(order);
- dto.OrderStatus = OrderStatus.IN_PROGRESS;
-
- logger.LogInformation("Got {count} domain events.", order.DomainEvents.Count);
- var @events = new IDomainEvent[order.DomainEvents.Count];
- order.DomainEvents.CopyTo(@events);
- order.DomainEvents.Clear();
-
- var baristaEvents = new Dictionary();
- var kitchenEvents = new Dictionary();
- foreach (var @event in @events)
- {
- switch (@event)
- {
- case BaristaOrderIn baristaOrderInEvent:
- if (!baristaEvents.TryGetValue(baristaOrderInEvent.OrderId, out _))
- {
- baristaEvents.Add(baristaOrderInEvent.OrderId, new BaristaOrderPlaced
- {
- OrderId = baristaOrderInEvent.OrderId,
- ItemLines =
- [
- new(baristaOrderInEvent.ItemLineId, baristaOrderInEvent.ItemType, ItemStatus.IN_PROGRESS)
- ]
- });
- }
- else
- {
- baristaEvents[baristaOrderInEvent.OrderId].ItemLines.Add(
- new OrderItemLineDto(baristaOrderInEvent.ItemLineId, baristaOrderInEvent.ItemType, ItemStatus.IN_PROGRESS));
- }
-
- break;
- case KitchenOrderIn kitchenOrderInEvent:
- if (!kitchenEvents.TryGetValue(kitchenOrderInEvent.OrderId, out _))
- {
- kitchenEvents.Add(kitchenOrderInEvent.OrderId, new KitchenOrderPlaced
- {
- OrderId = kitchenOrderInEvent.OrderId,
- ItemLines =
- [
- new(kitchenOrderInEvent.ItemLineId, kitchenOrderInEvent.ItemType, ItemStatus.IN_PROGRESS)
- ]
- });
- }
- else
- {
- kitchenEvents[kitchenOrderInEvent.OrderId].ItemLines.Add(
- new OrderItemLineDto(kitchenOrderInEvent.ItemLineId, kitchenOrderInEvent.ItemType, ItemStatus.IN_PROGRESS));
- }
-
- break;
- }
- }
+ public async Task Handle(PlaceOrderCommand placeOrderCommand, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(placeOrderCommand);
- if (baristaEvents.Count > 0)
- {
- logger.LogInformation("Pushlish barista events.");
- foreach(var @event in baristaEvents) {
- await publisher.Publish(@event.Value, cancellationToken);
- }
- }
+ var itemTypes = new List { ItemType.ESPRESSO }; // todo: remove hard-code
+ var items = await itemGateway.GetItemsByType(itemTypes.ToArray());
+ logger.LogInformation("[ProductAPI] Query: {JsonObject}", JsonSerializer.Serialize(items));
- if (kitchenEvents.Count > 0)
- {
- logger.LogInformation("Pushlish kitchen events.");
- foreach(var @event in kitchenEvents) {
- await publisher.Publish(@event.Value, cancellationToken);
- }
- }
+ var order = await Order.From(placeOrderCommand, itemGateway);
+ order.DomainEventAggregation();
+ await order.RelayAndPublishEvents(publisher, cancellationToken);
- return Results.Ok();
- }
+ return Results.Ok();
+ }
}
diff --git a/counter-api/appsettings.Development.json b/counter-api/appsettings.Development.json
index b844778..5e9fd7f 100755
--- a/counter-api/appsettings.Development.json
+++ b/counter-api/appsettings.Development.json
@@ -4,7 +4,5 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
- },
- "RabbitMqUrl": "localhost",
- "ProductApiUrl": "http://productApi"
+ }
}
\ No newline at end of file
diff --git a/counter-api/appsettings.json b/counter-api/appsettings.json
index 4d56694..e43f94d 100755
--- a/counter-api/appsettings.json
+++ b/counter-api/appsettings.json
@@ -1,9 +1,14 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
- "AllowedHosts": "*"
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "postgres": "Server=localhost;Port=5432;Database=postgres;",
+ "rabbitmq": "amqp://localhost"
+ },
+ "ProductApiUrl": "http://product-api"
}
diff --git a/deployment-product-api.yaml b/deployment-product-api.yaml
new file mode 100644
index 0000000..7859b8d
--- /dev/null
+++ b/deployment-product-api.yaml
@@ -0,0 +1,69 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations:
+ deployment.kubernetes.io/revision: "1"
+ creationTimestamp: "2024-05-31T08:04:58Z"
+ generation: 1
+ labels:
+ app: product-api
+ name: product-api
+ namespace: coffeeshop
+ resourceVersion: "1270"
+ uid: c29c2b79-f604-4973-a349-a316c82b8680
+spec:
+ minReadySeconds: 60
+ progressDeadlineSeconds: 600
+ replicas: 1
+ revisionHistoryLimit: 10
+ selector:
+ matchLabels:
+ app: product-api
+ strategy:
+ type: Recreate
+ template:
+ metadata:
+ creationTimestamp: null
+ labels:
+ app: product-api
+ spec:
+ containers:
+ - envFrom:
+ - configMapRef:
+ name: product-api
+ image: k3d-myregistry.localhost:12345/product-api:latest
+ imagePullPolicy: IfNotPresent
+ name: product-api
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ - containerPort: 8443
+ name: https
+ protocol: TCP
+ resources: {}
+ terminationMessagePath: /dev/termination-log
+ terminationMessagePolicy: File
+ dnsPolicy: ClusterFirst
+ restartPolicy: Always
+ schedulerName: default-scheduler
+ securityContext: {}
+ terminationGracePeriodSeconds: 180
+status:
+ conditions:
+ - lastTransitionTime: "2024-05-31T08:04:58Z"
+ lastUpdateTime: "2024-05-31T08:04:58Z"
+ message: Deployment does not have minimum availability.
+ reason: MinimumReplicasUnavailable
+ status: "False"
+ type: Available
+ - lastTransitionTime: "2024-05-31T08:04:58Z"
+ lastUpdateTime: "2024-05-31T08:04:58Z"
+ message: ReplicaSet "product-api-788f967db8" is progressing.
+ reason: ReplicaSetUpdated
+ status: "True"
+ type: Progressing
+ observedGeneration: 1
+ replicas: 1
+ unavailableReplicas: 1
+ updatedReplicas: 1
diff --git a/global.json b/global.json
index fe565a4..abed231 100755
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
- "version": "8.0.0",
- "rollForward": "latestMajor",
+ "version": "8.0.301",
+ "rollForward": "major",
"allowPrerelease": true
}
}
\ No newline at end of file
diff --git a/kitchen-api/kitchen-api.csproj b/kitchen-api/CoffeeShop.KitchenApi.csproj
old mode 100755
new mode 100644
similarity index 65%
rename from kitchen-api/kitchen-api.csproj
rename to kitchen-api/CoffeeShop.KitchenApi.csproj
index a8f6b19..42a9c58
--- a/kitchen-api/kitchen-api.csproj
+++ b/kitchen-api/CoffeeShop.KitchenApi.csproj
@@ -1,24 +1,23 @@
-
-
-
- Exe
- KitchenApi
- kitchen-api
- latest
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Exe
+ KitchenApi
+ kitchen-api
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kitchen-api/Program.cs b/kitchen-api/Program.cs
index 37b1a21..8c9df8c 100755
--- a/kitchen-api/Program.cs
+++ b/kitchen-api/Program.cs
@@ -1,19 +1,29 @@
+using CoffeeShop.Shared.Exceptions;
+using CoffeeShop.Shared.OpenTelemetry;
+
using FluentValidation;
+
using KitchenApi.IntegrationEvents.EventHandlers;
+
using MassTransit;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
+builder.Services.AddExceptionHandler();
+builder.Services.AddExceptionHandler();
builder.Services.AddProblemDetails();
builder.Services.AddHttpContextAccessor();
-builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining());
-builder.Services.AddValidatorsFromAssemblyContaining();
+builder.Services.AddMediatR(cfg => {
+ cfg.RegisterServicesFromAssemblyContaining();
+ cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
+ cfg.AddOpenBehavior(typeof(HandlerBehavior<,>));
+});
+builder.Services.AddValidatorsFromAssemblyContaining(includeInternalTypes: true);
builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddSwaggerGen();
builder.Services.AddMassTransit(x =>
{
@@ -23,29 +33,23 @@
x.UsingRabbitMq((context, cfg) =>
{
- // cfg.Host(builder.Configuration.GetValue("RabbitMqUrl")!);
-
cfg.Host(builder.Configuration.GetConnectionString("rabbitmq")!);
cfg.ConfigureEndpoints(context);
});
});
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
var app = builder.Build();
app.UseExceptionHandler();
-if (app.Environment.IsDevelopment())
-{
- app.UseSwagger();
- app.UseSwaggerUI();
-}
-
app.UseRouting();
app.MapDefaultEndpoints();
-app.Map("/", () => Results.Redirect("/swagger"));
-
-// _ = app.MapOrderUpApiRoutes();
-
app.Run();
+
+public partial class Program;
diff --git a/kitchen-api/appsettings.json b/kitchen-api/appsettings.json
index 4d56694..430039c 100755
--- a/kitchen-api/appsettings.json
+++ b/kitchen-api/appsettings.json
@@ -1,9 +1,13 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
- "AllowedHosts": "*"
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "postgres": "Server=localhost;Port=5432;Database=postgres;",
+ "rabbitmq": "amqp://localhost"
+ }
}
diff --git a/order-summary/CoffeeShop.OrderSummary.csproj b/order-summary/CoffeeShop.OrderSummary.csproj
new file mode 100644
index 0000000..64e3f97
--- /dev/null
+++ b/order-summary/CoffeeShop.OrderSummary.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/order-summary/Consumers/OrderConsumer.cs b/order-summary/Consumers/OrderConsumer.cs
new file mode 100644
index 0000000..109cf78
--- /dev/null
+++ b/order-summary/Consumers/OrderConsumer.cs
@@ -0,0 +1,52 @@
+using System.Diagnostics;
+
+using CoffeeShop.MessageContracts;
+using CoffeeShop.OrderSummary.Models;
+using CoffeeShop.Shared.Helpers;
+
+namespace CoffeeShop.OrderSummary.Consumers;
+
+public class OrderConsumer(IDocumentSession documentSession, ILogger logger) : IConsumer, IConsumer
+{
+ public async Task Consume(ConsumeContext context)
+ {
+ logger.LogInformation("Consumer: {0} with orderId={1}", nameof(BaristaOrderPlaced), context.Message.OrderId);
+
+ using var activity = new ActivitySource("masstransit").StartActivity($"Consumer: {nameof(BaristaOrderPlaced)} with orderId={context.Message.OrderId}");
+ try
+ {
+ await documentSession.Events.WriteToAggregate(
+ GuidHelper.NewGuid(),
+ stream => { stream.AppendOne(context.Message); });
+ }
+ catch (Exception ex)
+ {
+ activity?.AddTag("exception.message", ex.Message);
+ activity?.AddTag("exception.stacktrace", ex.ToString());
+ activity?.AddTag("exception.type", ex.GetType().FullName);
+ activity?.SetStatus(ActivityStatusCode.Error);
+ throw;
+ }
+ }
+
+ public async Task Consume(ConsumeContext context)
+ {
+ logger.LogInformation("Consumer: {0} with orderId={1}", nameof(KitchenOrderPlaced), context.Message.OrderId);
+
+ using var activity = new ActivitySource("masstransit").StartActivity($"Consumer: {nameof(BaristaOrderPlaced)} with orderId={context.Message.OrderId}");
+ try
+ {
+ await documentSession.Events.WriteToAggregate(
+ GuidHelper.NewGuid(),
+ stream => { stream.AppendOne(context.Message); });
+ }
+ catch (Exception ex)
+ {
+ activity?.AddTag("exception.message", ex.Message);
+ activity?.AddTag("exception.stacktrace", ex.ToString());
+ activity?.AddTag("exception.type", ex.GetType().FullName);
+ activity?.SetStatus(ActivityStatusCode.Error);
+ throw;
+ }
+ }
+}
diff --git a/order-summary/Events/Events.cs b/order-summary/Events/Events.cs
new file mode 100644
index 0000000..b3b499b
--- /dev/null
+++ b/order-summary/Events/Events.cs
@@ -0,0 +1,50 @@
+namespace CoffeeShop.MessageContracts;
+
+public enum ItemType
+{
+ // Beverages
+ CAPPUCCINO,
+ COFFEE_BLACK,
+ COFFEE_WITH_ROOM,
+ ESPRESSO,
+ ESPRESSO_DOUBLE,
+ LATTE,
+ // Food
+ CAKEPOP,
+ CROISSANT,
+ MUFFIN,
+ CROISSANT_CHOCOLATE
+}
+
+public enum ItemStatus
+{
+ PLACED,
+ IN_PROGRESS,
+ FULFILLED
+}
+
+public record OrderItemLineDto(Guid ItemLineId, ItemType ItemType, ItemStatus ItemStatus);
+
+public record BaristaOrderPlaced
+{
+ public Guid OrderId { get; init; }
+ public List ItemLines { get; init; } = [];
+}
+
+public record KitchenOrderPlaced
+{
+ public Guid OrderId { get; init; }
+ public List ItemLines { get; init; } = [];
+}
+
+public record BaristaOrderUpdated
+{
+ public Guid OrderId { get; init; }
+ public List ItemLines { get; init; } = [];
+}
+
+public record KitchenOrderUpdated
+{
+ public Guid OrderId { get; init; }
+ public List ItemLines { get; init; } = [];
+}
diff --git a/order-summary/Features/OrderSummaryQuery.cs b/order-summary/Features/OrderSummaryQuery.cs
new file mode 100644
index 0000000..29be632
--- /dev/null
+++ b/order-summary/Features/OrderSummaryQuery.cs
@@ -0,0 +1,54 @@
+using CoffeeShop.MessageContracts;
+using CoffeeShop.Shared.Endpoint;
+
+namespace CoffeeShop.OrderSummary.Features;
+
+public class OrderSummaryEndpoint : IEndpoint
+{
+ public void MapEndpoint(IEndpointRouteBuilder app)
+ {
+ app.MapGet("summary", (HttpContext context, IQuerySession querySession, Guid orderId) =>
+ querySession.Json.WriteById(orderId, context)
+ );
+ }
+}
+
+public class OrderSummaryQuery
+{
+ public Guid Id { get; set; }
+ public int NumberOfBaristaProcessed { get; set; }
+ public int NumberOfKitchenProcessed { get; set; }
+ public int NumberOfBaristaUpdated { get; set; }
+ public int NumberOfKitchenUpdated { get; set; }
+}
+
+public class OrderSummaryProjection : MultiStreamProjection
+{
+ public OrderSummaryProjection()
+ {
+ Identity(e => e.OrderId);
+ Identity(e => e.OrderId);
+ Identity(e => e.OrderId);
+ Identity(e => e.OrderId);
+ }
+
+ public void Apply(BaristaOrderPlaced @event, OrderSummaryQuery current)
+ {
+ current.NumberOfBaristaProcessed++;
+ }
+
+ public void Apply(KitchenOrderPlaced @event, OrderSummaryQuery current)
+ {
+ current.NumberOfKitchenProcessed++;
+ }
+
+ public void Apply(BaristaOrderUpdated @event, OrderSummaryQuery current)
+ {
+ current.NumberOfBaristaUpdated++;
+ }
+
+ public void Apply(KitchenOrderUpdated @event, OrderSummaryQuery current)
+ {
+ current.NumberOfKitchenUpdated++;
+ }
+}
diff --git a/order-summary/GlobalUsings.cs b/order-summary/GlobalUsings.cs
new file mode 100644
index 0000000..2b4364f
--- /dev/null
+++ b/order-summary/GlobalUsings.cs
@@ -0,0 +1,12 @@
+global using FluentValidation;
+global using MassTransit;
+global using JasperFx.CodeGeneration;
+global using Marten;
+global using Marten.AspNetCore;
+global using Marten.Events.Daemon.Resiliency;
+global using Marten.Events.Projections;
+global using Weasel.Core;
+global using MediatR;
+global using Asp.Versioning;
+global using Asp.Versioning.Builder;
+global using System.Text.Json;
\ No newline at end of file
diff --git a/order-summary/Internal/Generated/DocumentStorage/OrderProvider60467594.cs b/order-summary/Internal/Generated/DocumentStorage/OrderProvider60467594.cs
new file mode 100644
index 0000000..ca822b8
--- /dev/null
+++ b/order-summary/Internal/Generated/DocumentStorage/OrderProvider60467594.cs
@@ -0,0 +1,1079 @@
+//
+#pragma warning disable
+using CoffeeShop.OrderSummary.Models;
+using Marten.Internal;
+using Marten.Internal.Storage;
+using Marten.Schema;
+using Marten.Schema.Arguments;
+using Npgsql;
+using System;
+using System.Collections.Generic;
+using Weasel.Core;
+using Weasel.Postgresql;
+
+namespace Marten.Generated.DocumentStorage
+{
+ // START: UpsertOrderOperation60467594
+ public class UpsertOrderOperation60467594 : Marten.Internal.Operations.StorageOperation
+ {
+ private readonly CoffeeShop.OrderSummary.Models.Order _document;
+ private readonly System.Guid _id;
+ private readonly System.Collections.Generic.Dictionary _versions;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public UpsertOrderOperation60467594(CoffeeShop.OrderSummary.Models.Order document, System.Guid id, System.Collections.Generic.Dictionary versions, Marten.Schema.DocumentMapping mapping) : base(document, id, versions, mapping)
+ {
+ _document = document;
+ _id = id;
+ _versions = versions;
+ _mapping = mapping;
+ }
+
+
+ public const string COMMAND_TEXT = "select order_summary.mt_upsert_order(?, ?, ?, ?)";
+
+
+ public override void Postprocess(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions)
+ {
+ if (postprocessRevision(reader, exceptions))
+ {
+ }
+
+ }
+
+
+ public override async System.Threading.Tasks.Task PostprocessAsync(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions, System.Threading.CancellationToken token)
+ {
+ if (await postprocessRevisionAsync(reader, exceptions, token))
+ {
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.OperationRole Role()
+ {
+ return Marten.Internal.Operations.OperationRole.Upsert;
+ }
+
+
+ public override string CommandText()
+ {
+ return COMMAND_TEXT;
+ }
+
+
+ public override NpgsqlTypes.NpgsqlDbType DbType()
+ {
+ return NpgsqlTypes.NpgsqlDbType.Uuid;
+ }
+
+
+ public override void ConfigureParameters(Npgsql.NpgsqlParameter[] parameters, CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session)
+ {
+ parameters[0].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Jsonb;
+ parameters[0].Value = session.Serializer.ToJson(_document);
+ // .Net Class Type
+ parameters[1].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Varchar;
+ parameters[1].Value = _document.GetType().FullName;
+ parameters[2].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Uuid;
+ parameters[2].Value = document.Id;
+ setCurrentRevisionParameter(parameters[3]);
+ }
+
+ }
+
+ // END: UpsertOrderOperation60467594
+
+
+ // START: InsertOrderOperation60467594
+ public class InsertOrderOperation60467594 : Marten.Internal.Operations.StorageOperation
+ {
+ private readonly CoffeeShop.OrderSummary.Models.Order _document;
+ private readonly System.Guid _id;
+ private readonly System.Collections.Generic.Dictionary _versions;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public InsertOrderOperation60467594(CoffeeShop.OrderSummary.Models.Order document, System.Guid id, System.Collections.Generic.Dictionary versions, Marten.Schema.DocumentMapping mapping) : base(document, id, versions, mapping)
+ {
+ _document = document;
+ _id = id;
+ _versions = versions;
+ _mapping = mapping;
+ }
+
+
+ public const string COMMAND_TEXT = "select order_summary.mt_insert_order(?, ?, ?, ?)";
+
+
+ public override void Postprocess(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions)
+ {
+ if (postprocessRevision(reader, exceptions))
+ {
+ }
+
+ }
+
+
+ public override async System.Threading.Tasks.Task PostprocessAsync(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions, System.Threading.CancellationToken token)
+ {
+ if (await postprocessRevisionAsync(reader, exceptions, token))
+ {
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.OperationRole Role()
+ {
+ return Marten.Internal.Operations.OperationRole.Insert;
+ }
+
+
+ public override string CommandText()
+ {
+ return COMMAND_TEXT;
+ }
+
+
+ public override NpgsqlTypes.NpgsqlDbType DbType()
+ {
+ return NpgsqlTypes.NpgsqlDbType.Uuid;
+ }
+
+
+ public override void ConfigureParameters(Npgsql.NpgsqlParameter[] parameters, CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session)
+ {
+ parameters[0].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Jsonb;
+ parameters[0].Value = session.Serializer.ToJson(_document);
+ // .Net Class Type
+ parameters[1].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Varchar;
+ parameters[1].Value = _document.GetType().FullName;
+ parameters[2].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Uuid;
+ parameters[2].Value = document.Id;
+ setCurrentRevisionParameter(parameters[3]);
+ }
+
+ }
+
+ // END: InsertOrderOperation60467594
+
+
+ // START: UpdateOrderOperation60467594
+ public class UpdateOrderOperation60467594 : Marten.Internal.Operations.StorageOperation
+ {
+ private readonly CoffeeShop.OrderSummary.Models.Order _document;
+ private readonly System.Guid _id;
+ private readonly System.Collections.Generic.Dictionary _versions;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public UpdateOrderOperation60467594(CoffeeShop.OrderSummary.Models.Order document, System.Guid id, System.Collections.Generic.Dictionary versions, Marten.Schema.DocumentMapping mapping) : base(document, id, versions, mapping)
+ {
+ _document = document;
+ _id = id;
+ _versions = versions;
+ _mapping = mapping;
+ }
+
+
+ public const string COMMAND_TEXT = "select order_summary.mt_update_order(?, ?, ?, ?)";
+
+
+ public override void Postprocess(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions)
+ {
+ if (postprocessRevision(reader, exceptions))
+ {
+ }
+
+ }
+
+
+ public override async System.Threading.Tasks.Task PostprocessAsync(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions, System.Threading.CancellationToken token)
+ {
+ if (await postprocessRevisionAsync(reader, exceptions, token))
+ {
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.OperationRole Role()
+ {
+ return Marten.Internal.Operations.OperationRole.Update;
+ }
+
+
+ public override string CommandText()
+ {
+ return COMMAND_TEXT;
+ }
+
+
+ public override NpgsqlTypes.NpgsqlDbType DbType()
+ {
+ return NpgsqlTypes.NpgsqlDbType.Uuid;
+ }
+
+
+ public override void ConfigureParameters(Npgsql.NpgsqlParameter[] parameters, CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session)
+ {
+ parameters[0].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Jsonb;
+ parameters[0].Value = session.Serializer.ToJson(_document);
+ // .Net Class Type
+ parameters[1].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Varchar;
+ parameters[1].Value = _document.GetType().FullName;
+ parameters[2].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Uuid;
+ parameters[2].Value = document.Id;
+ setCurrentRevisionParameter(parameters[3]);
+ }
+
+ }
+
+ // END: UpdateOrderOperation60467594
+
+
+ // START: QueryOnlyOrderSelector60467594
+ public class QueryOnlyOrderSelector60467594 : Marten.Internal.CodeGeneration.DocumentSelectorWithOnlySerializer, Marten.Linq.Selectors.ISelector
+ {
+ private readonly Marten.Internal.IMartenSession _session;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public QueryOnlyOrderSelector60467594(Marten.Internal.IMartenSession session, Marten.Schema.DocumentMapping mapping) : base(session, mapping)
+ {
+ _session = session;
+ _mapping = mapping;
+ }
+
+
+
+ public CoffeeShop.OrderSummary.Models.Order Resolve(System.Data.Common.DbDataReader reader)
+ {
+
+ CoffeeShop.OrderSummary.Models.Order document;
+ document = _serializer.FromJson(reader, 0);
+ return document;
+ }
+
+
+ public async System.Threading.Tasks.Task ResolveAsync(System.Data.Common.DbDataReader reader, System.Threading.CancellationToken token)
+ {
+
+ CoffeeShop.OrderSummary.Models.Order document;
+ document = await _serializer.FromJsonAsync(reader, 0, token).ConfigureAwait(false);
+ return document;
+ }
+
+ }
+
+ // END: QueryOnlyOrderSelector60467594
+
+
+ // START: LightweightOrderSelector60467594
+ public class LightweightOrderSelector60467594 : Marten.Internal.CodeGeneration.DocumentSelectorWithOnlySerializer, Marten.Linq.Selectors.ISelector
+ {
+ private readonly Marten.Internal.IMartenSession _session;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public LightweightOrderSelector60467594(Marten.Internal.IMartenSession session, Marten.Schema.DocumentMapping mapping) : base(session, mapping)
+ {
+ _session = session;
+ _mapping = mapping;
+ }
+
+
+
+ public CoffeeShop.OrderSummary.Models.Order Resolve(System.Data.Common.DbDataReader reader)
+ {
+ var id = reader.GetFieldValue(0);
+
+ CoffeeShop.OrderSummary.Models.Order document;
+ document = _serializer.FromJson(reader, 1);
+ var version = reader.GetFieldValue(2);
+ _session.MarkAsDocumentLoaded(id, document);
+ return document;
+ }
+
+
+ public async System.Threading.Tasks.Task ResolveAsync(System.Data.Common.DbDataReader reader, System.Threading.CancellationToken token)
+ {
+ var id = await reader.GetFieldValueAsync(0, token);
+
+ CoffeeShop.OrderSummary.Models.Order document;
+ document = await _serializer.FromJsonAsync(reader, 1, token).ConfigureAwait(false);
+ var version = await reader.GetFieldValueAsync(2, token);
+ _session.MarkAsDocumentLoaded(id, document);
+ return document;
+ }
+
+ }
+
+ // END: LightweightOrderSelector60467594
+
+
+ // START: IdentityMapOrderSelector60467594
+ public class IdentityMapOrderSelector60467594 : Marten.Internal.CodeGeneration.DocumentSelectorWithIdentityMap, Marten.Linq.Selectors.ISelector
+ {
+ private readonly Marten.Internal.IMartenSession _session;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public IdentityMapOrderSelector60467594(Marten.Internal.IMartenSession session, Marten.Schema.DocumentMapping mapping) : base(session, mapping)
+ {
+ _session = session;
+ _mapping = mapping;
+ }
+
+
+
+ public CoffeeShop.OrderSummary.Models.Order Resolve(System.Data.Common.DbDataReader reader)
+ {
+ var id = reader.GetFieldValue(0);
+ if (_identityMap.TryGetValue(id, out var existing)) return existing;
+
+ CoffeeShop.OrderSummary.Models.Order document;
+ document = _serializer.FromJson(reader, 1);
+ var version = reader.GetFieldValue(2);
+ _session.MarkAsDocumentLoaded(id, document);
+ _identityMap[id] = document;
+ return document;
+ }
+
+
+ public async System.Threading.Tasks.Task ResolveAsync(System.Data.Common.DbDataReader reader, System.Threading.CancellationToken token)
+ {
+ var id = await reader.GetFieldValueAsync(0, token);
+ if (_identityMap.TryGetValue(id, out var existing)) return existing;
+
+ CoffeeShop.OrderSummary.Models.Order document;
+ document = await _serializer.FromJsonAsync(reader, 1, token).ConfigureAwait(false);
+ var version = await reader.GetFieldValueAsync(2, token);
+ _session.MarkAsDocumentLoaded(id, document);
+ _identityMap[id] = document;
+ return document;
+ }
+
+ }
+
+ // END: IdentityMapOrderSelector60467594
+
+
+ // START: DirtyTrackingOrderSelector60467594
+ public class DirtyTrackingOrderSelector60467594 : Marten.Internal.CodeGeneration.DocumentSelectorWithDirtyChecking, Marten.Linq.Selectors.ISelector
+ {
+ private readonly Marten.Internal.IMartenSession _session;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public DirtyTrackingOrderSelector60467594(Marten.Internal.IMartenSession session, Marten.Schema.DocumentMapping mapping) : base(session, mapping)
+ {
+ _session = session;
+ _mapping = mapping;
+ }
+
+
+
+ public CoffeeShop.OrderSummary.Models.Order Resolve(System.Data.Common.DbDataReader reader)
+ {
+ var id = reader.GetFieldValue(0);
+ if (_identityMap.TryGetValue(id, out var existing)) return existing;
+
+ CoffeeShop.OrderSummary.Models.Order document;
+ document = _serializer.FromJson(reader, 1);
+ var version = reader.GetFieldValue(2);
+ _session.MarkAsDocumentLoaded(id, document);
+ _identityMap[id] = document;
+ StoreTracker(_session, document);
+ return document;
+ }
+
+
+ public async System.Threading.Tasks.Task ResolveAsync(System.Data.Common.DbDataReader reader, System.Threading.CancellationToken token)
+ {
+ var id = await reader.GetFieldValueAsync(0, token);
+ if (_identityMap.TryGetValue(id, out var existing)) return existing;
+
+ CoffeeShop.OrderSummary.Models.Order document;
+ document = await _serializer.FromJsonAsync(reader, 1, token).ConfigureAwait(false);
+ var version = await reader.GetFieldValueAsync(2, token);
+ _session.MarkAsDocumentLoaded(id, document);
+ _identityMap[id] = document;
+ StoreTracker(_session, document);
+ return document;
+ }
+
+ }
+
+ // END: DirtyTrackingOrderSelector60467594
+
+
+ // START: OverwriteOrderOperation60467594
+ public class OverwriteOrderOperation60467594 : Marten.Internal.Operations.StorageOperation
+ {
+ private readonly CoffeeShop.OrderSummary.Models.Order _document;
+ private readonly System.Guid _id;
+ private readonly System.Collections.Generic.Dictionary _versions;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public OverwriteOrderOperation60467594(CoffeeShop.OrderSummary.Models.Order document, System.Guid id, System.Collections.Generic.Dictionary versions, Marten.Schema.DocumentMapping mapping) : base(document, id, versions, mapping)
+ {
+ _document = document;
+ _id = id;
+ _versions = versions;
+ _mapping = mapping;
+ }
+
+
+ public const string COMMAND_TEXT = "select order_summary.mt_overwrite_order(?, ?, ?, ?)";
+
+
+ public override void Postprocess(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions)
+ {
+ if (postprocessRevision(reader, exceptions))
+ {
+ }
+
+ }
+
+
+ public override async System.Threading.Tasks.Task PostprocessAsync(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions, System.Threading.CancellationToken token)
+ {
+ if (await postprocessRevisionAsync(reader, exceptions, token))
+ {
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.OperationRole Role()
+ {
+ return Marten.Internal.Operations.OperationRole.Update;
+ }
+
+
+ public override string CommandText()
+ {
+ return COMMAND_TEXT;
+ }
+
+
+ public override NpgsqlTypes.NpgsqlDbType DbType()
+ {
+ return NpgsqlTypes.NpgsqlDbType.Uuid;
+ }
+
+
+ public override void ConfigureParameters(Npgsql.NpgsqlParameter[] parameters, CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session)
+ {
+ parameters[0].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Jsonb;
+ parameters[0].Value = session.Serializer.ToJson(_document);
+ // .Net Class Type
+ parameters[1].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Varchar;
+ parameters[1].Value = _document.GetType().FullName;
+ parameters[2].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Uuid;
+ parameters[2].Value = document.Id;
+ setCurrentRevisionParameter(parameters[3]);
+ }
+
+ }
+
+ // END: OverwriteOrderOperation60467594
+
+
+ // START: QueryOnlyOrderDocumentStorage60467594
+ public class QueryOnlyOrderDocumentStorage60467594 : Marten.Internal.Storage.QueryOnlyDocumentStorage
+ {
+ private readonly Marten.Schema.DocumentMapping _document;
+
+ public QueryOnlyOrderDocumentStorage60467594(Marten.Schema.DocumentMapping document) : base(document)
+ {
+ _document = document;
+ }
+
+
+
+ public override System.Guid AssignIdentity(CoffeeShop.OrderSummary.Models.Order document, string tenantId, Marten.Storage.IMartenDatabase database)
+ {
+ if (document.Id == Guid.Empty) _setter(document, Marten.Schema.Identity.CombGuidIdGeneration.NewGuid());
+ return document.Id;
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Update(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+ if (session.Concurrency == Marten.Services.ConcurrencyChecks.Disabled)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ else
+ {
+
+ return new Marten.Generated.DocumentStorage.UpdateOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Insert(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+
+ return new Marten.Generated.DocumentStorage.InsertOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Upsert(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+ if (session.Concurrency == Marten.Services.ConcurrencyChecks.Disabled)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ else
+ {
+
+ return new Marten.Generated.DocumentStorage.UpsertOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Overwrite(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+
+ public override System.Guid Identity(CoffeeShop.OrderSummary.Models.Order document)
+ {
+ return document.Id;
+ }
+
+
+ public override Marten.Linq.Selectors.ISelector BuildSelector(Marten.Internal.IMartenSession session)
+ {
+ return new Marten.Generated.DocumentStorage.QueryOnlyOrderSelector60467594(session, _document);
+ }
+
+ }
+
+ // END: QueryOnlyOrderDocumentStorage60467594
+
+
+ // START: LightweightOrderDocumentStorage60467594
+ public class LightweightOrderDocumentStorage60467594 : Marten.Internal.Storage.LightweightDocumentStorage
+ {
+ private readonly Marten.Schema.DocumentMapping _document;
+
+ public LightweightOrderDocumentStorage60467594(Marten.Schema.DocumentMapping document) : base(document)
+ {
+ _document = document;
+ }
+
+
+
+ public override System.Guid AssignIdentity(CoffeeShop.OrderSummary.Models.Order document, string tenantId, Marten.Storage.IMartenDatabase database)
+ {
+ if (document.Id == Guid.Empty) _setter(document, Marten.Schema.Identity.CombGuidIdGeneration.NewGuid());
+ return document.Id;
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Update(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+ if (session.Concurrency == Marten.Services.ConcurrencyChecks.Disabled)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ else
+ {
+
+ return new Marten.Generated.DocumentStorage.UpdateOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Insert(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+
+ return new Marten.Generated.DocumentStorage.InsertOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Upsert(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+ if (session.Concurrency == Marten.Services.ConcurrencyChecks.Disabled)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ else
+ {
+
+ return new Marten.Generated.DocumentStorage.UpsertOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Overwrite(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+
+ public override System.Guid Identity(CoffeeShop.OrderSummary.Models.Order document)
+ {
+ return document.Id;
+ }
+
+
+ public override Marten.Linq.Selectors.ISelector BuildSelector(Marten.Internal.IMartenSession session)
+ {
+ return new Marten.Generated.DocumentStorage.LightweightOrderSelector60467594(session, _document);
+ }
+
+ }
+
+ // END: LightweightOrderDocumentStorage60467594
+
+
+ // START: IdentityMapOrderDocumentStorage60467594
+ public class IdentityMapOrderDocumentStorage60467594 : Marten.Internal.Storage.IdentityMapDocumentStorage
+ {
+ private readonly Marten.Schema.DocumentMapping _document;
+
+ public IdentityMapOrderDocumentStorage60467594(Marten.Schema.DocumentMapping document) : base(document)
+ {
+ _document = document;
+ }
+
+
+
+ public override System.Guid AssignIdentity(CoffeeShop.OrderSummary.Models.Order document, string tenantId, Marten.Storage.IMartenDatabase database)
+ {
+ if (document.Id == Guid.Empty) _setter(document, Marten.Schema.Identity.CombGuidIdGeneration.NewGuid());
+ return document.Id;
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Update(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+ if (session.Concurrency == Marten.Services.ConcurrencyChecks.Disabled)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ else
+ {
+
+ return new Marten.Generated.DocumentStorage.UpdateOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Insert(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+
+ return new Marten.Generated.DocumentStorage.InsertOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Upsert(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+ if (session.Concurrency == Marten.Services.ConcurrencyChecks.Disabled)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ else
+ {
+
+ return new Marten.Generated.DocumentStorage.UpsertOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Overwrite(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+
+ public override System.Guid Identity(CoffeeShop.OrderSummary.Models.Order document)
+ {
+ return document.Id;
+ }
+
+
+ public override Marten.Linq.Selectors.ISelector BuildSelector(Marten.Internal.IMartenSession session)
+ {
+ return new Marten.Generated.DocumentStorage.IdentityMapOrderSelector60467594(session, _document);
+ }
+
+ }
+
+ // END: IdentityMapOrderDocumentStorage60467594
+
+
+ // START: DirtyTrackingOrderDocumentStorage60467594
+ public class DirtyTrackingOrderDocumentStorage60467594 : Marten.Internal.Storage.DirtyCheckedDocumentStorage
+ {
+ private readonly Marten.Schema.DocumentMapping _document;
+
+ public DirtyTrackingOrderDocumentStorage60467594(Marten.Schema.DocumentMapping document) : base(document)
+ {
+ _document = document;
+ }
+
+
+
+ public override System.Guid AssignIdentity(CoffeeShop.OrderSummary.Models.Order document, string tenantId, Marten.Storage.IMartenDatabase database)
+ {
+ if (document.Id == Guid.Empty) _setter(document, Marten.Schema.Identity.CombGuidIdGeneration.NewGuid());
+ return document.Id;
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Update(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+ if (session.Concurrency == Marten.Services.ConcurrencyChecks.Disabled)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ else
+ {
+
+ return new Marten.Generated.DocumentStorage.UpdateOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Insert(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+
+ return new Marten.Generated.DocumentStorage.InsertOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Upsert(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+ if (session.Concurrency == Marten.Services.ConcurrencyChecks.Disabled)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ else
+ {
+
+ return new Marten.Generated.DocumentStorage.UpsertOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.IStorageOperation Overwrite(CoffeeShop.OrderSummary.Models.Order document, Marten.Internal.IMartenSession session, string tenant)
+ {
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+
+ return new Marten.Generated.DocumentStorage.OverwriteOrderOperation60467594
+ (
+ document, Identity(document),
+ null,
+ _document
+
+ );
+ }
+
+
+ public override System.Guid Identity(CoffeeShop.OrderSummary.Models.Order document)
+ {
+ return document.Id;
+ }
+
+
+ public override Marten.Linq.Selectors.ISelector BuildSelector(Marten.Internal.IMartenSession session)
+ {
+ return new Marten.Generated.DocumentStorage.DirtyTrackingOrderSelector60467594(session, _document);
+ }
+
+ }
+
+ // END: DirtyTrackingOrderDocumentStorage60467594
+
+
+ // START: OrderBulkLoader60467594
+ public class OrderBulkLoader60467594 : Marten.Internal.CodeGeneration.BulkLoader
+ {
+ private readonly Marten.Internal.Storage.IDocumentStorage _storage;
+
+ public OrderBulkLoader60467594(Marten.Internal.Storage.IDocumentStorage storage) : base(storage)
+ {
+ _storage = storage;
+ }
+
+
+ public const string MAIN_LOADER_SQL = "COPY order_summary.mt_doc_order(\"mt_dotnet_type\", \"id\", \"mt_version\", \"data\") FROM STDIN BINARY";
+
+ public const string TEMP_LOADER_SQL = "COPY mt_doc_order_temp(\"mt_dotnet_type\", \"id\", \"mt_version\", \"data\") FROM STDIN BINARY";
+
+ public const string COPY_NEW_DOCUMENTS_SQL = "insert into order_summary.mt_doc_order (\"id\", \"data\", \"mt_dotnet_type\", \"mt_version\", mt_last_modified) (select mt_doc_order_temp.\"id\", mt_doc_order_temp.\"data\", mt_doc_order_temp.\"mt_dotnet_type\", mt_doc_order_temp.\"mt_version\", transaction_timestamp() from mt_doc_order_temp left join order_summary.mt_doc_order on mt_doc_order_temp.id = order_summary.mt_doc_order.id where order_summary.mt_doc_order.id is null)";
+
+ public const string OVERWRITE_SQL = "update order_summary.mt_doc_order target SET data = source.data, mt_dotnet_type = source.mt_dotnet_type, mt_version = source.mt_version, mt_last_modified = transaction_timestamp() FROM mt_doc_order_temp source WHERE source.id = target.id";
+
+ public const string CREATE_TEMP_TABLE_FOR_COPYING_SQL = "create temporary table mt_doc_order_temp as select * from order_summary.mt_doc_order limit 0";
+
+
+ public override string CreateTempTableForCopying()
+ {
+ return CREATE_TEMP_TABLE_FOR_COPYING_SQL;
+ }
+
+
+ public override string CopyNewDocumentsFromTempTable()
+ {
+ return COPY_NEW_DOCUMENTS_SQL;
+ }
+
+
+ public override string OverwriteDuplicatesFromTempTable()
+ {
+ return OVERWRITE_SQL;
+ }
+
+
+ public override void LoadRow(Npgsql.NpgsqlBinaryImporter writer, CoffeeShop.OrderSummary.Models.Order document, Marten.Storage.Tenant tenant, Marten.ISerializer serializer)
+ {
+ writer.Write(document.GetType().FullName, NpgsqlTypes.NpgsqlDbType.Varchar);
+ writer.Write(document.Id, NpgsqlTypes.NpgsqlDbType.Uuid);
+ writer.Write(1, NpgsqlTypes.NpgsqlDbType.Integer);
+ writer.Write(serializer.ToJson(document), NpgsqlTypes.NpgsqlDbType.Jsonb);
+ }
+
+
+ public override async System.Threading.Tasks.Task LoadRowAsync(Npgsql.NpgsqlBinaryImporter writer, CoffeeShop.OrderSummary.Models.Order document, Marten.Storage.Tenant tenant, Marten.ISerializer serializer, System.Threading.CancellationToken cancellation)
+ {
+ await writer.WriteAsync(document.GetType().FullName, NpgsqlTypes.NpgsqlDbType.Varchar, cancellation);
+ await writer.WriteAsync(document.Id, NpgsqlTypes.NpgsqlDbType.Uuid, cancellation);
+ await writer.WriteAsync(1, NpgsqlTypes.NpgsqlDbType.Integer, cancellation);
+ await writer.WriteAsync(serializer.ToJson(document), NpgsqlTypes.NpgsqlDbType.Jsonb, cancellation);
+ }
+
+
+ public override string MainLoaderSql()
+ {
+ return MAIN_LOADER_SQL;
+ }
+
+
+ public override string TempLoaderSql()
+ {
+ return TEMP_LOADER_SQL;
+ }
+
+ }
+
+ // END: OrderBulkLoader60467594
+
+
+ // START: OrderProvider60467594
+ public class OrderProvider60467594 : Marten.Internal.Storage.DocumentProvider
+ {
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public OrderProvider60467594(Marten.Schema.DocumentMapping mapping) : base(new OrderBulkLoader60467594(new QueryOnlyOrderDocumentStorage60467594(mapping)), new QueryOnlyOrderDocumentStorage60467594(mapping), new LightweightOrderDocumentStorage60467594(mapping), new IdentityMapOrderDocumentStorage60467594(mapping), new DirtyTrackingOrderDocumentStorage60467594(mapping))
+ {
+ _mapping = mapping;
+ }
+
+
+ }
+
+ // END: OrderProvider60467594
+
+
+}
+
diff --git a/order-summary/Internal/Generated/DocumentStorage/OrderSummaryQueryProvider244148371.cs b/order-summary/Internal/Generated/DocumentStorage/OrderSummaryQueryProvider244148371.cs
new file mode 100644
index 0000000..89bee3a
--- /dev/null
+++ b/order-summary/Internal/Generated/DocumentStorage/OrderSummaryQueryProvider244148371.cs
@@ -0,0 +1,1079 @@
+//
+#pragma warning disable
+using CoffeeShop.OrderSummary.Features;
+using Marten.Internal;
+using Marten.Internal.Storage;
+using Marten.Schema;
+using Marten.Schema.Arguments;
+using Npgsql;
+using System;
+using System.Collections.Generic;
+using Weasel.Core;
+using Weasel.Postgresql;
+
+namespace Marten.Generated.DocumentStorage
+{
+ // START: UpsertOrderSummaryQueryOperation244148371
+ public class UpsertOrderSummaryQueryOperation244148371 : Marten.Internal.Operations.StorageOperation
+ {
+ private readonly CoffeeShop.OrderSummary.Features.OrderSummaryQuery _document;
+ private readonly System.Guid _id;
+ private readonly System.Collections.Generic.Dictionary _versions;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public UpsertOrderSummaryQueryOperation244148371(CoffeeShop.OrderSummary.Features.OrderSummaryQuery document, System.Guid id, System.Collections.Generic.Dictionary versions, Marten.Schema.DocumentMapping mapping) : base(document, id, versions, mapping)
+ {
+ _document = document;
+ _id = id;
+ _versions = versions;
+ _mapping = mapping;
+ }
+
+
+ public const string COMMAND_TEXT = "select order_summary.mt_upsert_ordersummaryquery(?, ?, ?, ?)";
+
+
+ public override void Postprocess(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions)
+ {
+ if (postprocessRevision(reader, exceptions))
+ {
+ }
+
+ }
+
+
+ public override async System.Threading.Tasks.Task PostprocessAsync(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions, System.Threading.CancellationToken token)
+ {
+ if (await postprocessRevisionAsync(reader, exceptions, token))
+ {
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.OperationRole Role()
+ {
+ return Marten.Internal.Operations.OperationRole.Upsert;
+ }
+
+
+ public override string CommandText()
+ {
+ return COMMAND_TEXT;
+ }
+
+
+ public override NpgsqlTypes.NpgsqlDbType DbType()
+ {
+ return NpgsqlTypes.NpgsqlDbType.Uuid;
+ }
+
+
+ public override void ConfigureParameters(Npgsql.NpgsqlParameter[] parameters, CoffeeShop.OrderSummary.Features.OrderSummaryQuery document, Marten.Internal.IMartenSession session)
+ {
+ parameters[0].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Jsonb;
+ parameters[0].Value = session.Serializer.ToJson(_document);
+ // .Net Class Type
+ parameters[1].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Varchar;
+ parameters[1].Value = _document.GetType().FullName;
+ parameters[2].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Uuid;
+ parameters[2].Value = document.Id;
+ setCurrentRevisionParameter(parameters[3]);
+ }
+
+ }
+
+ // END: UpsertOrderSummaryQueryOperation244148371
+
+
+ // START: InsertOrderSummaryQueryOperation244148371
+ public class InsertOrderSummaryQueryOperation244148371 : Marten.Internal.Operations.StorageOperation
+ {
+ private readonly CoffeeShop.OrderSummary.Features.OrderSummaryQuery _document;
+ private readonly System.Guid _id;
+ private readonly System.Collections.Generic.Dictionary _versions;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public InsertOrderSummaryQueryOperation244148371(CoffeeShop.OrderSummary.Features.OrderSummaryQuery document, System.Guid id, System.Collections.Generic.Dictionary versions, Marten.Schema.DocumentMapping mapping) : base(document, id, versions, mapping)
+ {
+ _document = document;
+ _id = id;
+ _versions = versions;
+ _mapping = mapping;
+ }
+
+
+ public const string COMMAND_TEXT = "select order_summary.mt_insert_ordersummaryquery(?, ?, ?, ?)";
+
+
+ public override void Postprocess(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions)
+ {
+ if (postprocessRevision(reader, exceptions))
+ {
+ }
+
+ }
+
+
+ public override async System.Threading.Tasks.Task PostprocessAsync(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList exceptions, System.Threading.CancellationToken token)
+ {
+ if (await postprocessRevisionAsync(reader, exceptions, token))
+ {
+ }
+
+ }
+
+
+ public override Marten.Internal.Operations.OperationRole Role()
+ {
+ return Marten.Internal.Operations.OperationRole.Insert;
+ }
+
+
+ public override string CommandText()
+ {
+ return COMMAND_TEXT;
+ }
+
+
+ public override NpgsqlTypes.NpgsqlDbType DbType()
+ {
+ return NpgsqlTypes.NpgsqlDbType.Uuid;
+ }
+
+
+ public override void ConfigureParameters(Npgsql.NpgsqlParameter[] parameters, CoffeeShop.OrderSummary.Features.OrderSummaryQuery document, Marten.Internal.IMartenSession session)
+ {
+ parameters[0].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Jsonb;
+ parameters[0].Value = session.Serializer.ToJson(_document);
+ // .Net Class Type
+ parameters[1].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Varchar;
+ parameters[1].Value = _document.GetType().FullName;
+ parameters[2].NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Uuid;
+ parameters[2].Value = document.Id;
+ setCurrentRevisionParameter(parameters[3]);
+ }
+
+ }
+
+ // END: InsertOrderSummaryQueryOperation244148371
+
+
+ // START: UpdateOrderSummaryQueryOperation244148371
+ public class UpdateOrderSummaryQueryOperation244148371 : Marten.Internal.Operations.StorageOperation
+ {
+ private readonly CoffeeShop.OrderSummary.Features.OrderSummaryQuery _document;
+ private readonly System.Guid _id;
+ private readonly System.Collections.Generic.Dictionary _versions;
+ private readonly Marten.Schema.DocumentMapping _mapping;
+
+ public UpdateOrderSummaryQueryOperation244148371(CoffeeShop.OrderSummary.Features.OrderSummaryQuery document, System.Guid id, System.Collections.Generic.Dictionary versions, Marten.Schema.DocumentMapping mapping) : base(document, id, versions, mapping)
+ {
+ _document = document;
+ _id = id;
+ _versions = versions;
+ _mapping = mapping;
+ }
+
+
+ public const string COMMAND_TEXT = "select order_summary.mt_update_ordersummaryquery(?, ?, ?, ?)";
+
+
+ public override void Postprocess(System.Data.Common.DbDataReader reader, System.Collections.Generic.IList