From 2f3df2715d2deb5520044cb351144641b8d8f7c7 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 15 Mar 2017 08:57:01 -0700 Subject: [PATCH 1/9] Set data protection application discriminators This prevents cookie confusion when applications are hosted at the same domain and path. For example, under default settings, WebMVC may attempt to decrypt Identity's antiforgery cookie rather than its own. --- src/Services/Identity/Identity.API/Startup.cs | 5 +++++ src/Web/WebMVC/Startup.cs | 5 +++++ src/Web/WebSPA/Startup.cs | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/src/Services/Identity/Identity.API/Startup.cs b/src/Services/Identity/Identity.API/Startup.cs index d29459395..81c26eb16 100644 --- a/src/Services/Identity/Identity.API/Startup.cs +++ b/src/Services/Identity/Identity.API/Startup.cs @@ -54,6 +54,11 @@ namespace eShopOnContainers.Identity services.Configure(Configuration); + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.identity"; + }); + services.AddMvc(); services.AddTransient(); diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index ee2412bee..f6ac17e6f 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -42,6 +42,11 @@ namespace Microsoft.eShopOnContainers.WebMVC // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webmvc"; + }); + services.AddMvc(); services.Configure(Configuration); diff --git a/src/Web/WebSPA/Startup.cs b/src/Web/WebSPA/Startup.cs index a0f33d8b3..1386849f7 100644 --- a/src/Web/WebSPA/Startup.cs +++ b/src/Web/WebSPA/Startup.cs @@ -41,6 +41,11 @@ namespace eShopConContainers.WebSPA { services.Configure(Configuration); + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webspa"; + }); + services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); services.AddMvc() From eba2ea8bf9d09ceae4f40dacf2230763c7f2a4d3 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Mar 2017 10:19:11 -0700 Subject: [PATCH 2/9] Support IP address connection string in Basket.API Dns.GetHostAddressesAsync can return problematic results when passed an IP address, and if the connection string is already an IP address, we needn't call it anyway. --- .../Basket.API/Model/RedisBasketRepository.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs index fc5c256f8..393e9ae7b 100644 --- a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs +++ b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs @@ -94,13 +94,21 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model private async Task ConnectToRedisAsync() { - //TODO: Need to make this more robust. Also want to understand why the static connection method cannot accept dns names. - var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString); - _logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}"); - _redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString()); + // TODO: Need to make this more robust. ConnectionMultiplexer.ConnectAsync doesn't like domain names or IPv6 addresses. + if (IPAddress.TryParse(_settings.ConnectionString, out var ip)) + { + _redis = await ConnectionMultiplexer.ConnectAsync(ip.ToString()); + _logger.LogInformation($"Connecting to database at {_settings.ConnectionString}"); + } + else + { + // workaround for https://github.com/StackExchange/StackExchange.Redis/issues/410 + var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString); + _logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}"); + _redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString()); + } } - - + } } From 33281474afe907d5607e04168573755e30a28bc6 Mon Sep 17 00:00:00 2001 From: Cesar De la Torre Date: Sat, 8 Apr 2017 12:06:51 -0700 Subject: [PATCH 3/9] Minor change in label --- src/Web/WebMVC/Views/Shared/Components/CartList/Default.cshtml | 2 +- src/Web/WebSPA/Client/modules/basket/basket.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Web/WebMVC/Views/Shared/Components/CartList/Default.cshtml b/src/Web/WebMVC/Views/Shared/Components/CartList/Default.cshtml index 43d3b9f55..99a9bfc1e 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/WebSPA/Client/modules/basket/basket.component.html b/src/Web/WebSPA/Client/modules/basket/basket.component.html index a23a4e91f..3f27b0290 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 @@
- +
From c33513303da831e39d71507903a46ec54aeb6e5b Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Castro Date: Mon, 17 Apr 2017 12:28:12 +0200 Subject: [PATCH 4/9] Review on 17/04/2017 --- .../Basket/Basket.API/BasketSettings.cs | 7 +- .../InternalServerErrorObjectResult.cs | 4 - .../OrderStartedIntegrationEventHandler.cs | 5 +- ...ductPriceChangedIntegrationEventHandler.cs | 9 ++- .../Events/OrderStartedIntegrationEvent.cs | 4 - .../ProductPriceChangedIntegrationEvent.cs | 3 - .../Basket.API/Model/IBasketRepository.cs | 6 +- .../Basket.API/Model/RedisBasketRepository.cs | 20 ++--- src/Services/Basket/Basket.API/Startup.cs | 74 +++++++++++-------- .../Catalog/Catalog.API/CatalogSettings.cs | 9 +++ .../Controllers/CatalogController.cs | 49 ++++++------ .../Catalog.API/Controllers/HomeController.cs | 6 +- .../Catalog.API/Controllers/PicController.cs | 12 ++- .../InternalServerErrorObjectResult.cs | 4 - .../Exceptions/CatalogDomainException.cs | 3 - .../CatalogIntegrationEventService.cs | 1 + .../ICatalogIntegrationEventService.cs | 3 - .../Catalog/Catalog.API/Model/CatalogBrand.cs | 6 -- .../Catalog/Catalog.API/Model/CatalogType.cs | 5 -- src/Services/Catalog/Catalog.API/Startup.cs | 44 ++++++----- src/Services/Catalog/Catalog.API/settings.cs | 14 ---- .../Commands/IdentifierCommandHandler.cs | 6 +- .../Application/Queries/IOrderQueries.cs | 6 +- .../Application/Queries/OrderQueries.cs | 6 +- .../CreateOrderCommandValidator.cs | 24 +++--- .../Validations/IdentifierCommandValidator.cs | 6 +- .../Controllers/OrdersController.cs | 10 ++- src/Services/Ordering/Ordering.API/Startup.cs | 16 ++-- .../AggregatesModel/BuyerAggregate/Buyer.cs | 4 + .../OrderAggregate/IOrderRepository.cs | 4 +- .../Ordering.Domain/SeedWork/Enumeration.cs | 6 +- .../{ => Idempotency}/ClientRequest.cs | 4 +- .../Idempotency/IRequestManager.cs | 3 +- .../Idempotency/RequestManager.cs | 20 ++--- .../MediatorExtension.cs | 15 +++- .../OrderingContext.cs | 3 +- .../Ordering/Application/OrdersWebApiTest.cs | 6 +- 37 files changed, 209 insertions(+), 218 deletions(-) create mode 100644 src/Services/Catalog/Catalog.API/CatalogSettings.cs delete mode 100644 src/Services/Catalog/Catalog.API/settings.cs rename src/Services/Ordering/Ordering.Infrastructure/{ => Idempotency}/ClientRequest.cs (80%) diff --git a/src/Services/Basket/Basket.API/BasketSettings.cs b/src/Services/Basket/Basket.API/BasketSettings.cs index 6aae45015..9d143545a 100644 --- a/src/Services/Basket/Basket.API/BasketSettings.cs +++ b/src/Services/Basket/Basket.API/BasketSettings.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.Services.Basket.API +namespace Microsoft.eShopOnContainers.Services.Basket.API { public class BasketSettings { diff --git a/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs index 2ec3727a6..a0b988156 100644 --- a/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs +++ b/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -1,9 +1,5 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Basket.API.Infrastructure.ActionResults { diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs index e35badc64..19ae1b594 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs @@ -2,8 +2,6 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.Services.Basket.API.Model; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Basket.API.IntegrationEvents.EventHandling @@ -11,9 +9,10 @@ namespace Basket.API.IntegrationEvents.EventHandling public class OrderStartedIntegrationEventHandler : IIntegrationEventHandler { private readonly IBasketRepository _repository; + public OrderStartedIntegrationEventHandler(IBasketRepository repository) { - _repository = repository; + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); } public async Task Handle(OrderStartedIntegrationEvent @event) diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs index 50d6b9e25..88244404a 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs @@ -1,6 +1,7 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Basket.API.Model; +using System; using System.Linq; using System.Threading.Tasks; @@ -9,17 +10,20 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Even public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler { private readonly IBasketRepository _repository; + public ProductPriceChangedIntegrationEventHandler(IBasketRepository repository) { - _repository = repository; + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); } public async Task Handle(ProductPriceChangedIntegrationEvent @event) { - var userIds = await _repository.GetUsers(); + var userIds = await _repository.GetUsersAsync(); + foreach (var id in userIds) { var basket = await _repository.GetBasketAsync(id); + await UpdatePriceInBasketItems(@event.ProductId, @event.NewPrice, @event.OldPrice, basket); } } @@ -27,6 +31,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Even private async Task UpdatePriceInBasketItems(int productId, decimal newPrice, decimal oldPrice, CustomerBasket basket) { var itemsToUpdate = basket?.Items?.Where(x => int.Parse(x.ProductId) == productId).ToList(); + if (itemsToUpdate != null) { foreach (var item in itemsToUpdate) diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs b/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs index 3b5726e83..a32ad0beb 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs @@ -1,8 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Basket.API.IntegrationEvents.Events { diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs b/src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs index 87d2e9e81..6f51010be 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs @@ -1,7 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events { diff --git a/src/Services/Basket/Basket.API/Model/IBasketRepository.cs b/src/Services/Basket/Basket.API/Model/IBasketRepository.cs index 7f75ea342..fcdc69faa 100644 --- a/src/Services/Basket/Basket.API/Model/IBasketRepository.cs +++ b/src/Services/Basket/Basket.API/Model/IBasketRepository.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Basket.API.Model @@ -8,7 +6,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model public interface IBasketRepository { Task GetBasketAsync(string customerId); - Task> GetUsers(); + Task> GetUsersAsync(); Task UpdateBasketAsync(CustomerBasket basket); Task DeleteBasketAsync(string id); } diff --git a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs index 59ca0da03..7bfdbaada 100644 --- a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs +++ b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using StackExchange.Redis; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; -using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using System.Collections.Generic; +using System.Linq; using System.Net; +using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Basket.API.Model { @@ -31,7 +30,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model return await database.KeyDeleteAsync(id.ToString()); } - public async Task> GetUsers() + public async Task> GetUsersAsync() { var server = await GetServer(); @@ -63,11 +62,12 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model var created = await database.StringSetAsync(basket.BuyerId, JsonConvert.SerializeObject(basket)); if (!created) { - _logger.LogInformation("Problem persisting the item"); + _logger.LogInformation("Problem occur persisting the item."); return null; } - _logger.LogInformation("basket item persisted succesfully"); + _logger.LogInformation("Basket item persisted succesfully."); + return await GetBasketAsync(basket.BuyerId); } @@ -96,7 +96,9 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model { //TODO: Need to make this more robust. Also want to understand why the static connection method cannot accept dns names. var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString); + _logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}"); + _redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString()); } diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs index 11a6ebc97..f24818ab6 100644 --- a/src/Services/Basket/Basket.API/Startup.cs +++ b/src/Services/Basket/Basket.API/Startup.cs @@ -1,24 +1,23 @@ -using System.Linq; +using Basket.API.Infrastructure.Filters; +using Basket.API.IntegrationEvents.EventHandling; +using Basket.API.IntegrationEvents.Events; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +using Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server; +using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling; +using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; +using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.eShopOnContainers.Services.Basket.API.Model; -using StackExchange.Redis; -using Microsoft.Extensions.Options; -using System.Net; -using Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; -using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; -using System; using Microsoft.Extensions.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using System.Linq; +using System.Net; using System.Threading.Tasks; -using Basket.API.Infrastructure.Filters; -using Basket.API.IntegrationEvents.Events; -using Basket.API.IntegrationEvents.EventHandling; namespace Microsoft.eShopOnContainers.Services.Basket.API { @@ -59,17 +58,24 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API //and then creating the connection it seems reasonable to move //that cost to startup instead of having the first request pay the //penalty. - services.AddSingleton((sp) => { - var config = sp.GetRequiredService>().Value; - var ips = Dns.GetHostAddressesAsync(config.ConnectionString).Result; + services.AddSingleton(sp => { + var settings = sp.GetRequiredService>().Value; + var ips = Dns.GetHostAddressesAsync(settings.ConnectionString).Result; + return ConnectionMultiplexer.Connect(ips.First().ToString()); }); + services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + + return new EventBusRabbitMQ(settings.EventBusConnection); + }); + services.AddSwaggerGen(); - //var sch = new IdentitySecurityScheme(); + services.ConfigureSwaggerGen(options => { - //options.AddSecurityDefinition("IdentityServer", sch); options.OperationFilter(); options.DescribeAllEnumsAsStrings(); options.SingleApiVersion(new Swashbuckle.Swagger.Model.Info() @@ -95,10 +101,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API services.AddTransient, ProductPriceChangedIntegrationEventHandler>(); services.AddTransient, OrderStartedIntegrationEventHandler>(); - var serviceProvider = services.BuildServiceProvider(); - var configuration = serviceProvider.GetRequiredService>().Value; - services.AddSingleton(provider => new EventBusRabbitMQ(configuration.EventBusConnection)); - + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -119,11 +122,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API app.UseSwagger() .UseSwaggerUi(); - var catalogPriceHandler = app.ApplicationServices.GetService>(); - var orderStartedHandler = app.ApplicationServices.GetService>(); - var eventBus = app.ApplicationServices.GetRequiredService(); - eventBus.Subscribe(catalogPriceHandler); - eventBus.Subscribe(orderStartedHandler); + ConfigureEventBus(app); } @@ -136,6 +135,21 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API ScopeName = "basket", RequireHttpsMetadata = false }); - } + } + + protected virtual void ConfigureEventBus(IApplicationBuilder app) + { + var catalogPriceHandler = app.ApplicationServices + .GetService>(); + + var orderStartedHandler = app.ApplicationServices + .GetService>(); + + var eventBus = app.ApplicationServices + .GetRequiredService(); + + eventBus.Subscribe(catalogPriceHandler); + eventBus.Subscribe(orderStartedHandler); + } } } diff --git a/src/Services/Catalog/Catalog.API/CatalogSettings.cs b/src/Services/Catalog/Catalog.API/CatalogSettings.cs new file mode 100644 index 000000000..af6e0ab13 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/CatalogSettings.cs @@ -0,0 +1,9 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API +{ + public class CatalogSettings + { + public string ExternalCatalogBaseUrl {get;set;} + + public string EventBusConnection { get; set; } + } +} diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index 83a2de02d..bd5966a9c 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -1,9 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +using Catalog.API.IntegrationEvents; +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.Services; using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Catalog.API.Model; @@ -11,12 +8,8 @@ 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; -using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; -using Catalog.API.IntegrationEvents; namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { @@ -24,16 +17,16 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers public class CatalogController : ControllerBase { private readonly CatalogContext _catalogContext; - private readonly IOptionsSnapshot _settings; + private readonly CatalogSettings _settings; private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; - public CatalogController(CatalogContext Context, IOptionsSnapshot settings, ICatalogIntegrationEventService catalogIntegrationEventService) + public CatalogController(CatalogContext context, IOptionsSnapshot settings, ICatalogIntegrationEventService catalogIntegrationEventService) { - _catalogContext = Context; - _catalogIntegrationEventService = catalogIntegrationEventService; - _settings = settings; + _catalogContext = context ?? throw new ArgumentNullException(nameof(context)); + _catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService)); - ((DbContext)Context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + _settings = settings.Value; + ((DbContext)context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } // GET api/v1/[controller]/items[?pageSize=3&pageIndex=10] @@ -51,7 +44,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers .Take(pageSize) .ToListAsync(); - itemsOnPage = ComposePicUri(itemsOnPage); + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); var model = new PaginatedItemsViewModel( pageIndex, pageSize, totalItems, itemsOnPage); @@ -75,7 +68,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers .Take(pageSize) .ToListAsync(); - itemsOnPage = ComposePicUri(itemsOnPage); + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); var model = new PaginatedItemsViewModel( pageIndex, pageSize, totalItems, itemsOnPage); @@ -108,7 +101,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers .Take(pageSize) .ToListAsync(); - itemsOnPage = ComposePicUri(itemsOnPage); + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); var model = new PaginatedItemsViewModel( pageIndex, pageSize, totalItems, itemsOnPage); @@ -143,10 +136,17 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers [HttpPost] public async Task UpdateProduct([FromBody]CatalogItem productToUpdate) { - var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); - if (catalogItem == null) return NotFound(); - var raiseProductPriceChangedEvent = catalogItem.Price != productToUpdate.Price; + var catalogItem = await _catalogContext.CatalogItems + .SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); + + if (catalogItem == null) + { + return NotFound(); + } + var oldPrice = catalogItem.Price; + var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price; + // Update current product catalogItem = productToUpdate; @@ -205,13 +205,16 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers } _catalogContext.CatalogItems.Remove(product); + await _catalogContext.SaveChangesAsync(); return Ok(); } - private List ComposePicUri(List items) { - var baseUri = _settings.Value.ExternalCatalogBaseUrl; + private List ChangeUriPlaceholder(List items) + { + var baseUri = _settings.ExternalCatalogBaseUrl; + items.ForEach(x => { x.PictureUri = x.PictureUri.Replace("http://externalcatalogbaseurltobereplaced", baseUri); diff --git a/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs b/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs index e6ade8cc5..9401b56f9 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 diff --git a/src/Services/Catalog/Catalog.API/Controllers/PicController.cs b/src/Services/Catalog/Catalog.API/Controllers/PicController.cs index fa6b4ec94..8d8aaf9f2 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/PicController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/PicController.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using System.IO; -using Microsoft.AspNetCore.Hosting; // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 @@ -25,8 +21,10 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { var webRoot = _env.WebRootPath; var path = Path.Combine(webRoot, id + ".png"); - Byte[] b = System.IO.File.ReadAllBytes(path); - return File(b, "image/png"); + + var buffer = System.IO.File.ReadAllBytes(path); + + return File(buffer, "image/png"); } } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs index 3ac3d0f78..a6138b476 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -1,9 +1,5 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Catalog.API.Infrastructure.ActionResults { diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs b/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs index 0b27131cf..45295994e 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Catalog.API.Infrastructure.Exceptions { diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs index e6e48c54b..1b82251e3 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs @@ -30,6 +30,7 @@ namespace Catalog.API.IntegrationEvents public async Task PublishThroughEventBusAsync(IntegrationEvent evt) { _eventBus.Publish(evt); + await _eventLogService.MarkEventAsPublishedAsync(evt); } diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs index bb958eeaa..4d87e8e94 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs @@ -1,7 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Catalog.API.IntegrationEvents diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs b/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs index eb1fabf7d..84d72899e 100644 --- a/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs +++ b/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs @@ -1,11 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - - public class CatalogBrand { public int Id { get; set; } diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogType.cs b/src/Services/Catalog/Catalog.API/Model/CatalogType.cs index ac71914ff..0bc640dee 100644 --- a/src/Services/Catalog/Catalog.API/Model/CatalogType.cs +++ b/src/Services/Catalog/Catalog.API/Model/CatalogType.cs @@ -1,10 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - public class CatalogType { public int Id { get; set; } diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index f3f0735b2..e3f671652 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -1,6 +1,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API { using global::Catalog.API.Infrastructure.Filters; + using global::Catalog.API.IntegrationEvents; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; @@ -16,12 +17,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; - using System.Data.SqlClient; - using System.IO; using System.Data.Common; + using System.Data.SqlClient; using System.Reflection; - using global::Catalog.API.IntegrationEvents; - using System.Threading.Tasks; public class Startup { @@ -47,7 +45,7 @@ public void ConfigureServices(IServiceCollection services) { // Add framework services. - + services.AddHealthChecks(checks => { checks.AddSqlCheck("CatalogDb", Configuration["ConnectionString"]); @@ -62,18 +60,19 @@ { 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. options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval }); - services.Configure(Configuration); + services.Configure(Configuration); // Add framework services. services.AddSwaggerGen(); @@ -99,11 +98,18 @@ }); services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); - var serviceProvider = services.BuildServiceProvider(); - var configuration = serviceProvider.GetRequiredService>().Value; + sp => (DbConnection c) => new IntegrationEventLogService(c)); + services.AddTransient(); - services.AddSingleton(new EventBusRabbitMQ(configuration.EventBusConnection)); + + + var serviceProvider = services.BuildServiceProvider(); + + services.AddSingleton(sp => + { + var settings = serviceProvider.GetRequiredService>().Value; + return new EventBusRabbitMQ(settings.EventBusConnection); + }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) @@ -124,25 +130,28 @@ .ApplicationServices.GetService(typeof(CatalogContext)); WaitForSqlAvailability(context, loggerFactory); + //Seed Data CatalogContextSeed.SeedAsync(app, loggerFactory) - .Wait(); + .Wait(); var integrationEventLogContext = new IntegrationEventLogContext( new DbContextOptionsBuilder() .UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Catalog.API")) .Options); + integrationEventLogContext.Database.Migrate(); } private void WaitForSqlAvailability(CatalogContext ctx, ILoggerFactory loggerFactory, int? retry = 0) { - int retryForAvailability = retry.Value; + int retryForAvailability = retry.Value; + try { ctx.Database.OpenConnection(); } - catch(SqlException ex) + catch (SqlException ex) { if (retryForAvailability < 10) { @@ -152,11 +161,10 @@ WaitForSqlAvailability(ctx, loggerFactory, retryForAvailability); } } - finally { - ctx.Database.CloseConnection(); + finally + { + ctx.Database.CloseConnection(); } - - } } } diff --git a/src/Services/Catalog/Catalog.API/settings.cs b/src/Services/Catalog/Catalog.API/settings.cs deleted file mode 100644 index a6e959552..000000000 --- a/src/Services/Catalog/Catalog.API/settings.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.Services.Catalog.API -{ - // TODO: Rename CatalogSettings for consistency? - public class Settings - { - public string ExternalCatalogBaseUrl {get;set;} - public string EventBusConnection { get; set; } - } -} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs index de7dc4fea..529c1aa05 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs @@ -45,9 +45,11 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands return CreateResultForDuplicateRequest(); } else - { - var result = await _mediator.SendAsync(message.Command); + { await _requestManager.CreateRequestForCommandAsync(message.Id); + + var result = await _mediator.SendAsync(message.Command); + return result; } } diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs index f12a9e418..8d78524ea 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs @@ -4,10 +4,10 @@ public interface IOrderQueries { - Task GetOrder(int id); + Task GetOrderAsync(int id); - Task GetOrders(); + Task GetOrdersAsync(); - Task GetCardTypes(); + Task GetCardTypesAsync(); } } diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs index f10f76273..9d909e254 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs @@ -19,7 +19,7 @@ } - public async Task GetOrder(int id) + public async Task GetOrderAsync(int id) { using (var connection = new SqlConnection(_connectionString)) { @@ -44,7 +44,7 @@ } } - public async Task GetOrders() + public async Task GetOrdersAsync() { using (var connection = new SqlConnection(_connectionString)) { @@ -58,7 +58,7 @@ } } - public async Task GetCardTypes() + public async Task GetCardTypesAsync() { using (var connection = new SqlConnection(_connectionString)) { diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs index 449f95df5..e6bbdd4d9 100644 --- a/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs +++ b/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs @@ -1,10 +1,8 @@ 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 @@ -13,17 +11,17 @@ namespace Ordering.API.Application.Validations { 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"); + RuleFor(command => command.City).NotEmpty(); + RuleFor(command => command.Street).NotEmpty(); + RuleFor(command => command.State).NotEmpty(); + RuleFor(command => command.Country).NotEmpty(); + RuleFor(command => command.ZipCode).NotEmpty(); + RuleFor(command => command.CardNumber).NotEmpty().Length(12, 19); + RuleFor(command => command.CardHolderName).NotEmpty(); + RuleFor(command => command.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date"); + RuleFor(command => command.CardSecurityNumber).NotEmpty().Length(3); + RuleFor(command => command.CardTypeId).NotEmpty(); + RuleFor(command => command.OrderItems).Must(ContainOrderItems).WithMessage("No order items found"); } private bool BeValidExpirationDate(DateTime dateTime) diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs index 44b374ee6..e1482287f 100644 --- a/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs +++ b/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs @@ -1,9 +1,5 @@ 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 { @@ -11,7 +7,7 @@ namespace Ordering.API.Application.Validations { public IdentifierCommandValidator() { - RuleFor(customer => customer.Id).NotEmpty(); + RuleFor(command => command.Id).NotEmpty(); } } } diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index 4121e3214..d9a3752ed 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -57,7 +57,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers { try { - var order = await _orderQueries.GetOrder(orderId); + var order = await _orderQueries + .GetOrderAsync(orderId); + return Ok(order); } catch (KeyNotFoundException) @@ -70,7 +72,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers [HttpGet] public async Task GetOrders() { - var orders = await _orderQueries.GetOrders(); + var orders = await _orderQueries + .GetOrdersAsync(); return Ok(orders); } @@ -79,7 +82,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers [HttpGet] public async Task GetCardTypes() { - var cardTypes = await _orderQueries.GetCardTypes(); + var cardTypes = await _orderQueries + .GetCardTypesAsync(); return Ok(cardTypes); } diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index 6b120eb78..ae5c4c3ff 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -65,14 +65,14 @@ services.AddEntityFrameworkSqlServer() .AddDbContext(options => { - 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 //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) + 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 //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.Domain/AggregatesModel/BuyerAggregate/Buyer.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs index 84040d8a1..c806cebeb 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs @@ -16,6 +16,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B public IEnumerable PaymentMethods => _paymentMethods.AsReadOnly(); protected Buyer() { + _paymentMethods = new List(); } @@ -34,6 +35,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B if (existingPayment != null) { AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, existingPayment, orderId)); + return existingPayment; } else @@ -41,7 +43,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B var payment = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration); _paymentMethods.Add(payment); + AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, payment, orderId)); + return payment; } } diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs index a35188d8f..d7346ee4f 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs @@ -9,9 +9,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O public interface IOrderRepository : IRepository { Order Add(Order order); + + void Update(Order order); Task GetAsync(int orderId); - - void Update(Order order); } } diff --git a/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs b/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs index fd74ab1df..ec8dfd968 100644 --- a/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs +++ b/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs @@ -71,17 +71,17 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork public static T FromValue(int value) where T : Enumeration, new() { - var matchingItem = parse(value, "value", item => item.Id == value); + var matchingItem = Parse(value, "value", item => item.Id == value); return matchingItem; } public static T FromDisplayName(string displayName) where T : Enumeration, new() { - var matchingItem = parse(displayName, "display name", item => item.Name == displayName); + var matchingItem = Parse(displayName, "display name", item => item.Name == displayName); return matchingItem; } - private static T parse(K value, string description, Func predicate) where T : Enumeration, new() + private static T Parse(K value, string description, Func predicate) where T : Enumeration, new() { var matchingItem = GetAll().FirstOrDefault(predicate); diff --git a/src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/ClientRequest.cs similarity index 80% rename from src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs rename to src/Services/Ordering/Ordering.Infrastructure/Idempotency/ClientRequest.cs index 47a401aab..7ca49fa41 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/ClientRequest.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; -namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure +namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency { public class ClientRequest { diff --git a/src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs index a8a02f8ca..d38c23e09 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency @@ -8,6 +6,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempoten public interface IRequestManager { Task ExistAsync(Guid id); + Task CreateRequestForCommandAsync(Guid id); } } diff --git a/src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs index 0ef005161..6b6a96579 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs @@ -1,8 +1,5 @@ -using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; -using Ordering.Domain.Exceptions; +using Ordering.Domain.Exceptions; using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency @@ -10,22 +7,25 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempoten public class RequestManager : IRequestManager { private readonly OrderingContext _context; - public RequestManager(OrderingContext ctx) + + public RequestManager(OrderingContext context) { - _context = ctx; + _context = context ?? throw new ArgumentNullException(nameof(context)); } public async Task ExistAsync(Guid id) { - var request = await _context.FindAsync(id); + var request = await _context. + FindAsync(id); + return request != null; } public async Task CreateRequestForCommandAsync(Guid id) - { - + { var exists = await ExistAsync(id); + var request = exists ? throw new OrderingDomainException($"Request with {id} already exists") : new ClientRequest() @@ -36,8 +36,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempoten }; _context.Add(request); + await _context.SaveChangesAsync(); } - } } diff --git a/src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs b/src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs index 99e94e086..9135188cf 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs @@ -6,13 +6,20 @@ using System.Threading.Tasks; namespace Ordering.Infrastructure { - public static class MediatorExtension + static class MediatorExtension { public static async Task DispatchDomainEventsAsync(this IMediator mediator, OrderingContext ctx) { - var domainEntities = ctx.ChangeTracker.Entries().Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); - var domainEvents = domainEntities.SelectMany(x => x.Entity.DomainEvents).ToList(); - domainEntities.ToList().ForEach(entity => entity.Entity.DomainEvents.Clear()); + var domainEntities = ctx.ChangeTracker + .Entries() + .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); + + var domainEvents = domainEntities + .SelectMany(x => x.Entity.DomainEvents) + .ToList(); + + domainEntities.ToList() + .ForEach(entity => entity.Entity.DomainEvents.Clear()); var tasks = domainEvents .Select(async (domainEvent) => { diff --git a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs index 70bb8a51a..5b95ee23c 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; using Ordering.Infrastructure; using System; using System.Threading; @@ -34,7 +35,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure public OrderingContext(DbContextOptions options, IMediator mediator) : base(options) { - _mediator = mediator; + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs index e81840bb3..8c7659862 100644 --- a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs +++ b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs @@ -60,7 +60,7 @@ namespace UnitTest.Ordering.Application { //Arrange var fakeDynamicResult = new Object(); - _orderQueriesMock.Setup(x => x.GetOrders()) + _orderQueriesMock.Setup(x => x.GetOrdersAsync()) .Returns(Task.FromResult(fakeDynamicResult)); //Act @@ -77,7 +77,7 @@ namespace UnitTest.Ordering.Application //Arrange var fakeOrderId = 123; var fakeDynamicResult = new Object(); - _orderQueriesMock.Setup(x => x.GetOrder(It.IsAny())) + _orderQueriesMock.Setup(x => x.GetOrderAsync(It.IsAny())) .Returns(Task.FromResult(fakeDynamicResult)); //Act @@ -93,7 +93,7 @@ namespace UnitTest.Ordering.Application { //Arrange var fakeDynamicResult = new Object(); - _orderQueriesMock.Setup(x => x.GetCardTypes()) + _orderQueriesMock.Setup(x => x.GetCardTypesAsync()) .Returns(Task.FromResult(fakeDynamicResult)); //Act From c99e5e8e3bae3a8b5a9b41b8b88ed41712cd07ff Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Castro Date: Mon, 17 Apr 2017 15:00:53 +0200 Subject: [PATCH 5/9] Pending review files --- .../EventBus/EventBus/Abstractions/IEventBus.cs | 3 --- .../EventBus/Abstractions/IIntegrationEventHandler.cs | 3 --- .../EventBus/EventBus/Events/IntegrationEvent.cs | 2 -- .../EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs | 5 +++-- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs index af6ee1aa7..63f9f1b99 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs @@ -1,7 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions { diff --git a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs index d755bc066..828aed26a 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs @@ -1,7 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions diff --git a/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs b/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs index c9e60a0cf..e01a7aaa8 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events { diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs index 3388875ab..903601378 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs @@ -5,7 +5,6 @@ using Newtonsoft.Json; using RabbitMQ.Client; using RabbitMQ.Client.Events; using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -35,7 +34,9 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ public void Publish(IntegrationEvent @event) { - var eventName = @event.GetType().Name; + var eventName = @event.GetType() + .Name; + var factory = new ConnectionFactory() { HostName = _connectionString }; using (var connection = factory.CreateConnection()) using (var channel = connection.CreateModel()) From 9ed2f1ce7bd4101ee6939a93cfcadb004eb402a7 Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Tue, 18 Apr 2017 17:58:52 +0200 Subject: [PATCH 6/9] Catalog.API methods following more rest conventions: Create/Update routed by POST/PUT & Location header returned A new GET endpoint for returning single item by id created to honour Location header of previous methods. --- .../Controllers/CatalogController.cs | 72 ++++++++++++------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index bd5966a9c..374d8ec7c 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -39,7 +39,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers .LongCountAsync(); var itemsOnPage = await _catalogContext.CatalogItems - .OrderBy(c=>c.Name) + .OrderBy(c => c.Name) .Skip(pageSize * pageIndex) .Take(pageSize) .ToListAsync(); @@ -47,11 +47,29 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers itemsOnPage = ChangeUriPlaceholder(itemsOnPage); var model = new PaginatedItemsViewModel( - pageIndex, pageSize, totalItems, itemsOnPage); + pageIndex, pageSize, totalItems, itemsOnPage); return Ok(model); } + [HttpGet] + [Route("items/{id:int}")] + public async Task GetItemById(int id) + { + if (id <= 0) + { + return BadRequest(); + } + + var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == id); + if (item != null) + { + return Ok(item); + } + + return NotFound(); + } + // GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10] [HttpGet] [Route("[action]/withname/{name:minlength(1)}")] @@ -131,9 +149,9 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers return Ok(items); } - //POST api/v1/[controller]/update - [Route("update")] - [HttpPost] + //PUT api/v1/[controller]/items + [Route("items")] + [HttpPut] public async Task UpdateProduct([FromBody]CatalogItem productToUpdate) { var catalogItem = await _catalogContext.CatalogItems @@ -141,13 +159,13 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers if (catalogItem == null) { - return NotFound(); + return NotFound(new { Message = $"Item with id {productToUpdate.Id} not found." }); } var oldPrice = catalogItem.Price; var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price; - - + + // Update current product catalogItem = productToUpdate; _catalogContext.CatalogItems.Update(catalogItem); @@ -156,40 +174,40 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { //Create Integration Event to be published through the Event Bus var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); - + // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); - + // Publish through the Event Bus and mark the saved event as published await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); } else // Save updated product { await _catalogContext.SaveChangesAsync(); - } + } - return Ok(); + return CreatedAtAction(nameof(GetItemById), new { id = productToUpdate.Id }, null); } - //POST api/v1/[controller]/create - [Route("create")] + //POST api/v1/[controller]/items + [Route("items")] [HttpPost] public async Task CreateProduct([FromBody]CatalogItem product) { - _catalogContext.CatalogItems.Add( - new CatalogItem - { - CatalogBrandId = product.CatalogBrandId, - CatalogTypeId = product.CatalogTypeId, - Description = product.Description, - Name = product.Name, - PictureUri = product.PictureUri, - Price = product.Price - }); + var item = new CatalogItem + { + CatalogBrandId = product.CatalogBrandId, + CatalogTypeId = product.CatalogTypeId, + Description = product.Description, + Name = product.Name, + PictureUri = product.PictureUri, + Price = product.Price + }; + _catalogContext.CatalogItems.Add(item); await _catalogContext.SaveChangesAsync(); - return Ok(); + return CreatedAtAction(nameof(GetItemById), new { id = item.Id }, null); } //DELETE api/v1/[controller]/id @@ -202,13 +220,13 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers if (product == null) { return NotFound(); - } + } _catalogContext.CatalogItems.Remove(product); await _catalogContext.SaveChangesAsync(); - return Ok(); + return NoContent(); } private List ChangeUriPlaceholder(List items) From 39813424f99ec8c0bae463d2e8638966995615ec Mon Sep 17 00:00:00 2001 From: Eduard Tomas Date: Wed, 19 Apr 2017 15:45:32 +0200 Subject: [PATCH 7/9] Solves error building SPA on linux host due to case error --- src/Web/WebSPA/Client/modules/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/WebSPA/Client/modules/app.module.ts b/src/Web/WebSPA/Client/modules/app.module.ts index 997eacbfb..f05d466d8 100644 --- a/src/Web/WebSPA/Client/modules/app.module.ts +++ b/src/Web/WebSPA/Client/modules/app.module.ts @@ -2,7 +2,7 @@ import { NgModule, NgModuleFactoryLoader } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; // import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; -import { RouterModule } from '@angular/Router'; +import { RouterModule } from '@angular/router'; import { routing } from './app.routes'; import { AppService } from './app.service'; From 09a53f01376de824ff4eb391c87a0cd88f1b9bd8 Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Castro Date: Thu, 20 Apr 2017 10:53:17 +0200 Subject: [PATCH 8/9] Added IRabbitMQPersisterConnection and more resilient work on rabbitmq event bus --- .../DefaultRabbitMQPersisterConnection.cs | 130 +++++++++++++ .../EventBusRabbitMQ/EventBusRabbitMQ.cs | 177 +++++++++++------- .../EventBusRabbitMQ/EventBusRabbitMQ.csproj | 2 + .../IRabbitMQPersisterConnection.cs | 16 ++ src/Services/Basket/Basket.API/Startup.cs | 20 +- src/Services/Catalog/Catalog.API/Startup.cs | 18 +- src/Services/Ordering/Ordering.API/Startup.cs | 17 +- 7 files changed, 296 insertions(+), 84 deletions(-) create mode 100644 src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs create mode 100644 src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs new file mode 100644 index 000000000..894afb4e4 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs @@ -0,0 +1,130 @@ +using Microsoft.Extensions.Logging; +using Polly; +using Polly.Retry; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; +using System; +using System.IO; +using System.Net.Sockets; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ +{ + public class DefaultRabbitMQPersisterConnection + : IRabbitMQPersisterConnection + { + private readonly IConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + IConnection _connection; + bool _disposed; + + object sync_root = new object(); + + public DefaultRabbitMQPersisterConnection(IConnectionFactory connectionFactory,ILogger logger) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public bool IsConnected + { + get + { + return _connection != null && _connection.IsOpen && !_disposed; + } + } + + public IModel CreateModel() + { + if (!IsConnected) + { + throw new InvalidOperationException("No RabbitMQ connections are available to perform this action"); + } + + return _connection.CreateModel(); + } + + public void Dispose() + { + if (_disposed) return; + + _disposed = true; + + try + { + _connection.Dispose(); + } + catch (IOException ex) + { + _logger.LogCritical(ex.ToString()); + } + } + + public bool TryConnect() + { + _logger.LogInformation("RabbitMQ Client is trying to connect"); + + lock (sync_root) + { + var policy = RetryPolicy.Handle() + .Or() + .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => + { + _logger.LogWarning(ex.ToString()); + } + ); + + policy.Execute(() => + { + _connection = _connectionFactory + .CreateConnection(); + }); + + if (IsConnected) + { + _connection.ConnectionShutdown += OnConnectionShutdown; + _connection.CallbackException += OnCallbackException; + _connection.ConnectionBlocked += OnConnectionBlocked; + + _logger.LogInformation($"RabbitMQ persister connection acquire a connection {_connection.Endpoint.HostName} and is subscribed to failure events"); + + return true; + } + else + { + _logger.LogCritical("FATAL ERROR: RabbitMQ connections can't be created and opened"); + + return false; + } + } + } + + private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) + { + if (_disposed) return; + + _logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect..."); + + TryConnect(); + } + + void OnCallbackException(object sender, CallbackExceptionEventArgs e) + { + if (_disposed) return; + + _logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect..."); + + TryConnect(); + } + + void OnConnectionShutdown(object sender, ShutdownEventArgs reason) + { + if (_disposed) return; + + _logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect..."); + + TryConnect(); + } + } +} diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs index 903601378..cbdb233f8 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs @@ -1,12 +1,16 @@ - -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Polly; +using Polly.Retry; using RabbitMQ.Client; using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; using System; using System.Collections.Generic; using System.Linq; +using System.Net.Sockets; using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -15,70 +19,97 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ { public class EventBusRabbitMQ : IEventBus, IDisposable { - private readonly string _brokerName = "eshop_event_bus"; - private readonly string _connectionString; - private readonly Dictionary> _handlers; - private readonly List _eventTypes; + const string BROKER_NAME = "eshop_event_bus"; - private IModel _model; - private IConnection _connection; - private string _queueName; - + private readonly IRabbitMQPersisterConnection _persisterConnection; + private readonly ILogger _logger; - public EventBusRabbitMQ(string connectionString) + private readonly Dictionary> _handlers + = new Dictionary>(); + + private readonly List _eventTypes + = new List(); + + private IModel _consumerChannel; + + public EventBusRabbitMQ(IRabbitMQPersisterConnection persisterConnection, ILogger logger) { - _connectionString = connectionString; - _handlers = new Dictionary>(); - _eventTypes = new List(); + _persisterConnection = persisterConnection ?? throw new ArgumentNullException(nameof(persisterConnection)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _consumerChannel = CreateConsumerChannel(); } + public void Publish(IntegrationEvent @event) { - var eventName = @event.GetType() - .Name; - - var factory = new ConnectionFactory() { HostName = _connectionString }; - using (var connection = factory.CreateConnection()) - using (var channel = connection.CreateModel()) + if (!_persisterConnection.IsConnected) { - channel.ExchangeDeclare(exchange: _brokerName, + _persisterConnection.TryConnect(); + } + + var policy = RetryPolicy.Handle() + .Or() + .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => + { + _logger.LogWarning(ex.ToString()); + }); + + using (var channel = _persisterConnection.CreateModel()) + { + var eventName = @event.GetType() + .Name; + + channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); - string message = JsonConvert.SerializeObject(@event); + var message = JsonConvert.SerializeObject(@event); var body = Encoding.UTF8.GetBytes(message); - channel.BasicPublish(exchange: _brokerName, + policy.Execute(() => + { + channel.BasicPublish(exchange: BROKER_NAME, routingKey: eventName, basicProperties: null, - body: body); + body: body); + }); } - } public void Subscribe(IIntegrationEventHandler handler) where T : IntegrationEvent { var eventName = typeof(T).Name; - if (_handlers.ContainsKey(eventName)) + + if (_handlers.ContainsKey(eventName)) { _handlers[eventName].Add(handler); } else { - var channel = GetChannel(); - channel.QueueBind(queue: _queueName, - exchange: _brokerName, - routingKey: eventName); - - _handlers.Add(eventName, new List()); - _handlers[eventName].Add(handler); - _eventTypes.Add(typeof(T)); + if (!_persisterConnection.IsConnected) + { + _persisterConnection.TryConnect(); + } + + using (var channel = _persisterConnection.CreateModel()) + { + channel.QueueBind(queue: channel.QueueDeclare().QueueName, + exchange: BROKER_NAME, + routingKey: eventName); + + _handlers.Add(eventName, new List()); + _handlers[eventName].Add(handler); + _eventTypes.Add(typeof(T)); + } + } - + } public void Unsubscribe(IIntegrationEventHandler handler) where T : IntegrationEvent { var eventName = typeof(T).Name; + if (_handlers.ContainsKey(eventName) && _handlers[eventName].Contains(handler)) { _handlers[eventName].Remove(handler); @@ -86,56 +117,46 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ if (_handlers[eventName].Count == 0) { _handlers.Remove(eventName); - var eventType = _eventTypes.Single(e => e.Name == eventName); - _eventTypes.Remove(eventType); - _model.QueueUnbind(queue: _queueName, - exchange: _brokerName, - routingKey: eventName); - if (_handlers.Keys.Count == 0) + var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); + + if (eventType != null) { - _queueName = string.Empty; - _model.Dispose(); - _connection.Dispose(); + _eventTypes.Remove(eventType); + + if (!_persisterConnection.IsConnected) + { + _persisterConnection.TryConnect(); + } + + using (var channel = _persisterConnection.CreateModel()) + { + channel.QueueUnbind(queue: channel.QueueDeclare().QueueName, + exchange: BROKER_NAME, + routingKey: eventName); + } } - } } } public void Dispose() { + _consumerChannel.Dispose(); _handlers.Clear(); - _model?.Dispose(); - _connection?.Dispose(); } - private IModel GetChannel() + private IModel CreateConsumerChannel() { - if (_model != null) + if (!_persisterConnection.IsConnected) { - return _model; + _persisterConnection.TryConnect(); } - else - { - (_model, _connection) = CreateConnection(); - return _model; - } - } + var channel = _persisterConnection.CreateModel(); - private (IModel model, IConnection connection) CreateConnection() - { - var factory = new ConnectionFactory() { HostName = _connectionString }; - var con = factory.CreateConnection(); - var channel = con.CreateModel(); - - channel.ExchangeDeclare(exchange: _brokerName, - type: "direct"); - if (string.IsNullOrEmpty(_queueName)) - { - _queueName = channel.QueueDeclare().QueueName; - } + channel.ExchangeDeclare(exchange: BROKER_NAME, + type: "direct"); var consumer = new EventingBasicConsumer(channel); consumer.Received += async (model, ea) => @@ -145,11 +166,24 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ await ProcessEvent(eventName, message); }; - channel.BasicConsume(queue: _queueName, + + channel.BasicConsume(queue: channel.QueueDeclare().QueueName, noAck: true, consumer: consumer); - return (channel, con); + channel.ModelShutdown += (sender, ea) => + { + _consumerChannel.Dispose(); + _consumerChannel = CreateConsumerChannel(); + }; + + channel.CallbackException += (sender, ea) => + { + _consumerChannel.Dispose(); + _consumerChannel = CreateConsumerChannel(); + }; + + return channel; } private async Task ProcessEvent(string eventName, string message) @@ -157,7 +191,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ if (_handlers.ContainsKey(eventName)) { Type eventType = _eventTypes.Single(t => t.Name == eventName); - var integrationEvent = JsonConvert.DeserializeObject(message, eventType); + var integrationEvent = JsonConvert.DeserializeObject(message, eventType); var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); var handlers = _handlers[eventName]; @@ -167,6 +201,5 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ } } } - } } diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj index cf36a2222..023a5d5ec 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj @@ -7,7 +7,9 @@ + + diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs new file mode 100644 index 000000000..b9debe743 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs @@ -0,0 +1,16 @@ +using RabbitMQ.Client; +using System; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ +{ + + public interface IRabbitMQPersisterConnection + : IDisposable + { + bool IsConnected { get; } + + bool TryConnect(); + + IModel CreateModel(); + } +} diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs index f24818ab6..60fc46de2 100644 --- a/src/Services/Basket/Basket.API/Startup.cs +++ b/src/Services/Basket/Basket.API/Startup.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using RabbitMQ.Client; using StackExchange.Redis; using System.Linq; using System.Net; @@ -58,22 +59,31 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API //and then creating the connection it seems reasonable to move //that cost to startup instead of having the first request pay the //penalty. - services.AddSingleton(sp => { + services.AddSingleton(sp => + { var settings = sp.GetRequiredService>().Value; var ips = Dns.GetHostAddressesAsync(settings.ConnectionString).Result; return ConnectionMultiplexer.Connect(ips.First().ToString()); }); - services.AddSingleton(sp => + + services.AddSingleton(sp => { var settings = sp.GetRequiredService>().Value; + var logger = sp.GetRequiredService>(); + var factory = new ConnectionFactory() + { + HostName = settings.EventBusConnection + }; - return new EventBusRabbitMQ(settings.EventBusConnection); + return new DefaultRabbitMQPersisterConnection(factory, logger); }); + services.AddSingleton(); + services.AddSwaggerGen(); - + services.ConfigureSwaggerGen(options => { options.OperationFilter(); @@ -101,7 +111,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API services.AddTransient, ProductPriceChangedIntegrationEventHandler>(); services.AddTransient, OrderStartedIntegrationEventHandler>(); - + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index e3f671652..c13ac2d1b 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using RabbitMQ.Client; using System; using System.Data.Common; using System.Data.SqlClient; @@ -102,14 +103,19 @@ services.AddTransient(); - - var serviceProvider = services.BuildServiceProvider(); - - services.AddSingleton(sp => + services.AddSingleton(sp => { - var settings = serviceProvider.GetRequiredService>().Value; - return new EventBusRabbitMQ(settings.EventBusConnection); + var settings = sp.GetRequiredService>().Value; + var logger = sp.GetRequiredService>(); + var factory = new ConnectionFactory() + { + HostName = settings.EventBusConnection + }; + + return new DefaultRabbitMQPersisterConnection(factory, logger); }); + + services.AddSingleton(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index ae5c4c3ff..0d6e222b6 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; using Ordering.Infrastructure; + using RabbitMQ.Client; using System; using System.Data.Common; using System.Reflection; @@ -105,7 +106,21 @@ sp => (DbConnection c) => new IntegrationEventLogService(c)); var serviceProvider = services.BuildServiceProvider(); services.AddTransient(); - services.AddSingleton(new EventBusRabbitMQ(Configuration["EventBusConnection"])); + + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + var factory = new ConnectionFactory() + { + HostName = Configuration["EventBusConnection"] + }; + + return new DefaultRabbitMQPersisterConnection(factory, logger); + }); + + services.AddSingleton(); + services.AddOptions(); //configure autofac From 70f50a0fce82ed65a1de2902e46890f9c021ead3 Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Castro Date: Thu, 20 Apr 2017 16:44:07 +0200 Subject: [PATCH 9/9] Fix bug with queue names --- .../EventBusRabbitMQ/EventBusRabbitMQ.cs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs index cbdb233f8..e7a493c10 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs @@ -31,6 +31,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ = new List(); private IModel _consumerChannel; + private string _queueName; public EventBusRabbitMQ(IRabbitMQPersisterConnection persisterConnection, ILogger logger) { @@ -93,7 +94,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ using (var channel = _persisterConnection.CreateModel()) { - channel.QueueBind(queue: channel.QueueDeclare().QueueName, + channel.QueueBind(queue: _queueName, exchange: BROKER_NAME, routingKey: eventName); @@ -131,9 +132,16 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ using (var channel = _persisterConnection.CreateModel()) { - channel.QueueUnbind(queue: channel.QueueDeclare().QueueName, + channel.QueueUnbind(queue: _queueName, exchange: BROKER_NAME, routingKey: eventName); + + if (_handlers.Keys.Count == 0) + { + _queueName = string.Empty; + + _consumerChannel.Close(); + } } } } @@ -142,7 +150,11 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ public void Dispose() { - _consumerChannel.Dispose(); + if (_consumerChannel != null) + { + _consumerChannel.Dispose(); + } + _handlers.Clear(); } @@ -158,6 +170,8 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); + _queueName = channel.QueueDeclare().QueueName; + var consumer = new EventingBasicConsumer(channel); consumer.Received += async (model, ea) => { @@ -167,16 +181,10 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ await ProcessEvent(eventName, message); }; - channel.BasicConsume(queue: channel.QueueDeclare().QueueName, + channel.BasicConsume(queue: _queueName, noAck: true, consumer: consumer); - channel.ModelShutdown += (sender, ea) => - { - _consumerChannel.Dispose(); - _consumerChannel = CreateConsumerChannel(); - }; - channel.CallbackException += (sender, ea) => { _consumerChannel.Dispose();