diff --git a/_docker/rabbitmq/Dockerfile.nanowin b/_docker/rabbitmq/Dockerfile.nanowin new file mode 100644 index 000000000..26474c235 --- /dev/null +++ b/_docker/rabbitmq/Dockerfile.nanowin @@ -0,0 +1,29 @@ +#https://github.com/spring2/dockerfiles/tree/master/rabbitmq + +FROM microsoft/windowsservercore + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +ENV chocolateyUseWindowsCompression false + +RUN iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1')); \ + choco install -y curl; + +RUN choco install -y erlang +ENV ERLANG_SERVICE_MANAGER_PATH="C:\Program Files\erl8.2\erts-8.2\bin" +RUN choco install -y rabbitmq +ENV RABBITMQ_SERVER="C:\Program Files\RabbitMQ Server\rabbitmq_server-3.6.5" + +ENV RABBITMQ_CONFIG_FILE="c:\rabbitmq" +COPY rabbitmq.config C:/ +COPY rabbitmq.config C:/Users/ContainerAdministrator/AppData/Roaming/RabbitMQ/ +COPY enabled_plugins C:/Users/ContainerAdministrator/AppData/Roaming/RabbitMQ/ + + +EXPOSE 4369 +EXPOSE 5672 +EXPOSE 5671 +EXPOSE 15672 + +WORKDIR C:/Program\ Files/RabbitMQ\ Server/rabbitmq_server-3.6.5/sbin +CMD .\rabbitmq-server.bat \ No newline at end of file diff --git a/_docker/rabbitmq/enabled_plugins b/_docker/rabbitmq/enabled_plugins new file mode 100644 index 000000000..9eafc419b --- /dev/null +++ b/_docker/rabbitmq/enabled_plugins @@ -0,0 +1 @@ +[rabbitmq_amqp1_0,rabbitmq_management]. diff --git a/_docker/rabbitmq/rabbitmq.config b/_docker/rabbitmq/rabbitmq.config new file mode 100644 index 000000000..f7837f213 --- /dev/null +++ b/_docker/rabbitmq/rabbitmq.config @@ -0,0 +1 @@ +[{rabbit, [{loopback_users, []}]}]. \ No newline at end of file diff --git a/docker-compose-windows.override.yml b/docker-compose-windows.override.yml new file mode 100644 index 000000000..c85864d72 --- /dev/null +++ b/docker-compose-windows.override.yml @@ -0,0 +1,79 @@ +version: '2.1' + +# The default docker-compose.override file can use the "localhost" as the external name for testing web apps within the same dev machine. +# The ESHOP_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: +# ESHOP_EXTERNAL_DNS_NAME_OR_IP=localhost +# but values present in the environment vars at runtime will always override those defined inside the .env file +# An external IP or DNS name has to be used (instead localhost and the 10.0.75.1 IP) when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. + +services: + + basket.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:5103 + - ConnectionString=basket.data + - identityUrl=http://identity.api:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + - EventBusConnection=rabbitmq + ports: + - "5103:5103" + + catalog.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:5101 + - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word + - ExternalCatalogBaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + - EventBusConnection=rabbitmq + ports: + - "5101:5101" + + identity.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:5105 + - SpaClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5104 + - ConnectionStrings__DefaultConnection=Server=sql.data;Database=Microsoft.eShopOnContainers.Service.IdentityDb;User Id=sa;Password=Pass@word + - MvcClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5100 #Local: You need to open your local dev-machine firewall at range 5100-5105. + ports: + - "5105:5105" + + ordering.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:5102 + - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word + - identityUrl=http://identity.api:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + - EventBusConnection=rabbitmq + ports: + - "5102:5102" + + webspa: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:5104 + - CatalogUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 + - OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 + - IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + - BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 + ports: + - "5104:5104" + + webmvc: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:5100 + - CatalogUrl=http://catalog.api:5101 + - OrderingUrl=http://ordering.api:5102 + - BasketUrl=http://basket.api:5103 + - IdentityUrl=http://10.0.75.1:5105 #Local: Use 10.0.75.1 in a "Docker for Windows" environment, if using "localhost" from browser. + #Remote: Use ${ESHOP_EXTERNAL_DNS_NAME_OR_IP} if using external IP or DNS name from browser. + ports: + - "5100:5100" + + sql.data: + environment: + - SA_PASSWORD=Pass@word + - ACCEPT_EULA=Y + ports: + - "5433:1433" \ No newline at end of file diff --git a/docker-compose-windows.prod.yml b/docker-compose-windows.prod.yml new file mode 100644 index 000000000..8d00df4ca --- /dev/null +++ b/docker-compose-windows.prod.yml @@ -0,0 +1,80 @@ +version: '2.1' + +# The Production docker-compose file has to have the external/real IPs or DNS names for the services +# The ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: +# ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP=192.168.88.248 +# but values present in the environment vars at runtime will always override those defined inside the .env file +# An external IP or DNS name has to be used when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. +# +# Set ASPNETCORE_ENVIRONMENT=Development to get errors while testing. +# +# You need to start it with the following CLI command: +# docker-compose -f docker-compose-windows.yml -f docker-compose-windows.prod.yml up -d + +services: + + basket.api: + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=http://0.0.0.0:5103 + - ConnectionString=basket.data + - identityUrl=http://identity.api:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. + ports: + - "5103:5103" + + catalog.api: + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=http://0.0.0.0:5101 + - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word + - ExternalCatalogBaseUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. + ports: + - "5101:5101" + + identity.api: + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=http://0.0.0.0:5105 + - SpaClient=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5104 + - ConnectionStrings__DefaultConnection=Server=sql.data;Database=Microsoft.eShopOnContainers.Service.IdentityDb;User Id=sa;Password=Pass@word + - MvcClient=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5100 #Local: You need to open your host's firewall at range 5100-5105. + ports: + - "5105:5105" + + ordering.api: + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=http://0.0.0.0:5102 + - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word + - identityUrl=http://identity.api:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. + ports: + - "5102:5102" + + webspa: + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=http://0.0.0.0:5104 + - CatalogUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101 + - OrderingUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5102 + - IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. + - BasketUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5103 + ports: + - "5104:5104" + + webmvc: + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=http://0.0.0.0:5100 + - CatalogUrl=http://catalog.api:5101 + - OrderingUrl=http://ordering.api:5102 + - IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. + - BasketUrl=http://basket.api:5103 + ports: + - "5100:5100" + + sql.data: + environment: + - SA_PASSWORD=Pass@word + - ACCEPT_EULA=Y + ports: + - "5433:1433" \ No newline at end of file diff --git a/docker-compose-windows.yml b/docker-compose-windows.yml new file mode 100644 index 000000000..b8eaabfc8 --- /dev/null +++ b/docker-compose-windows.yml @@ -0,0 +1,80 @@ +version: '2.1' + +services: + basket.api: + image: eshop/basket.api + build: + context: ./src/Services/Basket/Basket.API + dockerfile: Dockerfile.nanowin + depends_on: + - basket.data + - identity.api + + catalog.api: + image: eshop/catalog.api + build: + context: ./src/Services/Catalog/Catalog.API + dockerfile: Dockerfile.nanowin + depends_on: + - sql.data + + identity.api: + image: eshop/identity.api + build: + context: ./src/Services/Identity/Identity.API + dockerfile: Dockerfile.nanowin + depends_on: + - sql.data + + ordering.api: + image: eshop/ordering.api + build: + context: ./src/Services/Ordering/Ordering.API + dockerfile: Dockerfile.nanowin + depends_on: + - sql.data + + webspa: + image: eshop/webspa + build: + context: ./src/Web/WebSPA + dockerfile: Dockerfile.nanowin + depends_on: + - identity.api + - basket.api + + webmvc: + image: eshop/webmvc + build: + context: ./src/Web/WebMVC + dockerfile: Dockerfile.nanowin + depends_on: + - catalog.api + - ordering.api + - identity.api + - basket.api + + sql.data: + image: microsoft/mssql-server-windows + + basket.data: + image: redis + build: + context: ./_docker/redis + dockerfile: Dockerfile.nanowin + ports: + - "6379:6379" + + rabbitmq: + image: rabbitmq + build: + context: ./_docker/rabbitmq + dockerfile: Dockerfile.nanowin + ports: + - "5672:5672" + +networks: + default: + external: + name: nat + diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 14d8114ea..e760fed47 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -8,7 +8,7 @@ version: '2' # # Set ASPNETCORE_ENVIRONMENT=Development to get errors while testing. # -# You need to start it with the following CLI command: +# You need to start it with the following CLI command: # docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d services: diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/IIntegrationEventLogService.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs similarity index 60% rename from src/Services/Catalog/Catalog.API/IntegrationEvents/IIntegrationEventLogService.cs rename to src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs index 423a0eb98..ed1f74616 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/IIntegrationEventLogService.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs @@ -1,14 +1,15 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Threading.Tasks; -namespace Catalog.API.IntegrationEvents +namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services { public interface IIntegrationEventLogService { - Task SaveEventAsync(IntegrationEvent @event); + Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction); Task MarkEventAsPublishedAsync(IntegrationEvent @event); } } diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/IntegrationEventLogService.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs similarity index 66% rename from src/Services/Catalog/Catalog.API/IntegrationEvents/IntegrationEventLogService.cs rename to src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs index e9859c5ae..bef74b452 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/IntegrationEventLogService.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/Services/IntegrationEventLogService.cs @@ -1,37 +1,39 @@ -using System; -using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System.Data.Common; using System.Linq; using System.Threading.Tasks; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; -using Microsoft.EntityFrameworkCore.Infrastructure; +using System; -namespace Catalog.API.IntegrationEvents +namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services { public class IntegrationEventLogService : IIntegrationEventLogService { private readonly IntegrationEventLogContext _integrationEventLogContext; - private readonly CatalogContext _catalogContext; + private readonly DbConnection _dbConnection; - public IntegrationEventLogService(CatalogContext catalogContext) + public IntegrationEventLogService(DbConnection dbConnection) { - _catalogContext = catalogContext; + _dbConnection = dbConnection?? throw new ArgumentNullException("dbConnection"); _integrationEventLogContext = new IntegrationEventLogContext( new DbContextOptionsBuilder() - .UseSqlServer(catalogContext.Database.GetDbConnection()) + .UseSqlServer(_dbConnection) .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)) .Options); } - public Task SaveEventAsync(IntegrationEvent @event) + public Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction) { + if(transaction == null) + { + throw new ArgumentNullException("transaction", $"A {typeof(DbTransaction).FullName} is required as a pre-requisite to save the event."); + } + var eventLogEntry = new IntegrationEventLogEntry(@event); - // as a constraint this transaction has to be done together with a catalogContext transaction - _integrationEventLogContext.Database.UseTransaction(_catalogContext.Database.CurrentTransaction.GetDbTransaction()); + _integrationEventLogContext.Database.UseTransaction(transaction); _integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry); return _integrationEventLogContext.SaveChangesAsync(); diff --git a/src/Services/Basket/Basket.API/Dockerfile.nanowin b/src/Services/Basket/Basket.API/Dockerfile.nanowin index 0541a26cc..9c664f4e4 100644 --- a/src/Services/Basket/Basket.API/Dockerfile.nanowin +++ b/src/Services/Basket/Basket.API/Dockerfile.nanowin @@ -1,4 +1,5 @@ FROM microsoft/dotnet:1.1-runtime-nanoserver +SHELL ["powershell"] ARG source WORKDIR /app RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs index 84d874271..28c9a9afa 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs @@ -20,11 +20,11 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Even foreach (var id in userIds) { var basket = await _repository.GetBasket(id); - await UpdateBasket(@event.ProductId, @event.NewPrice, basket); + await UpdatePriceInBasketItems(@event.ProductId, @event.NewPrice, basket); } } - private async Task UpdateBasket(int productId, decimal newPrice, CustomerBasket basket) + private async Task UpdatePriceInBasketItems(int productId, decimal newPrice, CustomerBasket basket) { var itemsToUpdate = basket?.Items?.Where(x => int.Parse(x.ProductId) == productId).ToList(); if (itemsToUpdate != null) diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index bbc30a772..f4864f2cd 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -1,17 +1,20 @@ -using Catalog.API.IntegrationEvents; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Catalog.API.Model; using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; using Microsoft.Extensions.Options; +using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Threading.Tasks; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { @@ -21,14 +24,14 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers private readonly CatalogContext _catalogContext; private readonly IOptionsSnapshot _settings; private readonly IEventBus _eventBus; - private readonly IIntegrationEventLogService _integrationEventLogService; + private readonly Func _integrationEventLogServiceFactory; - public CatalogController(CatalogContext Context, IOptionsSnapshot settings, IEventBus eventBus, IIntegrationEventLogService integrationEventLogService) + public CatalogController(CatalogContext Context, IOptionsSnapshot settings, IEventBus eventBus, Func integrationEventLogServiceFactory) { _catalogContext = Context; _settings = settings; _eventBus = eventBus; - _integrationEventLogService = integrationEventLogService; + _integrationEventLogServiceFactory = integrationEventLogServiceFactory; ((DbContext)Context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } @@ -135,38 +138,58 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers return Ok(items); } - //POST api/v1/[controller]/edit - [Route("edit")] + //POST api/v1/[controller]/update + [Route("update")] [HttpPost] - public async Task EditProduct([FromBody]CatalogItem product) + public async Task UpdateProduct([FromBody]CatalogItem productToUpdate) { - var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == product.Id); + var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); + if (catalogItem == null) return NotFound(); - if (item == null) + bool raiseProductPriceChangedEvent = false; + IntegrationEvent priceChangedEvent = null; + + if (catalogItem.Price != productToUpdate.Price) raiseProductPriceChangedEvent = true; + + if (raiseProductPriceChangedEvent) // Create event if price has changed { - return NotFound(); + var oldPrice = catalogItem.Price; + priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); } - if (item.Price != product.Price) - { - var oldPrice = item.Price; - item.Price = product.Price; - var @event = new ProductPriceChangedIntegrationEvent(item.Id, item.Price, oldPrice); + //Update current product + catalogItem = productToUpdate; + //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): + //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + var strategy = _catalogContext.Database.CreateExecutionStrategy(); + var eventLogService = _integrationEventLogServiceFactory(_catalogContext.Database.GetDbConnection()); + await strategy.ExecuteAsync(async () => + { + // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction using (var transaction = _catalogContext.Database.BeginTransaction()) { - _catalogContext.CatalogItems.Update(item); + _catalogContext.CatalogItems.Update(catalogItem); await _catalogContext.SaveChangesAsync(); - await _integrationEventLogService.SaveEventAsync(@event); + //Save to EventLog only if product price changed + if (raiseProductPriceChangedEvent) + { + + await eventLogService.SaveEventAsync(priceChangedEvent, _catalogContext.Database.CurrentTransaction.GetDbTransaction()); + } transaction.Commit(); } + }); - _eventBus.Publish(@event); - await _integrationEventLogService.MarkEventAsPublishedAsync(@event); - } + //Publish to Event Bus only if product price changed + if (raiseProductPriceChangedEvent) + { + _eventBus.Publish(priceChangedEvent); + await eventLogService.MarkEventAsPublishedAsync(priceChangedEvent); + } return Ok(); } diff --git a/src/Services/Catalog/Catalog.API/Dockerfile.nanowin b/src/Services/Catalog/Catalog.API/Dockerfile.nanowin index c40d3a4d0..193ddaef6 100644 --- a/src/Services/Catalog/Catalog.API/Dockerfile.nanowin +++ b/src/Services/Catalog/Catalog.API/Dockerfile.nanowin @@ -1,4 +1,5 @@ FROM microsoft/dotnet:1.1-runtime-nanoserver +SHELL ["powershell"] ARG source WORKDIR /app RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs index 55ed81d23..cb1befc48 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs @@ -84,14 +84,14 @@ { return new List() { - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Bot Black Sweatshirt", Name = ".NET Bot Black Sweatshirt", Price = 19.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/1" }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/1" }, new CatalogItem() { CatalogTypeId=1,CatalogBrandId=2, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/2" }, new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/3" }, - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Foundation Sweatshirt", Name = ".NET Foundation Sweatshirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/4" }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/4" }, new CatalogItem() { CatalogTypeId=3,CatalogBrandId=5, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/5" }, - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Blue Sweatshirt", Name = ".NET Blue Sweatshirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/6" }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/6" }, new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/7" }, - new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Kudu Purple Sweatshirt", Name = "Kudu Purple Sweatshirt", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/8" }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/8" }, new CatalogItem() { CatalogTypeId=1,CatalogBrandId=5, Description = "Cup White Mug", Name = "Cup White Mug", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/9" }, new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/10" }, new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup Sheet", Name = "Cup Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/11" }, diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index 2a7e84b2f..92e7a658c 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -9,11 +9,14 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; + using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using System; + using System.Data.Common; using System.Data.SqlClient; using System.Reflection; @@ -40,7 +43,6 @@ public void ConfigureServices(IServiceCollection services) { - var sqlConnection = new SqlConnection(Configuration["ConnectionString"]); // Add framework services. services.AddMvc(options => @@ -50,19 +52,19 @@ services.AddDbContext(c => { - c.UseSqlServer(sqlConnection); + options.UseSqlServer(Configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); // Changing default behavior when client evaluation occurs to throw. // Default in EF Core would be to log a warning when client evaluation is performed. - c.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); + options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval }); - services.AddDbContext(c => - { - c.UseSqlServer(sqlConnection, b => b.MigrationsAssembly("Catalog.API")); - c.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); - }); - services.Configure(Configuration); // Add framework services. @@ -88,14 +90,15 @@ .AllowCredentials()); }); - services.AddTransient(); - + services.AddTransient>( + sp => (DbConnection c) => new IntegrationEventLogService(c)); + var serviceProvider = services.BuildServiceProvider(); var configuration = serviceProvider.GetRequiredService>().Value; services.AddSingleton(new EventBusRabbitMQ(configuration.EventBusConnection)); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IntegrationEventLogContext integrationEventLogContext) + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { //Configure logs @@ -112,7 +115,11 @@ //Seed Data CatalogContextSeed.SeedAsync(app, loggerFactory) .Wait(); - + + var integrationEventLogContext = new IntegrationEventLogContext( + new DbContextOptionsBuilder() + .UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Catalog.API")) + .Options); integrationEventLogContext.Database.Migrate(); } diff --git a/src/Services/Identity/Identity.API/Dockerfile.nanowin b/src/Services/Identity/Identity.API/Dockerfile.nanowin index b950d8334..9d24ccf1a 100644 --- a/src/Services/Identity/Identity.API/Dockerfile.nanowin +++ b/src/Services/Identity/Identity.API/Dockerfile.nanowin @@ -1,4 +1,5 @@ FROM microsoft/dotnet:1.1-runtime-nanoserver +SHELL ["powershell"] ARG source WORKDIR /app RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord diff --git a/src/Services/Identity/Identity.API/Views/Account/Login.cshtml b/src/Services/Identity/Identity.API/Views/Account/Login.cshtml index 88f6884cc..da36c31a4 100644 --- a/src/Services/Identity/Identity.API/Views/Account/Login.cshtml +++ b/src/Services/Identity/Identity.API/Views/Account/Login.cshtml @@ -45,6 +45,15 @@

