Idempotent updates based on requestid

This commit is contained in:
etomas 2017-03-03 12:03:31 +01:00
parent dba93514e8
commit f9b15481d1
27 changed files with 536 additions and 65 deletions

@ -0,0 +1,9 @@
version: '2'
services:
sql.data:
environment:
- SA_PASSWORD=Pass@word
- ACCEPT_EULA=Y
ports:
- "5433:1433"

@ -0,0 +1,10 @@
version: '2'
services:
sql.data:
image: microsoft/mssql-server-linux
basket.data:
image: redis
ports:
- "6379:6379"

@ -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

@ -49,7 +49,7 @@
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0-msbuild3-final" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
</ItemGroup>
<ItemGroup>

@ -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();

@ -64,7 +64,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands
_orderItems = new List<OrderItemDTO>();
}
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()
{

@ -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<CreateOrderCommand, bool>
{
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<CreateOrderCommand, bool>
{

@ -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<T, R> : IAsyncRequest<R>
where T : IAsyncRequest<R>
{
public T Command { get; }
public Guid Id { get; }
public IdentifiedCommand(T command, Guid id)
{
Command = command;
Id = id;
}
}
}

@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">Type of the command handler that performs the operation if request is not duplicated</typeparam>
/// <typeparam name="R">Return value of the inner command handler</typeparam>
public class IdentifierCommandHandler<T, R> : IAsyncRequestHandler<IdentifiedCommand<T, R>, R>
where T : IAsyncRequest<R>
{
private readonly IMediator _mediator;
private readonly IRequestManager _requestManager;
public IdentifierCommandHandler(IMediator mediator, IRequestManager requestManager)
{
_mediator = mediator;
_requestManager = requestManager;
}
/// <summary>
/// Creates the result value to return if a previous request was found
/// </summary>
/// <returns></returns>
protected virtual R CreateResultForDuplicateRequest()
{
return default(R);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="message">IdentifiedCommand which contains both original command & request ID</param>
/// <returns>Return value of inner command or default value if request same ID was found</returns>
public async Task<R> Handle(IdentifiedCommand<T, R> message)
{
var alreadyExists = await _requestManager.ExistAsync(message.Id);
if (alreadyExists)
{
return CreateResultForDuplicateRequest();
}
else
{
await _requestManager.CreateRequestForCommandAsync<T>(message.Id);
var result = await _mediator.SendAsync(message.Command);
return result;
}
}
}
}

@ -28,9 +28,14 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
[Route("new")]
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody]CreateOrderCommand createOrderCommand)
public async Task<IActionResult> 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, bool>(createOrderCommand, guid);
result = await _mediator.SendAsync(requestCreateOrder);
}
if (result)
{
return Ok();

@ -33,6 +33,10 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Autof
builder.RegisterType<OrderRepository>()
.As<IOrderRepository<Order>>()
.InstancePerLifetimeScope();
builder.RegisterType<RequestManager>()
.As<IRequestManager>()
.InstancePerLifetimeScope();
}
}
}

