Merge from Dev

This commit is contained in:
Ramón Tomás 2017-03-27 15:00:46 +02:00
commit a14b7e580f
37 changed files with 576 additions and 89 deletions

View File

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

View File

@ -0,0 +1 @@
[rabbitmq_amqp1_0,rabbitmq_management].

View File

@ -0,0 +1 @@
[{rabbit, [{loopback_users, []}]}].

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ version: '2'
# #
# Set ASPNETCORE_ENVIRONMENT=Development to get errors while testing. # 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 # docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
services: services:

View File

@ -1,14 +1,15 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.Common;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Catalog.API.IntegrationEvents namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services
{ {
public interface IIntegrationEventLogService public interface IIntegrationEventLogService
{ {
Task SaveEventAsync(IntegrationEvent @event); Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction);
Task MarkEventAsPublishedAsync(IntegrationEvent @event); Task MarkEventAsPublishedAsync(IntegrationEvent @event);
} }
} }

View File

@ -1,37 +1,39 @@
using System; using Microsoft.EntityFrameworkCore;
using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System.Data.Common;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using System;
using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Catalog.API.IntegrationEvents namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services
{ {
public class IntegrationEventLogService : IIntegrationEventLogService public class IntegrationEventLogService : IIntegrationEventLogService
{ {
private readonly IntegrationEventLogContext _integrationEventLogContext; 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( _integrationEventLogContext = new IntegrationEventLogContext(
new DbContextOptionsBuilder<IntegrationEventLogContext>() new DbContextOptionsBuilder<IntegrationEventLogContext>()
.UseSqlServer(catalogContext.Database.GetDbConnection()) .UseSqlServer(_dbConnection)
.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)) .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning))
.Options); .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); var eventLogEntry = new IntegrationEventLogEntry(@event);
// as a constraint this transaction has to be done together with a catalogContext transaction _integrationEventLogContext.Database.UseTransaction(transaction);
_integrationEventLogContext.Database.UseTransaction(_catalogContext.Database.CurrentTransaction.GetDbTransaction());
_integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry); _integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry);
return _integrationEventLogContext.SaveChangesAsync(); return _integrationEventLogContext.SaveChangesAsync();

View File

@ -1,4 +1,5 @@
FROM microsoft/dotnet:1.1-runtime-nanoserver FROM microsoft/dotnet:1.1-runtime-nanoserver
SHELL ["powershell"]
ARG source ARG source
WORKDIR /app WORKDIR /app
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord

View File

