public async Task When_a_projector_update_fails_then_an_entry_is_added_to_EventHandlingErrors() { // arrange var errorMessage = Any.Paragraph(10); var productName = Any.Paragraph(); var projector = new Projector <Order.ItemAdded> { OnUpdate = (work, e) => { throw new Exception(errorMessage); } }; var order = new Order(); var repository = new SqlEventSourcedRepository <Order>(); order.Apply(new AddItem { Price = 1m, ProductName = productName }); await repository.Save(order); // act using (var catchup = CreateReadModelCatchup(projector)) { await catchup.Run(); } // assert using (var db = ReadModelDbContext()) { var error = db.Set <EventHandlingError>().Single(e => e.AggregateId == order.Id); error.StreamName.Should().Be("Order"); error.EventTypeName.Should().Be("ItemAdded"); error.SerializedEvent.Should().Contain(productName); error.Error.Should().Contain(errorMessage); } }
public void When_the_aggregate_is_saved_then_the_reservation_is_confirmed() { // arrange var username = Any.Email(); var account = new CustomerAccount(); account.Apply(new RequestUserName { UserName = username, Principal = new Customer(username) }); var bus = new FakeEventBus(); bus.Subscribe(new UserNameConfirmer()); var repository = new SqlEventSourcedRepository <CustomerAccount>(bus); // act repository.Save(account); // assert using (var db = new ReservationServiceDbContext()) { db.Set <ReservedValue>() .Single(v => v.Value == username && v.Scope == "UserName") .Expiration .Should() .BeNull(); } }
public void Posting_an_invalid_command_does_not_affect_the_aggregate_state() { var order = new Order(Guid.NewGuid(), new Order.CustomerInfoChanged { CustomerName = "Joe" }, new Order.Fulfilled()).SaveToEventStore(); var json = new AddItem { Quantity = 5, Price = 19.99m, ProductName = "Bag o' Treats" }.ToJson(); var request = new HttpRequestMessage(HttpMethod.Post, string.Format("http://contoso.com/orders/{0}/additem", order.Id)) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; var testApi = new TestApi<Order>(); var client = testApi.GetClient(); var response = client.SendAsync(request).Result; response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var updatedOrder = new SqlEventSourcedRepository<Order>().GetLatest(order.Id); updatedOrder.Items.Count().Should().Be(0); }
public override async Task When_storage_fails_then_no_events_are_published() { var order = new Order(); var bus = new InProcessEventBus(); var eventsPublished = new List <IEvent>(); bus.Events <IEvent>().Subscribe(eventsPublished.Add); Func <EventStoreDbContext> eventStoreDbContext = () => { throw new Exception("oops!"); }; var repository = new SqlEventSourcedRepository <Order>(bus, eventStoreDbContext); order .Apply(new AddItem { ProductName = "Widget", Price = 10m, Quantity = 2 }); try { await repository.Save(order); } catch { } eventsPublished.Should().BeEmpty(); }
public async Task When_using_Update_then_failed_writes_are_logged_to_EventHandlingErrors() { // preload some events for the catchup. replay will hit the barrier on the last one. var order = new Order(); var productName = Any.Paragraph(4); order.Apply(new AddItem { Quantity = 1, ProductName = productName, Price = .01m }); var repository = new SqlEventSourcedRepository <Order>(new FakeEventBus()); await repository.Save(order); Projector1 projector = null; projector = new Projector1 { OnUpdate = (_, e) => { using (var work = projector.Update()) { var db = work.Resource <ReadModelDbContext>(); // do something that will trigger a db exception when the UnitOfWork is committed var inventory = db.Set <ProductInventory>(); inventory.Add(new ProductInventory { ProductName = e.ProductName, QuantityReserved = e.Quantity }); inventory.Add(new ProductInventory { ProductName = e.ProductName, QuantityReserved = e.Quantity }); work.VoteCommit(); } } }; // act using (var catchup = CreateReadModelCatchup(projector)) { await catchup.Run(); } // assert using (var db = new ReadModelDbContext()) { var error = db.Set <EventHandlingError>().Single(e => e.AggregateId == order.Id); error.Error.Should() .Contain( string.Format( "Violation of PRIMARY KEY constraint 'PK_dbo.ProductInventories'. Cannot insert duplicate key in object 'dbo.ProductInventories'. The duplicate key value is ({0})", productName)); } }
public async Task When_using_Update_then_failed_writes_do_not_interrupt_catchup() { // preload some events for the catchup. replay will hit the barrier on the last one. var order = new Order(); var productName = Any.Paragraph(4); Action addEvent = () => order.Apply(new AddItem { Quantity = 1, ProductName = productName, Price = .01m }); Enumerable.Range(1, 30).ForEach(_ => addEvent()); var repository = new SqlEventSourcedRepository <Order>(new FakeEventBus()); await repository.Save(order); var count = 0; Projector1 projector = null; projector = new Projector1 { OnUpdate = (_, e) => { using (var work = projector.Update()) { var db = work.Resource <ReadModelDbContext>(); if (count++ == 15) { // do something that will trigger a db exception when the UnitOfWork is committed var inventory = db.Set <ProductInventory>(); inventory.Add(new ProductInventory { ProductName = e.ProductName, QuantityReserved = e.Quantity }); inventory.Add(new ProductInventory { ProductName = e.ProductName, QuantityReserved = e.Quantity }); } work.VoteCommit(); } } }; // act using (var catchup = CreateReadModelCatchup(projector)) { await catchup.Run(); } // assert count.Should().Be(30); }
public void Configuration(IAppBuilder app) { IMessageHandler messageHandler = null; IMessageBus messageBus = new ImmediateMessageBus( new Lazy <IMessageHandler>(() => messageHandler)); var serializer = new JsonMessageSerializer(); Func <TodoListEventStoreDbContext> dbContextFactory = () => { var context = new TodoListEventStoreDbContext(); context.Database.Log += m => Debug.WriteLine(m); return(context); }; IEventSourcedRepository <Domain.TodoItem> repository = new SqlEventSourcedRepository <Domain.TodoItem>( new SqlEventStore( dbContextFactory, serializer), new SqlEventPublisher( dbContextFactory, serializer, messageBus), new SqlMementoStore( dbContextFactory, serializer), Domain.TodoItem.Factory, Domain.TodoItem.Factory); messageHandler = new CompositeMessageHandler( new TodoItemCommandHandler(repository), new ReadModelGenerator(() => new ReadModelDbContext())); IReadModelFacade readModelFacade = new ReadModelFacade(() => new ReadModelDbContext()); app.Use(async(context, next) => { context.Set(nameof(IMessageBus), messageBus); context.Set(nameof(IReadModelFacade), readModelFacade); await next.Invoke(); }); var properties = new AppProperties(app.Properties); repository.EventPublisher.EnqueueAll(properties.OnAppDisposing); }
private void TriggerConcurrencyExceptionOnOrderCommands(Guid orderId) { var orderRepository = Configuration.Current.Repository<Order>(); Configuration.Current.UseDependency(_ => orderRepository); ((SqlEventSourcedRepository<Order>) orderRepository).GetEventStoreContext = () => { // quick, add a new event in order to trigger a concurrency exception at the moment the scheduler tries to apply the command var repository = new SqlEventSourcedRepository<Order>(); var o = repository.GetLatest(orderId).Result; o.Apply(new Annotate<Order>("triggering a concurrency exception", Any.Guid().ToString())); repository.Save(o).Wait(); return EventStoreDbContext(); }; }
private void TriggerConcurrencyExceptionOnOrderCommands(Guid orderId) { Func <EventStoreDbContext> eventStoreContext = () => { // quick, add a new event in order to trigger a concurrency exception at the moment the scheduler tries to apply the command var repository = new SqlEventSourcedRepository <Order>(); var o = repository.GetLatest(orderId).Result; o.Apply(new Annotate <Order>("triggering a concurrency exception", Any.Guid().ToString())); repository.Save(o).Wait(); return(EventStoreDbContext()); }; var orderRepository = new SqlEventSourcedRepository <Order>(createEventStoreDbContext: eventStoreContext); Configuration.Current.UseDependency <IEventSourcedRepository <Order> >(_ => orderRepository); }
public async Task ApplyBatch_can_accept_an_array_of_commands() { var repository = new SqlEventSourcedRepository <Order>(new FakeEventBus()); var order = new Order(); await repository.Save(order); var json = new[] { new { AddItem = new { Quantity = 1, Price = 1, ProductName = "Sprocket" } }, new { AddItem = new { Quantity = 1, Price = 2, ProductName = "Cog" } } }.ToJson(); var testApi = new TestApi <Order>(); var client = testApi.GetClient(); var request = new HttpRequestMessage(HttpMethod.Post, string.Format("http://contoso.com/orders/{0}", order.Id)) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; var response = await client.SendAsync(request); response.ShouldSucceed(); order = await repository.GetLatest(order.Id); order.Items.Count.Should().Be(2); order.Balance.Should().Be(3); }
public async Task When_Run_is_called_while_already_running_then_it_skips_the_run() { var repository = new SqlEventSourcedRepository <Order>(new FakeEventBus()); await repository.Save(new Order()); var mre = new ManualResetEventSlim(); var barrier = new Barrier(2); var progress = new List <ReadModelCatchupStatus>(); Events.Write(10); var projector = new Projector <Order.ItemAdded> { OnUpdate = (work, e) => { barrier.SignalAndWait(1000); mre.Wait(5000); } }; using (var catchup = CreateReadModelCatchup(projector)) using (catchup.Progress.Subscribe(s => { progress.Add(s); Console.WriteLine("progress: " + s); })) { #pragma warning disable 4014 // don't await Task.Run(() => catchup.Run()); #pragma warning restore 4014 // make sure the first catchup is blocked inside the projector barrier.SignalAndWait(1000); // try to start another catchup var result = await catchup.Run(); result.Should().Be(ReadModelCatchupResult.CatchupAlreadyInProgress); await Task.Delay(2000); } mre.Set(); progress.Should().ContainSingle(s => s.IsStartOfBatch); }
public async Task When_not_using_Update_then_failed_writes_do_not_interrupt_catchup() { // arrange // preload some events for the catchup. replay will hit the barrier on the last one. var order = new Order(); var productName = Any.Paragraph(3); Action addEvent = () => order.Apply(new AddItem { Quantity = 1, ProductName = productName, Price = .01m }); Enumerable.Range(1, 30).ForEach(_ => addEvent()); var repository = new SqlEventSourcedRepository <Order>(new FakeEventBus()); await repository.Save(order); var count = 0; var projector = new Projector1 { OnUpdate = (work, e) => { using (var db = ReadModelDbContext()) { // throw one exception in the middle if (count++ == 15) { throw new Exception("drat!"); } db.SaveChanges(); } } }; // act using (var catchup = CreateReadModelCatchup(projector)) { await catchup.Run(); } // assert count.Should().Be(30); }
public override void SetUp() { base.SetUp(); using (VirtualClock.Start(DateTimeOffset.Now.AddMonths(1))) { disposables = new CompositeDisposable(); Settings.Sources = new ISettingsSource[] { new ConfigDirectorySettings(@"c:\dev\.config") }.Concat(Settings.Sources); serviceBusSettings = Settings.Get <ServiceBusSettings>(); serviceBusSettings.NamePrefix = "itscqrstests"; serviceBusSettings.ConfigureQueue = q => { q.AutoDeleteOnIdle = TimeSpan.FromMinutes(15); }; bus = new FakeEventBus(); orderRepository = new SqlEventSourcedRepository <Order>(bus); var configuration = new Configuration() .UseSqlEventStore(() => new EventStoreDbContext()) .UseEventBus(bus) .UseSqlCommandScheduling() .UseDependency <IEventSourcedRepository <Order> >(t => orderRepository); var clockName = Any.Paragraph(4); scheduler = new SqlCommandScheduler(configuration) { GetClockName = @event => clockName }; queueSender = new ServiceBusCommandQueueSender(serviceBusSettings) { MessageDeliveryOffsetFromCommandDueTime = TimeSpan.FromSeconds(30) }; disposables.Add(scheduler.Activity.Subscribe(s => Console.WriteLine("SqlCommandScheduler: " + s.ToJson()))); disposables.Add(queueSender.Messages.Subscribe(s => Console.WriteLine("ServiceBusCommandQueueSender: " + s.ToJson()))); disposables.Add(bus.Subscribe(scheduler)); disposables.Add(configuration); disposables.Add(ConfigurationContext.Establish(configuration)); } }
public async Task ReadModelCatchup_only_queries_events_since_the_last_consumed_event_id() { var bus = new FakeEventBus(); var repository = new SqlEventSourcedRepository <Order>(bus); // save the order with no projectors running var order = new Order(); order.Apply(new AddItem { Price = 1m, ProductName = "Widget" }); await repository.Save(order); // subscribe one projector for catchup var projector1 = new Projector1(); using (var catchup = CreateReadModelCatchup(projector1)) { catchup.Progress.ForEachAsync(s => Console.WriteLine(s)); await catchup.Run(); } order.Apply(new AddItem { Price = 1m, ProductName = "Widget" }); await repository.Save(order); // subscribe both projectors var projector2 = new Projector2(); using (var catchup = CreateReadModelCatchup(projector1, projector2)) { catchup.Progress.ForEachAsync(s => Console.WriteLine(s)); await catchup.Run(); } projector1.CallCount.Should().Be(2, "A given event should only be passed to a given projector once"); projector2.CallCount.Should().Be(2, "A projector should be passed events it has not previously seen."); }
public override void SetUp() { base.SetUp(); using (VirtualClock.Start(DateTimeOffset.Now.AddMonths(1))) { disposables = new CompositeDisposable(); Settings.Sources = new ISettingsSource[] { new ConfigDirectorySettings(@"c:\dev\.config") }.Concat(Settings.Sources); serviceBusSettings = Settings.Get<ServiceBusSettings>(); serviceBusSettings.NamePrefix = "itscqrstests"; serviceBusSettings.ConfigureQueue = q => { q.AutoDeleteOnIdle = TimeSpan.FromMinutes(15); }; bus = new FakeEventBus(); orderRepository = new SqlEventSourcedRepository<Order>(bus); var configuration = new Configuration() .UseSqlEventStore(() => new EventStoreDbContext()) .UseEventBus(bus) .UseSqlCommandScheduling() .UseDependency<IEventSourcedRepository<Order>>(t => orderRepository); var clockName = Any.Paragraph(4); scheduler = new SqlCommandScheduler(configuration) { GetClockName = @event => clockName }; queueSender = new ServiceBusCommandQueueSender(serviceBusSettings) { MessageDeliveryOffsetFromCommandDueTime = TimeSpan.FromSeconds(30) }; disposables.Add(scheduler.Activity.Subscribe(s => Console.WriteLine("SqlCommandScheduler: " + s.ToJson()))); disposables.Add(queueSender.Messages.Subscribe(s => Console.WriteLine("ServiceBusCommandQueueSender: " + s.ToJson()))); disposables.Add(bus.Subscribe(scheduler)); disposables.Add(configuration); disposables.Add(ConfigurationContext.Establish(configuration)); } }
public void When_Run_is_called_while_already_running_then_it_skips_the_run() { var repository = new SqlEventSourcedRepository <Order>(new FakeEventBus()); new Order().Apply(new AddItem { Price = 1m, ProductName = MethodBase.GetCurrentMethod().Name }); repository.Save(new Order()); var mre = new ManualResetEventSlim(); var progress = new List <ReadModelCatchupStatus>(); var signal = new ReplaySubject <Unit>(); var projector = new Projector <Order.ItemAdded>(() => new ReadModelDbContext()) { OnUpdate = (work, e) => { signal.OnNext(Unit.Default); mre.Wait(20000); } }; using (var catchup = CreateReadModelCatchup(projector)) { catchup.Progress.ForEachAsync(s => { progress.Add(s); Console.WriteLine(s); }); Task.Run(() => { catchup.Run(); }); mre.Set(); catchup.Run(); } progress.Should().ContainSingle(s => s.IsStartOfBatch); }
public async Task An_ETag_header_is_applied_to_the_command() { var order = new Order(Guid.NewGuid(), new Order.CustomerInfoChanged { CustomerName = "Joe" }).SaveToEventStore(); var json = new AddItem { Quantity = 5, Price = 19.99m, ProductName = "Bag o' Treats" }.ToJson(); var etag = new EntityTagHeaderValue("\"" + Any.Guid() + "\""); Func<HttpRequestMessage> createRequest = () => { var request = new HttpRequestMessage(HttpMethod.Post, string.Format("http://contoso.com/orders/{0}/additem", order.Id)) { Content = new StringContent(json, Encoding.UTF8, "application/json"), }; request.Headers.IfNoneMatch.Add(etag); return request; }; var testApi = new TestApi<Order>(); var client = testApi.GetClient(); // act: send the request twice var response1 = await client.SendAsync(createRequest()); var response2 = await client.SendAsync(createRequest()); // assert response1.ShouldSucceed(HttpStatusCode.OK); response2.ShouldFailWith(HttpStatusCode.NotModified); var updatedOrder = new SqlEventSourcedRepository<Order>().GetLatest(order.Id); updatedOrder.Items.Single().Quantity.Should().Be(5); }
public async Task When_Save_fails_then_a_scheduled_command_error_is_recorded() { // arrange var order = CommandSchedulingTests_EventSourced.CreateOrder(); var innerRepository = new SqlEventSourcedRepository <Order>(); var saveCount = 0; Configuration.Current .Container .Register <IEventSourcedRepository <Order> >(c => new FakeEventSourcedRepository <Order>(innerRepository) { OnSave = async o => { saveCount++; // throw on the second save attempt, which is when the clock is advanced delivering the scheduled command if (saveCount == 2) { throw new Exception("oops!"); } await innerRepository.Save(o); } }); // act order.Apply(new ShipOn(Clock.Now().AddDays(30))); await Save(order); await AdvanceClock(TimeSpan.FromDays(31)); //assert using (var db = CommandSchedulerDbContext()) { var error = db.Errors.Single(c => c.ScheduledCommand.AggregateId == order.Id).Error; error.Should().Contain("oops!"); } }
public async Task When_a_scheduled_command_fails_then_the_error_is_recorded() { // arrange var order = CommandSchedulingTests.CreateOrder(); var innerRepository = new SqlEventSourcedRepository<Order>(); var saveCount = 0; Configuration.Current .Container .Register<IEventSourcedRepository<Order>>(c => new FakeEventSourcedRepository<Order>(innerRepository) { OnSave = async o => { if (saveCount > 0) { throw new Exception("oops!"); } await innerRepository.Save(o); saveCount++; } }); // act order.Apply(new ShipOn(Clock.Now().AddDays(30))); await Configuration.Current.Repository<Order>().Save(order); await clockTrigger.AdvanceClock(clockName, TimeSpan.FromDays(31)); //assert using (var db = new CommandSchedulerDbContext()) { var error = db.Errors.Single(c => c.ScheduledCommand.AggregateId == order.Id).Error; error.Should().Contain("oops!"); } }
public OrderController(SqlEventSourcedRepository <Order> sqlEventSourcedRepository) : base(sqlEventSourcedRepository) { }
public async Task When_Save_fails_then_a_scheduled_command_error_is_recorded() { // arrange var order = CommandSchedulingTests_EventSourced.CreateOrder(); var innerRepository = new SqlEventSourcedRepository<Order>(); var saveCount = 0; Configuration.Current .Container .Register<IEventSourcedRepository<Order>>(c => new FakeEventSourcedRepository<Order>(innerRepository) { OnSave = async o => { saveCount++; // throw on the second save attempt, which is when the clock is advanced delivering the scheduled command if (saveCount == 2) { throw new Exception("oops!"); } await innerRepository.Save(o); } }); // act order.Apply(new ShipOn(Clock.Now().AddDays(30))); await Save(order); await AdvanceClock(TimeSpan.FromDays(31)); //assert using (var db = CommandSchedulerDbContext()) { var error = db.Errors.Single(c => c.ScheduledCommand.AggregateId == order.Id).Error; error.Should().Contain("oops!"); } }
public async Task Insertion_of_new_events_during_catchup_does_not_interrupt_catchup() { var barrier = new Barrier(2); // preload some events for the catchup. replay will hit the barrier on the last one. var order = new Order(); Action addEvent = () => order.Apply(new AddItem { Quantity = 1, ProductName = "Penny candy", Price = .01m }); Enumerable.Range(1, 100).ForEach(_ => addEvent()); var repository = new SqlEventSourcedRepository <Order>(new FakeEventBus()); await repository.Save(order); // queue the catchup on a background task #pragma warning disable 4014 // don't await Task.Run(() => #pragma warning restore 4014 { var projector = new Projector1 { OnUpdate = (work, e) => { if (e.SequenceNumber == 10) { Console.WriteLine("pausing read model catchup"); barrier.SignalAndWait(MaxWaitTime); //1 barrier.SignalAndWait(MaxWaitTime); //2 Console.WriteLine("resuming read model catchup"); } } }; using (var db = EventStoreDbContext()) using (var catchup = CreateReadModelCatchup(projector)) { var events = db.Events.Where(e => e.Id > HighestEventId); Console.WriteLine(string.Format("starting read model catchup for {0} events", events.Count())); catchup.Run().Wait(); Console.WriteLine("done with read model catchup"); barrier.SignalAndWait(MaxWaitTime); //3 } }); Console.WriteLine("queued read model catchup task"); barrier.SignalAndWait(MaxWaitTime); //1 EventStoreDbContext().DisposeAfter(c => { Console.WriteLine("adding one more event, bypassing read model tracking"); c.Events.Add(new Order.ItemAdded { AggregateId = Guid.NewGuid(), SequenceNumber = 1 }.ToStorableEvent()); c.SaveChanges(); Console.WriteLine("done adding one more event"); }); barrier.SignalAndWait(MaxWaitTime); //2 barrier.SignalAndWait(MaxWaitTime); //3 // check that everything worked: var projector2 = new Projector1(); var projectorName = ReadModelInfo.NameForProjector(projector2); using (var readModels = ReadModelDbContext()) { var readModelInfo = readModels.Set <ReadModelInfo>().Single(i => i.Name == projectorName); readModelInfo.CurrentAsOfEventId.Should().Be(HighestEventId + 101); using (var catchup = CreateReadModelCatchup(projector2)) { await catchup.Run(); } readModels.Entry(readModelInfo).Reload(); readModelInfo.CurrentAsOfEventId.Should().Be(HighestEventId + 102); } }
public FakeEventSourcedRepository(SqlEventSourcedRepository<TAggregate> innerRepository) { this.innerRepository = innerRepository; OnSave = innerRepository.Save; }