Skip to content

Commit b0e6f24

Browse files
author
MPCoreDeveloper
committed
updated nuget refs updated radme and demo project for CQRS
1 parent c09a162 commit b0e6f24

File tree

10 files changed

+622
-8
lines changed

10 files changed

+622
-8
lines changed
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
// <copyright file="OrderCqrsDemo.cs" company="MPCoreDeveloper">
2+
// Copyright (c) 2026 MPCoreDeveloper and GitHub Copilot. All rights reserved.
3+
// Licensed under the MIT License.
4+
// </copyright>
5+
6+
namespace OrderManagement.CqrsDemo;
7+
8+
using SharpCoreDB.CQRS;
9+
10+
/// <summary>
11+
/// Command for creating an order on the write side.
12+
/// </summary>
13+
internal readonly record struct PlaceOrderCommand(string OrderId, string CustomerId, IReadOnlyList<OrderLineInput> Lines) : ICommand;
14+
15+
/// <summary>
16+
/// Command for adding a line to an existing order on the write side.
17+
/// </summary>
18+
internal readonly record struct AddOrderLineCommand(string OrderId, OrderLineInput Line) : ICommand;
19+
20+
/// <summary>
21+
/// Command for confirming an existing order.
22+
/// </summary>
23+
internal readonly record struct ConfirmOrderCommand(string OrderId) : ICommand;
24+
25+
/// <summary>
26+
/// Command for marking an existing order as paid.
27+
/// </summary>
28+
internal readonly record struct MarkOrderPaidCommand(string OrderId, string PaymentReference) : ICommand;
29+
30+
/// <summary>
31+
/// Query contract for retrieving read-model order summary data.
32+
/// </summary>
33+
internal readonly record struct GetOrderSummaryQuery(string OrderId);
34+
35+
/// <summary>
36+
/// Input DTO used by command payloads.
37+
/// </summary>
38+
internal readonly record struct OrderLineInput(string ProductId, string ProductName, int Quantity, decimal UnitPrice);
39+
40+
internal enum OrderStatus
41+
{
42+
Draft,
43+
Confirmed,
44+
Paid,
45+
}
46+
47+
internal sealed class PlaceOrderCommandHandler(InMemoryOrderWriteRepository writeRepository, OrderReadProjector projector) : ICommandHandler<PlaceOrderCommand>
48+
{
49+
private readonly InMemoryOrderWriteRepository _writeRepository = writeRepository;
50+
private readonly OrderReadProjector _projector = projector;
51+
52+
public Task<CommandDispatchResult> HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken = default)
53+
{
54+
cancellationToken.ThrowIfCancellationRequested();
55+
56+
if (string.IsNullOrWhiteSpace(command.OrderId))
57+
{
58+
return Task.FromResult(CommandDispatchResult.Fail("OrderId is required."));
59+
}
60+
61+
if (string.IsNullOrWhiteSpace(command.CustomerId))
62+
{
63+
return Task.FromResult(CommandDispatchResult.Fail("CustomerId is required."));
64+
}
65+
66+
if (command.Lines.Count == 0)
67+
{
68+
return Task.FromResult(CommandDispatchResult.Fail("At least one line is required."));
69+
}
70+
71+
if (_writeRepository.Exists(command.OrderId))
72+
{
73+
return Task.FromResult(CommandDispatchResult.Fail($"Order '{command.OrderId}' already exists."));
74+
}
75+
76+
var lines = command.Lines
77+
.Select(static line => new OrderLine(line.ProductId, line.ProductName, line.Quantity, line.UnitPrice))
78+
.ToList();
79+
80+
var aggregate = new OrderWriteModel(
81+
command.OrderId,
82+
command.CustomerId,
83+
OrderStatus.Draft,
84+
lines,
85+
1,
86+
null);
87+
88+
_writeRepository.Save(aggregate);
89+
_projector.Project(new OrderPlacedNotification(command.OrderId, command.CustomerId, aggregate.TotalAmount, aggregate.Lines.Count));
90+
91+
return Task.FromResult(CommandDispatchResult.Ok("Order created."));
92+
}
93+
}
94+
95+
internal sealed class AddOrderLineCommandHandler(InMemoryOrderWriteRepository writeRepository, OrderReadProjector projector) : ICommandHandler<AddOrderLineCommand>
96+
{
97+
private readonly InMemoryOrderWriteRepository _writeRepository = writeRepository;
98+
private readonly OrderReadProjector _projector = projector;
99+
100+
public Task<CommandDispatchResult> HandleAsync(AddOrderLineCommand command, CancellationToken cancellationToken = default)
101+
{
102+
cancellationToken.ThrowIfCancellationRequested();
103+
104+
if (!_writeRepository.TryGet(command.OrderId, out var order))
105+
{
106+
return Task.FromResult(CommandDispatchResult.Fail($"Order '{command.OrderId}' was not found."));
107+
}
108+
109+
if (order.Status != OrderStatus.Draft)
110+
{
111+
return Task.FromResult(CommandDispatchResult.Fail("Only draft orders can be changed."));
112+
}
113+
114+
order.Lines.Add(new OrderLine(command.Line.ProductId, command.Line.ProductName, command.Line.Quantity, command.Line.UnitPrice));
115+
order.Version++;
116+
_writeRepository.Save(order);
117+
118+
_projector.Project(new OrderLineAddedNotification(command.OrderId, order.TotalAmount, order.Lines.Count));
119+
return Task.FromResult(CommandDispatchResult.Ok("Line added."));
120+
}
121+
}
122+
123+
internal sealed class ConfirmOrderCommandHandler(InMemoryOrderWriteRepository writeRepository, OrderReadProjector projector) : ICommandHandler<ConfirmOrderCommand>
124+
{
125+
private readonly InMemoryOrderWriteRepository _writeRepository = writeRepository;
126+
private readonly OrderReadProjector _projector = projector;
127+
128+
public Task<CommandDispatchResult> HandleAsync(ConfirmOrderCommand command, CancellationToken cancellationToken = default)
129+
{
130+
cancellationToken.ThrowIfCancellationRequested();
131+
132+
if (!_writeRepository.TryGet(command.OrderId, out var order))
133+
{
134+
return Task.FromResult(CommandDispatchResult.Fail($"Order '{command.OrderId}' was not found."));
135+
}
136+
137+
if (order.Status != OrderStatus.Draft)
138+
{
139+
return Task.FromResult(CommandDispatchResult.Fail("Only draft orders can be confirmed."));
140+
}
141+
142+
order.Status = OrderStatus.Confirmed;
143+
order.Version++;
144+
_writeRepository.Save(order);
145+
146+
_projector.Project(new OrderConfirmedNotification(command.OrderId));
147+
return Task.FromResult(CommandDispatchResult.Ok("Order confirmed."));
148+
}
149+
}
150+
151+
internal sealed class MarkOrderPaidCommandHandler(InMemoryOrderWriteRepository writeRepository, OrderReadProjector projector) : ICommandHandler<MarkOrderPaidCommand>
152+
{
153+
private readonly InMemoryOrderWriteRepository _writeRepository = writeRepository;
154+
private readonly OrderReadProjector _projector = projector;
155+
156+
public Task<CommandDispatchResult> HandleAsync(MarkOrderPaidCommand command, CancellationToken cancellationToken = default)
157+
{
158+
cancellationToken.ThrowIfCancellationRequested();
159+
160+
if (!_writeRepository.TryGet(command.OrderId, out var order))
161+
{
162+
return Task.FromResult(CommandDispatchResult.Fail($"Order '{command.OrderId}' was not found."));
163+
}
164+
165+
if (order.Status != OrderStatus.Confirmed)
166+
{
167+
return Task.FromResult(CommandDispatchResult.Fail("Only confirmed orders can be marked as paid."));
168+
}
169+
170+
if (string.IsNullOrWhiteSpace(command.PaymentReference))
171+
{
172+
return Task.FromResult(CommandDispatchResult.Fail("Payment reference is required."));
173+
}
174+
175+
order.Status = OrderStatus.Paid;
176+
order.PaymentReference = command.PaymentReference;
177+
order.Version++;
178+
_writeRepository.Save(order);
179+
180+
_projector.Project(new OrderPaidNotification(command.OrderId, command.PaymentReference));
181+
return Task.FromResult(CommandDispatchResult.Ok("Order paid."));
182+
}
183+
}
184+
185+
internal sealed class InMemoryOrderWriteRepository
186+
{
187+
private readonly Dictionary<string, OrderWriteModel> _orders = [];
188+
189+
public bool Exists(string orderId) => _orders.ContainsKey(orderId);
190+
191+
public bool TryGet(string orderId, out OrderWriteModel order)
192+
{
193+
ArgumentException.ThrowIfNullOrWhiteSpace(orderId);
194+
return _orders.TryGetValue(orderId, out order!);
195+
}
196+
197+
public void Save(OrderWriteModel order)
198+
{
199+
ArgumentNullException.ThrowIfNull(order);
200+
_orders[order.OrderId] = order;
201+
}
202+
203+
public OrderWriteModel GetRequired(string orderId)
204+
{
205+
if (!TryGet(orderId, out var order))
206+
{
207+
throw new InvalidOperationException($"Order '{orderId}' was not found.");
208+
}
209+
210+
return order;
211+
}
212+
}
213+
214+
internal sealed class InMemoryOrderReadStore
215+
{
216+
private readonly Dictionary<string, OrderReadModel> _orders = [];
217+
218+
public OrderReadModel? Get(string orderId)
219+
{
220+
ArgumentException.ThrowIfNullOrWhiteSpace(orderId);
221+
_orders.TryGetValue(orderId, out var model);
222+
return model;
223+
}
224+
225+
public void Upsert(OrderReadModel model)
226+
{
227+
ArgumentNullException.ThrowIfNull(model);
228+
_orders[model.OrderId] = model;
229+
}
230+
}
231+
232+
internal sealed class OrderReadProjector(InMemoryOrderReadStore readStore)
233+
{
234+
private readonly InMemoryOrderReadStore _readStore = readStore;
235+
236+
public void Project(OrderPlacedNotification notification)
237+
{
238+
var now = DateTimeOffset.UtcNow;
239+
var readModel = new OrderReadModel(
240+
notification.OrderId,
241+
notification.CustomerId,
242+
OrderStatus.Draft,
243+
notification.LineCount,
244+
notification.TotalAmount,
245+
PaymentReference: null,
246+
UpdatedAtUtc: now);
247+
248+
_readStore.Upsert(readModel);
249+
}
250+
251+
public void Project(OrderLineAddedNotification notification)
252+
{
253+
var model = _readStore.Get(notification.OrderId)
254+
?? throw new InvalidOperationException($"Read model for '{notification.OrderId}' was not found.");
255+
256+
_readStore.Upsert(model with
257+
{
258+
LineCount = notification.LineCount,
259+
TotalAmount = notification.TotalAmount,
260+
UpdatedAtUtc = DateTimeOffset.UtcNow,
261+
});
262+
}
263+
264+
public void Project(OrderConfirmedNotification notification)
265+
{
266+
var model = _readStore.Get(notification.OrderId)
267+
?? throw new InvalidOperationException($"Read model for '{notification.OrderId}' was not found.");
268+
269+
_readStore.Upsert(model with
270+
{
271+
Status = OrderStatus.Confirmed,
272+
UpdatedAtUtc = DateTimeOffset.UtcNow,
273+
});
274+
}
275+
276+
public void Project(OrderPaidNotification notification)
277+
{
278+
var model = _readStore.Get(notification.OrderId)
279+
?? throw new InvalidOperationException($"Read model for '{notification.OrderId}' was not found.");
280+
281+
_readStore.Upsert(model with
282+
{
283+
Status = OrderStatus.Paid,
284+
PaymentReference = notification.PaymentReference,
285+
UpdatedAtUtc = DateTimeOffset.UtcNow,
286+
});
287+
}
288+
}
289+
290+
internal sealed class OrderQueryService(InMemoryOrderReadStore readStore)
291+
{
292+
private readonly InMemoryOrderReadStore _readStore = readStore;
293+
294+
public OrderReadModel? GetOrderSummary(GetOrderSummaryQuery query)
295+
{
296+
ArgumentException.ThrowIfNullOrWhiteSpace(query.OrderId);
297+
return _readStore.Get(query.OrderId);
298+
}
299+
}
300+
301+
internal sealed class OrderWriteModel(
302+
string orderId,
303+
string customerId,
304+
OrderStatus status,
305+
List<OrderLine> lines,
306+
long version,
307+
string? paymentReference)
308+
{
309+
public string OrderId { get; } = orderId;
310+
311+
public string CustomerId { get; } = customerId;
312+
313+
public OrderStatus Status { get; set; } = status;
314+
315+
public List<OrderLine> Lines { get; } = lines;
316+
317+
public long Version { get; set; } = version;
318+
319+
public string? PaymentReference { get; set; } = paymentReference;
320+
321+
public decimal TotalAmount => Lines.Sum(static line => line.LineTotal);
322+
}
323+
324+
internal readonly record struct OrderLine(string ProductId, string ProductName, int Quantity, decimal UnitPrice)
325+
{
326+
public decimal LineTotal => Quantity * UnitPrice;
327+
}
328+
329+
internal readonly record struct OrderPlacedNotification(string OrderId, string CustomerId, decimal TotalAmount, int LineCount);
330+
331+
internal readonly record struct OrderLineAddedNotification(string OrderId, decimal TotalAmount, int LineCount);
332+
333+
internal readonly record struct OrderConfirmedNotification(string OrderId);
334+
335+
internal readonly record struct OrderPaidNotification(string OrderId, string PaymentReference);
336+
337+
internal sealed record class OrderReadModel(
338+
string OrderId,
339+
string CustomerId,
340+
OrderStatus Status,
341+
int LineCount,
342+
decimal TotalAmount,
343+
string? PaymentReference,
344+
DateTimeOffset UpdatedAtUtc);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<LangVersion>14.0</LangVersion>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<Nullable>enable</Nullable>
9+
<Version>1.6.0</Version>
10+
<IsPackable>false</IsPackable>
11+
<UseLocalSharpCoreDbSources Condition="'$(UseLocalSharpCoreDbSources)' == '' and Exists('..\..\..\src\SharpCoreDB.CQRS\SharpCoreDB.CQRS.csproj')">true</UseLocalSharpCoreDbSources>
12+
<UseLocalSharpCoreDbSources Condition="'$(UseLocalSharpCoreDbSources)' == ''">false</UseLocalSharpCoreDbSources>
13+
</PropertyGroup>
14+
15+
<ItemGroup Condition="'$(UseLocalSharpCoreDbSources)' == 'true'">
16+
<ProjectReference Include="..\..\..\src\SharpCoreDB.CQRS\SharpCoreDB.CQRS.csproj" />
17+
</ItemGroup>
18+
19+
<ItemGroup Condition="'$(UseLocalSharpCoreDbSources)' != 'true'">
20+
<PackageReference Include="SharpCoreDB.CQRS" Version="1.6.0" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="10.0.201" />
25+
</ItemGroup>
26+
27+
</Project>

0 commit comments

Comments
 (0)