@ -20,11 +20,11 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Even
foreach (var id in userIds) foreach (var id in userIds)
{ {
var basket = await _repository.GetBasket(id); 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(); var itemsToUpdate = basket?.Items?.Where(x => int.Parse(x.ProductId) == productId).ToList();
if (itemsToUpdate != null) if (itemsToUpdate != null)

View File

@ -1,17 +1,20 @@
using Catalog.API.IntegrationEvents; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; 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.Infrastructure;
using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events;
using Microsoft.eShopOnContainers.Services.Catalog.API.Model; using Microsoft.eShopOnContainers.Services.Catalog.API.Model;
using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.Common;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
{ {
@ -21,14 +24,14 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
private readonly CatalogContext _catalogContext; private readonly CatalogContext _catalogContext;
private readonly IOptionsSnapshot<Settings> _settings; private readonly IOptionsSnapshot<Settings> _settings;
private readonly IEventBus _eventBus; private readonly IEventBus _eventBus;
private readonly IIntegrationEventLogService _integrationEventLogService; private readonly Func<DbConnection, IIntegrationEventLogService> _integrationEventLogServiceFactory;
public CatalogController(CatalogContext Context, IOptionsSnapshot<Settings> settings, IEventBus eventBus, IIntegrationEventLogService integrationEventLogService) public CatalogController(CatalogContext Context, IOptionsSnapshot<Settings> settings, IEventBus eventBus, Func<DbConnection, IIntegrationEventLogService> integrationEventLogServiceFactory)
{ {
_catalogContext = Context; _catalogContext = Context;
_settings = settings; _settings = settings;
_eventBus = eventBus; _eventBus = eventBus;
_integrationEventLogService = integrationEventLogService; _integrationEventLogServiceFactory = integrationEventLogServiceFactory;
((DbContext)Context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; ((DbContext)Context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
} }
@ -135,38 +138,58 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
return Ok(items); return Ok(items);
} }
//POST api/v1/[controller]/edit //POST api/v1/[controller]/update
[Route("edit")] [Route("update")]
[HttpPost] [HttpPost]
public async Task<IActionResult> EditProduct([FromBody]CatalogItem product) public async Task<IActionResult> 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) //Update current product
{ catalogItem = productToUpdate;
var oldPrice = item.Price;
item.Price = product.Price;
var @event = new ProductPriceChangedIntegrationEvent(item.Id, item.Price, oldPrice);
//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()) using (var transaction = _catalogContext.Database.BeginTransaction())
{ {
_catalogContext.CatalogItems.Update(item); _catalogContext.CatalogItems.Update(catalogItem);
await _catalogContext.SaveChangesAsync(); 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(); 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(); return Ok();
} }

View File

@ -1,4 +1,5 @@
FROM microsoft/dotnet:1.1-runtime-nanoserver FROM microsoft/dotnet:1.1-runtime-nanoserver
SHELL ["powershell"]
ARG source ARG source
WORKDIR /app WORKDIR /app
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord

View File

@ -84,14 +84,14 @@
{ {
return new List<CatalogItem>() return new List<CatalogItem>()
{ {
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=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=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=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 = "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<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/9" }, new CatalogItem() { CatalogTypeId=1,CatalogBrandId=5, Description = "Cup<T> White Mug", Name = "Cup<T> 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 = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/10" },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/11" }, new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/11" },

View File

@ -9,11 +9,14 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF;
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services;
using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System;
using System.Data.Common;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Reflection; using System.Reflection;
@ -40,7 +43,6 @@
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
var sqlConnection = new SqlConnection(Configuration["ConnectionString"]);
// Add framework services. // Add framework services.
services.AddMvc(options => services.AddMvc(options =>
@ -50,19 +52,19 @@
services.AddDbContext<CatalogContext>(c => services.AddDbContext<CatalogContext>(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. // Changing default behavior when client evaluation occurs to throw.
// Default in EF Core would be to log a warning when client evaluation is performed. // 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 //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval
}); });
services.AddDbContext<IntegrationEventLogContext>(c =>
{
c.UseSqlServer(sqlConnection, b => b.MigrationsAssembly("Catalog.API"));
c.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));
});
services.Configure<Settings>(Configuration); services.Configure<Settings>(Configuration);
// Add framework services. // Add framework services.
@ -88,14 +90,15 @@
.AllowCredentials()); .AllowCredentials());
}); });
services.AddTransient<IIntegrationEventLogService, IntegrationEventLogService>(); services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
sp => (DbConnection c) => new IntegrationEventLogService(c));
var serviceProvider = services.BuildServiceProvider(); var serviceProvider = services.BuildServiceProvider();
var configuration = serviceProvider.GetRequiredService<IOptionsSnapshot<Settings>>().Value; var configuration = serviceProvider.GetRequiredService<IOptionsSnapshot<Settings>>().Value;
services.AddSingleton<IEventBus>(new EventBusRabbitMQ(configuration.EventBusConnection)); services.AddSingleton<IEventBus>(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 //Configure logs
@ -112,7 +115,11 @@
//Seed Data //Seed Data
CatalogContextSeed.SeedAsync(app, loggerFactory) CatalogContextSeed.SeedAsync(app, loggerFactory)
.Wait(); .Wait();
var integrationEventLogContext = new IntegrationEventLogContext(
new DbContextOptionsBuilder<IntegrationEventLogContext>()
.UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Catalog.API"))
.Options);
integrationEventLogContext.Database.Migrate(); integrationEventLogContext.Database.Migrate();
} }

