Skip to content

Commit 54c91c4

Browse files
committed
Add Sagas implementation. Update README
1 parent caee6f9 commit 54c91c4

21 files changed

+872
-51
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp2.1</TargetFramework>
5+
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
11+
<PackageReference Include="Moq" Version="4.10.1" />
12+
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
13+
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\OrangeLoop.Sagas\OrangeLoop.Sagas.csproj" />
18+
</ItemGroup>
19+
20+
</Project>

OrangeLoop.Sagas.Tests/SagaTests.cs

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2+
using Moq;
3+
using OrangeLoop.Sagas.Interfaces;
4+
using System;
5+
using System.Threading.Tasks;
6+
7+
namespace OrangeLoop.Sagas.Tests
8+
{
9+
[TestClass]
10+
public class SagaTests
11+
{
12+
//private AutoMoq.AutoMoqer _mocker = new AutoMoq.AutoMoqer();
13+
private readonly Mock<IUnitOfWorkFactory> _unitOfWorkFactoryMock;
14+
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
15+
16+
public SagaTests()
17+
{
18+
_unitOfWorkMock = new Mock<IUnitOfWork>();
19+
_unitOfWorkMock.Setup(x => x.Commit());
20+
_unitOfWorkMock.Setup(x => x.Rollback());
21+
22+
_unitOfWorkFactoryMock = new Mock<IUnitOfWorkFactory>();
23+
_unitOfWorkFactoryMock.Setup(x => x.Create()).Returns(_unitOfWorkMock.Object);
24+
}
25+
26+
[TestMethod]
27+
public async Task Saga_Run_All_Steps_Are_Executed()
28+
{
29+
// Arrange
30+
var saga = new SimpleSaga();
31+
32+
// Act
33+
var context = await saga.Run(new SagaContext()).ConfigureAwait(false);
34+
35+
// Assert
36+
Assert.IsTrue(context.Step1_Invoked);
37+
Assert.IsTrue(context.Step2_Invoked);
38+
Assert.IsTrue(context.Step3_Invoked);
39+
40+
Assert.IsFalse(context.Step1_Rollback);
41+
Assert.IsFalse(context.Step2_Rollback);
42+
Assert.IsFalse(context.Step3_Rollback);
43+
}
44+
45+
[TestMethod]
46+
public async Task Saga_Run_All_Steps_Are_Rolled_Back()
47+
{
48+
// Arrange
49+
var saga = new SimpleSagaWithRollback();
50+
51+
// Act
52+
var context = new SagaContext();
53+
try
54+
{
55+
context = await saga.Run(context).ConfigureAwait(false);
56+
57+
Assert.IsTrue(false, "Expected Exception");
58+
}
59+
catch
60+
{
61+
// Assert
62+
Assert.IsTrue(context.Step1_Invoked);
63+
Assert.IsTrue(context.Step2_Invoked);
64+
Assert.IsFalse(context.Step3_Invoked);
65+
66+
Assert.IsTrue(context.Step1_Rollback);
67+
Assert.IsTrue(context.Step2_Rollback);
68+
Assert.IsTrue(context.Step3_Rollback);
69+
}
70+
}
71+
72+
[TestMethod]
73+
public async Task UnitOfWorkSaga_CallsCommit()
74+
{
75+
// Arrange
76+
var saga = new SimpleUOWSaga(_unitOfWorkFactoryMock.Object);
77+
78+
// Act
79+
var context = await saga.Run(new SagaContext());
80+
81+
// Assert
82+
Assert.IsTrue(context.Step1_Invoked);
83+
Assert.IsTrue(context.Step2_Invoked);
84+
Assert.IsTrue(context.Step3_Invoked);
85+
_unitOfWorkMock.Verify(x => x.Commit(), Times.Once());
86+
_unitOfWorkMock.Verify(x => x.Rollback(), Times.Never());
87+
}
88+
89+
[TestMethod]
90+
public async Task UnitOfWorkSaga_CallsRollback()
91+
{
92+
// Arrange
93+
var saga = new SimpleUOWSagaWithRollback(_unitOfWorkFactoryMock.Object);
94+
95+
// Act
96+
var context = new SagaContext();
97+
try
98+
{
99+
context = await saga.Run(context).ConfigureAwait(false);
100+
101+
Assert.IsTrue(false, "Expected Exception");
102+
}
103+
catch
104+
{
105+
// Assert
106+
Assert.IsTrue(context.Step1_Invoked);
107+
Assert.IsTrue(context.Step2_Invoked);
108+
Assert.IsFalse(context.Step3_Invoked);
109+
110+
Assert.IsTrue(context.Step1_Rollback);
111+
Assert.IsTrue(context.Step2_Rollback);
112+
Assert.IsTrue(context.Step3_Rollback);
113+
114+
_unitOfWorkMock.Verify(x => x.Commit(), Times.Never());
115+
_unitOfWorkMock.Verify(x => x.Rollback(), Times.Once());
116+
}
117+
}
118+
}
119+
120+
#region Sample Sagas for Testing
121+
public class SagaContext
122+
{
123+
public bool Step1_Invoked { get; set; }
124+
public bool Step2_Invoked { get; set; }
125+
public bool Step3_Invoked { get; set; }
126+
public bool Step1_Rollback { get; set; }
127+
public bool Step2_Rollback { get; set; }
128+
public bool Step3_Rollback { get; set; }
129+
}
130+
131+
public class Step1 : SagaStep<SagaContext>
132+
{
133+
public Step1()
134+
{
135+
this.ExecuteMethod = context => { context.Step1_Invoked = true; return Task.FromResult(context); };
136+
this.RollbackMethod = context => { context.Step1_Rollback = true; return Task.FromResult(context); };
137+
}
138+
}
139+
140+
public class Step2 : SagaStep<SagaContext>
141+
{
142+
public Step2() : base(context => { context.Step2_Invoked = true; return Task.FromResult(context); }, context => { context.Step2_Rollback = true; return Task.FromResult(context); }) { }
143+
}
144+
145+
public class Step3 : SagaStep<SagaContext>
146+
{
147+
public Step3() : base(context => { context.Step3_Invoked = true; return Task.FromResult(context); }, context => { context.Step3_Rollback = true; return Task.FromResult(context); }) { }
148+
}
149+
150+
public class Step3WithException : SagaStep<SagaContext>
151+
{
152+
public Step3WithException()
153+
{
154+
this.ExecuteMethod = context => { throw new Exception("Foo"); };
155+
this.RollbackMethod = context => { context.Step3_Rollback = true; return Task.FromResult(context); };
156+
}
157+
}
158+
159+
public class SimpleSaga : Saga<SagaContext>
160+
{
161+
public SimpleSaga()
162+
{
163+
Configure(cfg =>
164+
{
165+
cfg.AddStep(new Step1());
166+
cfg.AddStep(new Step2());
167+
cfg.AddStep(new Step3());
168+
});
169+
}
170+
}
171+
172+
public class SimpleSagaWithRollback : Saga<SagaContext>
173+
{
174+
public SimpleSagaWithRollback()
175+
{
176+
Configure(cfg =>
177+
{
178+
cfg.AddStep(new Step1());
179+
cfg.AddStep(new Step2());
180+
cfg.AddStep(new Step3WithException());
181+
});
182+
}
183+
}
184+
185+
public class UOWStep1 : UnitOfWorkStep<SagaContext>
186+
{
187+
public UOWStep1()
188+
{
189+
this.ExecuteMethod = (context, uow) => { context.Step1_Invoked = true; return Task.FromResult(context); };
190+
this.RollbackMethod = (context, uow) => { context.Step1_Rollback = true; return Task.FromResult(context); };
191+
}
192+
}
193+
194+
public class UOWStep2 : UnitOfWorkStep<SagaContext>
195+
{
196+
public UOWStep2()
197+
{
198+
this.ExecuteMethod = (context, uow) => { context.Step2_Invoked = true; return Task.FromResult(context); };
199+
this.RollbackMethod = (context, uow) => { context.Step2_Rollback = true; return Task.FromResult(context); };
200+
}
201+
}
202+
203+
public class UOWStep3 : UnitOfWorkStep<SagaContext>
204+
{
205+
public UOWStep3()
206+
{
207+
this.ExecuteMethod = (context, uow) => { context.Step3_Invoked = true; return Task.FromResult(context); };
208+
this.RollbackMethod = (context, uow) => { context.Step3_Rollback = true; return Task.FromResult(context); };
209+
}
210+
}
211+
212+
public class UOWStep3WithException : UnitOfWorkStep<SagaContext>
213+
{
214+
public UOWStep3WithException()
215+
{
216+
ExecuteMethod = (context, uow) => { throw new Exception("Foo"); };
217+
this.RollbackMethod = (context, uow) => { context.Step3_Rollback = true; return Task.FromResult(context); };
218+
}
219+
}
220+
221+
public class SimpleUOWSaga : UnitOfWorkSaga<SagaContext>
222+
{
223+
public SimpleUOWSaga(IUnitOfWorkFactory factory) : base(factory)
224+
{
225+
Configure(cfg =>
226+
{
227+
cfg.AddStep(new UOWStep1());
228+
cfg.AddStep(new UOWStep2());
229+
cfg.AddStep(new UOWStep3());
230+
});
231+
}
232+
}
233+
234+
public class SimpleUOWSagaWithRollback : UnitOfWorkSaga<SagaContext>
235+
{
236+
public SimpleUOWSagaWithRollback(IUnitOfWorkFactory factory) : base(factory)
237+
{
238+
Configure(cfg =>
239+
{
240+
cfg.AddStep(new UOWStep1());
241+
cfg.AddStep(new UOWStep2());
242+
cfg.AddStep(new UOWStep3WithException());
243+
});
244+
}
245+
}
246+
#endregion
247+
}

