Skip to content

Commit d2f9d4e

Browse files
committed
[DEVEX-227] Added example of Decider
1 parent 1f6f829 commit d2f9d4e

File tree

2 files changed

+150
-63
lines changed

2 files changed

+150
-63
lines changed

Diff for: src/Kurrent.Client/Streams/DecisionMaking/Decide.cs

+40-7
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ public static Task<IWriteResult> DecideAsync<TState>(
2424
DecideRetryPolicy(options).ExecuteAsync(
2525
async ct => {
2626
var (state, streamPosition, position) =
27-
await eventStore.GetStateAsync(streamName, stateBuilder, options?.GetStateOptions, ct);
27+
await eventStore.GetStateAsync(streamName, stateBuilder, options?.GetStateOptions, ct)
28+
.ConfigureAwait(false);
2829

29-
var messages = await decide(state, ct);
30+
var messages = await decide(state, ct).ConfigureAwait(false);
3031

3132
if (messages.Length == 0) {
3233
return new SuccessResult(
@@ -50,17 +51,49 @@ public static Task<IWriteResult> DecideAsync<TState>(
5051
messages,
5152
appendToStreamOptions,
5253
ct
53-
);
54+
).ConfigureAwait(false);
5455
},
5556
cancellationToken
5657
);
5758

59+
public static Task<IWriteResult> DecideAsync<TState, TCommand, TEvent>(
60+
this KurrentClient eventStore,
61+
string streamName,
62+
TCommand command,
63+
Decider<TState, TCommand, TEvent> decider,
64+
CancellationToken ct = default
65+
) where TState : notnull
66+
where TEvent : notnull =>
67+
eventStore.DecideAsync(
68+
streamName,
69+
command,
70+
decider.ToAsyncDecider(),
71+
ct
72+
);
73+
74+
public static Task<IWriteResult> DecideAsync<TState, TCommand, TEvent>(
75+
this KurrentClient eventStore,
76+
string streamName,
77+
TCommand command,
78+
Decider<TState, TCommand, TEvent> decider,
79+
DecideOptions<TState>? options,
80+
CancellationToken ct = default
81+
) where TState : notnull
82+
where TEvent : notnull =>
83+
eventStore.DecideAsync(
84+
streamName,
85+
command,
86+
decider.ToAsyncDecider(),
87+
options,
88+
ct
89+
);
90+
5891
public static Task<IWriteResult> DecideAsync<TState, TCommand>(
5992
this KurrentClient eventStore,
6093
string streamName,
6194
TCommand command,
6295
Decider<TState, TCommand> decider,
63-
CancellationToken ct
96+
CancellationToken ct = default
6497
) where TState : notnull =>
6598
eventStore.DecideAsync(
6699
streamName,
@@ -75,7 +108,7 @@ public static Task<IWriteResult> DecideAsync<TState, TCommand>(
75108
TCommand command,
76109
Decider<TState, TCommand> decider,
77110
DecideOptions<TState>? options,
78-
CancellationToken ct
111+
CancellationToken ct = default
79112
) where TState : notnull =>
80113
eventStore.DecideAsync(
81114
streamName,
@@ -90,7 +123,7 @@ public static Task<IWriteResult> DecideAsync<TState, TCommand>(
90123
string streamName,
91124
TCommand command,
92125
AsyncDecider<TState, TCommand> asyncDecider,
93-
CancellationToken ct
126+
CancellationToken ct = default
94127
) where TState : notnull =>
95128
eventStore.DecideAsync(
96129
streamName,
@@ -105,7 +138,7 @@ public static Task<IWriteResult> DecideAsync<TState, TCommand>(
105138
TCommand command,
106139
AsyncDecider<TState, TCommand> asyncDecider,
107140
DecideOptions<TState>? options,
108-
CancellationToken ct
141+
CancellationToken ct = default
109142
) where TState : notnull =>
110143
eventStore.DecideAsync(
111144
streamName,

Diff for: test/Kurrent.Client.Tests/Streams/DecisionMaking/UnionTypes/GettingStateTests.cs renamed to test/Kurrent.Client.Tests/Streams/DecisionMaking/UnionTypes/DecisionMakingWithDeciderTests.cs

+110-56
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
using System.Collections.Immutable;
22
using EventStore.Client;
3+
using Kurrent.Client.Streams.DecisionMaking;
34
using Kurrent.Client.Streams.GettingState;
45

56
namespace Kurrent.Client.Tests.Streams.DecisionMaking.UnionTypes;
67

78
using static ShoppingCart;
89
using static ShoppingCart.Event;
10+
using static ShoppingCart.Command;
911

1012
[Trait("Category", "Target:Streams")]
11-
[Trait("Category", "Operation:GetState")]
12-
public class GettingStateTests(ITestOutputHelper output, KurrentPermanentFixture fixture)
13+
[Trait("Category", "Operation:Decide")]
14+
public class DecisionMakingWithDeciderTests(ITestOutputHelper output, KurrentPermanentFixture fixture)
1315
: KurrentPermanentTests<KurrentPermanentFixture>(output, fixture) {
1416
[RetryFact]
15-
public async Task gets_state_for_state_builder_with_evolve_function_and_typed_events() {
17+
public async Task runs_business_logic_with_decider_and_typed_events() {
1618
// Given
1719
var shoppingCartId = Guid.NewGuid();
1820
var clientId = Guid.NewGuid();
@@ -22,38 +24,56 @@ public async Task gets_state_for_state_builder_with_evolve_function_and_typed_ev
2224
var pairOfShoes = new PricedProductItem(shoesId, 1, 100);
2325
var tShirt = new PricedProductItem(tShirtId, 1, 50);
2426

25-
var events = new Event[] {
26-
new Opened(shoppingCartId, clientId, DateTime.UtcNow),
27-
new ProductItemAdded(shoppingCartId, twoPairsOfShoes, DateTime.UtcNow),
28-
new ProductItemAdded(shoppingCartId, tShirt, DateTime.UtcNow),
29-
new ProductItemRemoved(shoppingCartId, pairOfShoes, DateTime.UtcNow),
30-
new Confirmed(shoppingCartId, DateTime.UtcNow),
31-
new Canceled(shoppingCartId, DateTime.UtcNow)
32-
};
33-
3427
var streamName = $"shopping_cart-{shoppingCartId}";
3528

36-
await Fixture.Streams.AppendToStreamAsync(streamName, events);
37-
38-
var stateBuilder = StateBuilder.For<ShoppingCart, Event>(Evolve, () => new Initial());
39-
40-
// When
41-
var result = await Fixture.Streams.GetStateAsync(streamName, stateBuilder);
42-
43-
var shoppingCart = result.State;
44-
45-
// Then
46-
Assert.IsType<Closed>(shoppingCart);
47-
// TODO: Add some time travelling
48-
// Assert.Equal(2, shoppingCart.);
49-
//
50-
// Assert.Equal(shoesId, shoppingCart.ProductItems[0].ProductId);
51-
// Assert.Equal(pairOfShoes.Quantity, shoppingCart.ProductItems[0].Quantity);
52-
// Assert.Equal(pairOfShoes.UnitPrice, shoppingCart.ProductItems[0].UnitPrice);
53-
//
54-
// Assert.Equal(tShirtId, shoppingCart.ProductItems[1].ProductId);
55-
// Assert.Equal(tShirt.Quantity, shoppingCart.ProductItems[1].Quantity);
56-
// Assert.Equal(tShirt.UnitPrice, shoppingCart.ProductItems[1].UnitPrice);
29+
var result = await Fixture.Streams.DecideAsync(
30+
streamName,
31+
new Open(clientId, DateTime.UtcNow),
32+
Decider
33+
);
34+
35+
Assert.IsType<SuccessResult>(result);
36+
37+
result = await Fixture.Streams.DecideAsync(
38+
streamName,
39+
new AddProductItem(twoPairsOfShoes, DateTime.UtcNow),
40+
Decider
41+
);
42+
43+
Assert.IsType<SuccessResult>(result);
44+
45+
result = await Fixture.Streams.DecideAsync(
46+
streamName,
47+
new AddProductItem(tShirt, DateTime.UtcNow),
48+
Decider
49+
);
50+
51+
Assert.IsType<SuccessResult>(result);
52+
53+
result = await Fixture.Streams.DecideAsync(
54+
streamName,
55+
new RemoveProductItem(pairOfShoes, DateTime.UtcNow),
56+
Decider
57+
);
58+
59+
Assert.IsType<SuccessResult>(result);
60+
61+
result = await Fixture.Streams.DecideAsync(
62+
streamName,
63+
new Confirm(DateTime.UtcNow),
64+
Decider
65+
);
66+
67+
Assert.IsType<SuccessResult>(result);
68+
69+
await Assert.ThrowsAsync<InvalidOperationException>(
70+
() =>
71+
Fixture.Streams.DecideAsync(
72+
streamName,
73+
new Cancel(DateTime.UtcNow),
74+
Decider
75+
)
76+
);
5777
}
5878
}
5979

@@ -68,30 +88,25 @@ decimal UnitPrice
6888
public abstract record ShoppingCart {
6989
public abstract record Event {
7090
public record Opened(
71-
Guid ShoppingCartId,
7291
Guid ClientId,
7392
DateTimeOffset OpenedAt
7493
) : Event;
7594

7695
public record ProductItemAdded(
77-
Guid ShoppingCartId,
7896
PricedProductItem ProductItem,
7997
DateTimeOffset AddedAt
8098
) : Event;
8199

82100
public record ProductItemRemoved(
83-
Guid ShoppingCartId,
84101
PricedProductItem ProductItem,
85102
DateTimeOffset RemovedAt
86103
) : Event;
87104

88105
public record Confirmed(
89-
Guid ShoppingCartId,
90106
DateTimeOffset ConfirmedAt
91107
) : Event;
92108

93109
public record Canceled(
94-
Guid ShoppingCartId,
95110
DateTimeOffset CanceledAt
96111
) : Event;
97112

@@ -110,10 +125,10 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
110125
(Initial, Opened) =>
111126
new Pending(ProductItems.Empty),
112127

113-
(Pending(var productItems), ProductItemAdded(_, var productItem, _)) =>
128+
(Pending(var productItems), ProductItemAdded(var productItem, _)) =>
114129
new Pending(productItems.Add(productItem)),
115130

116-
(Pending(var productItems), ProductItemRemoved(_, var productItem, _)) =>
131+
(Pending(var productItems), ProductItemRemoved(var productItem, _)) =>
117132
new Pending(productItems.Remove(productItem)),
118133

119134
(Pending, Confirmed) =>
@@ -124,6 +139,61 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
124139

125140
_ => state
126141
};
142+
143+
public abstract record Command {
144+
public record Open(
145+
Guid ClientId,
146+
DateTimeOffset Now
147+
) : Command;
148+
149+
public record AddProductItem(
150+
PricedProductItem ProductItem,
151+
DateTimeOffset Now
152+
) : Command;
153+
154+
public record RemoveProductItem(
155+
PricedProductItem ProductItem,
156+
DateTimeOffset Now
157+
) : Command;
158+
159+
public record Confirm(
160+
DateTimeOffset Now
161+
) : Command;
162+
163+
public record Cancel(
164+
DateTimeOffset Now
165+
) : Command;
166+
167+
Command() { }
168+
}
169+
170+
public static Event[] Decide(Command command, ShoppingCart state) =>
171+
(state, command) switch {
172+
(Pending, Open) => [],
173+
174+
(Initial, Open(var clientId, var now)) => [new Opened(clientId, now)],
175+
176+
(Pending, AddProductItem(var productItem, var now)) => [new ProductItemAdded(productItem, now)],
177+
178+
(Pending(var productItems), RemoveProductItem(var productItem, var now)) =>
179+
productItems.HasEnough(productItem)
180+
? [new ProductItemRemoved(productItem, now)]
181+
: throw new InvalidOperationException("Not enough product items to remove"),
182+
183+
(Pending, Confirm(var now)) => [new Confirmed(now)],
184+
185+
(Pending, Cancel(var now)) => [new Canceled(now)],
186+
187+
_ => throw new InvalidOperationException(
188+
$"Cannot {command.GetType().Name} for {state.GetType().Name} shopping cart"
189+
)
190+
};
191+
192+
public static readonly Decider<ShoppingCart, Command, Event> Decider = new Decider<ShoppingCart, Command, Event>(
193+
Decide,
194+
Evolve,
195+
() => new Initial()
196+
);
127197
}
128198

129199
public record ProductItems(ImmutableDictionary<string, int> Items) {
@@ -144,19 +214,3 @@ static string Key(PricedProductItem pricedProductItem) =>
144214
ProductItems IncrementQuantity(string key, int quantity) =>
145215
new(Items.SetItem(key, Items.TryGetValue(key, out var current) ? current + quantity : quantity));
146216
}
147-
148-
public static class DictionaryExtensions {
149-
public static ImmutableDictionary<TKey, TValue> Set<TKey, TValue>(
150-
this ImmutableDictionary<TKey, TValue> dictionary,
151-
TKey key,
152-
Func<TValue?, TValue> set
153-
) where TKey : notnull =>
154-
dictionary.SetItem(key, set(dictionary.TryGetValue(key, out var current) ? current : default));
155-
156-
public static void Set<TKey, TValue>(
157-
this Dictionary<TKey, TValue> dictionary,
158-
TKey key,
159-
Func<TValue?, TValue> set
160-
) where TKey : notnull =>
161-
dictionary[key] = set(dictionary.TryGetValue(key, out var current) ? current : default);
162-
}

0 commit comments

Comments
 (0)