View File

@ -1,4 +1,5 @@
FROM microsoft/dotnet:1.1-runtime-nanoserver FROM microsoft/dotnet:1.1-runtime-nanoserver
SHELL ["powershell"]
ARG source ARG source
WORKDIR /app WORKDIR /app
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord

View File

@ -45,6 +45,15 @@
<p> <p>
<a asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" class="text">Register as a new user?</a> <a asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" class="text">Register as a new user?</a>
</p> </p>
<p>
Note that for demo purposes you don't need to register and can login with these credentials:
</p>
<p>
User: <b>demouser@microsoft.com</b>
</p>
<p>
Password: <b>Pass@word1</b>
</p>
</form> </form>
</section> </section>
</div> </div>

View File

@ -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<TRequest, TResponse>
: IAsyncRequestHandler<TRequest, TResponse>
where TRequest : IAsyncRequest<TResponse>
{
private readonly IAsyncRequestHandler<TRequest, TResponse> _inner;
private readonly IValidator<TRequest>[] _validators;
public ValidatorDecorator(
IAsyncRequestHandler<TRequest, TResponse> inner,
IValidator<TRequest>[] validators)
{
_inner = inner;
_validators = validators;
}
public async Task<TResponse> 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;
}
}
}

View File

@ -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<CreateOrderCommand>
{
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<OrderItemDTO> orderItems)
{
return orderItems.Any();
}
}
}

View File

@ -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<IdentifiedCommand<CreateOrderCommand,bool>>
{
public IdentifierCommandValidator()
{
RuleFor(customer => customer.Id).NotEmpty();
}
}
}

View File

@ -1,4 +1,5 @@
FROM microsoft/dotnet:1.1-runtime-nanoserver FROM microsoft/dotnet:1.1-runtime-nanoserver
SHELL ["powershell"]
ARG source ARG source
WORKDIR /app WORKDIR /app
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord

View File

@ -1,9 +1,12 @@
using Autofac; using Autofac;
using Autofac.Core; using Autofac.Core;
using FluentValidation;
using MediatR; using MediatR;
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Decorators; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Decorators;
using Ordering.API.Application.Decorators;
using Ordering.API.Application.DomainEventHandlers.OrderStartedEvent; using Ordering.API.Application.DomainEventHandlers.OrderStartedEvent;
using Ordering.API.Application.Validations;
using Ordering.Domain.Events; using Ordering.Domain.Events;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -24,11 +27,17 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Autof
.Where(i => i.IsClosedTypeOf(typeof(IAsyncRequestHandler<,>))) .Where(i => i.IsClosedTypeOf(typeof(IAsyncRequestHandler<,>)))
.Select(i => new KeyedService("IAsyncRequestHandler", i))); .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 builder
.RegisterAssemblyTypes(typeof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler).GetTypeInfo().Assembly) .RegisterAssemblyTypes(typeof(CreateOrderCommandValidator).GetTypeInfo().Assembly)
.Where(t => t.IsClosedTypeOf(typeof(IAsyncNotificationHandler<>))) .Where(t => t.IsClosedTypeOf(typeof(IValidator<>)))
.AsImplementedInterfaces(); .AsImplementedInterfaces();
builder.Register<SingleInstanceFactory>(context => builder.Register<SingleInstanceFactory>(context =>
{ {
@ -44,9 +53,16 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Autof
return t => (IEnumerable<object>)componentContext.Resolve(typeof(IEnumerable<>).MakeGenericType(t)); return t => (IEnumerable<object>)componentContext.Resolve(typeof(IEnumerable<>).MakeGenericType(t));
}); });
builder.RegisterGenericDecorator(typeof(LogDecorator<,>), builder.RegisterGenericDecorator(typeof(LogDecorator<,>),
typeof(IAsyncRequestHandler<,>), typeof(IAsyncRequestHandler<,>),
"IAsyncRequestHandler"); "IAsyncRequestHandler")
.Keyed("handlerDecorator", typeof(IAsyncRequestHandler<,>));
builder.RegisterGenericDecorator(typeof(ValidatorDecorator<,>),
typeof(IAsyncRequestHandler<,>),
fromKey: "handlerDecorator");
} }
} }
} }

