PageRenderTime 49ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/framework/Composable.CQRS.Tests/CQRS/EventStoreUpdaterTest.cs

https://github.com/mlidbom/Composable.Monolithic
C# | 679 lines | 553 code | 123 blank | 3 comment | 8 complexity | f4f94b51c6f3889790093b55638191b1 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using System.Transactions;
  7. using Composable.DependencyInjection;
  8. using Composable.DependencyInjection.Testing;
  9. using Composable.Messaging;
  10. using Composable.Messaging.Buses;
  11. using Composable.Persistence.EventStore;
  12. using Composable.Refactoring.Naming;
  13. using Composable.SystemCE;
  14. using Composable.SystemCE.DiagnosticsCE;
  15. using Composable.SystemCE.LinqCE;
  16. using Composable.SystemCE.ThreadingCE;
  17. using Composable.SystemCE.ThreadingCE.TasksCE;
  18. using Composable.SystemCE.TransactionsCE;
  19. using Composable.Testing;
  20. using Composable.Testing.Performance;
  21. using Composable.Testing.Threading;
  22. using Composable.Testing.Transactions;
  23. using FluentAssertions;
  24. using JetBrains.Annotations;
  25. using NUnit.Framework;
  26. // ReSharper disable AccessToDisposedClosure
  27. namespace Composable.Tests.CQRS
  28. {
  29. //[ConfigurationBasedDuplicateByDimensions]
  30. public class EventStoreUpdaterTest : DuplicateByPluggableComponentTest
  31. {
  32. class EventSpy
  33. {
  34. public IEnumerable<IExactlyOnceEvent> DispatchedMessages => _events.ToList();
  35. public void Receive(IExactlyOnceEvent @event) { _events.Add(@event); }
  36. readonly List<IExactlyOnceEvent> _events = new List<IExactlyOnceEvent>();
  37. }
  38. EventSpy _eventSpy;
  39. IServiceLocator _serviceLocator;
  40. [SetUp] public void SetupBus()
  41. {
  42. _serviceLocator = TestWiringHelper.SetupTestingServiceLocator();
  43. _eventSpy = new EventSpy();
  44. _serviceLocator.Resolve<IMessageHandlerRegistrar>()
  45. .ForEvent<IExactlyOnceEvent>(_eventSpy.Receive);
  46. _serviceLocator.Resolve<ITypeMappingRegistar>()
  47. .Map<Composable.Tests.CQRS.User>("2cfabb11-5e5a-494d-898f-8bfc654544eb")
  48. .Map<Composable.Tests.CQRS.IUserEvent>("0727c209-2f49-46ab-a56b-a1332415a895")
  49. .Map<Composable.Tests.CQRS.MigratedAfterUserChangedEmailEvent>("9ff42a12-f28c-447a-8aa1-79e6f685fa41")
  50. .Map<Composable.Tests.CQRS.MigratedBeforeUserRegisteredEvent>("3338e1d4-3839-4f63-9248-ea4dd30c8348")
  51. .Map<Composable.Tests.CQRS.MigratedReplaceUserChangedPasswordEvent>("45db6370-f7e7-4eb8-b792-845485d86295")
  52. .Map<Composable.Tests.CQRS.UserChangedEmail>("40ae1f6d-5f95-4c60-ac5f-21a3d1c85de9")
  53. .Map<Composable.Tests.CQRS.UserChangedPassword>("0b3b57f6-fd69-4da1-bb52-15033495f044")
  54. .Map<Composable.Tests.CQRS.UserEvent>("fa71e035-571d-4231-bd65-e667c138ec36")
  55. .Map<Composable.Tests.CQRS.UserRegistered>("03265864-8e1d-4eb7-a7a9-63dfc2b965de")
  56. .Map<Composable.Tests.CQRS.IMigratedAfterUserChangedEmailEvent>("4ed567a3-724a-48d5-80f2-58978ca66922")
  57. .Map<Composable.Tests.CQRS.IMigratedBeforeUserRegisteredEvent>("dbe74932-9a8d-4977-8f85-d55de7711f26")
  58. .Map<Composable.Tests.CQRS.IMigratedReplaceUserChangedPasswordEvent>("798d793d-1866-41e5-8d59-26bf285dfc80")
  59. .Map<Composable.Tests.CQRS.IUserChangedEmail>("27cfff73-9f21-4835-83d1-fbb4d58419e3")
  60. .Map<Composable.Tests.CQRS.IUserChangedPassword>("9ee2f0e9-af5c-469b-8b85-7eda6a856b81")
  61. .Map<Composable.Tests.CQRS.IUserRegistered>("6a9b3276-cedc-4dae-a15c-4d386c935a48");
  62. }
  63. [TearDown] public void TearDownTask()
  64. {
  65. _serviceLocator.Dispose();
  66. }
  67. protected void UseInTransactionalScope([InstantHandle] Action<IEventStoreUpdater> useSession)
  68. => _serviceLocator.ExecuteTransactionInIsolatedScope(
  69. () => useSession(_serviceLocator.Resolve<IEventStoreUpdater>()));
  70. protected void UseInScope([InstantHandle]Action<IEventStoreUpdater> useSession)
  71. => _serviceLocator.ExecuteInIsolatedScope(
  72. () => useSession(_serviceLocator.Resolve<IEventStoreUpdater>()));
  73. [Test]
  74. public void WhenFetchingAggregateThatDoesNotExistNoSuchAggregateExceptionIsThrown()
  75. {
  76. UseInTransactionalScope(session => Assert.Throws<AggregateNotFoundException>(() => session.Get<User>(Guid.NewGuid())));
  77. }
  78. [Test]
  79. public void CanSaveAndLoadAggregate()
  80. {
  81. var user = new User();
  82. user.Register("email@email.se", "password", Guid.NewGuid());
  83. user.ChangePassword("NewPassword");
  84. user.ChangeEmail("NewEmail");
  85. UseInTransactionalScope(session => session.Save(user));
  86. UseInTransactionalScope(session =>
  87. {
  88. var loadedUser = session.Get<User>(user.Id);
  89. Assert.That(loadedUser.Id, Is.EqualTo(user.Id));
  90. Assert.That(loadedUser.Email, Is.EqualTo(user.Email));
  91. Assert.That(loadedUser.Password, Is.EqualTo(user.Password));
  92. });
  93. }
  94. [Test]
  95. public void ThrowsIfUsedByMultipleThreads()
  96. {
  97. IEventStoreUpdater updater = null;
  98. IEventStoreReader reader = null;
  99. using var wait = new ManualResetEventSlim();
  100. ThreadPool.QueueUserWorkItem(_ =>
  101. {
  102. _serviceLocator.ExecuteInIsolatedScope(() =>
  103. {
  104. updater = _serviceLocator.Resolve<IEventStoreUpdater>();
  105. reader = _serviceLocator.Resolve<IEventStoreReader>();
  106. });
  107. wait.Set();
  108. });
  109. wait.Wait();
  110. Assert.Throws<MultiThreadedUseException>(() => updater.Get<User>(Guid.NewGuid()));
  111. Assert.Throws<MultiThreadedUseException>(() => updater.Dispose());
  112. Assert.Throws<MultiThreadedUseException>(() => reader.GetReadonlyCopyOfVersion<User>(Guid.NewGuid(), 1));
  113. Assert.Throws<MultiThreadedUseException>(() => updater.Save(new User()));
  114. Assert.Throws<MultiThreadedUseException>(() => updater.TryGet(Guid.NewGuid(), out User _));
  115. }
  116. [Test]
  117. public void CanLoadSpecificVersionOfAggregate()
  118. {
  119. var user = new User();
  120. user.Register("email@email.se", "password", Guid.NewGuid());
  121. user.ChangePassword("NewPassword");
  122. user.ChangeEmail("NewEmail");
  123. UseInTransactionalScope(session => session.Save(user));
  124. UseInScope(session =>
  125. {
  126. var reader = _serviceLocator.Resolve<IEventStoreReader>();
  127. var loadedUser = reader.GetReadonlyCopyOfVersion<User>(user.Id, 1);
  128. Assert.That(loadedUser.Id, Is.EqualTo(user.Id));
  129. Assert.That(loadedUser.Email, Is.EqualTo("email@email.se"));
  130. Assert.That(loadedUser.Password, Is.EqualTo("password"));
  131. loadedUser = reader.GetReadonlyCopyOfVersion<User>(user.Id, 2);
  132. Assert.That(loadedUser.Id, Is.EqualTo(user.Id));
  133. Assert.That(loadedUser.Email, Is.EqualTo("email@email.se"));
  134. Assert.That(loadedUser.Password, Is.EqualTo("NewPassword"));
  135. loadedUser = reader.GetReadonlyCopyOfVersion<User>(user.Id, 3);
  136. Assert.That(loadedUser.Id, Is.EqualTo(user.Id));
  137. Assert.That(loadedUser.Email, Is.EqualTo("NewEmail"));
  138. Assert.That(loadedUser.Password, Is.EqualTo("NewPassword"));
  139. });
  140. }
  141. [Test]
  142. public void ReturnsSameInstanceOnRepeatedLoads()
  143. {
  144. var user = new User();
  145. user.Register("email@email.se", "password", Guid.NewGuid());
  146. UseInTransactionalScope(session => session.Save(user));
  147. UseInTransactionalScope(session =>
  148. {
  149. var loaded1 = session.Get<User>(user.Id);
  150. var loaded2 = session.Get<User>(user.Id);
  151. Assert.That(loaded1, Is.SameAs(loaded2));
  152. });
  153. }
  154. [Test]
  155. public void ReturnsSameInstanceOnLoadAfterSave()
  156. {
  157. var user = new User();
  158. user.Register("email@email.se", "password", Guid.NewGuid());
  159. UseInTransactionalScope(session =>
  160. {
  161. session.Save(user);
  162. var loaded1 = session.Get<User>(user.Id);
  163. var loaded2 = session.Get<User>(user.Id);
  164. Assert.That(loaded1, Is.SameAs(loaded2));
  165. Assert.That(loaded1, Is.SameAs(user));
  166. });
  167. }
  168. [Test]
  169. public void TracksAndUpdatesLoadedAggregates()
  170. {
  171. var user = new User();
  172. user.Register("email@email.se", "password", Guid.NewGuid());
  173. UseInTransactionalScope(session => session.Save(user));
  174. UseInTransactionalScope(session =>
  175. {
  176. var loadedUser = session.Get<User>(user.Id);
  177. loadedUser.ChangePassword("NewPassword");
  178. });
  179. UseInTransactionalScope(session =>
  180. {
  181. var loadedUser = session.Get<User>(user.Id);
  182. Assert.That(loadedUser.Password, Is.EqualTo("NewPassword"));
  183. });
  184. }
  185. [Test]
  186. public void DoesNotUpdateAggregatesLoadedViaSpecificVersion()
  187. {
  188. var user = new User();
  189. user.Register("OriginalEmail", "password", Guid.NewGuid());
  190. UseInTransactionalScope(session => session.Save(user));
  191. UseInTransactionalScope(session =>
  192. {
  193. var loadedUser = _serviceLocator.Resolve<IEventStoreReader>().GetReadonlyCopyOfVersion<User>(user.Id, 1);
  194. loadedUser.ChangeEmail("NewEmail");
  195. });
  196. UseInTransactionalScope(session =>
  197. {
  198. var loadedUser = session.Get<User>(user.Id);
  199. Assert.That(loadedUser.Email, Is.EqualTo("OriginalEmail"));
  200. });
  201. }
  202. [Test]
  203. public void ResetsAggregatesAfterSaveChanges()
  204. {
  205. var user = new User();
  206. user.Register("OriginalEmail", "password", Guid.NewGuid());
  207. UseInTransactionalScope(session => session.Save(user));
  208. ((IEventStored)user).Commit(events => events.Should().BeEmpty());
  209. }
  210. [Test]
  211. public void ThrowsWhenAttemptingToSaveExistingAggregate()
  212. {
  213. var user = new User();
  214. user.Register("OriginalEmail", "password", Guid.NewGuid());
  215. UseInTransactionalScope(session => session.Save(user));
  216. UseInTransactionalScope(
  217. session => Assert.Throws<AttemptToSaveAlreadyPersistedAggregateException>(
  218. () => session.Save(user)));
  219. }
  220. [Test]
  221. public void DoesNotExplodeWhenSavingMoreThan10Events()
  222. {
  223. var user = new User();
  224. user.Register("OriginalEmail", "password", Guid.NewGuid());
  225. 1.Through(100).ForEach(index => user.ChangeEmail("email" + index));
  226. UseInTransactionalScope(session => session.Save(user));
  227. }
  228. [Test]
  229. public void AggregateCannotBeRetrievedAfterBeingDeleted()
  230. {
  231. var user1 = new User();
  232. user1.Register("email1@email.se", "password", Guid.NewGuid());
  233. var user2 = new User();
  234. user2.Register("email2@email.se", "password", Guid.NewGuid());
  235. UseInTransactionalScope(session =>
  236. {
  237. session.Save(user1);
  238. session.Save(user2);
  239. });
  240. UseInTransactionalScope(session =>
  241. {
  242. session.Delete(user1.Id);
  243. var loadedUser2 = session.Get<User>(user2.Id);
  244. Assert.That(loadedUser2.Id, Is.EqualTo(user2.Id));
  245. Assert.That(loadedUser2.Email, Is.EqualTo(user2.Email));
  246. Assert.That(loadedUser2.Password, Is.EqualTo(user2.Password));
  247. });
  248. UseInTransactionalScope(session => Assert.IsFalse(session.TryGet(user1.Id, out User _)));
  249. }
  250. [Test]
  251. public void DeletingAnAggregateDoesNotPreventEventsFromItFromBeingRaised()
  252. {
  253. var user1 = new User();
  254. user1.Register("email1@email.se", "password", Guid.NewGuid());
  255. var user2 = new User();
  256. user2.Register("email2@email.se", "password", Guid.NewGuid());
  257. UseInTransactionalScope(session =>
  258. {
  259. session.Save(user1);
  260. session.Save(user2);
  261. });
  262. _eventSpy.DispatchedMessages.Count().Should().Be(2);
  263. UseInTransactionalScope(session =>
  264. {
  265. user1 = session.Get<User>(user1.Id);
  266. user1.ChangeEmail("new_email");
  267. session.Delete(user1.Id);
  268. });
  269. var published = _eventSpy.DispatchedMessages.ToList();
  270. _eventSpy.DispatchedMessages.Count()
  271. .Should()
  272. .Be(3);
  273. Assert.That(published.Last(), Is.InstanceOf<UserChangedEmail>());
  274. }
  275. [Test] public void Events_should_be_published_immediately()
  276. {
  277. UseInTransactionalScope(session =>
  278. {
  279. var user1 = new User();
  280. user1.Register("email1@email.se", "password", Guid.NewGuid());
  281. session.Save(user1);
  282. _eventSpy.DispatchedMessages.Last()
  283. .Should()
  284. .BeOfType<UserRegistered>();
  285. user1 = session.Get<User>(user1.Id);
  286. user1.ChangeEmail("new_email");
  287. _eventSpy.DispatchedMessages.Last()
  288. .Should()
  289. .BeOfType<UserChangedEmail>();
  290. });
  291. }
  292. [Test]
  293. public void When_fetching_history_from_the_same_instance_after_updating_an_aggregate_the_fetched_history_includes_the_new_events()
  294. {
  295. var userId = Guid.NewGuid();
  296. UseInTransactionalScope(session =>
  297. {
  298. var user = new User();
  299. user.Register("test@email.com", "Password1", userId);
  300. session.Save(user);
  301. });
  302. UseInTransactionalScope(session =>
  303. {
  304. var user = session.Get<User>(userId);
  305. user.ChangeEmail("new_email@email.com");
  306. });
  307. UseInScope(session =>
  308. {
  309. var history = ((IEventStoreReader)session).GetHistory(userId);
  310. Assert.That(history.Count, Is.EqualTo(2));
  311. });
  312. }
  313. [Test]
  314. public void When_deleting_and_then_fetching_an_aggregates_history_the_history_should_be_gone()
  315. {
  316. var userId = Guid.NewGuid();
  317. UseInTransactionalScope(session =>
  318. {
  319. var user = new User();
  320. user.Register("test@email.com", "Password1", userId);
  321. session.Save(user);
  322. });
  323. UseInTransactionalScope(session => session.Delete(userId));
  324. UseInScope(session =>
  325. {
  326. var history = ((IEventStoreReader)session).GetHistory(userId);
  327. Assert.That(history.Count, Is.EqualTo(0));
  328. });
  329. }
  330. [Test]
  331. public void When_fetching_and_deleting_an_aggregate_then_fetching_history_again_the_history_should_be_gone()
  332. {
  333. var userId = Guid.NewGuid();
  334. UseInTransactionalScope(session =>
  335. {
  336. var user = new User();
  337. user.Register("test@email.com", "Password1", userId);
  338. session.Save(user);
  339. });
  340. UseInTransactionalScope(session =>
  341. {
  342. session.Get<User>(userId);
  343. session.Delete(userId);
  344. });
  345. UseInScope(session =>
  346. {
  347. var history = ((IEventStoreReader)session).GetHistory(userId);
  348. Assert.That(history.Count, Is.EqualTo(0));
  349. });
  350. }
  351. [Test]
  352. public void Concurrent_read_only_access_to_aggregate_history_can_occur_in_parallel()
  353. {
  354. var user = new User();
  355. user.Register("email@email.se", "password", Guid.NewGuid());
  356. UseInTransactionalScope(session => session.Save(user));
  357. var threadedIterations = 20;
  358. var delayEachTransactionBy = 1.Milliseconds();
  359. void ReadUserHistory()
  360. {
  361. UseInTransactionalScope(session =>
  362. {
  363. ((IEventStoreReader)session).GetHistory(user.Id);
  364. Thread.Sleep(delayEachTransactionBy);
  365. });
  366. }
  367. var singleThreadedExecutionTime = StopwatchCE.TimeExecution(ReadUserHistory, iterations: threadedIterations).Total;
  368. var timingsSummary = TimeAsserter.ExecuteThreaded(
  369. action: ReadUserHistory,
  370. iterations: threadedIterations,
  371. maxTotal: singleThreadedExecutionTime / 2,
  372. maxDegreeOfParallelism: 5,
  373. description: $"If access is serialized the time will be approximately {singleThreadedExecutionTime} milliseconds. If parallelized it should be far below this value.");
  374. timingsSummary.IndividualExecutionTimes.Sum().Should().BeGreaterThan(timingsSummary.Total, "If the sum elapsed time of the parts that run in parallel is not greater than the clock time passed parallelism is not taking place.");
  375. }
  376. [Test]
  377. public void EventsArePublishedImmediatelyOnAggregateChanges()
  378. {
  379. var users = 1.Through(9).Select(i => { var u = new User(); u.Register(i + "@test.com", "abcd", Guid.NewGuid()); u.ChangeEmail("new" + i + "@test.com"); return u; }).ToList();
  380. UseInTransactionalScope(session =>
  381. {
  382. users.Take(3).ForEach(session.Save);
  383. Assert.That(_eventSpy.DispatchedMessages.Count, Is.EqualTo(6));
  384. });
  385. UseInTransactionalScope(session =>
  386. {
  387. Assert.That(_eventSpy.DispatchedMessages.Count, Is.EqualTo(6));
  388. users.Skip(3).Take(3).ForEach(session.Save);
  389. Assert.That(_eventSpy.DispatchedMessages.Count, Is.EqualTo(12));
  390. });
  391. UseInTransactionalScope(session =>
  392. {
  393. Assert.That(_eventSpy.DispatchedMessages.Count, Is.EqualTo(12));
  394. users.Skip(6).Take(3).ForEach(session.Save);
  395. Assert.That(_eventSpy.DispatchedMessages.Count, Is.EqualTo(18));
  396. });
  397. UseInTransactionalScope(session =>
  398. {
  399. Assert.That(_eventSpy.DispatchedMessages.Count, Is.EqualTo(18));
  400. var dispatchedEvents = _eventSpy.DispatchedMessages.OfType<IAggregateEvent>().ToList();
  401. Assert.That(dispatchedEvents.Select(e => e.MessageId).Distinct().Count(), Is.EqualTo(18));
  402. var allPersistedEvents = _serviceLocator.EventStore().ListAllEventsForTestingPurposesAbsolutelyNotUsableForARealEventStoreOfAnySize();
  403. EventStorageTestHelper.StripSeventhDecimalPointFromSecondFractionOnUtcUpdateTime(dispatchedEvents);
  404. EventStorageTestHelper.StripSeventhDecimalPointFromSecondFractionOnUtcUpdateTime(allPersistedEvents);
  405. allPersistedEvents.Should().BeEquivalentTo(dispatchedEvents, options => options.WithStrictOrdering());
  406. });
  407. }
  408. [Test]
  409. public void InsertNewEventType_should_not_throw_exception_if_the_event_type_has_been_inserted_by_something_else()
  410. {
  411. User otherUser = null;
  412. User user = null;
  413. void ChangeAnotherUsersEmailInOtherInstance()
  414. {
  415. using var clonedServiceLocator = _serviceLocator.Clone();
  416. clonedServiceLocator.ExecuteTransactionInIsolatedScope(() =>
  417. {
  418. // ReSharper disable once AccessToDisposedClosure
  419. var session = clonedServiceLocator.Resolve<IEventStoreUpdater>();
  420. otherUser = User.Register(session,
  421. "email@email.se",
  422. "password",
  423. Guid.NewGuid());
  424. otherUser.ChangeEmail("otheruser@email.new");
  425. });
  426. }
  427. UseInTransactionalScope(session => user = User.Register(session, "email@email.se", "password", Guid.NewGuid()));
  428. ChangeAnotherUsersEmailInOtherInstance();
  429. UseInTransactionalScope(session => session.Get<User>(otherUser.Id).Email.Should().Be("otheruser@email.new"));
  430. UseInTransactionalScope(session => user.ChangeEmail("some@email.new"));
  431. }
  432. [Test] public void If_the_first_transaction_to_insert_an_event_of_specific_type_fails_the_next_succeeds()
  433. {
  434. var user = new User();
  435. user.Register("email@email.se", "password", Guid.NewGuid());
  436. UseInTransactionalScope(session => session.Save(user));
  437. void ChangeUserEmail(bool failOnPrepare)
  438. {
  439. UseInTransactionalScope(session =>
  440. {
  441. if(failOnPrepare)
  442. {
  443. Transaction.Current.FailOnPrepare();
  444. }
  445. var loadedUser = session.Get<User>(user.Id);
  446. loadedUser.ChangeEmail("new@email.com");
  447. });
  448. }
  449. AssertThrows.Exception<Exception>(() => ChangeUserEmail(failOnPrepare: true));
  450. ChangeUserEmail(failOnPrepare: false);
  451. }
  452. [Test, LongRunning]
  453. public void Serializes_access_to_an_aggregate_so_that_concurrent_transactions_succeed_even_if_history_has_been_read_outside_of_modifying_transactions()
  454. {
  455. var user = new User();
  456. user.Register("email@email.se", "password", Guid.NewGuid());
  457. UseInTransactionalScope(session =>
  458. {
  459. session.Save(user);
  460. user.ChangeEmail("newemail@somewhere.not");
  461. });
  462. var getHistorySection = GatedCodeSection.WithTimeout(2.Seconds());
  463. var changeEmailSection = GatedCodeSection.WithTimeout(2.Seconds());
  464. void UpdateEmail()
  465. {
  466. UseInScope(session =>
  467. {
  468. using(getHistorySection.Enter())
  469. {
  470. ((IEventStoreReader)session).GetHistory(user.Id);
  471. }
  472. TransactionScopeCe.Execute(() =>
  473. {
  474. using(changeEmailSection.Enter())
  475. {
  476. var userToUpdate = session.Get<User>(user.Id);
  477. userToUpdate.ChangeEmail($"newemail_{userToUpdate.Version}@somewhere.not");
  478. }
  479. });
  480. });
  481. }
  482. var threads = 2;
  483. var tasks = 1.Through(threads).Select(resetEvent => TaskCE.Run(nameof(UpdateEmail), UpdateEmail)).ToArray();
  484. getHistorySection.LetOneThreadPass();
  485. changeEmailSection.LetOneThreadEnterAndReachExit();
  486. changeEmailSection.Open();
  487. getHistorySection.Open();
  488. Task.WaitAll(tasks);//Sql duplicate key (AggregateId, Version) Exception would be thrown here if history was not serialized
  489. UseInScope(
  490. session =>
  491. {
  492. var userHistory = ((IEventStoreReader)session).GetHistory(user.Id)
  493. .ToArray(); //Reading the aggregate will throw an exception if the history is invalid.
  494. userHistory.Length.Should()
  495. .Be(threads + 2); //Make sure that all of the transactions completed
  496. });
  497. }
  498. [Test, LongRunning]
  499. public void Serializes_access_to_an_aggregate_so_that_concurrent_transactions_succeed()
  500. {
  501. var user = new User();
  502. user.Register("email@email.se", "password", Guid.NewGuid());
  503. UseInTransactionalScope(session =>
  504. {
  505. session.Save(user);
  506. user.ChangeEmail("newemail@somewhere.not");
  507. });
  508. var changeEmailSection = GatedCodeSection.WithTimeout(20.Seconds());
  509. var hasFetchedUser = ThreadGate.CreateOpenWithTimeout(20.Seconds());
  510. void UpdateEmail()
  511. {
  512. UseInTransactionalScope(session =>
  513. {
  514. using(changeEmailSection.Enter())
  515. {
  516. var userToUpdate = session.Get<User>(user.Id);
  517. hasFetchedUser.AwaitPassThrough();
  518. userToUpdate.ChangeEmail($"newemail_{userToUpdate.Version}@somewhere.not");
  519. }
  520. });
  521. }
  522. var threads = 2;
  523. var tasks = 1.Through(threads).Select(resetEvent => TaskCE.Run(nameof(UpdateEmail), UpdateEmail)).ToArray();
  524. changeEmailSection.EntranceGate.Open();
  525. changeEmailSection.EntranceGate.AwaitPassedThroughCountEqualTo(2);
  526. changeEmailSection.ExitGate.AwaitQueueLengthEqualTo(1);
  527. Thread.Sleep(100.Milliseconds());
  528. var bothTasksReadUserException = ExceptionCE.TryCatch(() => hasFetchedUser.Passed.Should().Be(1, "Only one thread should have been able to fetch the aggregate"));
  529. var bothTasksCompletedException = ExceptionCE.TryCatch(() => changeEmailSection.ExitGate.Queued.Should().Be(1, "One thread should be blocked by transaction and never reach here until the other completes the transaction."));
  530. changeEmailSection.Open();
  531. var taskException = ExceptionCE.TryCatch(() => Task.WaitAll(tasks)) as AggregateException;//Sql duplicate key (AggregateId, Version) Exception would be thrown here if history was not serialized. Or a deadlock will be thrown if the locking is not done correctly.
  532. if(bothTasksCompletedException != null || taskException != null || bothTasksReadUserException != null)throw new AggregateException(EnumerableCE.Create(bothTasksCompletedException).Append(bothTasksReadUserException).Concat(taskException.InnerExceptions).Where(@this => @this != null));
  533. UseInScope(
  534. session =>
  535. {
  536. var userHistory = ((IEventStoreReader)session).GetHistory(user.Id)
  537. .ToArray(); //Reading the aggregate will throw an exception if the history is invalid.
  538. userHistory.Length.Should()
  539. .Be(threads + 2); //Make sure that all of the transactions completed
  540. });
  541. }
  542. [Test]
  543. public void If_an_updater_is_used_in_two_transactions_an_exception_is_thrown()
  544. {
  545. using (_serviceLocator.BeginScope())
  546. {
  547. using var updater = _serviceLocator.Resolve<IEventStoreUpdater>();
  548. var user = new User();
  549. user.Register("email@email.se", "password", Guid.NewGuid());
  550. TransactionScopeCe.Execute(() => updater.Save(user));
  551. AssertThrows.Exception<ComponentUsedByMultipleTransactionsException>(() => TransactionScopeCe.Execute(() => updater.Get<User>(user.Id)));
  552. }
  553. }
  554. public EventStoreUpdaterTest([NotNull] string _) : base(_) {}
  555. }
  556. }