-
-
Notifications
You must be signed in to change notification settings - Fork 506
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added example of closing the books with Marten assuming that open and…
… close are seprate actions
- Loading branch information
1 parent
0bcbc01
commit e4bfb29
Showing
14 changed files
with
565 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
Sample/ClosingTheBooks/PointOfSales.Api/Core/Http/ETagExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.Net.Http.Headers; | ||
|
||
namespace PointOfSales.Api.Core; | ||
|
||
public static class ETagExtensions | ||
{ | ||
public static int ToExpectedVersion(string? eTag) | ||
{ | ||
if (eTag is null) | ||
throw new ArgumentNullException(nameof(eTag)); | ||
|
||
var value = EntityTagHeaderValue.Parse(eTag).Tag.Value; | ||
|
||
if (value is null) | ||
throw new ArgumentNullException(nameof(eTag)); | ||
|
||
return int.Parse(value.Substring(1, value.Length - 2)); | ||
} | ||
} | ||
|
||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] | ||
public class FromIfMatchHeaderAttribute: FromHeaderAttribute | ||
{ | ||
public FromIfMatchHeaderAttribute() | ||
{ | ||
Name = "If-Match"; | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
Sample/ClosingTheBooks/PointOfSales.Api/Core/Marten/DocumentSessionExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using Marten; | ||
|
||
namespace Helpdesk.Api.Core.Marten; | ||
|
||
public static class DocumentSessionExtensions | ||
{ | ||
public static Task Add<T>(this IDocumentSession documentSession, string id, object[] events, CancellationToken ct) | ||
where T : class | ||
{ | ||
if (events.Length == 0) | ||
return Task.CompletedTask; | ||
|
||
documentSession.Events.StartStream<T>(id, events); | ||
return documentSession.SaveChangesAsync(token: ct); | ||
} | ||
|
||
public static Task GetAndUpdate<T>( | ||
this IDocumentSession documentSession, | ||
string id, | ||
int version, | ||
Func<T, object[]> handle, | ||
CancellationToken ct | ||
) where T : class => | ||
documentSession.Events.WriteToAggregate<T>(id, version, stream => | ||
{ | ||
var result = handle(stream.Aggregate); | ||
if (result.Length != 0) | ||
stream.AppendMany(result); | ||
}, ct); | ||
} |
21 changes: 21 additions & 0 deletions
21
Sample/ClosingTheBooks/PointOfSales.Api/PointOfSales.Api.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/> | ||
<PackageReference Include="Marten" Version="7.0.0-beta.5"/> | ||
<PackageReference Include="Marten.AspNetCore" Version="7.0.0-beta.5"/> | ||
<PackageReference Include="Marten.CommandLine" Version="7.0.0-beta.5" /> | ||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0"/> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\PointOfSales\PointOfSales.csproj" /> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
using System.Text.Json.Serialization; | ||
using Helpdesk.Api.Core.Marten; | ||
using JasperFx.CodeGeneration; | ||
using Marten; | ||
using Marten.AspNetCore; | ||
using Marten.Events; | ||
using Marten.Events.Daemon.Resiliency; | ||
using Marten.Events.Projections; | ||
using Marten.Schema.Identity; | ||
using Marten.Services.Json; | ||
using Oakton; | ||
using PointOfSales.Api.Core; | ||
using PointOfSales.CashierShifts; | ||
using PointOfSales.CashRegister; | ||
using Weasel.Core; | ||
using static Microsoft.AspNetCore.Http.TypedResults; | ||
using static PointOfSales.CashierShifts.CashierShiftDecider; | ||
using static PointOfSales.CashRegister.CashRegisterDecider; | ||
using static PointOfSales.CashierShifts.CashierShiftCommand; | ||
using static PointOfSales.CashierShifts.CashierShiftEvent; | ||
using static PointOfSales.Api.Core.ETagExtensions; | ||
using static System.DateTimeOffset; | ||
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; | ||
|
||
var builder = WebApplication.CreateBuilder(args); | ||
|
||
|
||
builder.Services | ||
.AddEndpointsApiExplorer() | ||
.AddSwaggerGen() | ||
.AddMarten(options => | ||
{ | ||
var schemaName = Environment.GetEnvironmentVariable("SchemaName") ?? "PointOfSales"; | ||
options.Events.DatabaseSchemaName = schemaName; | ||
options.DatabaseSchemaName = schemaName; | ||
options.Connection(builder.Configuration.GetConnectionString("PointOfSales") ?? | ||
throw new InvalidOperationException()); | ||
options.UseDefaultSerialization( | ||
EnumStorage.AsString, | ||
nonPublicMembersStorage: NonPublicMembersStorage.All, | ||
serializerType: SerializerType.SystemTextJson | ||
); | ||
// THIS IS IMPORTANT! | ||
options.Events.StreamIdentity = StreamIdentity.AsString; | ||
options.Projections.LiveStreamAggregation<CashierShift>(); | ||
options.Projections.LiveStreamAggregation<CashRegister>(); | ||
options.Projections.Add<CashierShiftTrackerProjection>(ProjectionLifecycle.Async); | ||
}) | ||
.OptimizeArtifactWorkflow(TypeLoadMode.Static) | ||
.UseLightweightSessions() | ||
.AddAsyncDaemon(DaemonMode.Solo); | ||
|
||
builder.Services | ||
.Configure<JsonOptions>(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter())) | ||
.Configure<Microsoft.AspNetCore.Mvc.JsonOptions>(o => | ||
o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); | ||
|
||
builder.Host.ApplyOaktonExtensions(); | ||
|
||
var app = builder.Build(); | ||
|
||
app.MapPost("/api/cash-registers/{cashRegisterId}", | ||
async ( | ||
IDocumentSession documentSession, | ||
string cashRegisterId, | ||
CancellationToken ct) => | ||
{ | ||
await documentSession.Add<CashRegister>(cashRegisterId, | ||
Decide(new InitializeCashRegister(cashRegisterId, Now)), ct); | ||
return Created($"/api/cash-registers/{cashRegisterId}", cashRegisterId); | ||
} | ||
); | ||
|
||
app.MapPost("/api/cash-registers/{cashRegisterId}/cashier-shifts/open", | ||
async ( | ||
IDocumentSession documentSession, | ||
string cashRegisterId, | ||
OpenShiftRequest body, | ||
CancellationToken ct | ||
) => | ||
{ | ||
var lastClosedShift = await documentSession.GetLastCashierShift(cashRegisterId); | ||
var result = Decide(new OpenShift(cashRegisterId, body.CashierId, Now), lastClosedShift); | ||
if (result.Length == 0 || result[0] is not ShiftOpened opened) | ||
throw new InvalidOperationException("Cannot Open Shift"); | ||
await documentSession.Add<CashierShift>(opened.CashierShiftId, result, ct); | ||
return Created( | ||
$"/api/cash-registers/{cashRegisterId}/cashier-shifts/{opened.CashierShiftId.ShiftNumber}", | ||
cashRegisterId | ||
); | ||
} | ||
); | ||
|
||
app.MapPost("/api/cash-registers/{cashRegisterId}/cashier-shifts/{shiftNumber:int}/transactions", | ||
( | ||
IDocumentSession documentSession, | ||
string cashRegisterId, | ||
int shiftNumber, | ||
RegisterTransactionRequest body, | ||
[FromIfMatchHeader] string eTag, | ||
CancellationToken ct | ||
) => | ||
{ | ||
var cashierShiftId = new CashierShiftId(cashRegisterId, shiftNumber); | ||
var transactionId = CombGuidIdGeneration.NewGuid().ToString(); | ||
return documentSession.GetAndUpdate<CashierShift>(cashierShiftId, ToExpectedVersion(eTag), | ||
state => Decide(new RegisterTransaction(cashierShiftId, transactionId, body.Amount, Now), state), ct); | ||
} | ||
); | ||
|
||
app.MapPost("/api/cash-registers/{cashRegisterId}/cashier-shifts/{shiftNumber:int}/close", | ||
( | ||
IDocumentSession documentSession, | ||
string cashRegisterId, | ||
int shiftNumber, | ||
CloseShiftRequest body, | ||
[FromIfMatchHeader] string eTag, | ||
CancellationToken ct | ||
) => | ||
{ | ||
var cashierShiftId = new CashierShiftId(cashRegisterId, shiftNumber); | ||
return documentSession.GetAndUpdate<CashierShift>(cashierShiftId, ToExpectedVersion(eTag), | ||
state => Decide(new CloseShift(cashierShiftId, body.DeclaredTender, Now), state), ct); | ||
} | ||
); | ||
|
||
app.MapGet("/api/cash-registers/{cashRegisterId}/cashier-shifts/{shiftNumber:int}", | ||
(HttpContext context, IQuerySession querySession, string cashRegisterId, int shiftNumber) => | ||
querySession.Json.WriteById<CashierShiftDetails>(new CashierShiftId(cashRegisterId, shiftNumber), context) | ||
); | ||
|
||
|
||
if (app.Environment.IsDevelopment()) | ||
{ | ||
app.UseSwagger() | ||
.UseSwaggerUI(); | ||
} | ||
|
||
return await app.RunOaktonCommands(args); | ||
|
||
public record OpenShiftRequest( | ||
string CashierId | ||
); | ||
|
||
public record RegisterTransactionRequest( | ||
decimal Amount | ||
); | ||
|
||
|
||
public record CloseShiftRequest( | ||
decimal DeclaredTender | ||
); | ||
|
||
|
||
public partial class Program | ||
{ | ||
} |
18 changes: 18 additions & 0 deletions
18
Sample/ClosingTheBooks/PointOfSales/CashRegister/CashRegister.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
namespace PointOfSales.CashRegister; | ||
|
||
public static class CashRegisterId | ||
{ | ||
public static string From(string workstation) => | ||
$"urn:cash_register:{workstation}"; | ||
} | ||
|
||
public record CashRegister(string Id) | ||
{ | ||
public static CashRegister Create(CashRegisterInitialized @event) => | ||
new(@event.CashRegisterId); | ||
} | ||
|
||
public record CashRegisterInitialized( | ||
string CashRegisterId, | ||
DateTimeOffset InitializedAt | ||
); |
12 changes: 12 additions & 0 deletions
12
Sample/ClosingTheBooks/PointOfSales/CashRegister/CashRegisterDecider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
namespace PointOfSales.CashRegister; | ||
|
||
public record InitializeCashRegister( | ||
string CashRegisterId, | ||
DateTimeOffset Now | ||
); | ||
|
||
public static class CashRegisterDecider | ||
{ | ||
public static object[] Decide(InitializeCashRegister command) => | ||
[new CashRegisterInitialized(command.CashRegisterId, command.Now)]; | ||
} |
Oops, something went wrong.