From f9b15481d1a88c5dbfca21a3b9d1c199c750b422 Mon Sep 17 00:00:00 2001 From: etomas Date: Fri, 3 Mar 2017 12:03:31 +0100 Subject: [PATCH] Idempotent updates based on requestid --- docker-compose-external.override.yml | 9 + docker-compose-external.yml | 10 + eShopOnContainers-ServicesAndWebApps.sln | 50 ---- .../Catalog/Catalog.API/Catalog.API.csproj | 2 +- src/Services/Catalog/Catalog.API/Startup.cs | 3 +- .../Commands/CreateOrderCommand.cs | 2 +- .../Commands/CreateOrderCommandHandler.cs | 14 + .../Application/Commands/IdentifiedCommand.cs | 20 ++ .../Commands/IdentifierCommandHandler.cs | 60 +++++ .../Controllers/OrdersController.cs | 9 +- .../AutofacModules/ApplicationModule.cs | 4 + .../20170303085729_RequestsTable.Designer.cs | 245 ++++++++++++++++++ .../20170303085729_RequestsTable.cs | 33 +++ .../OrderingContextModelSnapshot.cs | 15 ++ .../Ordering/Ordering.API/Ordering.API.csproj | 3 +- src/Services/Ordering/Ordering.API/Startup.cs | 8 +- .../AggregatesModel/OrderAggregate/Order.cs | 2 +- .../Ordering.Infrastructure/ClientRequest.cs | 13 + .../OrderingContext.cs | 10 + .../Repositories/IRequestManager.cs | 13 + .../Repositories/RequestManager.cs | 42 +++ src/Web/WebMVC/Models/Order.cs | 4 + src/Web/WebMVC/Services/OrderingService.cs | 4 +- src/Web/WebMVC/Views/Order/Create.cshtml | 1 + src/Web/WebSPA/Client/guid.ts | 8 + .../Client/modules/orders/orders.service.ts | 2 +- .../modules/shared/services/data.service.ts | 15 +- 27 files changed, 536 insertions(+), 65 deletions(-) create mode 100644 docker-compose-external.override.yml create mode 100644 docker-compose-external.yml create mode 100644 src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommand.cs create mode 100644 src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs create mode 100644 src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.Designer.cs create mode 100644 src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.cs create mode 100644 src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs create mode 100644 src/Services/Ordering/Ordering.Infrastructure/Repositories/IRequestManager.cs create mode 100644 src/Services/Ordering/Ordering.Infrastructure/Repositories/RequestManager.cs create mode 100644 src/Web/WebSPA/Client/guid.ts diff --git a/docker-compose-external.override.yml b/docker-compose-external.override.yml new file mode 100644 index 000000000..f20440ad7 --- /dev/null +++ b/docker-compose-external.override.yml @@ -0,0 +1,9 @@ +version: '2' + +services: + sql.data: + environment: + - SA_PASSWORD=Pass@word + - ACCEPT_EULA=Y + ports: + - "5433:1433" \ No newline at end of file diff --git a/docker-compose-external.yml b/docker-compose-external.yml new file mode 100644 index 000000000..6bce32a3e --- /dev/null +++ b/docker-compose-external.yml @@ -0,0 +1,10 @@ +version: '2' + +services: + sql.data: + image: microsoft/mssql-server-linux + + basket.data: + image: redis + ports: + - "6379:6379" diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln index 0f959d467..c92dc8c7d 100644 --- a/eShopOnContainers-ServicesAndWebApps.sln +++ b/eShopOnContainers-ServicesAndWebApps.sln @@ -48,8 +48,6 @@ Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-co EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSPA", "src\Web\WebSPA\WebSPA.csproj", "{F16E3C6A-1C94-4EAB-BE91-099618060B68}" EndProject -Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose-windows", "docker-compose-windows.dcproj", "{2EDE831A-98F5-4F23-B2DB-7E9DC935766A}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -496,54 +494,6 @@ Global {F16E3C6A-1C94-4EAB-BE91-099618060B68}.Release|x64.Build.0 = Release|Any CPU {F16E3C6A-1C94-4EAB-BE91-099618060B68}.Release|x86.ActiveCfg = Release|Any CPU {F16E3C6A-1C94-4EAB-BE91-099618060B68}.Release|x86.Build.0 = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|ARM.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|iPhone.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|x64.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|x64.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|x86.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.AppStore|x86.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|ARM.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|ARM.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|iPhone.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|x64.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|x64.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|x86.ActiveCfg = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Debug|x86.Build.0 = Debug|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|Any CPU.Build.0 = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|ARM.ActiveCfg = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|ARM.Build.0 = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|iPhone.ActiveCfg = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|iPhone.Build.0 = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|x64.ActiveCfg = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|x64.Build.0 = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|x86.ActiveCfg = Release|Any CPU - {2EDE831A-98F5-4F23-B2DB-7E9DC935766A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Services/Catalog/Catalog.API/Catalog.API.csproj b/src/Services/Catalog/Catalog.API/Catalog.API.csproj index 15be6a8c2..6811e43cb 100644 --- a/src/Services/Catalog/Catalog.API/Catalog.API.csproj +++ b/src/Services/Catalog/Catalog.API/Catalog.API.csproj @@ -49,7 +49,7 @@ - + diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index 38bea3ee3..2b44d0183 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using System; using System.IO; + using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -28,7 +29,7 @@ if (env.IsDevelopment()) { - builder.AddUserSecrets(); + builder.AddUserSecrets(typeof(Startup).GetTypeInfo().Assembly); } builder.AddEnvironmentVariables(); diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs index 3bcb2425b..5633c3b37 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs @@ -64,7 +64,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands _orderItems = new List(); } - public CreateOrderCommand(string city, string street, string state, string country, string zipcode, + public CreateOrderCommand(string city, string street, string state, string country, string zipcode, string cardNumber, string cardHolderName, DateTime cardExpiration, string cardSecurityNumber, int cardTypeId) : this() { diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs index a73378735..bf8e90ea6 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs @@ -4,9 +4,23 @@ using Domain.AggregatesModel.OrderAggregate; using MediatR; using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; + using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories; using System; using System.Threading.Tasks; + + public class CreateOrderCommandIdentifiedHandler : IdentifierCommandHandler + { + public CreateOrderCommandIdentifiedHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) + { + } + + protected override bool CreateResultForDuplicateRequest() + { + return true; // Ignore duplicate requests for creating order. + } + } + public class CreateOrderCommandHandler : IAsyncRequestHandler { diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommand.cs new file mode 100644 index 000000000..823e10b69 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommand.cs @@ -0,0 +1,20 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands +{ + public class IdentifiedCommand : IAsyncRequest + where T : IAsyncRequest + { + public T Command { get; } + public Guid Id { get; } + public IdentifiedCommand(T command, Guid id) + { + Command = command; + Id = id; + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs new file mode 100644 index 000000000..5fdc0181b --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs @@ -0,0 +1,60 @@ +using MediatR; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands +{ + /// + /// Provides a base implementation for handling duplicate request and ensuring idempotent updates, in the cases where + /// a requestid sent by client is used to detect duplicate requests. + /// + /// Type of the command handler that performs the operation if request is not duplicated + /// Return value of the inner command handler + public class IdentifierCommandHandler : IAsyncRequestHandler, R> + where T : IAsyncRequest + { + private readonly IMediator _mediator; + private readonly IRequestManager _requestManager; + + public IdentifierCommandHandler(IMediator mediator, IRequestManager requestManager) + { + _mediator = mediator; + _requestManager = requestManager; + } + + /// + /// Creates the result value to return if a previous request was found + /// + /// + protected virtual R CreateResultForDuplicateRequest() + { + return default(R); + } + + /// + /// This method handles the command. It just ensures that no other request exists with the same ID, and if this is the case + /// just enqueues the original inner command. + /// + /// IdentifiedCommand which contains both original command & request ID + /// Return value of inner command or default value if request same ID was found + public async Task Handle(IdentifiedCommand message) + { + var alreadyExists = await _requestManager.ExistAsync(message.Id); + if (alreadyExists) + { + return CreateResultForDuplicateRequest(); + } + else + { + await _requestManager.CreateRequestForCommandAsync(message.Id); + var result = await _mediator.SendAsync(message.Command); + return result; + } + } + } + + +} diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index 4944617bd..df2dc23f7 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -28,9 +28,14 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers [Route("new")] [HttpPost] - public async Task CreateOrder([FromBody]CreateOrderCommand createOrderCommand) + public async Task CreateOrder([FromBody]CreateOrderCommand createOrderCommand, [FromHeader(Name = "x-requestid")] string requestId) { - var result = await _mediator.SendAsync(createOrderCommand); + bool result = false; + if (Guid.TryParse(requestId, out Guid guid) && guid != Guid.Empty) + { + var requestCreateOrder = new IdentifiedCommand(createOrderCommand, guid); + result = await _mediator.SendAsync(requestCreateOrder); + } if (result) { return Ok(); diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/ApplicationModule.cs b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/ApplicationModule.cs index a953c2d2d..0d8e34476 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/ApplicationModule.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/ApplicationModule.cs @@ -33,6 +33,10 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Autof builder.RegisterType() .As>() .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); } } } diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.Designer.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.Designer.cs new file mode 100644 index 000000000..84d560ddc --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.Designer.cs @@ -0,0 +1,245 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; + +namespace Ordering.API.Migrations +{ + [DbContext(typeof(OrderingContext))] + [Migration("20170303085729_RequestsTable")] + partial class RequestsTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.0-rtm-22752") + .HasAnnotation("SqlServer:Sequence:.orderitemseq", "'orderitemseq', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:ordering.buyerseq", "'buyerseq', 'ordering', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:ordering.orderseq", "'orderseq', 'ordering', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:ordering.paymentseq", "'paymentseq', 'ordering', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "buyerseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "ordering") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("IdentityGuid") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("IdentityGuid") + .IsUnique(); + + b.ToTable("buyers","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("cardtypes","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "paymentseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "ordering") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Alias") + .IsRequired() + .HasMaxLength(200); + + b.Property("BuyerId"); + + b.Property("CardHolderName") + .IsRequired() + .HasMaxLength(200); + + b.Property("CardNumber") + .IsRequired() + .HasMaxLength(25); + + b.Property("CardTypeId"); + + b.Property("Expiration"); + + b.HasKey("Id"); + + b.HasIndex("BuyerId"); + + b.HasIndex("CardTypeId"); + + b.ToTable("paymentmethods","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("City"); + + b.Property("Country"); + + b.Property("State"); + + b.Property("Street"); + + b.Property("ZipCode"); + + b.HasKey("Id"); + + b.ToTable("address","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "orderseq") + .HasAnnotation("SqlServer:HiLoSequenceSchema", "ordering") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("AddressId"); + + b.Property("BuyerId"); + + b.Property("OrderDate"); + + b.Property("OrderStatusId"); + + b.Property("PaymentMethodId"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("BuyerId"); + + b.HasIndex("OrderStatusId"); + + b.HasIndex("PaymentMethodId"); + + b.ToTable("orders","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "orderitemseq") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Discount"); + + b.Property("OrderId"); + + b.Property("PictureUrl"); + + b.Property("ProductId"); + + b.Property("ProductName") + .IsRequired(); + + b.Property("UnitPrice"); + + b.Property("Units"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("orderItems","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus", b => + { + b.Property("Id") + .HasDefaultValue(1); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.ToTable("orderstatus","ordering"); + }); + + modelBuilder.Entity("Ordering.Infrastructure.ClientRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name") + .IsRequired(); + + b.Property("Time"); + + b.HasKey("Id"); + + b.ToTable("requests","ordering"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer") + .WithMany("PaymentMethods") + .HasForeignKey("BuyerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType", "CardType") + .WithMany() + .HasForeignKey("CardTypeId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Order", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Address", "Address") + .WithMany() + .HasForeignKey("AddressId"); + + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer", "Buyer") + .WithMany() + .HasForeignKey("BuyerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus", "OrderStatus") + .WithMany() + .HasForeignKey("OrderStatusId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod", "PaymentMethod") + .WithMany() + .HasForeignKey("PaymentMethodId"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.cs new file mode 100644 index 000000000..5e5918c88 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Ordering.API.Migrations +{ + public partial class RequestsTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "requests", + schema: "ordering", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(nullable: false), + Time = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_requests", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "requests", + schema: "ordering"); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/OrderingContextModelSnapshot.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/OrderingContextModelSnapshot.cs index 5c7b6d25b..654f93d6d 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/OrderingContextModelSnapshot.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/OrderingContextModelSnapshot.cs @@ -183,6 +183,21 @@ namespace Ordering.API.Migrations b.ToTable("orderstatus","ordering"); }); + modelBuilder.Entity("Ordering.Infrastructure.ClientRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name") + .IsRequired(); + + b.Property("Time"); + + b.HasKey("Id"); + + b.ToTable("requests","ordering"); + }); + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod", b => { b.HasOne("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer") diff --git a/src/Services/Ordering/Ordering.API/Ordering.API.csproj b/src/Services/Ordering/Ordering.API/Ordering.API.csproj index d83aed609..ce710c6a0 100644 --- a/src/Services/Ordering/Ordering.API/Ordering.API.csproj +++ b/src/Services/Ordering/Ordering.API/Ordering.API.csproj @@ -51,10 +51,11 @@ + - + diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index 539647514..0f75b6370 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -29,7 +29,7 @@ if (env.IsDevelopment()) { - builder.AddUserSecrets(); + builder.AddUserSecrets(typeof(Startup).GetTypeInfo().Assembly); } builder.AddEnvironmentVariables(); @@ -67,7 +67,7 @@ Title = "Ordering HTTP API", Version = "v1", Description = "The Ordering Service HTTP API", - TermsOfService = "Terms Of Service" + TermsOfService = "Terms Of Service" }); }); @@ -82,7 +82,7 @@ // Add application services. services.AddSingleton(); - services.AddTransient(); + services.AddTransient(); services.AddOptions(); @@ -92,7 +92,7 @@ container.Populate(services); container.RegisterModule(new MediatorModule()); - container.RegisterModule(new ApplicationModule(Configuration["ConnectionString"] )); + container.RegisterModule(new ApplicationModule(Configuration["ConnectionString"])); return new AutofacServiceProvider(container.Build()); } diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs index 658f18967..fecc53d44 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -22,6 +22,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O public OrderStatus OrderStatus { get; private set; } private int _orderStatusId; + // DDD Patterns comment // Using a private collection field, better for DDD Aggregate's encapsulation // so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection, @@ -46,7 +47,6 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O _paymentMethodId = paymentMethodId; _orderStatusId = OrderStatus.InProcess.Id; _orderDate = DateTime.UtcNow; - Address = address; } diff --git a/src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs b/src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs new file mode 100644 index 000000000..47a401aab --- /dev/null +++ b/src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure +{ + public class ClientRequest + { + public Guid Id { get; set; } + public string Name { get; set; } + public DateTime Time { get; set; } + } +} diff --git a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs index 73d831eaf..7a9d192e4 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs @@ -30,6 +30,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure protected override void OnModelCreating(ModelBuilder modelBuilder) { + + modelBuilder.Entity(ConfigureRequests); modelBuilder.Entity
(ConfigureAddress); modelBuilder.Entity(ConfigurePayment); modelBuilder.Entity(ConfigureOrder); @@ -39,6 +41,14 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure modelBuilder.Entity(ConfigureBuyer); } + private void ConfigureRequests(EntityTypeBuilder requestConfiguration) + { + requestConfiguration.ToTable("requests", DEFAULT_SCHEMA); + requestConfiguration.HasKey(cr => cr.Id); + requestConfiguration.Property(cr => cr.Name).IsRequired(); + requestConfiguration.Property(cr => cr.Time).IsRequired(); + } + void ConfigureAddress(EntityTypeBuilder
addressConfiguration) { addressConfiguration.ToTable("address", DEFAULT_SCHEMA); diff --git a/src/Services/Ordering/Ordering.Infrastructure/Repositories/IRequestManager.cs b/src/Services/Ordering/Ordering.Infrastructure/Repositories/IRequestManager.cs new file mode 100644 index 000000000..ecb144695 --- /dev/null +++ b/src/Services/Ordering/Ordering.Infrastructure/Repositories/IRequestManager.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories +{ + public interface IRequestManager + { + Task ExistAsync(Guid id); + Task CreateRequestForCommandAsync(Guid id); + } +} diff --git a/src/Services/Ordering/Ordering.Infrastructure/Repositories/RequestManager.cs b/src/Services/Ordering/Ordering.Infrastructure/Repositories/RequestManager.cs new file mode 100644 index 000000000..1661ab6e5 --- /dev/null +++ b/src/Services/Ordering/Ordering.Infrastructure/Repositories/RequestManager.cs @@ -0,0 +1,42 @@ +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories +{ + public class RequestManager : IRequestManager + { + private readonly OrderingContext _context; + public RequestManager(OrderingContext ctx) + { + _context = ctx; + } + + + public async Task ExistAsync(Guid id) + { + var request = await _context.FindAsync(id); + return request != null; + } + + public async Task CreateRequestForCommandAsync(Guid id) + { + + var exists = await ExistAsync(id); + var request = exists ? + throw new Exception($"Request with {id} already exists") : + new ClientRequest() + { + Id = id, + Name = typeof(T).Name, + Time = DateTime.UtcNow + }; + + _context.Add(request); + await _context.SaveChangesAsync(); + } + + } +} diff --git a/src/Web/WebMVC/Models/Order.cs b/src/Web/WebMVC/Models/Order.cs index cfcae8a36..bddc740e3 100644 --- a/src/Web/WebMVC/Models/Order.cs +++ b/src/Web/WebMVC/Models/Order.cs @@ -55,6 +55,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Models public List OrderItems { get; } + [Required] + public Guid RequestId { get; set; } + + public void CardExpirationShortFormat() { CardExpirationShort = CardExpiration.ToString("MM/yy"); diff --git a/src/Web/WebMVC/Services/OrderingService.cs b/src/Web/WebMVC/Services/OrderingService.cs index 652ee18b0..4c816203c 100644 --- a/src/Web/WebMVC/Services/OrderingService.cs +++ b/src/Web/WebMVC/Services/OrderingService.cs @@ -79,14 +79,14 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services _apiClient = new HttpClient(); _apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); - + _apiClient.DefaultRequestHeaders.Add("x-requestid", order.RequestId.ToString()); var ordersUrl = $"{_remoteServiceBaseUrl}/new"; order.CardTypeId = 1; order.CardExpirationApiFormat(); SetFakeIdToProducts(order); StringContent content = new StringContent(JsonConvert.SerializeObject(order), System.Text.Encoding.UTF8, "application/json"); - + var response = await _apiClient.PostAsync(ordersUrl, content); if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) diff --git a/src/Web/WebMVC/Views/Order/Create.cshtml b/src/Web/WebMVC/Views/Order/Create.cshtml index f35262ccd..1aab06505 100644 --- a/src/Web/WebMVC/Views/Order/Create.cshtml +++ b/src/Web/WebMVC/Views/Order/Create.cshtml @@ -89,6 +89,7 @@ + diff --git a/src/Web/WebSPA/Client/guid.ts b/src/Web/WebSPA/Client/guid.ts new file mode 100644 index 000000000..1904902cb --- /dev/null +++ b/src/Web/WebSPA/Client/guid.ts @@ -0,0 +1,8 @@ +export class Guid { + static newGuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } +} diff --git a/src/Web/WebSPA/Client/modules/orders/orders.service.ts b/src/Web/WebSPA/Client/modules/orders/orders.service.ts index 9a7b9a34d..24991056a 100644 --- a/src/Web/WebSPA/Client/modules/orders/orders.service.ts +++ b/src/Web/WebSPA/Client/modules/orders/orders.service.ts @@ -44,7 +44,7 @@ export class OrdersService { } postOrder(item): Observable { - return this.service.post(this.ordersUrl + '/api/v1/orders/new', item).map((response: Response) => { + return this.service.postWithId(this.ordersUrl + '/api/v1/orders/new', item).map((response: Response) => { return true; }); } diff --git a/src/Web/WebSPA/Client/modules/shared/services/data.service.ts b/src/Web/WebSPA/Client/modules/shared/services/data.service.ts index 975086f70..785728943 100644 --- a/src/Web/WebSPA/Client/modules/shared/services/data.service.ts +++ b/src/Web/WebSPA/Client/modules/shared/services/data.service.ts @@ -9,6 +9,7 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/operator/catch'; import { SecurityService } from './security.service'; +import { Guid } from '../../../guid'; @Injectable() export class DataService { @@ -28,13 +29,25 @@ export class DataService { }).catch(this.handleError); } + postWithId(url: string, data: any, params?: any): Observable { + return this.doPost(url, data, true, params); + } + post(url: string, data: any, params?: any): Observable { + return this.doPost(url, data, false, params); + } + + private doPost(url: string, data: any, needId: boolean, params?: any): Observable { let options: RequestOptionsArgs = {}; + options.headers = new Headers(); if (this.securityService) { - options.headers = new Headers(); options.headers.append('Authorization', 'Bearer ' + this.securityService.GetToken()); } + if (needId) { + let guid = Guid.newGuid(); + options.headers.append('x-requestid', guid); + } return this.http.post(url, data, options).map( (res: Response) => {