public async Task Posting_command_JSON_applies_a_command_with_the_specified_name_to_an_aggregate_with_the_specified_id()
        {
            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 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.OK);

            var updatedOrder = await new SqlEventSourcedRepository<Order>().GetLatest(order.Id);

            updatedOrder.Items.Single().Quantity.Should().Be(5);
        }
示例#2
0
        public void Command_properties_can_be_validated()
        {
            var order = new Order(Guid.NewGuid())
                .Apply(new ChangeCustomerInfo { CustomerName = "Joe" })
                .Apply(new Deliver())
                .SavedToEventStore();

            var httpClient = new TestApi<Order>().GetClient();

            var result = httpClient.PostAsync(
                $"http://contoso.com/orders/{order.Id}/additem/validate",
                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.\"");
        }
示例#3
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 async Task Posting_an_invalid_command_does_not_affect_the_aggregate_state()
        {
            var order = new Order(Guid.NewGuid())
                .Apply(new ChangeCustomerInfo { CustomerName = "Joe" })
                .Apply(new Deliver())
                .SavedToEventStore();

            var json = new AddItem
            {
                Quantity = 5,
                Price = 19.99m,
                ProductName = "Bag o' Treats"
            }.ToJson();

            var request = new HttpRequestMessage(HttpMethod.Post, $"http://contoso.com/orders/{order.Id}/additem")
            {
                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 Configuration.Current.Repository<Order>().GetLatest(order.Id);

            updatedOrder.Items.Count.Should().Be(0);
        }
        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 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 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);
        }
示例#9
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);
        }
示例#10
0
        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 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 async Task ApplyBatch_can_accept_an_array_of_commands()
        {
            var order = new Order().SavedToEventStore();

            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, $"http://contoso.com/orders/{order.Id}")
            {
                Content = new StringContent(json, Encoding.UTF8, "application/json")
            };

            var response = await client.SendAsync(request);
            response.ShouldSucceed();

            order = await Configuration.Current.Repository<Order>().GetLatest(order.Id);

            order.Items.Count.Should().Be(2);
            order.Balance.Should().Be(3);
        }
        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_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 Save(order);

            var command = new AddItem
            {
                ProductName = Any.Word(),
                Price = 10m,
                ETag = Any.Guid().ToString()
            };

            // act
            var scheduledCommand = await Schedule(order.Id, command, Clock.Now().AddDays(1));

            var deliverer = Configuration
                .Current
                .CommandDeliverer<Order>();

            await deliverer.Deliver(scheduledCommand);
            await deliverer.Deliver(scheduledCommand);
    
            // assert
            order = await Get<Order>(order.Id);

            order.Balance.Should().Be(10);

            using (var db = 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 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 override async Task A_command_handler_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 Save(order);

            var result = await AdvanceClock(11.Days());

            // the command should fail validation 
            result.FailedCommands.Count().Should().Be(1);

            result = await AdvanceClock(1.Hours());

            result.FailedCommands.Count().Should().Be(0);
        }
        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 UseDependencies_can_be_used_to_set_dependencies_using_an_application_owned_container()
        {
            var applicationsContainer = new PocketContainer()
                .Register<IPaymentService>(_ => new CreditCardPaymentGateway(chargeLimit: 1));

            var configuration = new Configuration()
                .UseDependencies(type =>
                {
                    if (applicationsContainer.Any(reg => reg.Key == type))
                    {
                        return () => applicationsContainer.Resolve(type);
                    }

                    return null;
                });

            using (ConfigurationContext.Establish(configuration))
            {
                var order = new Order(new CreateOrder(Any.FullName()))
                    .Apply(new AddItem
                    {
                        Price = 5m,
                        ProductName = Any.Word()
                    })
                    .Apply(new Ship())
                    .Apply(new ChargeAccount
                    {
                        AccountNumber = Any.PositiveInt().ToString()
                    });

                order.Events()
                     .Last()
                     .Should()
                     .BeOfType<Order.PaymentConfirmed>();
            }
        }
        public override 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.CommandScheduler<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 override async Task A_command_handler_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 override async Task When_a_command_is_scheduled_but_the_target_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 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);
            }
        }
示例#24
0
        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 async Task Posting_a_command_that_causes_a_concurrency_error_returns_409_Conflict()
        {
            var order = new Order(Guid.NewGuid())
                .Apply(new ChangeCustomerInfo { CustomerName = "Joe" })
                .SavedToEventStore();

            var testApi = new TestApi<Order>();
            var repository = new Mock<IEventSourcedRepository<Order>>();
            repository.Setup(r => r.GetLatest(It.IsAny<Guid>()))
                      .Returns(Task.FromResult(order));
            repository.Setup(r => r.Save(It.IsAny<Order>()))
                      .Throws(new ConcurrencyException("oops!", new IEvent[0], new Exception("inner oops")));
            Configuration.Current.UseDependency(c => repository.Object);

            var client = testApi.GetClient();

            var response = await client.PostAsJsonAsync($"http://contoso.com/orders/{order.Id}/additem", new { Price = 3m, ProductName = Any.Word() });

            response.ShouldFailWith(HttpStatusCode.Conflict);
        }
        public async Task Posting_unauthorized_command_JSON_returns_403_Forbidden()
        {
            var order = new Order(Guid.NewGuid())
                .Apply(new ChangeCustomerInfo { CustomerName = "Joe" })
                .Apply(new Deliver())
                .SavedToEventStore();

            Command<Order>.AuthorizeDefault = (o, command) => false;
            var request = new HttpRequestMessage(HttpMethod.Post, $"http://contoso.com/orders/{order.Id}/cancel")
            {
                Content = new StringContent(new Cancel().ToJson(), Encoding.UTF8, "application/json")
            };

            var client = new TestApi<Order>().GetClient();

            var response = await client.SendAsync(request);

            response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
        }
        public async Task An_ETag_header_is_applied_to_the_command()
        {
            var order = new Order(Guid.NewGuid())
                .Apply(new ChangeCustomerInfo { CustomerName = "Joe" })
                .SavedToEventStore();

            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, $"http://contoso.com/orders/{order.Id}/additem")
                {
                    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 Configuration.Current.Repository<Order>().GetLatest(order.Id);
            updatedOrder.Items.Single().Quantity.Should().Be(5);
        }