Register as a new user?

+

+ Note that for demo purposes you don't need to register and can login with these credentials: +

+

+ User: demouser@microsoft.com +

+

+ Password: Pass@word1 +

diff --git a/src/Services/Ordering/Ordering.API/Application/Decorators/ValidatorDecorator.cs b/src/Services/Ordering/Ordering.API/Application/Decorators/ValidatorDecorator.cs new file mode 100644 index 000000000..5bdff8330 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Decorators/ValidatorDecorator.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Decorators +{ + public class ValidatorDecorator + : IAsyncRequestHandler + where TRequest : IAsyncRequest + { + private readonly IAsyncRequestHandler _inner; + private readonly IValidator[] _validators; + + + public ValidatorDecorator( + IAsyncRequestHandler inner, + IValidator[] validators) + { + _inner = inner; + _validators = validators; + } + + public async Task Handle(TRequest message) + { + var failures = _validators + .Select(v => v.Validate(message)) + .SelectMany(result => result.Errors) + .Where(error => error != null) + .ToList(); + + if (failures.Any()) + { + throw new ValidationException( + $"Command Validation Errors for type {typeof(TRequest).Name}", failures); + } + + var response = await _inner.Handle(message); + + return response; + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs new file mode 100644 index 000000000..449f95df5 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs @@ -0,0 +1,39 @@ +using FluentValidation; +using MediatR; +using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; + +namespace Ordering.API.Application.Validations +{ + public class CreateOrderCommandValidator : AbstractValidator + { + public CreateOrderCommandValidator() + { + RuleFor(order => order.City).NotEmpty(); + RuleFor(order => order.Street).NotEmpty(); + RuleFor(order => order.State).NotEmpty(); + RuleFor(order => order.Country).NotEmpty(); + RuleFor(order => order.ZipCode).NotEmpty(); + RuleFor(order => order.CardNumber).NotEmpty().Length(12, 19); + RuleFor(order => order.CardHolderName).NotEmpty(); + RuleFor(order => order.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date"); + RuleFor(order => order.CardSecurityNumber).NotEmpty().Length(3); + RuleFor(order => order.CardTypeId).NotEmpty(); + RuleFor(order => order.OrderItems).Must(ContainOrderItems).WithMessage("No order items found"); + } + + private bool BeValidExpirationDate(DateTime dateTime) + { + return dateTime >= DateTime.UtcNow; + } + + private bool ContainOrderItems(IEnumerable orderItems) + { + return orderItems.Any(); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs new file mode 100644 index 000000000..44b374ee6 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; +using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.Application.Validations +{ + public class IdentifierCommandValidator : AbstractValidator> + { + public IdentifierCommandValidator() + { + RuleFor(customer => customer.Id).NotEmpty(); + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Dockerfile.nanowin b/src/Services/Ordering/Ordering.API/Dockerfile.nanowin index eac299fc7..653531d0f 100644 --- a/src/Services/Ordering/Ordering.API/Dockerfile.nanowin +++ b/src/Services/Ordering/Ordering.API/Dockerfile.nanowin @@ -1,4 +1,5 @@ FROM microsoft/dotnet:1.1-runtime-nanoserver +SHELL ["powershell"] ARG source WORKDIR /app RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs index a6864e0ef..e741644f3 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs @@ -1,9 +1,12 @@ using Autofac; using Autofac.Core; +using FluentValidation; using MediatR; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Decorators; +using Ordering.API.Application.Decorators; using Ordering.API.Application.DomainEventHandlers.OrderStartedEvent; +using Ordering.API.Application.Validations; using Ordering.Domain.Events; using System.Collections.Generic; using System.Linq; @@ -24,11 +27,17 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Autof .Where(i => i.IsClosedTypeOf(typeof(IAsyncRequestHandler<,>))) .Select(i => new KeyedService("IAsyncRequestHandler", i))); - // Register all the Domain Event Handler classes (they implement IAsyncNotificationHandler<>) in assembly holding the Domain Events + // Register all the event classes (they implement IAsyncNotificationHandler) in assembly holding the Commands + builder.RegisterAssemblyTypes(typeof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler).GetTypeInfo().Assembly) + .As(o => o.GetInterfaces() + .Where(i => i.IsClosedTypeOf(typeof(IAsyncNotificationHandler<>))) + .Select(i => new KeyedService("IAsyncNotificationHandler", i))); + builder - .RegisterAssemblyTypes(typeof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler).GetTypeInfo().Assembly) - .Where(t => t.IsClosedTypeOf(typeof(IAsyncNotificationHandler<>))) - .AsImplementedInterfaces(); + .RegisterAssemblyTypes(typeof(CreateOrderCommandValidator).GetTypeInfo().Assembly) + .Where(t => t.IsClosedTypeOf(typeof(IValidator<>))) + .AsImplementedInterfaces(); + builder.Register(context => { @@ -44,9 +53,16 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Autof return t => (IEnumerable)componentContext.Resolve(typeof(IEnumerable<>).MakeGenericType(t)); }); + + builder.RegisterGenericDecorator(typeof(LogDecorator<,>), typeof(IAsyncRequestHandler<,>), - "IAsyncRequestHandler"); + "IAsyncRequestHandler") + .Keyed("handlerDecorator", typeof(IAsyncRequestHandler<,>)); + + builder.RegisterGenericDecorator(typeof(ValidatorDecorator<,>), + typeof(IAsyncRequestHandler<,>), + fromKey: "handlerDecorator"); } } } diff --git a/src/Services/Ordering/Ordering.API/Ordering.API.csproj b/src/Services/Ordering/Ordering.API/Ordering.API.csproj index d0102c19f..d7dd2bc1a 100644 --- a/src/Services/Ordering/Ordering.API/Ordering.API.csproj +++ b/src/Services/Ordering/Ordering.API/Ordering.API.csproj @@ -28,6 +28,8 @@ + + diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index 1c419fc0f..f90c9bb98 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -52,10 +52,14 @@ services.AddEntityFrameworkSqlServer() .AddDbContext(options => { - options.UseSqlServer(Configuration["ConnectionString"], - sqlop => sqlop.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name)); + options.UseSqlServer(Configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); }, - ServiceLifetime.Scoped //DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) + ServiceLifetime.Scoped //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) ); services.AddSwaggerGen(); diff --git a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs index f9f68cfb3..3870ad470 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs @@ -246,7 +246,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure await _mediator.DispatchDomainEventsAsync(this); - // After executing this line all the changes performed thought the DbContext will be commited + // After executing this line all the changes (from the Command Handler and Domain Event Handlers) + // performed thought the DbContext will be commited var result = await base.SaveChangesAsync(); return true; diff --git a/src/Web/WebMVC/Dockerfile.nanowin b/src/Web/WebMVC/Dockerfile.nanowin index 302526330..4eaad3b22 100644 --- a/src/Web/WebMVC/Dockerfile.nanowin +++ b/src/Web/WebMVC/Dockerfile.nanowin @@ -1,4 +1,5 @@ FROM microsoft/dotnet:1.1-runtime-nanoserver +SHELL ["powershell"] ARG source WORKDIR /app RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord diff --git a/src/Web/WebMVC/Views/Account/Login.cshtml b/src/Web/WebMVC/Views/Account/Login.cshtml index 595305f95..031998d69 100644 --- a/src/Web/WebMVC/Views/Account/Login.cshtml +++ b/src/Web/WebMVC/Views/Account/Login.cshtml @@ -44,7 +44,16 @@

Register as a new user?

- +

+ Note that for demo purposes you don't need to register and can login with these credentials: +

+

+ User: demouser@microsoft.com +

+

+ Password: Pass@word1 +

+ diff --git a/src/Web/WebMVC/Views/Shared/Components/CartList/Default.cshtml b/src/Web/WebMVC/Views/Shared/Components/CartList/Default.cshtml index 6ee70b035..43d3b9f55 100644 --- a/src/Web/WebMVC/Views/Shared/Components/CartList/Default.cshtml +++ b/src/Web/WebMVC/Views/Shared/Components/CartList/Default.cshtml @@ -38,7 +38,7 @@
@if (item.OldUnitPrice != 0) { - + }

diff --git a/src/Web/WebMVC/wwwroot/css/site.min.css b/src/Web/WebMVC/wwwroot/css/site.min.css index f95f6d90e..4d03fa783 100644 --- a/src/Web/WebMVC/wwwroot/css/site.min.css +++ b/src/Web/WebMVC/wwwroot/css/site.min.css @@ -1 +1 @@ -.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%}.esh-app-footer-brand{height:50px;width:230px}.esh-app-footer-text{color:#83d01b;line-height:50px;text-align:right;width:100%}@font-face{font-family:Montserrat;font-weight:400;src:url("../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item--navigable{display:inline-block;cursor:pointer}.esh-pager-item--navigable.is-disabled{opacity:0;pointer-events:none}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 4vw}}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:0;min-width:14rem;right:0;overflow:hidden;padding:.5rem;position:absolute;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:7rem;transition:height .35s}.esh-identity-item{cursor:pointer;display:block;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5) !important;line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-header-back:hover{color:#fff !important;transition:color .35s}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}.esh-orders_new{min-height:80vh}.esh-orders_new-header{background-color:#00a69c;height:4rem}.esh-orders_new-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders_new-back:hover{color:#fff;transition:color .35s}.esh-orders_new-section{padding:1rem 0}.esh-orders_new-section--right{text-align:right}.esh-orders_new-placeOrder{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-orders_new-placeOrder:hover{background-color:#4a760f;transition:all .35s}.esh-orders_new-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_new-title{font-size:1.25rem;text-transform:uppercase}.esh-orders_new-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_new-items--border:last-of-type{border-color:transparent}.esh-orders_new-item{font-size:1rem;font-weight:300}.esh-orders_new-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_new-item--middle{line-height:1rem}}.esh-orders_new-item--mark{color:#83d01b}.esh-orders_new-image{height:8rem}.esh-orders_detail{min-height:80vh}.esh-orders_detail-section{padding:1rem 0}.esh-orders_detail-section--right{text-align:right}.esh-orders_detail-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_detail-title{text-transform:uppercase}.esh-orders_detail-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_detail-items--border:last-of-type{border-color:transparent}.esh-orders_detail-item{font-size:1rem;font-weight:300}.esh-orders_detail-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_detail-item--middle{line-height:1rem}}.esh-orders_detail-item--mark{color:#83d01b}.esh-orders_detail-image{height:8rem}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;min-width:140px;-webkit-appearance:none}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-top:.65rem;margin-left:.5rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-label::after{background-image:url("../images/arrow-down.png");height:7px;content:'';position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;transform:translateY(.5rem);padding:.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{text-align:center;margin-bottom:1.5rem;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:none;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{text-align:center;font-weight:900;font-size:28px}.esh-catalog-price::before{content:'$'}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{border:none;border-radius:0;background-color:#83d01b;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s} \ No newline at end of file +.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%}.esh-app-footer-brand{height:50px;width:230px}.esh-app-footer-text{color:#83d01b;line-height:50px;text-align:right;width:100%}@font-face{font-family:Montserrat;font-weight:400;src:url("../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{border:none;border-radius:0;background-color:#83d01b;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basket-margin12{margin-left:12px}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;min-width:140px;-webkit-appearance:none}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-top:.65rem;margin-left:.5rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-label::after{background-image:url("../images/arrow-down.png");height:7px;content:'';position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;transform:translateY(.5rem);padding:.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{text-align:center;margin-bottom:1.5rem;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:none;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{text-align:center;font-weight:900;font-size:28px}.esh-catalog-price::before{content:'$'}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}.esh-orders_detail{min-height:80vh}.esh-orders_detail-section{padding:1rem 0}.esh-orders_detail-section--right{text-align:right}.esh-orders_detail-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_detail-title{text-transform:uppercase}.esh-orders_detail-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_detail-items--border:last-of-type{border-color:transparent}.esh-orders_detail-item{font-size:1rem;font-weight:300}.esh-orders_detail-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_detail-item--middle{line-height:1rem}}.esh-orders_detail-item--mark{color:#83d01b}.esh-orders_detail-image{height:8rem}.esh-orders_new{min-height:80vh}.esh-orders_new-header{background-color:#00a69c;height:4rem}.esh-orders_new-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders_new-back:hover{color:#fff;transition:color .35s}.esh-orders_new-section{padding:1rem 0}.esh-orders_new-section--right{text-align:right}.esh-orders_new-placeOrder{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-orders_new-placeOrder:hover{background-color:#4a760f;transition:all .35s}.esh-orders_new-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_new-title{font-size:1.25rem;text-transform:uppercase}.esh-orders_new-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_new-items--border:last-of-type{border-color:transparent}.esh-orders_new-item{font-size:1rem;font-weight:300}.esh-orders_new-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_new-item--middle{line-height:1rem}}.esh-orders_new-item--mark{color:#83d01b}.esh-orders_new-image{height:8rem}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5) !important;line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-header-back:hover{color:#fff !important;transition:color .35s}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:0;min-width:14rem;right:0;overflow:hidden;padding:.5rem;position:absolute;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:7rem;transition:height .35s}.esh-identity-item{cursor:pointer;display:block;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item--navigable{display:inline-block;cursor:pointer}.esh-pager-item--navigable.is-disabled{opacity:0;pointer-events:none}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 4vw}} \ No newline at end of file diff --git a/src/Web/WebSPA/Client/modules/basket/basket.component.html b/src/Web/WebSPA/Client/modules/basket/basket.component.html index 7cab7221a..a23a4e91f 100644 --- a/src/Web/WebSPA/Client/modules/basket/basket.component.html +++ b/src/Web/WebSPA/Client/modules/basket/basket.component.html @@ -29,7 +29,7 @@
- +
diff --git a/src/Web/WebSPA/Dockerfile.nanowin b/src/Web/WebSPA/Dockerfile.nanowin index 5f9a48294..700c23391 100644 --- a/src/Web/WebSPA/Dockerfile.nanowin +++ b/src/Web/WebSPA/Dockerfile.nanowin @@ -1,4 +1,5 @@ FROM microsoft/dotnet:1.1-runtime-nanoserver +SHELL ["powershell"] ARG source WORKDIR /app RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord diff --git a/test/Services/FunctionalTests/Extensions/HttpClientExtensions.cs b/test/Services/FunctionalTests/Extensions/HttpClientExtensions.cs new file mode 100644 index 000000000..a41ffd3a2 --- /dev/null +++ b/test/Services/FunctionalTests/Extensions/HttpClientExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.TestHost; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; + +namespace FunctionalTests.Extensions +{ + static class HttpClientExtensions + { + public static HttpClient CreateIdempotentClient(this TestServer server) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); + return client; + } + } +} diff --git a/test/Services/FunctionalTests/Services/Catalog/CatalogScenariosBase.cs b/test/Services/FunctionalTests/Services/Catalog/CatalogScenariosBase.cs index f7a847934..e524eedc9 100644 --- a/test/Services/FunctionalTests/Services/Catalog/CatalogScenariosBase.cs +++ b/test/Services/FunctionalTests/Services/Catalog/CatalogScenariosBase.cs @@ -34,7 +34,7 @@ namespace FunctionalTests.Services.Catalog public static class Post { - public static string UpdateCatalogProduct = "api/v1/catalog/edit"; + public static string UpdateCatalogProduct = "api/v1/catalog/update"; } } } diff --git a/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs b/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs index 57254db1f..8ed11be3a 100644 --- a/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs +++ b/test/Services/FunctionalTests/Services/IntegrationEventsScenarios.cs @@ -43,7 +43,7 @@ namespace FunctionalTests.Services var itemToModify = basket.Items[2]; var oldPrice = itemToModify.UnitPrice; var newPrice = oldPrice + priceModification; - var pRes = await catalogClient.PostAsync(CatalogScenariosBase.Post.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice), UTF8Encoding.UTF8, "application/json")); + var pRes = await catalogClient.PostAsync(CatalogScenariosBase.Post.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json")); var modifiedCatalogProducts = await GetCatalogAsync(catalogClient); @@ -100,14 +100,11 @@ namespace FunctionalTests.Services return JsonConvert.DeserializeObject>(items); } - private string ChangePrice(BasketItem itemToModify, decimal newPrice) + private string ChangePrice(BasketItem itemToModify, decimal newPrice, PaginatedItemsViewModel catalogProducts) { - var item = new CatalogItem() - { - Id = int.Parse(itemToModify.ProductId), - Price = newPrice - }; - return JsonConvert.SerializeObject(item); + var catalogProduct = catalogProducts.Data.Single(pr => pr.Id == int.Parse(itemToModify.ProductId)); + catalogProduct.Price = newPrice; + return JsonConvert.SerializeObject(catalogProduct); } private CustomerBasket ComposeBasket(string customerId, IEnumerable items) diff --git a/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs b/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs index 756bbed2a..5f52e1771 100644 --- a/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs +++ b/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs @@ -1,4 +1,5 @@ -using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +using FunctionalTests.Extensions; +using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Newtonsoft.Json; using System; @@ -19,7 +20,7 @@ namespace FunctionalTests.Services.Ordering { using (var server = CreateServer()) { - var client = server.CreateClient(); + var client = server.CreateIdempotentClient(); // GIVEN an order is created await client.PostAsync(Post.AddNewOrder, new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json")); @@ -62,7 +63,7 @@ namespace FunctionalTests.Services.Ordering order.AddOrderItem(new OrderItemDTO() { ProductId = 1, - Discount = 12M, + Discount = 8M, UnitPrice = 10, Units = 1, ProductName = "Some name" diff --git a/test/Services/IntegrationTests/Services/Extensions/HttpClientExtensions.cs b/test/Services/IntegrationTests/Services/Extensions/HttpClientExtensions.cs new file mode 100644 index 000000000..00ed918b6 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Extensions/HttpClientExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.TestHost; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; + +namespace IntegrationTests.Services.Extensions +{ + static class HttpClientExtensions + { + public static HttpClient CreateIdempotentClient(this TestServer server) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); + return client; + } + } +} diff --git a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs index ba77a3e7c..3e2350c9d 100644 --- a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs +++ b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs @@ -1,5 +1,7 @@ namespace IntegrationTests.Services.Ordering { + using IntegrationTests.Services.Extensions; + using Microsoft.AspNetCore.TestHost; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Newtonsoft.Json; using System; @@ -28,9 +30,9 @@ public async Task AddNewOrder_add_new_order_and_response_ok_status_code() { using (var server = CreateServer()) - { + { var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json"); - var response = await server.CreateClient() + var response = await server.CreateIdempotentClient() .PostAsync(Post.AddNewOrder, content); response.EnsureSuccessStatusCode(); @@ -44,7 +46,7 @@ { var content = new StringContent(BuildOrderWithInvalidExperationTime(), UTF8Encoding.UTF8, "application/json"); - var response = await server.CreateClient() + var response = await server.CreateIdempotentClient() .PostAsync(Post.AddNewOrder, content); Assert.True(response.StatusCode == System.Net.HttpStatusCode.BadRequest); @@ -102,5 +104,5 @@ return JsonConvert.SerializeObject(order); } - } + } }