public SqlCommandScheduler( Func <IEventSourcedRepository <TAggregate> > getRepository, Func <CommandSchedulerDbContext> createCommandSchedulerDbContext, IEventBus eventBus, CommandPreconditionVerifier commandPreconditionVerifier) { if (getRepository == null) { throw new ArgumentNullException("getRepository"); } if (createCommandSchedulerDbContext == null) { throw new ArgumentNullException("createCommandSchedulerDbContext"); } if (eventBus == null) { throw new ArgumentNullException("eventBus"); } if (commandPreconditionVerifier == null) { throw new ArgumentNullException("commandPreconditionVerifier"); } this.getRepository = getRepository; this.createCommandSchedulerDbContext = createCommandSchedulerDbContext; this.eventBus = eventBus; this.commandPreconditionVerifier = commandPreconditionVerifier; consequenter = Consequenter.Create <IScheduledCommand <TAggregate> >(e => { Task.Run(() => Schedule(e)).Wait(); }); }
public void Durable_consequenters_can_be_simulated_for_unit_testing_and_receive_messages() { var consequenter1WasCalled = false; var consequenter2WasCalled = false; var uselessSettings = new ServiceBusSettings(); using (ServiceBusDurabilityExtensions.Simulate()) { var consequenter1 = Consequenter.Create <Order.Cancelled>( e => { consequenter1WasCalled = true; }); var consequenter2 = Consequenter.Create <Order.CreditCardCharged>( e => { consequenter2WasCalled = true; }); var bus = new InProcessEventBus(); bus.Subscribe( consequenter1.UseServiceBusForDurability(uselessSettings), consequenter2.UseServiceBusForDurability(uselessSettings)); bus.PublishAsync(new Order.Cancelled()).Wait(); consequenter1WasCalled.Should().BeTrue(); consequenter2WasCalled.Should().BeFalse(); } }
public void Errors_during_catchup_result_in_retries() { var consequenter = Consequenter.Create <Order.CreditCardCharged>(e => { }) .UseServiceBusForDurability(new ServiceBusSettings { ConnectionString = "this will never work" }); var errors = new List <EventHandlingError>(); var aggregateId = Any.Guid(); Events.Write(1, i => new Order.CreditCardCharged { AggregateId = aggregateId }); using (Domain.Configuration.Global.EventBus.Errors.Subscribe(errors.Add)) using (var catchup = CreateReadModelCatchup <CommandSchedulerDbContext>(consequenter)) { catchup.Run(); errors.Should().Contain(e => e.AggregateId == aggregateId && e.Exception.ToString().Contains("excelsior!")); } // TODO (Errors_during_catchup_result_in_retries) this would require a slightly different catchup mechanism or a way to retry events found in the EventHandlingErrors table Assert.Fail("Test not written yet."); }
public async Task GetAggregate_can_be_used_when_no_aggregate_was_previously_sourced() { var order = new Order() .Apply(new ChangeCustomerInfo { CustomerName = Any.FullName() }); var repository = CreateRepository <Order>(); await repository.Save(order); Order aggregate = null; var consquenter = Consequenter.Create <Order.Placed>(e => { #pragma warning disable 612 aggregate = e.GetAggregate(); #pragma warning restore 612 }); consquenter.HaveConsequences(new Order.Placed { AggregateId = order.Id }); aggregate.Id.Should().Be(order.Id); }
public void IsEventHandlerType_returns_true_for_consequenters() { Consequenter.Create <Order.ItemRemoved>(e => { }) .GetType() .IsEventHandlerType() .Should() .BeTrue(); }
public void A_named_assigned_using_Named_is_not_lost_on_successive_wraps() { var name = Any.CamelCaseName(); var consequenter = Consequenter.Create <Order.CustomerInfoChanged>(e => { }) .Named(name) .WrapAll((e, next) => next(e)); EventHandler.Name(consequenter).Should().Be(name); EventHandler.FullName(consequenter).Should().Be(name); }
/// <summary> /// Initializes a new instance of the <see cref="InMemoryCommandScheduler{TAggregate}"/> class. /// </summary> /// <param name="repository">The repository.</param> /// <exception cref="System.ArgumentNullException">repository</exception> public InMemoryCommandScheduler(IEventSourcedRepository <TAggregate> repository) { if (repository == null) { throw new ArgumentNullException("repository"); } this.repository = repository; consequenter = Consequenter.Create <IScheduledCommand <TAggregate> >(e => Schedule(e).Wait()); }
public void EventHandler_FullName_differentiates_anonymous_consequenters_that_were_named_using_Named() { // deliberately different implementations to avoid compiler inlining -- basically the implementation is going to depend on the anonymous closure types being different var anonymousConsequenter1 = Consequenter.Create <Order.Cancelled>(e => Console.WriteLine(e.AggregateId)).Named("one"); var anonymousConsequenter2 = Consequenter.Create <Order.Cancelled>(e => Console.WriteLine(e.SequenceNumber)).Named("two"); var consequenter1Name = EventHandler.FullName(anonymousConsequenter1); var consequenter2Name = EventHandler.FullName(anonymousConsequenter2); consequenter1Name.Should().NotBe(consequenter2Name); }
public void Named_does_not_prevent_calls_to_inner_handler() { var callCount = 0; var handler = Consequenter.Create <Order.CreditCardCharged>(e => callCount++) .Named("something"); bus.Subscribe(handler); bus.PublishAsync(new Order.CreditCardCharged()).Wait(); callCount.Should().Be(1); }
public async Task GetAggregate_can_be_used_within_a_consequenter_to_access_an_aggregate_without_having_to_re_source() { // arrange var order = new Order() .Apply(new ChangeCustomerInfo { CustomerName = Any.FullName() }) .Apply(new AddItem { ProductName = "Cog", Price = 9.99m }) .Apply(new AddItem { ProductName = "Sprocket", Price = 9.99m }) .Apply(new ProvideCreditCardInfo { CreditCardNumber = Any.String(16, 16, Characters.Digits), CreditCardCvv2 = "123", CreditCardExpirationMonth = "12", CreditCardName = Any.FullName(), CreditCardExpirationYear = "16" }) .Apply(new SpecifyShippingInfo()) .Apply(new Place()); var repository = CreateRepository <Order>(); Configuration.Current.UseDependency <IEventSourcedRepository <Order> >(t => { throw new Exception("GetAggregate should not be triggering this call."); }); Order aggregate = null; #pragma warning disable 612 #pragma warning disable 618 var consquenter = Consequenter.Create <Order.Placed>(e => { aggregate = e.GetAggregate(); }); #pragma warning restore 618 #pragma warning restore 612 var bus = Configuration.Current.EventBus as FakeEventBus; bus.Subscribe(consquenter); // act await repository.Save(order); // assert aggregate.Should().Be(order); }
public async Task When_one_command_triggers_another_command_via_a_consequenter_then_the_second_command_acquires_the_first_commands_clock() { // arrange var orderId = Any.Guid(); var customerId = Any.Guid(); var bus = new InProcessEventBus(); var orderRepository = new InMemoryEventSourcedRepository <Order>(bus: bus); await orderRepository.Save(new Order(new CreateOrder(Any.FullName()) { AggregateId = orderId, CustomerId = customerId }).Apply(new AddItem { ProductName = Any.Word(), Quantity = 1, Price = Any.Decimal(.01m, 10m) })); var customerRepository = new InMemoryEventSourcedRepository <CustomerAccount>(); await customerRepository.Save(new CustomerAccount(customerId).Apply(new ChangeEmailAddress(Any.Email()))); #pragma warning disable 618 bus.Subscribe(Consequenter.Create <Order.Shipped>(e => #pragma warning restore 618 { var order = orderRepository.GetLatest(e.AggregateId).Result; var customer = customerRepository.GetLatest(order.CustomerId).Result; customer.Apply(new SendOrderConfirmationEmail(order.OrderNumber)); customerRepository.Save(customer).Wait(); })); var shipDate = DateTimeOffset.Parse("2014-05-15 01:01:01"); var ship = new Ship(); // act using (CommandContext.Establish(ship, Clock.Create(() => shipDate))) { var order = await orderRepository.GetLatest(orderId); order.Apply(ship); await orderRepository.Save(order); } // assert var last = (await customerRepository.GetLatest(customerId)).Events().Last(); last.Should() .BeOfType <CustomerAccount.OrderShipConfirmationEmailSent>(); last.Timestamp.Should().Be(shipDate); }
public void When_the_same_message_is_handled_by_multiple_consequenters_it_is_delivered_to_each_only_once() { var aggregateId = Any.Guid(); var consequenter1WasCalled = new AsyncValue <bool>(); var consequenter2WasCalled = new AsyncValue <bool>(); var consequenter1CallCount = 0; var consequenter2CallCount = 0; var consequenter1 = Consequenter.Create <Order.Fulfilled>(e => { if (e.AggregateId == aggregateId) { Interlocked.Increment(ref consequenter1CallCount); consequenter1WasCalled.Set(true); } }) .Named(MethodBase.GetCurrentMethod().Name + "-1") .UseServiceBusForDurability(settings, configuration: configuration); var consequenter2 = Consequenter.Create <Order.Fulfilled>(e => { if (e.AggregateId == aggregateId) { Interlocked.Increment(ref consequenter2CallCount); consequenter2WasCalled.Set(true); } }) .Named(MethodBase.GetCurrentMethod().Name + "-2") .UseServiceBusForDurability(settings, configuration: configuration); using (var catchup = CreateReadModelCatchup <CommandSchedulerDbContext>(consequenter1, consequenter2)) { Events.Write(1, i => new Order.Fulfilled { AggregateId = aggregateId }); catchup.Run(); // give the service bus messages times to be delivered Task.WaitAll(new Task[] { consequenter1WasCalled, consequenter2WasCalled }, DefaultTimeout()); consequenter1CallCount.Should().Be(1); consequenter2CallCount.Should().Be(1); } }
public async Task Catchup_can_be_used_with_intercepted_consequenters_to_queue_event_messages_on_the_service_bus() { var onCancelledCalled = new AsyncValue <bool>(); var onCreatedCalled = new AsyncValue <bool>(); var onDeliveredCalled = new AsyncValue <bool>(); var anonymousConsequenterCalled = new AsyncValue <bool>(); var consequenter1 = new TestConsequenter( onCancelled: e => onCancelledCalled.Set(true), onCreated: e => onCreatedCalled.Set(true), onDelivered: e => onDeliveredCalled.Set(true)) .UseServiceBusForDurability(settings, configuration: configuration); var name = MethodBase.GetCurrentMethod().Name; var consequenter2 = Consequenter.Create <Order.Fulfilled>(e => anonymousConsequenterCalled.Set(true)) .Named(name) .UseServiceBusForDurability(settings, configuration: configuration); using (var catchup = CreateReadModelCatchup <CommandSchedulerDbContext>( consequenter1, consequenter2)) { Events.Write(1, i => new Order.Created()); Events.Write(1, i => new Order.Delivered()); Events.Write(1, i => new Order.Fulfilled()); Events.Write(1, i => new Order.Cancelled()); catchup.Run(); // give the service bus messages times to be delivered Task.WaitAll(new Task[] { onCancelledCalled, onCreatedCalled, onDeliveredCalled, anonymousConsequenterCalled }, DefaultTimeout()); onCancelledCalled.Result.Should().Be(true); onCreatedCalled.Result.Should().Be(true); onDeliveredCalled.Result.Should().Be(true); anonymousConsequenterCalled.Result.Should().Be(true); } }
public void When_an_exception_is_thrown_by_a_IHaveConsequencesWhen_then_it_continues_to_receive_events() { int count = 0; bus.Subscribe(Consequenter.Create <Order.Paid>(e => { count++; if (count > 8) { throw new Exception("oops!"); } })); var events = Enumerable.Range(1, 10).Select(i => new Order.Paid(i)).ToArray(); using (bus.PublishAsync(events).Subscribe()) { count.Should().Be(10); } }
public void Errors_during_event_handling_are_published_back_to_the_global_bus_on_delivery() { var consequenter = Consequenter.Create <Order.CreditCardCharged>(e => { throw new Exception("whoa that's bad..."); }) .UseServiceBusForDurability(settings, configuration: configuration); var errors = new List <EventHandlingError>(); var aggregateId = Any.Guid(); Events.Write(1, i => new Order.CreditCardCharged { AggregateId = aggregateId }); using (configuration.EventBus.Errors.Subscribe(errors.Add)) using (var catchup = CreateReadModelCatchup <CommandSchedulerDbContext>(consequenter)) { catchup.Run(); errors.Should().Contain(e => e.AggregateId == aggregateId && e.Exception.ToString().Contains("whoa that's bad...")); } }
public void EventHandler_Name_returns_something_vaguely_informative_for_anonymous_consequenters() { var wrapper = Consequenter.Create <Order.ItemAdded>(e => { }); EventHandler.Name(wrapper).Should().Be("AnonymousConsequenter"); }