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);
        }
Beispiel #2
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);
        }
Beispiel #10
0
        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);
        }