Skip to content

Commit

Permalink
Cart API Introduced and Initial Use Cases (#39)
Browse files Browse the repository at this point in the history
* added dotnet ShoppingCart project -- for comparisons

* used the Eventuous class/record/namespace conventions

* forgot to add ItemAddedToCart event

* added behavior to state, created identities

* removed TODO -- design change

* removed the (virtual) solution folder ShoppingCart

* introduced barebones of shopping cart API

* generated endpoints currently don't support functional command services

* generated endpoints currently don't support functional command services

* work-around for no cart ID use case (TEMPORARY)

* added and removed use cases

* added shoppingcart to docker compose (//)
  • Loading branch information
erikshafer authored Jun 19, 2024
1 parent 2f09493 commit 9977081
Show file tree
Hide file tree
Showing 23 changed files with 621 additions and 17 deletions.
14 changes: 14 additions & 0 deletions EventSourcingEcommerce.sln
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shopping-cart", "shopping-c
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ecommerce.Eventuous", "src\Core\Ecommerce.Eventuous\Ecommerce.Eventuous.csproj", "{3C54979E-C2BB-448E-86EE-08F366B973DF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShoppingCart", "src\Retail\ShoppingCart\ShoppingCart.csproj", "{4280FEC2-9CB2-48B2-B565-17E81AF40333}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShoppingCart.Api", "src\Retail\ShoppingCart.Api\ShoppingCart.Api.csproj", "{99ECE0F8-DE98-4A6E-84D9-010BAABDACC9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -141,6 +145,14 @@ Global
{3C54979E-C2BB-448E-86EE-08F366B973DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C54979E-C2BB-448E-86EE-08F366B973DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C54979E-C2BB-448E-86EE-08F366B973DF}.Release|Any CPU.Build.0 = Release|Any CPU
{4280FEC2-9CB2-48B2-B565-17E81AF40333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4280FEC2-9CB2-48B2-B565-17E81AF40333}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4280FEC2-9CB2-48B2-B565-17E81AF40333}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4280FEC2-9CB2-48B2-B565-17E81AF40333}.Release|Any CPU.Build.0 = Release|Any CPU
{99ECE0F8-DE98-4A6E-84D9-010BAABDACC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{99ECE0F8-DE98-4A6E-84D9-010BAABDACC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99ECE0F8-DE98-4A6E-84D9-010BAABDACC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99ECE0F8-DE98-4A6E-84D9-010BAABDACC9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9CD0617F-6555-4F29-9C3E-211DC9A92CD5} = {5C748417-563A-41A4-AC92-67A815C5A929}
Expand All @@ -162,5 +174,7 @@ Global
{F42EAACF-D74A-4646-A311-AB9EB131AE8A} = {E8FE7001-AFB6-4FB2-B922-BB6A203A652E}
{E8D6C05C-F653-42AA-BB9B-6E57251C7E99} = {91DC34A2-E5DC-4BDD-9BAE-6B9D1E8E0769}
{3C54979E-C2BB-448E-86EE-08F366B973DF} = {5C748417-563A-41A4-AC92-67A815C5A929}
{4280FEC2-9CB2-48B2-B565-17E81AF40333} = {91DC34A2-E5DC-4BDD-9BAE-6B9D1E8E0769}
{99ECE0F8-DE98-4A6E-84D9-010BAABDACC9} = {91DC34A2-E5DC-4BDD-9BAE-6B9D1E8E0769}
EndGlobalSection
EndGlobal
26 changes: 9 additions & 17 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,6 @@ services:
networks:
- esdb_network

esdb_retail:
image: eventstore/eventstore:24.2.0-jammy
# use this image if you're running ARM-based processor like an Apple M1
# image: eventstore/eventstore:24.2.0-alpha-arm64v8
container_name: ecomm_esdb_retail
ports:
- '2114:2114'
- '1114:1114'
environment:
EVENTSTORE_INSECURE: 'true'
EVENTSTORE_CLUSTER_SIZE: 1
EVENTSTORE_RUN_PROJECTIONS: all
EVENTSTORE_START_STANDARD_PROJECTIONS: 'true'
EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP: 'true'
networks:
- esdb_network

zipkin:
image: openzipkin/zipkin
container_name: ecomm_zipkin
Expand Down Expand Up @@ -152,6 +135,15 @@ services:
# context: .
# dockerfile: src/Pricing/Discounts.Api/Dockerfile

# shoppingcart.api:
# image: shoppingcart.api
# container_name: ecomm_shoppingcart_api
# ports:
# - "5263:80"
# build:
# context: .
# dockerfile: src/Retail/ShoppingCart.Api/Dockerfile

# legacy.api:
# image: legacy.api
# container_name: ecomm_legacy_api
Expand Down
26 changes: 26 additions & 0 deletions src/Retail/ShoppingCart.Api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 5262
EXPOSE 5263

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["ShoppingCart.Api/ShoppingCart.Api.csproj", "ShoppingCart.Api/"]
COPY ["src/Retail/ShoppingCart/ShoppingCart.csproj", "src/Retail/ShoppingCart/"]
COPY ["src/Core/Ecommerce.Core/Ecommerce.Core.csproj", "src/Core/Ecommerce.Core/"]
COPY ["src/Core/Ecommerce.Eventuous/Ecommerce.Eventuous.csproj", "src/Core/Ecommerce.Eventuous/"]
RUN dotnet restore "ShoppingCart.Api/ShoppingCart.Api.csproj"
COPY . .
WORKDIR "/src/ShoppingCart.Api"
RUN dotnet build "ShoppingCart.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "ShoppingCart.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ShoppingCart.Api.dll"]
49 changes: 49 additions & 0 deletions src/Retail/ShoppingCart.Api/HttpApi/CommandApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Eventuous;
using Microsoft.AspNetCore.Mvc;
using static ShoppingCart.CartCommands.V1;

namespace ShoppingCart.Api.HttpApi;

[Route("/cart")]
public class CommandApi(IFuncCommandService<CartState> service) : ControllerBase
{
[HttpPost]
[Route("open")]
public async Task<ActionResult<Result>> OpenCart([FromBody] OpenCart cmd, CancellationToken ct)
{
var result = await service.Handle(cmd, ct);
return Ok(result);
}

[HttpPost]
[Route("open-with-id")]
public async Task<ActionResult<Result>> OpenCart([FromBody] OpenCartWithProvidedId cmd, CancellationToken ct)
{
var result = await service.Handle(cmd, ct);
return Ok(result);
}

[HttpPost]
[Route("add-product")]
public async Task<ActionResult<Result>> OpenCart([FromBody] AddProductToCart cmd, CancellationToken ct)
{
var result = await service.Handle(cmd, ct);
return Ok(result);
}

[HttpPost]
[Route("remove-product")]
public async Task<ActionResult<Result>> OpenCart([FromBody] RemoveProductFromCart cmd, CancellationToken ct)
{
var result = await service.Handle(cmd, ct);
return Ok(result);
}

[HttpPost]
[Route("confirm")]
public async Task<ActionResult<Result>> OpenCart([FromBody] ConfirmCart cmd, CancellationToken ct)
{
var result = await service.Handle(cmd, ct);
return Ok(result);
}
}
30 changes: 30 additions & 0 deletions src/Retail/ShoppingCart.Api/Infrastructure/Logging.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Serilog;
using Serilog.Events;

namespace ShoppingCart.Api.Infrastructure;

public static class Logging
{
public static void ConfigureLog(IConfiguration config)
=> Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc.Infrastructure", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
.MinimumLevel.Override("Grpc", LogEventLevel.Information)
.MinimumLevel.Override("Grpc.Net.Client.Internal.GrpcCall", LogEventLevel.Error)
.MinimumLevel.Override("EventStore", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
)
.WriteTo.Seq(config["Seq:ServerUrl"]!)
.CreateLogger();
}

public record SeqConfig
{
public string ServerUrl { get; init; } = null!;
}
55 changes: 55 additions & 0 deletions src/Retail/ShoppingCart.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Eventuous.Spyglass;
using Microsoft.AspNetCore.Http.Json;
using NodaTime;
using NodaTime.Serialization.SystemTextJson;
using Serilog;
using ShoppingCart.Api;
using ShoppingCart.Api.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

Logging.ConfigureLog(builder.Configuration);
builder.Host.UseSerilog();

builder.Services
.AddControllers()
.AddJsonOptions(options =>
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddTelemetry();
builder.Services.AddEventuous(builder.Configuration);
builder.Services.AddEventuousSpyglass();

builder.Services.Configure<JsonOptions>(options =>
options.SerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb)
);

var app = builder.Build();

app.UseSwagger(opts =>
{
opts.RouteTemplate = "api/{documentName}/swagger.json";
})
.UseSwaggerUI(opts =>
{
opts.SwaggerEndpoint("/api/v1/swagger.json", "ShoppingCart API");
opts.RoutePrefix = "api";
});
app.UseSerilogRequestLogging();
app.MapControllers();
app.UseOpenTelemetryPrometheusScrapingEndpoint();
app.MapEventuousSpyglass();

try {
app.Run("http://*:5262");
return 0;
}
catch (Exception e) {
Log.Fatal(e, "Host terminated unexpectedly");
return 1;
}
finally {
Log.CloseAndFlush();
}
14 changes: 14 additions & 0 deletions src/Retail/ShoppingCart.Api/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5262;https://localhost:5263",
"launchUrl": "api",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
80 changes: 80 additions & 0 deletions src/Retail/ShoppingCart.Api/Registrations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Text.Json;
using Ecommerce.Core.Identities;
using Eventuous;
using Eventuous.Diagnostics.OpenTelemetry;
using Eventuous.EventStore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

#pragma warning disable CS0618 // Type or member is obsolete

namespace ShoppingCart.Api;

public static class Registrations
{
private const string OTelServiceName = "shoppingcart";
private const string PostgresSchemaName = "shoppingcart";

public static void AddEventuous(this IServiceCollection services, IConfiguration configuration)
{
DefaultEventSerializer.SetDefaultSerializer(
new DefaultEventSerializer(
new JsonSerializerOptions(JsonSerializerDefaults.Web)
)
);

// event store (core)
services.AddEventStoreClient(configuration["EventStore:ConnectionString"]!);
services.AddAggregateStore<EsdbEventStore>();

// command services (functional services in this module)
services.AddFunctionalService<CartFuncService, CartState>();

// other internal and core services
services.AddSingleton<ISnowflakeIdGenerator, SnowflakeIdGenerator>();

// health checks for subscription service
services
.AddHealthChecks()
.AddSubscriptionsHealthCheck("subscriptions", HealthStatus.Unhealthy, new []{"tag"});
}

public static void AddTelemetry(this IServiceCollection services)
{
var otelEnabled = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") != null;

services.AddOpenTelemetry()
.WithMetrics(
builder =>
{
builder
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(OTelServiceName))
.AddAspNetCoreInstrumentation()
.AddEventuous()
.AddEventuousSubscriptions()
.AddPrometheusExporter();
if (otelEnabled) builder.AddOtlpExporter();
}
);

services.AddOpenTelemetry()
.WithTracing(
builder =>
{
builder
.AddAspNetCoreInstrumentation()
.AddGrpcClientInstrumentation()
.AddEventuousTracing()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(OTelServiceName))
.SetSampler(new AlwaysOnSampler());

if (otelEnabled)
builder.AddOtlpExporter();
else
builder.AddZipkinExporter();
}
);
}
}
37 changes: 37 additions & 0 deletions src/Retail/ShoppingCart.Api/ShoppingCart.Api.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Eventuous.Diagnostics.OpenTelemetry" Version="$(EventuousVersion)" />
<PackageReference Include="Eventuous.Application" Version="$(EventuousVersion)" />
<PackageReference Include="Eventuous.EventStore" Version="$(EventuousVersion)" />
<PackageReference Include="Eventuous.AspNetCore.Web" Version="$(EventuousVersion)" />
<PackageReference Include="Eventuous.Spyglass" Version="$(EventuousVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.6" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.4.0-rc.4" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.0.0-rc9.14" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Core\Ecommerce.Core.WebApi\Ecommerce.Core.WebApi.csproj" />
<ProjectReference Include="..\ShoppingCart\ShoppingCart.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions src/Retail/ShoppingCart.Api/ShoppingCart.Api.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@ShoppingCart.Api_HostAddress = http://localhost:5146

GET {{ShoppingCart.Api_HostAddress}}/weatherforecast/
Accept: application/json

###
15 changes: 15 additions & 0 deletions src/Retail/ShoppingCart.Api/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Seq": {
"ServerUrl": "http://localhost:5341"
},
"EventStore": {
"ConnectionString": "esdb://localhost:2113?tls=false"
}
}
Loading

0 comments on commit 9977081

Please sign in to comment.