@ -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<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:HiLoSequenceName", "buyerseq")
.HasAnnotation("SqlServer:HiLoSequenceSchema", "ordering")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo);
b.Property<string>("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<int>("Id")
.HasDefaultValue(1);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200);
b.HasKey("Id");
b.ToTable("cardtypes","ordering");
});
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:HiLoSequenceName", "paymentseq")
.HasAnnotation("SqlServer:HiLoSequenceSchema", "ordering")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo);
b.Property<string>("Alias")
.IsRequired()
.HasMaxLength(200);
b.Property<int>("BuyerId");
b.Property<string>("CardHolderName")
.IsRequired()
.HasMaxLength(200);
b.Property<string>("CardNumber")
.IsRequired()
.HasMaxLength(25);
b.Property<int>("CardTypeId");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("City");
b.Property<string>("Country");
b.Property<string>("State");
b.Property<string>("Street");
b.Property<string>("ZipCode");
b.HasKey("Id");
b.ToTable("address","ordering");
});
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:HiLoSequenceName", "orderseq")
.HasAnnotation("SqlServer:HiLoSequenceSchema", "ordering")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo);
b.Property<int?>("AddressId");
b.Property<int>("BuyerId");
b.Property<DateTime>("OrderDate");
b.Property<int>("OrderStatusId");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:HiLoSequenceName", "orderitemseq")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo);
b.Property<decimal>("Discount");
b.Property<int>("OrderId");
b.Property<string>("PictureUrl");
b.Property<int>("ProductId");
b.Property<string>("ProductName")
.IsRequired();
b.Property<decimal>("UnitPrice");
b.Property<int>("Units");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("orderItems","ordering");
});
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus", b =>
{
b.Property<int>("Id")
.HasDefaultValue(1);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200);
b.HasKey("Id");
b.ToTable("orderstatus","ordering");
});
modelBuilder.Entity("Ordering.Infrastructure.ClientRequest", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Name")
.IsRequired();
b.Property<DateTime>("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);
});
}
}
}

@ -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<Guid>(nullable: false),
Name = table.Column<string>(nullable: false),
Time = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_requests", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "requests",
schema: "ordering");
}
}
}

@ -183,6 +183,21 @@ namespace Ordering.API.Migrations
b.ToTable("orderstatus","ordering");
});
modelBuilder.Entity("Ordering.Infrastructure.ClientRequest", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Name")
.IsRequired();
b.Property<DateTime>("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")

@ -51,10 +51,11 @@
<PackageReference Include="System.Reflection" Version="4.3.0" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.0.1-rc3" />
<PackageReference Include="Dapper" Version="1.50.2" />
<PackageReference Include="System.ValueTuple" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.1.0-preview4-final" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
</ItemGroup>
<ItemGroup>

@ -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<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IIdentityService,IdentityService>();
services.AddTransient<IIdentityService, IdentityService>();
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());
}

@ -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;
}

@ -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; }
}
}

@ -30,6 +30,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ClientRequest>(ConfigureRequests);
modelBuilder.Entity<Address>(ConfigureAddress);
modelBuilder.Entity<PaymentMethod>(ConfigurePayment);
modelBuilder.Entity<Order>(ConfigureOrder);
@ -39,6 +41,14 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
modelBuilder.Entity<Buyer>(ConfigureBuyer);
}
private void ConfigureRequests(EntityTypeBuilder<ClientRequest> 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<Address> addressConfiguration)
{
addressConfiguration.ToTable("address", DEFAULT_SCHEMA);

@ -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<bool> ExistAsync(Guid id);
Task CreateRequestForCommandAsync<T>(Guid id);
}
}

@ -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<bool> ExistAsync(Guid id)
{
var request = await _context.FindAsync<ClientRequest>(id);
return request != null;
}
public async Task CreateRequestForCommandAsync<T>(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();
}
}
}

@ -55,6 +55,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Models
public List<OrderItem> OrderItems { get; }
[Required]
public Guid RequestId { get; set; }
public void CardExpirationShortFormat()
{
CardExpirationShort = CardExpiration.ToString("MM/yy");

@ -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)

@ -89,6 +89,7 @@
</div>
</section>
<input asp-for="ZipCode" type="hidden" />
<input asp-for="RequestId" type="hidden" value="@Guid.NewGuid().ToString()"/>
</form>
</div>

@ -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);
});
}
}

@ -44,7 +44,7 @@ export class OrdersService {
}
postOrder(item): Observable<boolean> {
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;
});
}

@ -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<Response> {
return this.doPost(url, data, true, params);
}
post(url: string, data: any, params?: any): Observable<Response> {
return this.doPost(url, data, false, params);
}
private doPost(url: string, data: any, needId: boolean, params?: any): Observable<Response> {
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) => {