View File

@ -28,6 +28,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="6.4.0" />
<PackageReference Include="FluentValidation.MVC6" Version="6.4.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="1.1.0" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="1.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.0.0" /> <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.0" />

View File

@ -52,10 +52,14 @@
services.AddEntityFrameworkSqlServer() services.AddEntityFrameworkSqlServer()
.AddDbContext<OrderingContext>(options => .AddDbContext<OrderingContext>(options =>
{ {
options.UseSqlServer(Configuration["ConnectionString"], options.UseSqlServer(Configuration["ConnectionString"],
sqlop => sqlop.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name)); 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(); services.AddSwaggerGen();

View File

@ -246,7 +246,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
await _mediator.DispatchDomainEventsAsync(this); 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(); var result = await base.SaveChangesAsync();
return true; return true;

View File

@ -1,4 +1,5 @@
FROM microsoft/dotnet:1.1-runtime-nanoserver FROM microsoft/dotnet:1.1-runtime-nanoserver
SHELL ["powershell"]
ARG source ARG source
WORKDIR /app WORKDIR /app
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord

View File

@ -44,7 +44,16 @@
<p> <p>
<a asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" class="text">Register as a new user?</a> <a asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" class="text">Register as a new user?</a>
</p> </p>
</form> <p>
Note that for demo purposes you don't need to register and can login with these credentials:
</p>
<p>
User: <b>demouser@microsoft.com</b>
</p>
<p>
Password: <b>Pass@word1</b>
</p>
</form>
</section> </section>
</div> </div>
</div> </div>

View File

@ -38,7 +38,7 @@
<div class="esh-basket-items--border row"> <div class="esh-basket-items--border row">
@if (item.OldUnitPrice != 0) @if (item.OldUnitPrice != 0)
{ {
<div class="alert alert-warning esh-basket-margin12" role="alert">&nbsp;The price of the item has changed.Old price was @item.OldUnitPrice $</div> <div class="alert alert-warning esh-basket-margin12" role="alert">&nbsp;Note that the price of this article changed in our Catalog. The old price when you originally added it to the basket was @item.OldUnitPrice $</div>
} }
</div> </div>
<br/> <br/>

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,7 @@
</article> </article>
<br/> <br/>
<div class="esh-basket-items-margin-left1 row"> <div class="esh-basket-items-margin-left1 row">
<div class="alert alert-warning" role="alert" *ngIf="item.oldUnitPrice > 0">&nbsp;The price of the item has changed.Old price was {{item.oldUnitPrice}} $</div> <div class="alert alert-warning" role="alert" *ngIf="item.oldUnitPrice > 0">&nbsp;Note that the price of this article changed in our Catalog. The old price when you originally added it to the basket was {{item.oldUnitPrice}} $</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,5 @@
FROM microsoft/dotnet:1.1-runtime-nanoserver FROM microsoft/dotnet:1.1-runtime-nanoserver
SHELL ["powershell"]
ARG source ARG source
WORKDIR /app WORKDIR /app
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord

View File

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

View File

@ -34,7 +34,7 @@ namespace FunctionalTests.Services.Catalog
public static class Post public static class Post
{ {
public static string UpdateCatalogProduct = "api/v1/catalog/edit"; public static string UpdateCatalogProduct = "api/v1/catalog/update";
} }
} }
} }

View File

