1
1
using System . Collections . Immutable ;
2
2
using EventStore . Client ;
3
+ using Kurrent . Client . Streams . DecisionMaking ;
3
4
using Kurrent . Client . Streams . GettingState ;
4
5
5
6
namespace Kurrent . Client . Tests . Streams . DecisionMaking . UnionTypes ;
6
7
7
8
using static ShoppingCart ;
8
9
using static ShoppingCart . Event ;
10
+ using static ShoppingCart . Command ;
9
11
10
12
[ 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 )
13
15
: KurrentPermanentTests < KurrentPermanentFixture > ( output , fixture ) {
14
16
[ 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 ( ) {
16
18
// Given
17
19
var shoppingCartId = Guid . NewGuid ( ) ;
18
20
var clientId = Guid . NewGuid ( ) ;
@@ -22,38 +24,56 @@ public async Task gets_state_for_state_builder_with_evolve_function_and_typed_ev
22
24
var pairOfShoes = new PricedProductItem ( shoesId , 1 , 100 ) ;
23
25
var tShirt = new PricedProductItem ( tShirtId , 1 , 50 ) ;
24
26
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
-
34
27
var streamName = $ "shopping_cart-{ shoppingCartId } ";
35
28
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
+ ) ;
57
77
}
58
78
}
59
79
@@ -68,30 +88,25 @@ decimal UnitPrice
68
88
public abstract record ShoppingCart {
69
89
public abstract record Event {
70
90
public record Opened (
71
- Guid ShoppingCartId ,
72
91
Guid ClientId ,
73
92
DateTimeOffset OpenedAt
74
93
) : Event ;
75
94
76
95
public record ProductItemAdded (
77
- Guid ShoppingCartId ,
78
96
PricedProductItem ProductItem ,
79
97
DateTimeOffset AddedAt
80
98
) : Event ;
81
99
82
100
public record ProductItemRemoved (
83
- Guid ShoppingCartId ,
84
101
PricedProductItem ProductItem ,
85
102
DateTimeOffset RemovedAt
86
103
) : Event ;
87
104
88
105
public record Confirmed (
89
- Guid ShoppingCartId ,
90
106
DateTimeOffset ConfirmedAt
91
107
) : Event ;
92
108
93
109
public record Canceled (
94
- Guid ShoppingCartId ,
95
110
DateTimeOffset CanceledAt
96
111
) : Event ;
97
112
@@ -110,10 +125,10 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
110
125
( Initial , Opened ) =>
111
126
new Pending ( ProductItems . Empty ) ,
112
127
113
- ( Pending ( var productItems ) , ProductItemAdded ( _ , var productItem , _ ) ) =>
128
+ ( Pending ( var productItems ) , ProductItemAdded ( var productItem , _ ) ) =>
114
129
new Pending ( productItems . Add ( productItem ) ) ,
115
130
116
- ( Pending ( var productItems ) , ProductItemRemoved ( _ , var productItem , _ ) ) =>
131
+ ( Pending ( var productItems ) , ProductItemRemoved ( var productItem , _ ) ) =>
117
132
new Pending ( productItems . Remove ( productItem ) ) ,
118
133
119
134
( Pending , Confirmed ) =>
@@ -124,6 +139,61 @@ public static ShoppingCart Evolve(ShoppingCart state, Event @event) =>
124
139
125
140
_ => state
126
141
} ;
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
+ ) ;
127
197
}
128
198
129
199
public record ProductItems ( ImmutableDictionary < string , int > Items ) {
@@ -144,19 +214,3 @@ static string Key(PricedProductItem pricedProductItem) =>
144
214
ProductItems IncrementQuantity ( string key , int quantity ) =>
145
215
new ( Items . SetItem ( key , Items . TryGetValue ( key , out var current ) ? current + quantity : quantity ) ) ;
146
216
}
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