public async Task Posting_an_invalid_command_does_not_affect_the_aggregate_state() { var order = new Order(Guid.NewGuid(), new Order.CustomerInfoChanged { CustomerName = "Joe" }, new Order.Fulfilled()); await order.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 = await new SqlEventSourcedRepository<Order>().GetLatest(order.Id); updatedOrder.Items.Count().Should().Be(0); }
public async Task Command_properties_can_be_validated() { var order = new Order( Guid.NewGuid(), new Order.Fulfilled()); await order.SaveToEventStore(); Console.WriteLine(order.Id); var httpClient = new TestApi<Order>().GetClient(); var result = httpClient.PostAsync( string.Format("http://contoso.com/orders/{0}/additem/validate", order.Id), new JsonContent(new AddItem { Price = 1m, Quantity = 1, ProductName = "Widget" })).Result; result.ShouldSucceed(); var content = result.Content.ReadAsStringAsync().Result; Console.WriteLine(content); content.Should().Contain("\"Failures\":[{\"Message\":\"The order has already been fulfilled.\""); }
public void The_clock_set_in_the_CommandContext_is_used_by_resulting_events() { var created = DateTimeOffset.Parse("2014-05-15 00:00:00"); var addItem = new AddItem { ProductName = "Widget", Price = 3.99m }; Order order; using (CommandContext.Establish(addItem, Clock.Create(() => created))) { order = new Order(new CreateOrder(Any.FullName())); order.Apply(addItem); } order.Events() .Count() .Should() .Be(3); order.Events() .Should() .OnlyContain(e => e.Timestamp == created); }
public void A_constructor_command_can_be_used_to_create_a_new_aggregate_instance() { var customerName = Any.FullName(); var order = new Order(new CreateOrder(customerName)); order.CustomerName.Should().Be(customerName); order.Version.Should().Be(2); }
public void The_last_event_in_the_sequence_should_win() { var order = new Order( Guid.NewGuid(), new Order.CustomerInfoChanged { CustomerName = "bob" }, new Order.CustomerInfoChanged { CustomerName = "alice" }); order.CustomerName.Should().Be("alice"); }
public void SetUp() { Command<Order>.AuthorizeDefault = delegate { return true; }; customerName = Any.FullName(); var order = new Order(new CreateOrder(customerName)); repository.Save(order).Wait(); aggregateId = order.Id; }
public void Properties_can_be_rehydrated_from_an_event_sequence() { var order = new Order( Guid.NewGuid(), new Order.CustomerInfoChanged { CustomerName = "joe" }, new Order.Cancelled()); order.CustomerName.Should().Be("joe"); order.IsCancelled.Should().Be(true); }
public void ConstructorCommand_AggregateId_is_used_to_specify_the_new_instances_Id() { var id = Any.Guid(); var createOrder = new CreateOrder(Any.Paragraph(2)) { AggregateId = id }; var order = new Order(createOrder); order.Id.Should().Be(id); }
public void AuthorizationPolicy_For_returns_an_instance_based_on_the_actual_rather_than_declared_type_of_the_principal() { AuthorizationFor<Customer>.ToApply<Cancel>.ToA<Order> .Requires((a, b, c) => true); IPrincipal iprincipal = new Customer(); var customer = new Customer(); var cancel = new Cancel(); var order = new Order(); iprincipal.IsAuthorizedTo(cancel, order).Should().Be(true); customer.IsAuthorizedTo(cancel, order).Should().Be(true); }
public void Command_applicability_can_be_validated_by_the_command_class() { var cancel = new Cancel(); var order = new Order(); order.IsValidTo(cancel).Should().Be(true); order.Apply(new Deliver()); order.IsValidTo(cancel).Should().Be(false); }
public void Constructor_commands_cannot_be_used_on_aggregates_that_have_prior_events() { var order = new Order(); order.Apply(new AddItem { Price = 1, ProductName = Any.CamelCaseName() }); Action apply = () => order.Apply(new CreateOrder(Any.CamelCaseName())); apply.ShouldThrow<ConcurrencyException>(); }
public void SetUp() { Command<Order>.AuthorizeDefault = delegate { return true; }; repository = CreateRepository(); var order = new Order().Apply(new AddItem { ProductName = "Widget", Price = 10m, Quantity = 2 }); repository.Save(order).Wait(); aggregateId = order.Id; repository.GetLatest(aggregateId).Result.EventHistory.Last().Should().BeOfType<Order.ItemAdded>(); }
public void SetUp() { // disable authorization checks Command<Order>.AuthorizeDefault = (o, c) => true; var events = new IEvent<Order>[] { new Order.ItemAdded { Price = 10m, Quantity = 2, ProductName = "Widget" }, new Order.FulfillmentMethodSelected { FulfillmentMethod = FulfillmentMethod.Delivery }, new Order.Placed(), new Order.Shipped(), new Order.Paid(20), new Order.Delivered(), new Order.Fulfilled() }; // set up an order to work with order = new Order(Guid.NewGuid(), events).SaveToEventStore(); api = new TestApi<Order>(); }
public void The_event_Actor_is_set_from_the_Command_Principal() { // arrange var customer = new Customer(Any.FullName()); var serviceRepresentative = new CustomerServiceRepresentative { Name = Any.FullName() }; var command = new SpecifyShippingInfo { Principal = serviceRepresentative }; var order = new Order(new CreateOrder(customer.Name) { Principal = customer }); // act order.Apply(command); // assert order.Events() .OfType<Order.Created>() .Single() .Actor() .Should() .Be(customer.Name); order.Events() .OfType<Order.ShippingMethodSelected>() .Single() .Actor() .Should() .Be(serviceRepresentative.Name); }
public async Task When_a_command_is_scheduled_but_the_event_that_triggered_it_was_not_successfully_written_then_the_command_is_not_applied() { VirtualClock.Start(); // create a customer account var customer = new CustomerAccount(Any.Guid()) .Apply(new ChangeEmailAddress(Any.Email())); await accountRepository.Save(customer); var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = customer.Id }); // act // cancel the order but don't save it order.Apply(new Cancel()); // assert // verify that the customer did not receive the scheduled NotifyOrderCanceled command customer = await accountRepository.GetLatest(customer.Id); customer.Events().Last().Should().BeOfType<CustomerAccount.EmailAddressChanged>(); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == customer.Id) .Should() .ContainSingle(c => c.AppliedTime == null && c.DueTime == null); } }
public async Task When_a_command_is_scheduled_but_an_exception_is_thrown_in_a_handler_then_an_error_is_recorded() { Configuration.Current.UseDependency(_ => new CustomerAccount.OrderEmailConfirmer { SendOrderConfirmationEmail = x => { throw new Exception("drat!"); } }); // create a customer account var customer = new CustomerAccount(Any.Guid()) .Apply(new ChangeEmailAddress(Any.Email())); await accountRepository.Save(customer); var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = customer.Id }); await orderRepository.Save(order); // act order.Apply(new Cancel()); await orderRepository.Save(order); await clockTrigger.AdvanceClock(clockName, TimeSpan.FromMinutes(1.2)); using (var db = new CommandSchedulerDbContext()) { db.Errors .Where(e => e.ScheduledCommand.AggregateId == customer.Id) .Should() .Contain(e => e.Error.Contains("drat!")); } }
public async Task When_a_command_is_delivered_a_second_time_with_the_same_ETag_it_is_not_retried_afterward() { // arrange var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = Any.Guid() }); await orderRepository.Save(order); var command = new AddItem { ProductName = Any.Word(), Price = 10m, ETag = Any.Guid().ToString() }; var commandScheduler = Configuration.Current.Container.Resolve<ICommandScheduler<Order>>(); // act await commandScheduler.Schedule(order.Id, command, Clock.Now().AddDays(1)); Thread.Sleep(1); // the sequence number is set from the current tick count, which every now and then produces a duplicate here await commandScheduler.Schedule(order.Id, command, Clock.Now().AddDays(1)); await clockTrigger.Trigger(cmd => cmd.Where(c => c.AggregateId == order.Id)); // assert order = await orderRepository.GetLatest(order.Id); order.Balance.Should().Be(10); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == order.Id) .Should() .OnlyContain(c => c.AppliedTime != null); db.Errors .Where(c => c.ScheduledCommand.AggregateId == order.Id) .Should() .BeEmpty(); } }
public async Task The_aggregate_can_cancel_a_scheduled_command_after_it_fails() { var order = new Order( new CreateOrder(Any.FullName())) .Apply(new AddItem { Price = 499.99m, ProductName = Any.Words(1, true).Single() }) .Apply(new SpecifyShippingInfo { Address = Any.Words(1, true).Single() + " St.", City = "Seattle", StateOrProvince = "WA", Country = "USA" }) .Apply(new ShipOn(Clock.Now().AddDays(10))) .Apply(new Ship()); await orderRepository.Save(order); Func<IQueryable<ScheduledCommand>, IQueryable<ScheduledCommand>> query = cmds => cmds.Where(cmd => cmd.AggregateId == order.Id) .Where(cmd => cmd.AppliedTime == null && cmd.FinalAttemptTime == null); var result = await clockTrigger.Trigger(query); // the command should fail validation result.FailedCommands.Count().Should().Be(1); result = await clockTrigger.Trigger(query); result.FailedCommands.Count().Should().Be(0); }
public async Task An_ETag_header_is_applied_to_the_command() { var order = new Order(Guid.NewGuid(), new Order.CustomerInfoChanged { CustomerName = "Joe" }); await order.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 = await new SqlEventSourcedRepository<Order>().GetLatest(order.Id); updatedOrder.Items.Single().Quantity.Should().Be(5); }
public void Aggregates_can_be_re_sourced_in_memory_to_older_versions() { var originalName = Any.FullName(); var order = new Order(new CreateOrder(originalName)); order.Apply(new ChangeCustomerInfo { CustomerName = Any.FullName() }); var orderAtOlderVersion = order.AsOfVersion(1); orderAtOlderVersion.CustomerName.Should().Be(originalName); }
public async Task A_command_can_be_scheduled_against_another_aggregate() { var order = new Order( new CreateOrder(Any.FullName()) { CustomerId = customerAccountId }) .Apply(new AddItem { ProductName = Any.Word(), Price = 12.99m }) .Apply(new Cancel()); await orderRepository.Save(order); var customerAccount = await customerRepository.GetLatest(customerAccountId); customerAccount.Events() .Last() .Should() .BeOfType<CustomerAccount.OrderCancelationConfirmationEmailSent>(); }
public void When_a_command_fails_then_its_updates_are_not_applied_to_the_aggregate() { var order = new Order( Guid.NewGuid(), new Order.Fulfilled()); order.IsCancelled.Should().Be(false); order.Invoking(o => o.Apply(new Cancel())) .ShouldThrow<CommandValidationException>(); order.IsCancelled.Should().Be(false); }
public void ToApplyAnyCommand_allows_an_authorization_rule_to_be_declared_for_all_commands_for_a_given_resource_type() { var principals = new List<IPrincipal>(); var resources = new List<EventSourcedAggregate>(); AuthorizationFor<Customer>.ToApplyAnyCommand.ToA<Order> .Requires((principal, resource) => { principals.Add(principal); resources.Add(resource); return true; }); var customer = new Customer(); var order1 = new Order(); var order2 = new Order(); customer.IsAuthorizedTo(new Cancel(), order1) .Should().BeTrue(); customer.IsAuthorizedTo(new Place(), order2) .Should().BeTrue(); principals.Should().Contain(customer); resources.Should().Contain(order1); resources.Should().Contain(order2); }
public void Gaps_in_the_event_sequence_do_not_cause_incorrect_sourcing() { var id = Guid.NewGuid(); var order = new Order(id, new Order.ItemAdded { AggregateId = id, SequenceNumber = 1, ProductName = "foo", Price = 1 }, new Order.ItemAdded { AggregateId = id, SequenceNumber = 4, ProductName = "foo", Price = 1 }, new Order.ItemAdded { AggregateId = id, SequenceNumber = 8, ProductName = "foo", Price = 1 }, new Order.ItemAdded { AggregateId = id, SequenceNumber = 103, ProductName = "foo", Price = 1 } ); order.Items.Single().Quantity.Should().Be(4); }
public void When_there_are_gaps_in_the_event_sequence_then_new_events_have_the_correct_sequence_numbers_prior_to_save() { // arrange var id = Guid.NewGuid(); var order = new Order(id, new Order.Created { AggregateId = id, SequenceNumber = 1, CustomerId = Any.Guid() }, new Order.ItemAdded { AggregateId = id, SequenceNumber = 4, ProductName = "foo", Price = 1 }, new Order.ItemAdded { AggregateId = id, SequenceNumber = 8, ProductName = "foo", Price = 1 }, new Order.ItemAdded { AggregateId = id, SequenceNumber = 103, ProductName = "foo", Price = 1 } ); // act order.Apply(new Cancel()); // assert order.Version.Should().Be(104); order.PendingEvents.Last().SequenceNumber.Should().Be(104); }
public void When_there_are_gaps_in_the_event_sequence_then_new_events_have_the_correct_sequence_numbers_after_save() { // arrange var id = Guid.NewGuid(); var order = new Order(id, new Order.Created { AggregateId = id, SequenceNumber = 1, CustomerId = Any.Guid() }, new Order.ItemAdded { AggregateId = id, SequenceNumber = 4, ProductName = "foo", Price = 1 }, new Order.ItemAdded { AggregateId = id, SequenceNumber = 8, ProductName = "foo", Price = 1 }, new Order.ItemAdded { AggregateId = id, SequenceNumber = 103, ProductName = "foo", Price = 1 } ); // act order.Apply(new Cancel()); order.ConfirmSave(); // assert order.EventHistory.Last().Should().BeOfType<Order.Cancelled>(); order.EventHistory.Last().SequenceNumber.Should().Be(104); }
public async Task When_a_command_is_scheduled_but_the_aggregate_it_applies_to_is_not_found_then_the_command_is_retried() { // create and cancel an order for a nonexistent customer account var customerId = Any.Guid(); Console.WriteLine(new { customerId }); var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = customerId }); order.Apply(new Cancel()); await orderRepository.Save(order); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == customerId) .Should() .ContainSingle(c => c.AppliedTime == null); } // act // now save the customer and advance the clock await accountRepository.Save(new CustomerAccount(customerId).Apply(new ChangeEmailAddress(Any.Email()))); await clockTrigger.AdvanceClock(clockName, TimeSpan.FromMinutes(2)); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == customerId) .Should() .ContainSingle(c => c.AppliedTime != null); var customer = await accountRepository.GetLatest(customerId); customer.Events() .Last() .Should().BeOfType<CustomerAccount.OrderCancelationConfirmationEmailSent>(); } }
public void When_one_command_triggers_another_command_within_EnactCommand_then_the_second_command_uses_the_CommandContext_clock() { var clockTime = DateTimeOffset.Parse("2014-05-13 09:28:42 AM"); var shipOn = new ShipOn(DateTimeOffset.Parse("2014-06-01 00:00:00")); Order order; using (CommandContext.Establish(shipOn, Clock.Create(() => clockTime))) { order = new Order().Apply(shipOn); } order.Events() .OfType<CommandScheduled<Order>>() .Single() .Timestamp .Should() .Be(clockTime); }
public void When_a_command_is_applied_its_updates_are_applied_to_the_state_of_the_aggregate() { var order = new Order(); order.IsCancelled.Should().Be(false); order.Apply(new Cancel()); order.IsCancelled.Should().Be(true); }
public void When_created_using_new_it_has_a_unique_id_immediately() { var order = new Order(); order.Id.Should().NotBe(Guid.Empty); }