Skip to content

Commit 86c22ab

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

File tree

2 files changed

+146
-48
lines changed

2 files changed

+146
-48
lines changed

src/Kurrent.Client/Streams/DecisionMaking/Decide.cs

Lines changed: 40 additions & 7 deletions
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,

test/Kurrent.Client.Tests/Streams/DecisionMaking/UnionTypes/GettingStateTests.cs

Lines changed: 106 additions & 41 deletions
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")]
13+
[Trait("Category", "Operation:Decide")]
1214
public class GettingStateTests(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();
@@ -23,16 +25,55 @@ public async Task gets_state_for_state_builder_with_evolve_function_and_typed_ev
2325
var tShirt = new PricedProductItem(tShirtId, 1, 50);
2426

2527
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)
28+
new Opened(clientId, DateTime.UtcNow),
29+
new ProductItemAdded(twoPairsOfShoes, DateTime.UtcNow),
30+
new ProductItemAdded(tShirt, DateTime.UtcNow),
31+
new ProductItemRemoved(pairOfShoes, DateTime.UtcNow),
32+
new Confirmed(DateTime.UtcNow),
33+
new Canceled(DateTime.UtcNow)
3234
};
3335

3436
var streamName = $"shopping_cart-{shoppingCartId}";
3537

38+
await Fixture.Streams.DecideAsync(
39+
streamName,
40+
new Open(clientId, DateTime.UtcNow),
41+
Decider
42+
);
43+
44+
await Fixture.Streams.DecideAsync(
45+
streamName,
46+
new AddProductItem(twoPairsOfShoes, DateTime.UtcNow),
47+
Decider
48+
);
49+
50+
await Fixture.Streams.DecideAsync(
51+
streamName,
52+
new AddProductItem(tShirt, DateTime.UtcNow),
53+
Decider
54+
);
55+
56+
await Fixture.Streams.DecideAsync(
57+
streamName,
58+
new RemoveProductItem(pairOfShoes, DateTime.UtcNow),
59+
Decider
60+
);
61+
62+
await Fixture.Streams.DecideAsync(
63+
streamName,
64+
new Confirm(DateTime.UtcNow),
65+
Decider
66+
);
67+
68+
await Assert.ThrowsAsync<InvalidOperationException>(
69+
() =>
70+
Fixture.Streams.DecideAsync(
71+
streamName,
72+
new Cancel(DateTime.UtcNow),
73+
Decider
74+
)
75+
);
76+
3677
await Fixture.Streams.AppendToStreamAsync(streamName, events);
3778

3879
var stateBuilder = StateBuilder.For<ShoppingCart, Event>(Evolve, () => new Initial());
@@ -44,16 +85,6 @@ public async Task gets_state_for_state_builder_with_evolve_function_and_typed_ev
4485

4586
// Then
4687
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);
5788
}
5889
}
5990

@@ -68,30 +99,25 @@ decimal UnitPrice
6899
public abstract record ShoppingCart {
69100
public abstract record Event {
70101
public record Opened(
71-
Guid ShoppingCartId,
72102
Guid ClientId,
73103
DateTimeOffset OpenedAt
74104
) : Event;
75105

76106
public record ProductItemAdded(
77-
Guid ShoppingCartId,
78107
PricedProductItem ProductItem,
79108
DateTimeOffset AddedAt
80109
) : Event;
81110

82111
public record ProductItemRemoved(
83-
Guid ShoppingCartId,
84112
PricedProductItem ProductItem,
85113
DateTimeOffset RemovedAt
86114
) : Event;
87115

88116
public record Confirmed(
89-
Guid ShoppingCartId,
90117
DateTimeOffset ConfirmedAt
91118
) : Event;
92119

93120
public record Canceled(
94-
Guid ShoppingCartId,
95121
DateTimeOffset CanceledAt
96122
) : Event;
97123

@@ -110,10 +136,10 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
110136
(Initial, Opened) =>
111137
new Pending(ProductItems.Empty),
112138

113-
(Pending(var productItems), ProductItemAdded(_, var productItem, _)) =>
139+
(Pending(var productItems), ProductItemAdded(var productItem, _)) =>
114140
new Pending(productItems.Add(productItem)),
115141

116-
(Pending(var productItems), ProductItemRemoved(_, var productItem, _)) =>
142+
(Pending(var productItems), ProductItemRemoved(var productItem, _)) =>
117143
new Pending(productItems.Remove(productItem)),
118144

119145
(Pending, Confirmed) =>
@@ -124,6 +150,61 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
124150

125151
_ => state
126152
};
153+
154+
public abstract record Command {
155+
public record Open(
156+
Guid ClientId,
157+
DateTimeOffset Now
158+
) : Command;
159+
160+
public record AddProductItem(
161+
PricedProductItem ProductItem,
162+
DateTimeOffset Now
163+
) : Command;
164+
165+
public record RemoveProductItem(
166+
PricedProductItem ProductItem,
167+
DateTimeOffset Now
168+
) : Command;
169+
170+
public record Confirm(
171+
DateTimeOffset Now
172+
) : Command;
173+
174+
public record Cancel(
175+
DateTimeOffset Now
176+
) : Command;
177+
178+
Command() { }
179+
}
180+
181+
public static Event[] Decide(Command command, ShoppingCart state) =>
182+
(state, command) switch {
183+
(Pending, Open) => [],
184+
185+
(Initial, Open(var clientId, var now)) => [new Opened(clientId, now)],
186+
187+
(Pending, AddProductItem(var productItem, var now)) => [new ProductItemAdded(productItem, now)],
188+
189+
(Pending(var productItems), RemoveProductItem(var productItem, var now)) =>
190+
productItems.HasEnough(productItem)
191+
? [new ProductItemRemoved(productItem, now)]
192+
: throw new InvalidOperationException("Not enough product items to remove"),
193+
194+
(Pending, Confirm(var now)) => [new Confirmed(now)],
195+
196+
(Pending, Cancel(var now)) => [new Canceled(now)],
197+
198+
_ => throw new InvalidOperationException(
199+
$"Cannot {command.GetType().Name} for {state.GetType().Name} shopping cart"
200+
)
201+
};
202+
203+
public static readonly Decider<ShoppingCart, Command, Event> Decider = new Decider<ShoppingCart, Command, Event>(
204+
Decide,
205+
Evolve,
206+
() => new Initial()
207+
);
127208
}
128209

129210
public record ProductItems(ImmutableDictionary<string, int> Items) {
@@ -144,19 +225,3 @@ static string Key(PricedProductItem pricedProductItem) =>
144225
ProductItems IncrementQuantity(string key, int quantity) =>
145226
new(Items.SetItem(key, Items.TryGetValue(key, out var current) ? current + quantity : quantity));
146227
}
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)