OrangeLoop.Sagas.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1010
README.md = README.md
1111
EndProjectSection
1212
EndProject
13+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrangeLoop.Sagas.Tests", "OrangeLoop.Sagas.Tests\OrangeLoop.Sagas.Tests.csproj", "{66A103F2-D3ED-4024-9F2B-82E4EB965F15}"
14+
EndProject
1315
Global
1416
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1517
Debug|Any CPU = Debug|Any CPU
@@ -20,6 +22,10 @@ Global
2022
{6CE73085-FCC6-4379-9E49-DD6EB4AC0BA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
2123
{6CE73085-FCC6-4379-9E49-DD6EB4AC0BA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
2224
{6CE73085-FCC6-4379-9E49-DD6EB4AC0BA4}.Release|Any CPU.Build.0 = Release|Any CPU
25+
{66A103F2-D3ED-4024-9F2B-82E4EB965F15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26+
{66A103F2-D3ED-4024-9F2B-82E4EB965F15}.Debug|Any CPU.Build.0 = Debug|Any CPU
27+
{66A103F2-D3ED-4024-9F2B-82E4EB965F15}.Release|Any CPU.ActiveCfg = Release|Any CPU
28+
{66A103F2-D3ED-4024-9F2B-82E4EB965F15}.Release|Any CPU.Build.0 = Release|Any CPU
2329
EndGlobalSection
2430
GlobalSection(SolutionProperties) = preSolution
2531
HideSolutionNode = FALSE

OrangeLoop.Sagas/BaseSaga.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using OrangeLoop.Sagas.Interfaces;
2+
using System;
3+
using System.Threading.Tasks;
4+
5+
namespace OrangeLoop.Sagas
6+
{
7+
public abstract class BaseSaga<K, T>
8+
{
9+
protected ISagaConfiguration<K> Configuration { get; private set; }
10+
11+
public BaseSaga(ISagaConfiguration<K> configuration)
12+
{
13+
Configuration = configuration;
14+
}
15+
16+
protected void Configure(Action<ISagaConfiguration<K>> config)
17+
{
18+
config.Invoke(Configuration);
19+
}
20+
21+
protected async Task<T> Run(T context, Func<K, T, Task<T>> invoker)
22+
{
23+
var step = Configuration.Steps.First;
24+
25+
try
26+
{
27+
while (step != null)
28+
{
29+
context = await invoker.Invoke(step.Value.ExecuteMethod, context).ConfigureAwait(false);
30+
step = step.Next;
31+
}
32+
}
33+
catch (Exception)
34+
{
35+
while (step != null)
36+
{
37+
// What do we do if the rollback itself fails??
38+
context = invoker.Invoke(step.Value.RollbackMethod, context).Result;
39+
step = step.Previous;
40+
}
41+
42+
throw;
43+
}
44+
45+
return context;
46+
}
47+
48+
public abstract Task<T> Run(T context);
49+
}
50+
}

OrangeLoop.Sagas/BaseSagaStep.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using OrangeLoop.Sagas.Interfaces;
2+
3+
namespace OrangeLoop.Sagas
4+
{
5+
public abstract class BaseSagaStep<K> : ISagaStep<K>
6+
{
7+
public BaseSagaStep() { }
8+
9+
public BaseSagaStep(K executeMethod, K rollbackMethod)
10+
{
11+
ExecuteMethod = executeMethod;
12+
RollbackMethod = rollbackMethod;
13+
}
14+
15+
public K ExecuteMethod { get; protected set; }
16+
public K RollbackMethod { get; protected set; }
17+
}
18+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using OrangeLoop.Sagas.Interfaces;
2+
using System.Data;
3+
4+
namespace OrangeLoop.Sagas.Config
5+
{
6+
public class ChaosConfig : IUnitOfWorkConfig
7+
{
8+
public IsolationLevel IsolationLevel => IsolationLevel.Chaos;
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using OrangeLoop.Sagas.Interfaces;
2+
using System.Data;
3+
4+
namespace OrangeLoop.Sagas.Config
5+
{
6+
public class ReadCommittedConfig : IUnitOfWorkConfig
7+
{
8+
public IsolationLevel IsolationLevel => IsolationLevel.ReadCommitted;
9+
}
10+
}

OrangeLoop.Sagas/DefaultConfig.cs renamed to OrangeLoop.Sagas/Config/ReadUncommittedConfig.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using OrangeLoop.Sagas.Interfaces;
22
using System.Data;
33

4-
namespace OrangeLoop.Sagas
4+
namespace OrangeLoop.Sagas.Config
55
{
6-
public class DefaultConfig : IUnitOfWorkConfig
6+
public class ReadUncommittedConfig : IUnitOfWorkConfig
77
{
88
public IsolationLevel IsolationLevel => IsolationLevel.ReadUncommitted;
99
}

0 commit comments

Comments
 (0)