Skip to content

Commit 7dc2853

Browse files
committed
feat: Initial version of "HelloCart" sample.
1 parent 02fb052 commit 7dc2853

9 files changed

+325
-0
lines changed

Samples.sln

+7
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "src\Blazor\Client
3030
EndProject
3131
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "src\Blazor\ConsoleClient\ConsoleClient.csproj", "{AB5D5E44-4E90-48BA-BF15-1108C3950550}"
3232
EndProject
33+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloCart", "src\HelloCart\HelloCart.csproj", "{B36E4A0D-11B7-4F54-8E98-2A9D1C4E6F2E}"
34+
EndProject
3335
Global
3436
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3537
Debug|Any CPU = Debug|Any CPU
@@ -49,6 +51,7 @@ Global
4951
{C2B70735-480E-4B22-9DF1-8AFAC2C0F634} = {E3EFB37C-E523-44D0-ADC9-DE1257A19125}
5052
{D6FEBDE1-2FF2-4354-A500-692DD41FA1CA} = {9B29BD3F-7D94-4936-8379-61A2F48259AF}
5153
{AB5D5E44-4E90-48BA-BF15-1108C3950550} = {9B29BD3F-7D94-4936-8379-61A2F48259AF}
54+
{B36E4A0D-11B7-4F54-8E98-2A9D1C4E6F2E} = {B5A8469D-C4BB-4808-8E44-FF4DFCA57BDE}
5255
EndGlobalSection
5356
GlobalSection(ProjectConfigurationPlatforms) = postSolution
5457
{BA3D19EB-DDFD-4DB6-B60F-A110E22E9C48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -95,5 +98,9 @@ Global
9598
{AB5D5E44-4E90-48BA-BF15-1108C3950550}.Debug|Any CPU.Build.0 = Debug|Any CPU
9699
{AB5D5E44-4E90-48BA-BF15-1108C3950550}.Release|Any CPU.ActiveCfg = Release|Any CPU
97100
{AB5D5E44-4E90-48BA-BF15-1108C3950550}.Release|Any CPU.Build.0 = Release|Any CPU
101+
{B36E4A0D-11B7-4F54-8E98-2A9D1C4E6F2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
102+
{B36E4A0D-11B7-4F54-8E98-2A9D1C4E6F2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
103+
{B36E4A0D-11B7-4F54-8E98-2A9D1C4E6F2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
104+
{B36E4A0D-11B7-4F54-8E98-2A9D1C4E6F2E}.Release|Any CPU.Build.0 = Release|Any CPU
98105
EndGlobalSection
99106
EndGlobal

src/HelloCart/Domain.cs

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Collections.Immutable;
2+
using System.Reactive;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Stl;
6+
using Stl.CommandR;
7+
using Stl.CommandR.Configuration;
8+
using Stl.Fusion;
9+
10+
namespace Samples.HelloCart
11+
{
12+
public record Product : IHasId<string>
13+
{
14+
public string Id { get; init; } = "";
15+
public decimal Price { get; init; } = 0;
16+
}
17+
18+
public record Cart : IHasId<string>
19+
{
20+
public string Id { get; init; } = "";
21+
public ImmutableDictionary<string, decimal> Items { get; init; } = ImmutableDictionary<string, decimal>.Empty;
22+
}
23+
24+
public record EditCommand<TValue>(string Id, TValue? Value = null) : ICommand<Unit>
25+
where TValue : class, IHasId<string>
26+
{
27+
public EditCommand(TValue value) : this(value.Id, value) { }
28+
public EditCommand() : this("", null) { } // Needed just for JSON deserialization
29+
}
30+
31+
public interface IProductService
32+
{
33+
[CommandHandler]
34+
Task EditAsync(EditCommand<Product> command, CancellationToken cancellationToken = default);
35+
[ComputeMethod]
36+
Task<Product?> FindAsync(string id, CancellationToken cancellationToken = default);
37+
}
38+
39+
public interface ICartService
40+
{
41+
[CommandHandler]
42+
Task EditAsync(EditCommand<Cart> command, CancellationToken cancellationToken = default);
43+
[ComputeMethod]
44+
Task<Cart?> FindAsync(string id, CancellationToken cancellationToken = default);
45+
[ComputeMethod]
46+
Task<decimal> GetTotalAsync(string id, CancellationToken cancellationToken = default);
47+
}
48+
}

src/HelloCart/HelloCart.csproj

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net5.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<LangVersion>latest</LangVersion>
7+
<AssemblyName>Samples.HelloCart</AssemblyName>
8+
<RootNamespace>Samples.HelloCart</RootNamespace>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Stl.Fusion" Version="0.10.18" />
13+
</ItemGroup>
14+
</Project>

src/HelloCart/HelloCart.sln

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio 15
4+
VisualStudioVersion = 15.0.26124.0
5+
MinimumVisualStudioVersion = 15.0.26124.0
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloCart", "HelloCart.csproj", "{7975AF13-0A71-46E5-A026-A66960B05FED}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Debug|x64 = Debug|x64
12+
Debug|x86 = Debug|x86
13+
Release|Any CPU = Release|Any CPU
14+
Release|x64 = Release|x64
15+
Release|x86 = Release|x86
16+
EndGlobalSection
17+
GlobalSection(SolutionProperties) = preSolution
18+
HideSolutionNode = FALSE
19+
EndGlobalSection
20+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
21+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Debug|Any CPU.Build.0 = Debug|Any CPU
23+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Debug|x64.ActiveCfg = Debug|Any CPU
24+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Debug|x64.Build.0 = Debug|Any CPU
25+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Debug|x86.ActiveCfg = Debug|Any CPU
26+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Debug|x86.Build.0 = Debug|Any CPU
27+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Release|Any CPU.ActiveCfg = Release|Any CPU
28+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Release|Any CPU.Build.0 = Release|Any CPU
29+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Release|x64.ActiveCfg = Release|Any CPU
30+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Release|x64.Build.0 = Release|Any CPU
31+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Release|x86.ActiveCfg = Release|Any CPU
32+
{7975AF13-0A71-46E5-A026-A66960B05FED}.Release|x86.Build.0 = Release|Any CPU
33+
EndGlobalSection
34+
EndGlobal

src/HelloCart/Program.cs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Samples.HelloCart;
8+
using Samples.HelloCart.V1;
9+
using Stl.Async;
10+
using Stl.Extensibility;
11+
using static System.Console;
12+
13+
// This is our initial data
14+
var pApple = new Product { Id = "apple", Price = 1.3M };
15+
var pBanana = new Product { Id = "banana", Price = 0.6M };
16+
var pCarrot = new Product { Id = "carrot", Price = 0.9M };
17+
var cart1 = new Cart() { Id = "cart_1apple_2banana",
18+
Items = ImmutableDictionary<string, decimal>.Empty
19+
.Add(pApple.Id, 1)
20+
.Add(pBanana.Id, 2)
21+
};
22+
var cart2 = new Cart() { Id = "cart_1banana_1carrot",
23+
Items = ImmutableDictionary<string, decimal>.Empty
24+
.Add(pBanana.Id, 1)
25+
.Add(pCarrot.Id, 1)
26+
};
27+
28+
// Creating services & add initial data there
29+
await using var services = new ServiceCollection()
30+
.UseModules(modules => modules.Add<InMemoryModule>())
31+
.AddSingleton<Watcher>()
32+
.BuildServiceProvider();
33+
var products = services.GetRequiredService<IProductService>();
34+
var carts = services.GetRequiredService<ICartService>();
35+
await products.EditAsync(new EditCommand<Product>(pApple));
36+
await products.EditAsync(new EditCommand<Product>(pBanana));
37+
await products.EditAsync(new EditCommand<Product>(pCarrot));
38+
await carts.EditAsync(new EditCommand<Cart>(cart1));
39+
await carts.EditAsync(new EditCommand<Cart>(cart2));
40+
41+
using var stopCts = new CancellationTokenSource();
42+
var watcher = services.GetRequiredService<Watcher>();
43+
watcher.WatchAsync(new[] {pApple, pBanana, pCarrot}, new[] {cart1, cart2}, stopCts.Token).Ignore();
44+
45+
await Task.Delay(100); // Just to make sure watch tasks print whatever they want before our prompt appears
46+
WriteLine();
47+
WriteLine("Change product price by typing [productId]=[price], e.g. \"apple=0\".");
48+
WriteLine("See the total of every affected cart changes.");
49+
while (true) {
50+
await Task.Delay(100);
51+
Write("[productId]=[price]: ");
52+
try {
53+
var parts = (ReadLine() ?? "").Split("=");
54+
if (parts.Length != 2)
55+
throw new ApplicationException("Invalid price expression.");
56+
var productId = parts[0];
57+
var price = decimal.Parse(parts[1]);
58+
var product = await products.FindAsync(productId);
59+
if (product == null)
60+
throw new KeyNotFoundException("Specified product doesn't exist.");
61+
await products.EditAsync(new EditCommand<Product>(product with { Price = price }));
62+
}
63+
catch (Exception e) {
64+
WriteLine($"Error: {e.Message}");
65+
}
66+
}

src/HelloCart/Watcher.cs

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Collections.Generic;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Stl.Fusion;
5+
using static System.Console;
6+
7+
namespace Samples.HelloCart
8+
{
9+
public class Watcher
10+
{
11+
private readonly IProductService _products;
12+
private readonly ICartService _carts;
13+
14+
public Watcher(IProductService products, ICartService carts)
15+
{
16+
_products = products;
17+
_carts = carts;
18+
}
19+
20+
public Task WatchAsync(Product[] products, Cart[] carts, CancellationToken cancellationToken)
21+
{
22+
var tasks = new List<Task>();
23+
foreach (var product in products)
24+
tasks.Add(WatchProductAsync(product.Id, cancellationToken));
25+
foreach (var cart in carts)
26+
tasks.Add(WatchCartTotalAsync(cart.Id, cancellationToken));
27+
return Task.WhenAll(tasks);
28+
}
29+
30+
public async Task WatchProductAsync(string productId, CancellationToken cancellationToken)
31+
{
32+
var computed = await Computed.CaptureAsync(ct => _products.FindAsync(productId, ct), cancellationToken);
33+
while (!cancellationToken.IsCancellationRequested) {
34+
WriteLine(computed.Value);
35+
await computed.WhenInvalidatedAsync(cancellationToken);
36+
// Computed instances are ~ immutable, so update means getting a new one
37+
computed = await computed.UpdateAsync(false, cancellationToken);
38+
}
39+
}
40+
41+
public async Task WatchCartTotalAsync(string cartId, CancellationToken cancellationToken)
42+
{
43+
var computed = await Computed.CaptureAsync(ct => _carts.GetTotalAsync(cartId, ct), cancellationToken);
44+
while (!cancellationToken.IsCancellationRequested) {
45+
WriteLine($"{cartId}: total = {computed.Value}");
46+
await computed.WhenInvalidatedAsync(cancellationToken);
47+
// Computed instances are ~ immutable, so update means getting a new one
48+
computed = await computed.UpdateAsync(false, cancellationToken);
49+
}
50+
}
51+
}
52+
}
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Stl.Async;
7+
using Stl.Fusion;
8+
9+
namespace Samples.HelloCart.V1
10+
{
11+
public class InMemoryCartService : ICartService
12+
{
13+
private readonly ConcurrentDictionary<string, Cart> _carts = new();
14+
private readonly IProductService _products;
15+
16+
public InMemoryCartService(IProductService products) => _products = products;
17+
18+
public virtual Task EditAsync(EditCommand<Cart> command, CancellationToken cancellationToken = default)
19+
{
20+
if (string.IsNullOrEmpty(command.Id))
21+
throw new ArgumentOutOfRangeException(nameof(command));
22+
if (Computed.IsInvalidating()) {
23+
FindAsync(command.Id, default).Ignore();
24+
GetTotalAsync(command.Id, default).Ignore();
25+
return Task.CompletedTask;
26+
}
27+
28+
if (command.Value == null)
29+
_carts.Remove(command.Id, out _);
30+
else
31+
_carts[command.Id] = command.Value;
32+
return Task.CompletedTask;
33+
}
34+
35+
public virtual Task<Cart?> FindAsync(string id, CancellationToken cancellationToken = default)
36+
=> Task.FromResult(_carts.GetValueOrDefault(id));
37+
38+
public virtual async Task<decimal> GetTotalAsync(string id, CancellationToken cancellationToken = default)
39+
{
40+
var cart = await FindAsync(id, cancellationToken);
41+
if (cart == null)
42+
return 0;
43+
var total = 0M;
44+
foreach (var (productId, quantity) in cart.Items) {
45+
var product = await _products.FindAsync(productId, cancellationToken);
46+
total += (product?.Price ?? 0M) * quantity;
47+
}
48+
return total;
49+
}
50+
}
51+
}

src/HelloCart/v1/InMemoryModule.cs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Stl.Extensibility;
3+
using Stl.Fusion;
4+
5+
namespace Samples.HelloCart.V1
6+
{
7+
public class InMemoryModule : ModuleBase
8+
{
9+
public InMemoryModule(IServiceCollection services) : base(services) { }
10+
11+
public override void Use()
12+
{
13+
Services.AddFusion(fusion => {
14+
fusion.AddComputeService<IProductService, InMemoryProductService>();
15+
fusion.AddComputeService<ICartService, InMemoryCartService>();
16+
});
17+
}
18+
}
19+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Stl.Async;
7+
using Stl.Fusion;
8+
9+
namespace Samples.HelloCart.V1
10+
{
11+
public class InMemoryProductService : IProductService
12+
{
13+
private readonly ConcurrentDictionary<string, Product> _products = new();
14+
15+
public virtual Task EditAsync(EditCommand<Product> command, CancellationToken cancellationToken = default)
16+
{
17+
if (string.IsNullOrEmpty(command.Id))
18+
throw new ArgumentOutOfRangeException(nameof(command));
19+
if (Computed.IsInvalidating()) {
20+
FindAsync(command.Id, default).Ignore();
21+
return Task.CompletedTask;
22+
}
23+
24+
if (command.Value == null)
25+
_products.Remove(command.Id, out _);
26+
else
27+
_products[command.Id] = command.Value;
28+
return Task.CompletedTask;
29+
}
30+
31+
public virtual Task<Product?> FindAsync(string id, CancellationToken cancellationToken = default)
32+
=> Task.FromResult(_products.GetValueOrDefault(id));
33+
}
34+
}

0 commit comments

Comments
 (0)