@ -43,7 +43,7 @@ namespace FunctionalTests.Services
var itemToModify = basket.Items[2]; var itemToModify = basket.Items[2];
var oldPrice = itemToModify.UnitPrice; var oldPrice = itemToModify.UnitPrice;
var newPrice = oldPrice + priceModification; 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); var modifiedCatalogProducts = await GetCatalogAsync(catalogClient);
@ -100,14 +100,11 @@ namespace FunctionalTests.Services
return JsonConvert.DeserializeObject<PaginatedItemsViewModel<CatalogItem>>(items); return JsonConvert.DeserializeObject<PaginatedItemsViewModel<CatalogItem>>(items);
} }
private string ChangePrice(BasketItem itemToModify, decimal newPrice) private string ChangePrice(BasketItem itemToModify, decimal newPrice, PaginatedItemsViewModel<CatalogItem> catalogProducts)
{ {
var item = new CatalogItem() var catalogProduct = catalogProducts.Data.Single(pr => pr.Id == int.Parse(itemToModify.ProductId));
{ catalogProduct.Price = newPrice;
Id = int.Parse(itemToModify.ProductId), return JsonConvert.SerializeObject(catalogProduct);
Price = newPrice
};
return JsonConvert.SerializeObject(item);
} }
private CustomerBasket ComposeBasket(string customerId, IEnumerable<CatalogItem> items) private CustomerBasket ComposeBasket(string customerId, IEnumerable<CatalogItem> items)

View File

@ -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 Microsoft.eShopOnContainers.WebMVC.ViewModels;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
@ -19,7 +20,7 @@ namespace FunctionalTests.Services.Ordering
{ {
using (var server = CreateServer()) using (var server = CreateServer())
{ {
var client = server.CreateClient(); var client = server.CreateIdempotentClient();
// GIVEN an order is created // GIVEN an order is created
await client.PostAsync(Post.AddNewOrder, new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json")); await client.PostAsync(Post.AddNewOrder, new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json"));
@ -62,7 +63,7 @@ namespace FunctionalTests.Services.Ordering
order.AddOrderItem(new OrderItemDTO() order.AddOrderItem(new OrderItemDTO()
{ {
ProductId = 1, ProductId = 1,
Discount = 12M, Discount = 8M,
UnitPrice = 10, UnitPrice = 10,
Units = 1, Units = 1,
ProductName = "Some name" ProductName = "Some name"

View File

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

View File

@ -1,5 +1,7 @@
namespace IntegrationTests.Services.Ordering namespace IntegrationTests.Services.Ordering
{ {
using IntegrationTests.Services.Extensions;
using Microsoft.AspNetCore.TestHost;
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
@ -28,9 +30,9 @@
public async Task AddNewOrder_add_new_order_and_response_ok_status_code() public async Task AddNewOrder_add_new_order_and_response_ok_status_code()
{ {
using (var server = CreateServer()) using (var server = CreateServer())
{ {
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json"); var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
var response = await server.CreateClient() var response = await server.CreateIdempotentClient()
.PostAsync(Post.AddNewOrder, content); .PostAsync(Post.AddNewOrder, content);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
@ -44,7 +46,7 @@
{ {
var content = new StringContent(BuildOrderWithInvalidExperationTime(), UTF8Encoding.UTF8, "application/json"); var content = new StringContent(BuildOrderWithInvalidExperationTime(), UTF8Encoding.UTF8, "application/json");
var response = await server.CreateClient() var response = await server.CreateIdempotentClient()
.PostAsync(Post.AddNewOrder, content); .PostAsync(Post.AddNewOrder, content);
Assert.True(response.StatusCode == System.Net.HttpStatusCode.BadRequest); Assert.True(response.StatusCode == System.Net.HttpStatusCode.BadRequest);
@ -102,5 +104,5 @@
return JsonConvert.SerializeObject(order); return JsonConvert.SerializeObject(order);
} }
} }
} }