* Seeking feedback: Build and run on .Net 6 preview 6 (#1734) * Updgrade build and hosting machines to .net6 latest * Target .net 6 * ILogger is ambiguous? * More ILogger ambiguity * Use preview 6... seeing errors in preview 7... * Of course the SDK version is different :) * downgrade the last nonworking component * Only restore the packages we need for the one off service stuck in .net 5 * Downgrade development docker files to use the preview 6 sdk * Updates `basket-api` to .NET 6 (#1742) * Use global usings * Use file-scoped namespaces * Updates docker images to preview 7 * Created a new migration plan * Included global usings for identity project * Updated docker file to preview version to 7 * Updated dockerfiles * Merged conent from Startup.cs to Program.cs * Removed Starup.cs * Removed unnecessary files * Revert "Removed unnecessary files" This reverts commitpull/1793/head536bddcd96
. * Revert "Removed Starup.cs" This reverts commit46175d7aa9
. * Revert "Merged conent from Startup.cs to Program.cs" This reverts commit2766ea86df
. * Removed extra spaces * Updated basket-api project file * Update src/Services/Basket/Basket.API/Grpc/BasketService.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Moved the fully qualified namespace on top * Updated relevant packages in basket.api project * Updated relevant packages in identity.api project Co-authored-by: David Pine <david.pine@microsoft.com> * Updates all the services to .NET 6.0 (#1770) * Created global using file for catalog.api * Moved individual usings statements to globalusing * Updated catalog.api project * Fixed local run bug for catalog.api * Included globalusing for payment.api * Refactored namespace statement for payment.api * Moved namespaces to ordering.domain project * Included globalusing for ordering.domain project * Included globalusings for ordering.infrastructure project * Refactored namespaces for ordering.infrastructure project * Updated relevant packages in ordering.infrastructure project * Included globalusings for ordering.signalrHub project * Moved all the namespace to globalusings * Updated packages in ordering.signalrHub csproj file * Refactored namespace statements in catalog.api project * Fixed namespace name in ordering.domain * Included global usings for ordering.api project * Moved all usings to globalusing file * Updated ordering.api csproj project * Fixed bug in statup.cs * Updated ordering.unittests.csproj file * Included globalusings in webhooks.api project * Moved using statements to globalusing file in webhooks.api * Included globalusing for web.bff.shoppping aggregator project * Moved namespaces to globalusing shopping aggregator * Included globalusing mobile.bff.shoppping project * Moved namespaces to globalusing file * Included globalusing for eventbus project * Moved namespaces to global usings for eventbus * Included globalusing for EventBusRabbitMQ project * Moved using statements to EventBusRabbitMQ project * Included global using in EventBusServiceBus project * Moved using statements to globalusing for EventBusServiceBus * Included globalusing file for IntegrationEventLogEF project * Move using statements to globalusing file * Updated packages of IntegrationEventLogEF project * Included globalusing to Devspaces.Support project * Moved using statements to globalusing Devspaces * Updated dependent packages for Devspaces.Support.csproj * Fixed bug in Basket API * Fixed bug in catalog.api * Fixed bug Identity.API * Included globalusing to Basket.UnitTest project * Moved namespaces to Basket.UnitTest project * Updated packages of Basket.UnitTest csproj * Included globalusing for Basket.FunctionalTests project * Included file-scoped namespaces Basket.FunctionalTests * Updated packages of Basket.FunctionalTests.csproj file * Updated catalog unit test project to Net 6.0 * Included global usings for Catalog.FunctionalTests * Included file-scope namespace catalog.functionaltests * Updated packages of catalog.functionaltest csproj * Included MigrateDbContext method in HostExtensions * Included globalusing for ordering.UnitTests project * Included file-scope statement for Ordering.UnitTest project * Included globalusing for Ordering.FunctionalTests * Included file-scope namespace statement for using * Updated packages in Ordering.FunctionalTests.csproj * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.API/Startup.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToSubmittedIntegrationEventHandler.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> Co-authored-by: David Pine <david.pine@microsoft.com> * Updates WebMVC to .NET 6.0 (#1773) * Included globalusing WebMVC * Included file scope namespaces for all files * Updated dockerfile * Updated packages to WebMVC * Fixes few bugs in Net 6.0 service migration (#1774) * Created global using file for catalog.api * Moved individual usings statements to globalusing * Updated catalog.api project * Fixed local run bug for catalog.api * Included globalusing for payment.api * Refactored namespace statement for payment.api * Moved namespaces to ordering.domain project * Included globalusing for ordering.domain project * Included globalusings for ordering.infrastructure project * Refactored namespaces for ordering.infrastructure project * Updated relevant packages in ordering.infrastructure project * Included globalusings for ordering.signalrHub project * Moved all the namespace to globalusings * Updated packages in ordering.signalrHub csproj file * Refactored namespace statements in catalog.api project * Fixed namespace name in ordering.domain * Included global usings for ordering.api project * Moved all usings to globalusing file * Updated ordering.api csproj project * Fixed bug in statup.cs * Updated ordering.unittests.csproj file * Included globalusings in webhooks.api project * Moved using statements to globalusing file in webhooks.api * Included globalusing for web.bff.shoppping aggregator project * Moved namespaces to globalusing shopping aggregator * Included globalusing mobile.bff.shoppping project * Moved namespaces to globalusing file * Included globalusing for eventbus project * Moved namespaces to global usings for eventbus * Included globalusing for EventBusRabbitMQ project * Moved using statements to EventBusRabbitMQ project * Included global using in EventBusServiceBus project * Moved using statements to globalusing for EventBusServiceBus * Included globalusing file for IntegrationEventLogEF project * Move using statements to globalusing file * Updated packages of IntegrationEventLogEF project * Included globalusing to Devspaces.Support project * Moved using statements to globalusing Devspaces * Updated dependent packages for Devspaces.Support.csproj * Fixed bug in Basket API * Fixed bug in catalog.api * Fixed bug Identity.API * Included globalusing to Basket.UnitTest project * Moved namespaces to Basket.UnitTest project * Updated packages of Basket.UnitTest csproj * Included globalusing for Basket.FunctionalTests project * Included file-scoped namespaces Basket.FunctionalTests * Updated packages of Basket.FunctionalTests.csproj file * Updated catalog unit test project to Net 6.0 * Included global usings for Catalog.FunctionalTests * Included file-scope namespace catalog.functionaltests * Updated packages of catalog.functionaltest csproj * Included MigrateDbContext method in HostExtensions * Included globalusing for ordering.UnitTests project * Included file-scope statement for Ordering.UnitTest project * Included globalusing for Ordering.FunctionalTests * Included file-scope namespace statement for using * Updated packages in Ordering.FunctionalTests.csproj * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.API/Startup.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToSubmittedIntegrationEventHandler.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Fixed bugs in Mobile.BFF.Shopping project * Fixed bugs in Web.Bff.Shopping aggregator project * Fixed bugs in EventBusServiceBus project * Fixed bug in Mobile.Bff.Shopping project Co-authored-by: David Pine <david.pine@microsoft.com> * Updates webhook client project to .NET 6.0 (#1777) * Included globalusing file for webhookclient * Included file scope namespaces for Webhookclient * Updated packages in WebHookClient project * Updates webspa project to Net 6.0 (#1778) * Included globalusing in webspa project * Included file scoped namespace for webspa project * Updated packages in WebSPA project * Updates the Application.FunctionalTests project to .NET 6.0 (#1781) * Included globalusing in Application.FunctionalTests project * Included file scoped namespace * Renamed Azure.Messaging.ServiceBus namespace * Updates .NET version of Dockerfile to 6.0 (#1785) * Updatated package versions to RC2 * Updated package versions to RC2 * Updated Dockerfiles to .NET 6 RC2 * Changed docker file tag to 6.0 * Updated Program class * Updated globalusing file * Removed preview tag reference from Dockerfile.develop file * Updated dotnet version to .NET 6.0 * Updated all packages to the .NET 6.0 * Removed RC tag from dockerfile * Fixed bundleconfig json * Updated readme files * Fixed ingress yaml indentation * Included globalusing for WebStatus project * Updated WebStatus project to .NET 6.0 * Included scoped namespace * Updated Dockerfile of WebStatus to .NET 6.0 Co-authored-by: Josh Coleman <83677148+JcolemanNR@users.noreply.github.com> Co-authored-by: David Pine <david.pine@microsoft.com>
@ -1,38 +1,35 @@ | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config | |||||
public class UrlsConfig | |||||
{ | { | ||||
public class UrlsConfig | |||||
public class CatalogOperations | |||||
{ | { | ||||
public class CatalogOperations | |||||
{ | |||||
public static string GetItemById(int id) => $"/api/v1/catalog/items/{id}"; | |||||
public static string GetItemById(int id) => $"/api/v1/catalog/items/{id}"; | |||||
public static string GetItemsById(IEnumerable<int> ids) => $"/api/v1/catalog/items?ids={string.Join(',', ids)}"; | |||||
} | |||||
public static string GetItemsById(IEnumerable<int> ids) => $"/api/v1/catalog/items?ids={string.Join(',', ids)}"; | |||||
} | |||||
public class BasketOperations | |||||
{ | |||||
public static string GetItemById(string id) => $"/api/v1/basket/{id}"; | |||||
public class BasketOperations | |||||
{ | |||||
public static string GetItemById(string id) => $"/api/v1/basket/{id}"; | |||||
public static string UpdateBasket() => "/api/v1/basket"; | |||||
} | |||||
public static string UpdateBasket() => "/api/v1/basket"; | |||||
} | |||||
public class OrdersOperations | |||||
{ | |||||
public static string GetOrderDraft() => "/api/v1/orders/draft"; | |||||
} | |||||
public class OrdersOperations | |||||
{ | |||||
public static string GetOrderDraft() => "/api/v1/orders/draft"; | |||||
} | |||||
public string Basket { get; set; } | |||||
public string Basket { get; set; } | |||||
public string Catalog { get; set; } | |||||
public string Catalog { get; set; } | |||||
public string Orders { get; set; } | |||||
public string Orders { get; set; } | |||||
public string GrpcBasket { get; set; } | |||||
public string GrpcBasket { get; set; } | |||||
public string GrpcCatalog { get; set; } | |||||
public string GrpcCatalog { get; set; } | |||||
public string GrpcOrdering { get; set; } | |||||
} | |||||
public string GrpcOrdering { get; set; } | |||||
} | } |
@ -1,156 +1,145 @@ | |||||
using Microsoft.AspNetCore.Authorization; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
using System; | |||||
using System.Linq; | |||||
using System.Net; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers; | |||||
[Route("api/v1/[controller]")] | |||||
[Authorize] | |||||
[ApiController] | |||||
public class BasketController : ControllerBase | |||||
{ | { | ||||
[Route("api/v1/[controller]")] | |||||
[Authorize] | |||||
[ApiController] | |||||
public class BasketController : ControllerBase | |||||
private readonly ICatalogService _catalog; | |||||
private readonly IBasketService _basket; | |||||
public BasketController(ICatalogService catalogService, IBasketService basketService) | |||||
{ | { | ||||
private readonly ICatalogService _catalog; | |||||
private readonly IBasketService _basket; | |||||
_catalog = catalogService; | |||||
_basket = basketService; | |||||
} | |||||
public BasketController(ICatalogService catalogService, IBasketService basketService) | |||||
[HttpPost] | |||||
[HttpPut] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data) | |||||
{ | |||||
if (data.Items == null || !data.Items.Any()) | |||||
{ | { | ||||
_catalog = catalogService; | |||||
_basket = basketService; | |||||
return BadRequest("Need to pass at least one basket line"); | |||||
} | } | ||||
[HttpPost] | |||||
[HttpPut] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data) | |||||
// Retrieve the current basket | |||||
var basket = await _basket.GetByIdAsync(data.BuyerId) ?? new BasketData(data.BuyerId); | |||||
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId)); | |||||
// group by product id to avoid duplicates | |||||
var itemsCalculated = data | |||||
.Items | |||||
.GroupBy(x => x.ProductId, x => x, (k, i) => new { productId = k, items = i }) | |||||
.Select(groupedItem => | |||||
{ | |||||
var item = groupedItem.items.First(); | |||||
item.Quantity = groupedItem.items.Sum(i => i.Quantity); | |||||
return item; | |||||
}); | |||||
foreach (var bitem in itemsCalculated) | |||||
{ | { | ||||
if (data.Items == null || !data.Items.Any()) | |||||
var catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId); | |||||
if (catalogItem == null) | |||||
{ | { | ||||
return BadRequest("Need to pass at least one basket line"); | |||||
return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})"); | |||||
} | } | ||||
// Retrieve the current basket | |||||
var basket = await _basket.GetById(data.BuyerId) ?? new BasketData(data.BuyerId); | |||||
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId)); | |||||
// group by product id to avoid duplicates | |||||
var itemsCalculated = data | |||||
.Items | |||||
.GroupBy(x => x.ProductId, x => x, (k, i) => new { productId = k, items = i }) | |||||
.Select(groupedItem => | |||||
{ | |||||
var item = groupedItem.items.First(); | |||||
item.Quantity = groupedItem.items.Sum(i => i.Quantity); | |||||
return item; | |||||
}); | |||||
foreach (var bitem in itemsCalculated) | |||||
var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId); | |||||
if (itemInBasket == null) | |||||
{ | { | ||||
var catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId); | |||||
if (catalogItem == null) | |||||
{ | |||||
return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})"); | |||||
} | |||||
var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId); | |||||
if (itemInBasket == null) | |||||
{ | |||||
basket.Items.Add(new BasketDataItem() | |||||
{ | |||||
Id = bitem.Id, | |||||
ProductId = catalogItem.Id, | |||||
ProductName = catalogItem.Name, | |||||
PictureUrl = catalogItem.PictureUri, | |||||
UnitPrice = catalogItem.Price, | |||||
Quantity = bitem.Quantity | |||||
}); | |||||
} | |||||
else | |||||
basket.Items.Add(new BasketDataItem() | |||||
{ | { | ||||
itemInBasket.Quantity = bitem.Quantity; | |||||
} | |||||
Id = bitem.Id, | |||||
ProductId = catalogItem.Id, | |||||
ProductName = catalogItem.Name, | |||||
PictureUrl = catalogItem.PictureUri, | |||||
UnitPrice = catalogItem.Price, | |||||
Quantity = bitem.Quantity | |||||
}); | |||||
} | } | ||||
else | |||||
{ | |||||
itemInBasket.Quantity = bitem.Quantity; | |||||
} | |||||
} | |||||
await _basket.UpdateAsync(basket); | |||||
await _basket.UpdateAsync(basket); | |||||
return basket; | |||||
} | |||||
return basket; | |||||
[HttpPut] | |||||
[Route("items")] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data) | |||||
{ | |||||
if (!data.Updates.Any()) | |||||
{ | |||||
return BadRequest("No updates sent"); | |||||
} | } | ||||
[HttpPut] | |||||
[Route("items")] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data) | |||||
// Retrieve the current basket | |||||
var currentBasket = await _basket.GetByIdAsync(data.BasketId); | |||||
if (currentBasket == null) | |||||
{ | { | ||||
if (!data.Updates.Any()) | |||||
{ | |||||
return BadRequest("No updates sent"); | |||||
} | |||||
return BadRequest($"Basket with id {data.BasketId} not found."); | |||||
} | |||||
// Retrieve the current basket | |||||
var currentBasket = await _basket.GetById(data.BasketId); | |||||
if (currentBasket == null) | |||||
{ | |||||
return BadRequest($"Basket with id {data.BasketId} not found."); | |||||
} | |||||
// Update with new quantities | |||||
foreach (var update in data.Updates) | |||||
{ | |||||
var basketItem = currentBasket.Items.SingleOrDefault(bitem => bitem.Id == update.BasketItemId); | |||||
// Update with new quantities | |||||
foreach (var update in data.Updates) | |||||
if (basketItem == null) | |||||
{ | { | ||||
var basketItem = currentBasket.Items.SingleOrDefault(bitem => bitem.Id == update.BasketItemId); | |||||
if (basketItem == null) | |||||
{ | |||||
return BadRequest($"Basket item with id {update.BasketItemId} not found"); | |||||
} | |||||
basketItem.Quantity = update.NewQty; | |||||
return BadRequest($"Basket item with id {update.BasketItemId} not found"); | |||||
} | } | ||||
// Save the updated basket | |||||
await _basket.UpdateAsync(currentBasket); | |||||
return currentBasket; | |||||
basketItem.Quantity = update.NewQty; | |||||
} | } | ||||
[HttpPost] | |||||
[Route("items")] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType((int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data) | |||||
// Save the updated basket | |||||
await _basket.UpdateAsync(currentBasket); | |||||
return currentBasket; | |||||
} | |||||
[HttpPost] | |||||
[Route("items")] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType((int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data) | |||||
{ | |||||
if (data == null || data.Quantity == 0) | |||||
{ | { | ||||
if (data == null || data.Quantity == 0) | |||||
{ | |||||
return BadRequest("Invalid payload"); | |||||
} | |||||
return BadRequest("Invalid payload"); | |||||
} | |||||
// Step 1: Get the item from catalog | |||||
var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId); | |||||
// Step 1: Get the item from catalog | |||||
var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId); | |||||
//item.PictureUri = | |||||
//item.PictureUri = | |||||
// Step 2: Get current basket status | |||||
var currentBasket = (await _basket.GetById(data.BasketId)) ?? new BasketData(data.BasketId); | |||||
// Step 3: Merge current status with new product | |||||
currentBasket.Items.Add(new BasketDataItem() | |||||
{ | |||||
UnitPrice = item.Price, | |||||
PictureUrl = item.PictureUri, | |||||
ProductId = item.Id, | |||||
ProductName = item.Name, | |||||
Quantity = data.Quantity, | |||||
Id = Guid.NewGuid().ToString() | |||||
}); | |||||
// Step 4: Update basket | |||||
await _basket.UpdateAsync(currentBasket); | |||||
return Ok(); | |||||
} | |||||
// Step 2: Get current basket status | |||||
var currentBasket = (await _basket.GetByIdAsync(data.BasketId)) ?? new BasketData(data.BasketId); | |||||
// Step 3: Merge current status with new product | |||||
currentBasket.Items.Add(new BasketDataItem() | |||||
{ | |||||
UnitPrice = item.Price, | |||||
PictureUrl = item.PictureUri, | |||||
ProductId = item.Id, | |||||
ProductName = item.Name, | |||||
Quantity = data.Quantity, | |||||
Id = Guid.NewGuid().ToString() | |||||
}); | |||||
// Step 4: Update basket | |||||
await _basket.UpdateAsync(currentBasket); | |||||
return Ok(); | |||||
} | } | ||||
} | } |
@ -1,14 +1,11 @@ | |||||
using Microsoft.AspNetCore.Mvc; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers | |||||
[Route("")] | |||||
public class HomeController : Controller | |||||
{ | { | ||||
[Route("")] | |||||
public class HomeController : Controller | |||||
[HttpGet] | |||||
public IActionResult Index() | |||||
{ | { | ||||
[HttpGet()] | |||||
public IActionResult Index() | |||||
{ | |||||
return new RedirectResult("~/swagger"); | |||||
} | |||||
return new RedirectResult("~/swagger"); | |||||
} | } | ||||
} | } |
@ -1,45 +1,37 @@ | |||||
using Microsoft.AspNetCore.Authorization; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
using System.Net; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers | |||||
[Route("api/v1/[controller]")] | |||||
[Authorize] | |||||
[ApiController] | |||||
public class OrderController : ControllerBase | |||||
{ | { | ||||
[Route("api/v1/[controller]")] | |||||
[Authorize] | |||||
[ApiController] | |||||
public class OrderController : ControllerBase | |||||
private readonly IBasketService _basketService; | |||||
private readonly IOrderingService _orderingService; | |||||
public OrderController(IBasketService basketService, IOrderingService orderingService) | |||||
{ | { | ||||
private readonly IBasketService _basketService; | |||||
private readonly IOrderingService _orderingService; | |||||
_basketService = basketService; | |||||
_orderingService = orderingService; | |||||
} | |||||
public OrderController(IBasketService basketService, IOrderingService orderingService) | |||||
[Route("draft/{basketId}")] | |||||
[HttpGet] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId) | |||||
{ | |||||
if (string.IsNullOrEmpty(basketId)) | |||||
{ | { | ||||
_basketService = basketService; | |||||
_orderingService = orderingService; | |||||
return BadRequest("Need a valid basketid"); | |||||
} | } | ||||
// Get the basket data and build a order draft based on it | |||||
var basket = await _basketService.GetByIdAsync(basketId); | |||||
[Route("draft/{basketId}")] | |||||
[HttpGet] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId) | |||||
if (basket == null) | |||||
{ | { | ||||
if (string.IsNullOrEmpty(basketId)) | |||||
{ | |||||
return BadRequest("Need a valid basketid"); | |||||
} | |||||
// Get the basket data and build a order draft based on it | |||||
var basket = await _basketService.GetById(basketId); | |||||
if (basket == null) | |||||
{ | |||||
return BadRequest($"No basket found for id {basketId}"); | |||||
} | |||||
return await _orderingService.GetOrderDraftAsync(basket); | |||||
return BadRequest($"No basket found for id {basketId}"); | |||||
} | } | ||||
return await _orderingService.GetOrderDraftAsync(basket); | |||||
} | } | ||||
} | } |
@ -0,0 +1,41 @@ | |||||
global using CatalogApi; | |||||
global using Devspaces.Support; | |||||
global using Grpc.Core.Interceptors; | |||||
global using Grpc.Core; | |||||
global using GrpcBasket; | |||||
global using GrpcOrdering; | |||||
global using HealthChecks.UI.Client; | |||||
global using Microsoft.AspNetCore.Authentication.JwtBearer; | |||||
global using Microsoft.AspNetCore.Authentication; | |||||
global using Microsoft.AspNetCore.Authorization; | |||||
global using Microsoft.AspNetCore.Builder; | |||||
global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | |||||
global using Microsoft.AspNetCore.Hosting; | |||||
global using Microsoft.AspNetCore.Http; | |||||
global using Microsoft.AspNetCore.Mvc; | |||||
global using Microsoft.AspNetCore; | |||||
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config; | |||||
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters; | |||||
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; | |||||
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator; | |||||
global using Microsoft.Extensions.Configuration; | |||||
global using Microsoft.Extensions.DependencyInjection; | |||||
global using Microsoft.Extensions.Diagnostics.HealthChecks; | |||||
global using Microsoft.Extensions.Hosting; | |||||
global using Microsoft.Extensions.Logging; | |||||
global using Microsoft.Extensions.Options; | |||||
global using Microsoft.OpenApi.Models; | |||||
global using Serilog; | |||||
global using Swashbuckle.AspNetCore.SwaggerGen; | |||||
global using System.Collections.Generic; | |||||
global using System.IdentityModel.Tokens.Jwt; | |||||
global using System.Linq; | |||||
global using System.Net.Http.Headers; | |||||
global using System.Net.Http; | |||||
global using System.Net; | |||||
global using System.Text.Json; | |||||
global using System.Threading.Tasks; | |||||
global using System.Threading; | |||||
global using System; |
@ -1,41 +1,35 @@ | |||||
using Grpc.Core; | |||||
using Grpc.Core.Interceptors; | |||||
using Microsoft.Extensions.Logging; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure | |||||
public class GrpcExceptionInterceptor : Interceptor | |||||
{ | { | ||||
public class GrpcExceptionInterceptor : Interceptor | |||||
private readonly ILogger<GrpcExceptionInterceptor> _logger; | |||||
public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger) | |||||
{ | { | ||||
private readonly ILogger<GrpcExceptionInterceptor> _logger; | |||||
_logger = logger; | |||||
} | |||||
public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger) | |||||
{ | |||||
_logger = logger; | |||||
} | |||||
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( | |||||
TRequest request, | |||||
ClientInterceptorContext<TRequest, TResponse> context, | |||||
AsyncUnaryCallContinuation<TRequest, TResponse> continuation) | |||||
{ | |||||
var call = continuation(request, context); | |||||
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( | |||||
TRequest request, | |||||
ClientInterceptorContext<TRequest, TResponse> context, | |||||
AsyncUnaryCallContinuation<TRequest, TResponse> continuation) | |||||
{ | |||||
var call = continuation(request, context); | |||||
return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose); | |||||
} | |||||
return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose); | |||||
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t) | |||||
{ | |||||
try | |||||
{ | |||||
var response = await t; | |||||
return response; | |||||
} | } | ||||
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t) | |||||
catch (RpcException e) | |||||
{ | { | ||||
try | |||||
{ | |||||
var response = await t; | |||||
return response; | |||||
} | |||||
catch (RpcException e) | |||||
{ | |||||
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); | |||||
return default; | |||||
} | |||||
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); | |||||
return default; | |||||
} | } | ||||
} | } | ||||
} | } |
@ -1,54 +1,44 @@ | |||||
using Microsoft.AspNetCore.Authentication; | |||||
using Microsoft.AspNetCore.Http; | |||||
using Microsoft.Extensions.Logging; | |||||
using System.Collections.Generic; | |||||
using System.Net.Http; | |||||
using System.Net.Http.Headers; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure | |||||
{ | |||||
public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler | |||||
{ | |||||
private readonly IHttpContextAccessor _httpContextAccessor; | |||||
private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; | |||||
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<HttpClientAuthorizationDelegatingHandler> logger) | |||||
{ | |||||
_httpContextAccessor = httpContextAccessor; | |||||
_logger = logger; | |||||
} | |||||
public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler | |||||
{ | |||||
private readonly IHttpContextAccessor _httpContextAccessor; | |||||
private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger; | |||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |||||
{ | |||||
request.Version = new System.Version(2, 0); | |||||
request.Method = HttpMethod.Get; | |||||
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<HttpClientAuthorizationDelegatingHandler> logger) | |||||
{ | |||||
_httpContextAccessor = httpContextAccessor; | |||||
_logger = logger; | |||||
} | |||||
var authorizationHeader = _httpContextAccessor.HttpContext | |||||
.Request.Headers["Authorization"]; | |||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |||||
{ | |||||
request.Version = new System.Version(2, 0); | |||||
request.Method = HttpMethod.Get; | |||||
if (!string.IsNullOrEmpty(authorizationHeader)) | |||||
{ | |||||
request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | |||||
} | |||||
var authorizationHeader = _httpContextAccessor.HttpContext | |||||
.Request.Headers["Authorization"]; | |||||
var token = await GetToken(); | |||||
if (!string.IsNullOrEmpty(authorizationHeader)) | |||||
{ | |||||
request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | |||||
} | |||||
if (token != null) | |||||
{ | |||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | |||||
} | |||||
var token = await GetToken(); | |||||
return await base.SendAsync(request, cancellationToken); | |||||
if (token != null) | |||||
{ | |||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | |||||
} | } | ||||
async Task<string> GetToken() | |||||
{ | |||||
const string ACCESS_TOKEN = "access_token"; | |||||
return await base.SendAsync(request, cancellationToken); | |||||
} | |||||
return await _httpContextAccessor.HttpContext | |||||
.GetTokenAsync(ACCESS_TOKEN); | |||||
} | |||||
async Task<string> GetToken() | |||||
{ | |||||
const string ACCESS_TOKEN = "access_token"; | |||||
return await _httpContextAccessor.HttpContext | |||||
.GetTokenAsync(ACCESS_TOKEN); | |||||
} | } | ||||
} | } |
@ -1,16 +1,15 @@ | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
public class AddBasketItemRequest | |||||
{ | { | ||||
public class AddBasketItemRequest | |||||
{ | |||||
public int CatalogItemId { get; set; } | |||||
public int CatalogItemId { get; set; } | |||||
public string BasketId { get; set; } | |||||
public string BasketId { get; set; } | |||||
public int Quantity { get; set; } | |||||
public int Quantity { get; set; } | |||||
public AddBasketItemRequest() | |||||
{ | |||||
Quantity = 1; | |||||
} | |||||
public AddBasketItemRequest() | |||||
{ | |||||
Quantity = 1; | |||||
} | } | ||||
} | } |
@ -1,22 +1,17 @@ | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
public class BasketData | |||||
{ | { | ||||
public string BuyerId { get; set; } | |||||
public class BasketData | |||||
{ | |||||
public string BuyerId { get; set; } | |||||
public List<BasketDataItem> Items { get; set; } = new List<BasketDataItem>(); | |||||
public BasketData() | |||||
{ | |||||
} | |||||
public List<BasketDataItem> Items { get; set; } = new(); | |||||
public BasketData(string buyerId) | |||||
{ | |||||
BuyerId = buyerId; | |||||
} | |||||
public BasketData() | |||||
{ | |||||
} | } | ||||
public BasketData(string buyerId) | |||||
{ | |||||
BuyerId = buyerId; | |||||
} | |||||
} | } |
@ -1,21 +1,18 @@ | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
{ | |||||
public class BasketDataItem | |||||
{ | |||||
public string Id { get; set; } | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
public int ProductId { get; set; } | |||||
public class BasketDataItem | |||||
{ | |||||
public string Id { get; set; } | |||||
public string ProductName { get; set; } | |||||
public int ProductId { get; set; } | |||||
public decimal UnitPrice { get; set; } | |||||
public string ProductName { get; set; } | |||||
public decimal OldUnitPrice { get; set; } | |||||
public decimal UnitPrice { get; set; } | |||||
public int Quantity { get; set; } | |||||
public decimal OldUnitPrice { get; set; } | |||||
public string PictureUrl { get; set; } | |||||
} | |||||
public int Quantity { get; set; } | |||||
public string PictureUrl { get; set; } | |||||
} | } |
@ -1,13 +1,12 @@ | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
public class CatalogItem | |||||
{ | { | ||||
public class CatalogItem | |||||
{ | |||||
public int Id { get; set; } | |||||
public int Id { get; set; } | |||||
public string Name { get; set; } | |||||
public string Name { get; set; } | |||||
public decimal Price { get; set; } | |||||
public decimal Price { get; set; } | |||||
public string PictureUri { get; set; } | |||||
} | |||||
public string PictureUri { get; set; } | |||||
} | } |
@ -1,48 +1,42 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
public class OrderData | |||||
{ | { | ||||
public string OrderNumber { get; set; } | |||||
public class OrderData | |||||
{ | |||||
public string OrderNumber { get; set; } | |||||
public DateTime Date { get; set; } | |||||
public DateTime Date { get; set; } | |||||
public string Status { get; set; } | |||||
public string Status { get; set; } | |||||
public decimal Total { get; set; } | |||||
public decimal Total { get; set; } | |||||
public string Description { get; set; } | |||||
public string Description { get; set; } | |||||
public string City { get; set; } | |||||
public string City { get; set; } | |||||
public string Street { get; set; } | |||||
public string Street { get; set; } | |||||
public string State { get; set; } | |||||
public string State { get; set; } | |||||
public string Country { get; set; } | |||||
public string Country { get; set; } | |||||
public string ZipCode { get; set; } | |||||
public string ZipCode { get; set; } | |||||
public string CardNumber { get; set; } | |||||
public string CardNumber { get; set; } | |||||
public string CardHolderName { get; set; } | |||||
public string CardHolderName { get; set; } | |||||
public bool IsDraft { get; set; } | |||||
public bool IsDraft { get; set; } | |||||
public DateTime CardExpiration { get; set; } | |||||
public DateTime CardExpiration { get; set; } | |||||
public string CardExpirationShort { get; set; } | |||||
public string CardExpirationShort { get; set; } | |||||
public string CardSecurityNumber { get; set; } | |||||
public string CardSecurityNumber { get; set; } | |||||
public int CardTypeId { get; set; } | |||||
public int CardTypeId { get; set; } | |||||
public string Buyer { get; set; } | |||||
public List<OrderItemData> OrderItems { get; } = new List<OrderItemData>(); | |||||
} | |||||
public string Buyer { get; set; } | |||||
public List<OrderItemData> OrderItems { get; } = new(); | |||||
} | } |
@ -1,19 +1,16 @@ | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
{ | |||||
public class OrderItemData | |||||
{ | |||||
public int ProductId { get; set; } | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
public string ProductName { get; set; } | |||||
public class OrderItemData | |||||
{ | |||||
public int ProductId { get; set; } | |||||
public decimal UnitPrice { get; set; } | |||||
public string ProductName { get; set; } | |||||
public decimal Discount { get; set; } | |||||
public decimal UnitPrice { get; set; } | |||||
public int Units { get; set; } | |||||
public decimal Discount { get; set; } | |||||
public string PictureUrl { get; set; } | |||||
} | |||||
public int Units { get; set; } | |||||
public string PictureUrl { get; set; } | |||||
} | } |
@ -1,16 +1,13 @@ | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
{ | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
public class UpdateBasketItemData | |||||
{ | |||||
public string BasketItemId { get; set; } | |||||
public class UpdateBasketItemData | |||||
{ | |||||
public string BasketItemId { get; set; } | |||||
public int NewQty { get; set; } | |||||
public int NewQty { get; set; } | |||||
public UpdateBasketItemData() | |||||
{ | |||||
NewQty = 0; | |||||
} | |||||
public UpdateBasketItemData() | |||||
{ | |||||
NewQty = 0; | |||||
} | } | ||||
} | } |
@ -1,19 +1,14 @@ | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
public class UpdateBasketItemsRequest | |||||
{ | { | ||||
public class UpdateBasketItemsRequest | |||||
{ | |||||
public string BasketId { get; set; } | |||||
public string BasketId { get; set; } | |||||
public ICollection<UpdateBasketItemData> Updates { get; set; } | |||||
public ICollection<UpdateBasketItemData> Updates { get; set; } | |||||
public UpdateBasketItemsRequest() | |||||
{ | |||||
Updates = new List<UpdateBasketItemData>(); | |||||
} | |||||
public UpdateBasketItemsRequest() | |||||
{ | |||||
Updates = new List<UpdateBasketItemData>(); | |||||
} | } | ||||
} | } |
@ -1,13 +1,8 @@ | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
public class UpdateBasketRequest | |||||
{ | { | ||||
public string BuyerId { get; set; } | |||||
public class UpdateBasketRequest | |||||
{ | |||||
public string BuyerId { get; set; } | |||||
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; } | |||||
} | |||||
} | |||||
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; } | |||||
} |
@ -1,13 +1,10 @@ | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | |||||
{ | |||||
public class UpdateBasketRequestItemData | |||||
{ | |||||
public string Id { get; set; } // Basket id | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
public int ProductId { get; set; } // Catalog item id | |||||
public class UpdateBasketRequestItemData | |||||
{ | |||||
public string Id { get; set; } // Basket id | |||||
public int Quantity { get; set; } // Quantity | |||||
} | |||||
public int ProductId { get; set; } // Catalog item id | |||||
public int Quantity { get; set; } // Quantity | |||||
} | } |
@ -1,90 +1,83 @@ | |||||
using GrpcBasket; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using Microsoft.Extensions.Logging; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services | |||||
public class BasketService : IBasketService | |||||
{ | { | ||||
public class BasketService : IBasketService | |||||
private readonly Basket.BasketClient _basketClient; | |||||
private readonly ILogger<BasketService> _logger; | |||||
public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger) | |||||
{ | { | ||||
private readonly Basket.BasketClient _basketClient; | |||||
private readonly ILogger<BasketService> _logger; | |||||
_basketClient = basketClient; | |||||
_logger = logger; | |||||
} | |||||
public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger) | |||||
{ | |||||
_basketClient = basketClient; | |||||
_logger = logger; | |||||
} | |||||
public async Task<BasketData> GetByIdAsync(string id) | |||||
{ | |||||
_logger.LogDebug("grpc client created, request = {@id}", id); | |||||
var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id }); | |||||
_logger.LogDebug("grpc response {@response}", response); | |||||
public async Task<BasketData> GetById(string id) | |||||
{ | |||||
_logger.LogDebug("grpc client created, request = {@id}", id); | |||||
var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id }); | |||||
_logger.LogDebug("grpc response {@response}", response); | |||||
return MapToBasketData(response); | |||||
} | |||||
return MapToBasketData(response); | |||||
} | |||||
public async Task UpdateAsync(BasketData currentBasket) | |||||
{ | |||||
_logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket); | |||||
var request = MapToCustomerBasketRequest(currentBasket); | |||||
_logger.LogDebug("Grpc update basket request {@request}", request); | |||||
public async Task UpdateAsync(BasketData currentBasket) | |||||
{ | |||||
_logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket); | |||||
var request = MapToCustomerBasketRequest(currentBasket); | |||||
_logger.LogDebug("Grpc update basket request {@request}", request); | |||||
await _basketClient.UpdateBasketAsync(request); | |||||
} | |||||
await _basketClient.UpdateBasketAsync(request); | |||||
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest) | |||||
{ | |||||
if (customerBasketRequest == null) | |||||
{ | |||||
return null; | |||||
} | } | ||||
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest) | |||||
var map = new BasketData | |||||
{ | { | ||||
if (customerBasketRequest == null) | |||||
{ | |||||
return null; | |||||
} | |||||
BuyerId = customerBasketRequest.Buyerid | |||||
}; | |||||
var map = new BasketData | |||||
{ | |||||
BuyerId = customerBasketRequest.Buyerid | |||||
}; | |||||
customerBasketRequest.Items.ToList().ForEach(item => map.Items.Add(new BasketDataItem | |||||
{ | |||||
Id = item.Id, | |||||
OldUnitPrice = (decimal)item.Oldunitprice, | |||||
PictureUrl = item.Pictureurl, | |||||
ProductId = item.Productid, | |||||
ProductName = item.Productname, | |||||
Quantity = item.Quantity, | |||||
UnitPrice = (decimal)item.Unitprice | |||||
})); | |||||
customerBasketRequest.Items.ToList().ForEach(item => map.Items.Add(new BasketDataItem | |||||
{ | |||||
Id = item.Id, | |||||
OldUnitPrice = (decimal)item.Oldunitprice, | |||||
PictureUrl = item.Pictureurl, | |||||
ProductId = item.Productid, | |||||
ProductName = item.Productname, | |||||
Quantity = item.Quantity, | |||||
UnitPrice = (decimal)item.Unitprice | |||||
})); | |||||
return map; | |||||
} | |||||
return map; | |||||
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData) | |||||
{ | |||||
if (basketData == null) | |||||
{ | |||||
return null; | |||||
} | } | ||||
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData) | |||||
var map = new CustomerBasketRequest | |||||
{ | { | ||||
if (basketData == null) | |||||
{ | |||||
return null; | |||||
} | |||||
var map = new CustomerBasketRequest | |||||
{ | |||||
Buyerid = basketData.BuyerId | |||||
}; | |||||
Buyerid = basketData.BuyerId | |||||
}; | |||||
basketData.Items.ToList().ForEach(item => map.Items.Add(new BasketItemResponse | |||||
{ | |||||
Id = item.Id, | |||||
Oldunitprice = (double)item.OldUnitPrice, | |||||
Pictureurl = item.PictureUrl, | |||||
Productid = item.ProductId, | |||||
Productname = item.ProductName, | |||||
Quantity = item.Quantity, | |||||
Unitprice = (double)item.UnitPrice | |||||
})); | |||||
basketData.Items.ToList().ForEach(item => map.Items.Add(new BasketItemResponse | |||||
{ | |||||
Id = item.Id, | |||||
Oldunitprice = (double)item.OldUnitPrice, | |||||
Pictureurl = item.PictureUrl, | |||||
Productid = item.ProductId, | |||||
Productname = item.ProductName, | |||||
Quantity = item.Quantity, | |||||
Unitprice = (double)item.UnitPrice | |||||
})); | |||||
return map; | |||||
} | |||||
return map; | |||||
} | } | ||||
} | } |
@ -1,43 +1,36 @@ | |||||
using CatalogApi; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services | |||||
public class CatalogService : ICatalogService | |||||
{ | { | ||||
public class CatalogService : ICatalogService | |||||
{ | |||||
private readonly Catalog.CatalogClient _client; | |||||
private readonly Catalog.CatalogClient _client; | |||||
public CatalogService(Catalog.CatalogClient client) | |||||
{ | |||||
_client = client; | |||||
} | |||||
public CatalogService(Catalog.CatalogClient client) | |||||
{ | |||||
_client = client; | |||||
} | |||||
public async Task<CatalogItem> GetCatalogItemAsync(int id) | |||||
{ | |||||
var request = new CatalogItemRequest { Id = id }; | |||||
var response = await _client.GetItemByIdAsync(request); | |||||
return MapToCatalogItemResponse(response); | |||||
} | |||||
public async Task<CatalogItem> GetCatalogItemAsync(int id) | |||||
{ | |||||
var request = new CatalogItemRequest { Id = id }; | |||||
var response = await _client.GetItemByIdAsync(request); | |||||
return MapToCatalogItemResponse(response); | |||||
} | |||||
public async Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids) | |||||
{ | |||||
var request = new CatalogItemsRequest { Ids = string.Join(",", ids), PageIndex = 1, PageSize = 10 }; | |||||
var response = await _client.GetItemsByIdsAsync(request); | |||||
return response.Data.Select(MapToCatalogItemResponse); | |||||
} | |||||
public async Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids) | |||||
{ | |||||
var request = new CatalogItemsRequest { Ids = string.Join(",", ids), PageIndex = 1, PageSize = 10 }; | |||||
var response = await _client.GetItemsByIdsAsync(request); | |||||
return response.Data.Select(MapToCatalogItemResponse); | |||||
} | |||||
private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse) | |||||
private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse) | |||||
{ | |||||
return new CatalogItem | |||||
{ | { | ||||
return new CatalogItem | |||||
{ | |||||
Id = catalogItemResponse.Id, | |||||
Name = catalogItemResponse.Name, | |||||
PictureUri = catalogItemResponse.PictureUri, | |||||
Price = (decimal)catalogItemResponse.Price | |||||
}; | |||||
} | |||||
Id = catalogItemResponse.Id, | |||||
Name = catalogItemResponse.Name, | |||||
PictureUri = catalogItemResponse.PictureUri, | |||||
Price = (decimal)catalogItemResponse.Price | |||||
}; | |||||
} | } | ||||
} | } |
@ -1,13 +1,9 @@ | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services | |||||
public interface IBasketService | |||||
{ | { | ||||
public interface IBasketService | |||||
{ | |||||
Task<BasketData> GetById(string id); | |||||
Task<BasketData> GetByIdAsync(string id); | |||||
Task UpdateAsync(BasketData currentBasket); | |||||
Task UpdateAsync(BasketData currentBasket); | |||||
} | |||||
} | } |
@ -1,13 +1,8 @@ | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using System.Collections.Generic; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services | |||||
public interface ICatalogService | |||||
{ | { | ||||
public interface ICatalogService | |||||
{ | |||||
Task<CatalogItem> GetCatalogItemAsync(int id); | |||||
Task<CatalogItem> GetCatalogItemAsync(int id); | |||||
Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids); | |||||
} | |||||
Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids); | |||||
} | } |
@ -1,10 +1,6 @@ | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services | |||||
public interface IOrderApiClient | |||||
{ | { | ||||
public interface IOrderApiClient | |||||
{ | |||||
Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket); | |||||
} | |||||
Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket); | |||||
} | } |
@ -1,10 +1,6 @@ | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services | |||||
public interface IOrderingService | |||||
{ | { | ||||
public interface IOrderingService | |||||
{ | |||||
Task<OrderData> GetOrderDraftAsync(BasketData basketData); | |||||
} | |||||
} | |||||
Task<OrderData> GetOrderDraftAsync(BasketData basketData); | |||||
} |
@ -1,40 +1,31 @@ | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using System.Net.Http; | |||||
using System.Threading.Tasks; | |||||
using System.Text.Json; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services | |||||
public class OrderApiClient : IOrderApiClient | |||||
{ | { | ||||
public class OrderApiClient : IOrderApiClient | |||||
{ | |||||
private readonly HttpClient _apiClient; | |||||
private readonly ILogger<OrderApiClient> _logger; | |||||
private readonly UrlsConfig _urls; | |||||
private readonly HttpClient _apiClient; | |||||
private readonly ILogger<OrderApiClient> _logger; | |||||
private readonly UrlsConfig _urls; | |||||
public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config) | |||||
{ | |||||
_apiClient = httpClient; | |||||
_logger = logger; | |||||
_urls = config.Value; | |||||
} | |||||
public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config) | |||||
{ | |||||
_apiClient = httpClient; | |||||
_logger = logger; | |||||
_urls = config.Value; | |||||
} | |||||
public async Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket) | |||||
{ | |||||
var uri = _urls.Orders + UrlsConfig.OrdersOperations.GetOrderDraft(); | |||||
var content = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json"); | |||||
var response = await _apiClient.PostAsync(uri, content); | |||||
public async Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket) | |||||
{ | |||||
var uri = _urls.Orders + UrlsConfig.OrdersOperations.GetOrderDraft(); | |||||
var content = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json"); | |||||
var response = await _apiClient.PostAsync(uri, content); | |||||
response.EnsureSuccessStatusCode(); | |||||
response.EnsureSuccessStatusCode(); | |||||
var ordersDraftResponse = await response.Content.ReadAsStringAsync(); | |||||
var ordersDraftResponse = await response.Content.ReadAsStringAsync(); | |||||
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions | |||||
{ | |||||
PropertyNameCaseInsensitive = true | |||||
}); | |||||
} | |||||
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions | |||||
{ | |||||
PropertyNameCaseInsensitive = true | |||||
}); | |||||
} | } | ||||
} | } |
@ -1,79 +1,72 @@ | |||||
using GrpcOrdering; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | |||||
using Microsoft.Extensions.Logging; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services | |||||
public class OrderingService : IOrderingService | |||||
{ | { | ||||
public class OrderingService : IOrderingService | |||||
private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | |||||
private readonly ILogger<OrderingService> _logger; | |||||
public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | |||||
{ | { | ||||
private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | |||||
private readonly ILogger<OrderingService> _logger; | |||||
_orderingGrpcClient = orderingGrpcClient; | |||||
_logger = logger; | |||||
} | |||||
public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | |||||
{ | |||||
_orderingGrpcClient = orderingGrpcClient; | |||||
_logger = logger; | |||||
} | |||||
public async Task<OrderData> GetOrderDraftAsync(BasketData basketData) | |||||
{ | |||||
_logger.LogDebug(" grpc client created, basketData={@basketData}", basketData); | |||||
public async Task<OrderData> GetOrderDraftAsync(BasketData basketData) | |||||
{ | |||||
_logger.LogDebug(" grpc client created, basketData={@basketData}", basketData); | |||||
var command = MapToOrderDraftCommand(basketData); | |||||
var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command); | |||||
_logger.LogDebug(" grpc response: {@response}", response); | |||||
var command = MapToOrderDraftCommand(basketData); | |||||
var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command); | |||||
_logger.LogDebug(" grpc response: {@response}", response); | |||||
return MapToResponse(response, basketData); | |||||
} | |||||
return MapToResponse(response, basketData); | |||||
private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData) | |||||
{ | |||||
if (orderDraft == null) | |||||
{ | |||||
return null; | |||||
} | } | ||||
private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData) | |||||
var data = new OrderData | |||||
{ | { | ||||
if (orderDraft == null) | |||||
{ | |||||
return null; | |||||
} | |||||
Buyer = basketData.BuyerId, | |||||
Total = (decimal)orderDraft.Total, | |||||
}; | |||||
var data = new OrderData | |||||
{ | |||||
Buyer = basketData.BuyerId, | |||||
Total = (decimal)orderDraft.Total, | |||||
}; | |||||
orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData | |||||
{ | |||||
Discount = (decimal)o.Discount, | |||||
PictureUrl = o.PictureUrl, | |||||
ProductId = o.ProductId, | |||||
ProductName = o.ProductName, | |||||
UnitPrice = (decimal)o.UnitPrice, | |||||
Units = o.Units, | |||||
})); | |||||
orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData | |||||
{ | |||||
Discount = (decimal)o.Discount, | |||||
PictureUrl = o.PictureUrl, | |||||
ProductId = o.ProductId, | |||||
ProductName = o.ProductName, | |||||
UnitPrice = (decimal)o.UnitPrice, | |||||
Units = o.Units, | |||||
})); | |||||
return data; | |||||
} | |||||
return data; | |||||
} | |||||
private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | |||||
private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | |||||
{ | |||||
var command = new CreateOrderDraftCommand | |||||
{ | { | ||||
var command = new CreateOrderDraftCommand | |||||
{ | |||||
BuyerId = basketData.BuyerId, | |||||
}; | |||||
BuyerId = basketData.BuyerId, | |||||
}; | |||||
basketData.Items.ForEach(i => command.Items.Add(new BasketItem | |||||
{ | |||||
Id = i.Id, | |||||
OldUnitPrice = (double)i.OldUnitPrice, | |||||
PictureUrl = i.PictureUrl, | |||||
ProductId = i.ProductId, | |||||
ProductName = i.ProductName, | |||||
Quantity = i.Quantity, | |||||
UnitPrice = (double)i.UnitPrice, | |||||
})); | |||||
return command; | |||||
} | |||||
basketData.Items.ForEach(i => command.Items.Add(new BasketItem | |||||
{ | |||||
Id = i.Id, | |||||
OldUnitPrice = (double)i.OldUnitPrice, | |||||
PictureUrl = i.PictureUrl, | |||||
ProductId = i.ProductId, | |||||
ProductName = i.ProductName, | |||||
Quantity = i.Quantity, | |||||
UnitPrice = (double)i.UnitPrice, | |||||
})); | |||||
return command; | |||||
} | } | ||||
} | } |
@ -1,222 +1,196 @@ | |||||
using CatalogApi; | |||||
using Devspaces.Support; | |||||
using GrpcBasket; | |||||
using GrpcOrdering; | |||||
using HealthChecks.UI.Client; | |||||
using Microsoft.AspNetCore.Authentication.JwtBearer; | |||||
using Microsoft.AspNetCore.Builder; | |||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks; | |||||
using Microsoft.AspNetCore.Hosting; | |||||
using Microsoft.AspNetCore.Http; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; | |||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | |||||
using Microsoft.Extensions.Configuration; | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
using Microsoft.Extensions.Diagnostics.HealthChecks; | |||||
using Microsoft.Extensions.Hosting; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using Microsoft.OpenApi.Models; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IdentityModel.Tokens.Jwt; | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator | |||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator; | |||||
public class Startup | |||||
{ | { | ||||
public class Startup | |||||
public Startup(IConfiguration configuration) | |||||
{ | { | ||||
public Startup(IConfiguration configuration) | |||||
{ | |||||
Configuration = configuration; | |||||
} | |||||
Configuration = configuration; | |||||
} | |||||
public IConfiguration Configuration { get; } | |||||
public IConfiguration Configuration { get; } | |||||
// This method gets called by the runtime. Use this method to add services to the container. | |||||
public void ConfigureServices(IServiceCollection services) | |||||
// This method gets called by the runtime. Use this method to add services to the container. | |||||
public void ConfigureServices(IServiceCollection services) | |||||
{ | |||||
services.AddHealthChecks() | |||||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||||
.AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); | |||||
services.AddCustomMvc(Configuration) | |||||
.AddCustomAuthentication(Configuration) | |||||
.AddDevspaces() | |||||
.AddHttpServices() | |||||
.AddGrpcServices(); | |||||
} | |||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | |||||
{ | |||||
var pathBase = Configuration["PATH_BASE"]; | |||||
if (!string.IsNullOrEmpty(pathBase)) | |||||
{ | { | ||||
services.AddHealthChecks() | |||||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||||
.AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); | |||||
services.AddCustomMvc(Configuration) | |||||
.AddCustomAuthentication(Configuration) | |||||
.AddDevspaces() | |||||
.AddHttpServices() | |||||
.AddGrpcServices(); | |||||
loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | |||||
app.UsePathBase(pathBase); | |||||
} | } | ||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | |||||
if (env.IsDevelopment()) | |||||
{ | { | ||||
var pathBase = Configuration["PATH_BASE"]; | |||||
if (!string.IsNullOrEmpty(pathBase)) | |||||
{ | |||||
loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | |||||
app.UsePathBase(pathBase); | |||||
} | |||||
if (env.IsDevelopment()) | |||||
{ | |||||
app.UseDeveloperExceptionPage(); | |||||
} | |||||
app.UseDeveloperExceptionPage(); | |||||
} | |||||
app.UseSwagger().UseSwaggerUI(c => | |||||
app.UseSwagger().UseSwaggerUI(c => | |||||
{ | |||||
c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Purchase BFF V1"); | |||||
c.OAuthClientId("mobileshoppingaggswaggerui"); | |||||
c.OAuthClientSecret(string.Empty); | |||||
c.OAuthRealm(string.Empty); | |||||
c.OAuthAppName("Purchase BFF Swagger UI"); | |||||
}); | |||||
app.UseRouting(); | |||||
app.UseCors("CorsPolicy"); | |||||
app.UseAuthentication(); | |||||
app.UseAuthorization(); | |||||
app.UseEndpoints(endpoints => | |||||
{ | |||||
endpoints.MapDefaultControllerRoute(); | |||||
endpoints.MapControllers(); | |||||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | |||||
{ | { | ||||
c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Purchase BFF V1"); | |||||
c.OAuthClientId("mobileshoppingaggswaggerui"); | |||||
c.OAuthClientSecret(string.Empty); | |||||
c.OAuthRealm(string.Empty); | |||||
c.OAuthAppName("Purchase BFF Swagger UI"); | |||||
Predicate = _ => true, | |||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||||
}); | }); | ||||
app.UseRouting(); | |||||
app.UseCors("CorsPolicy"); | |||||
app.UseAuthentication(); | |||||
app.UseAuthorization(); | |||||
app.UseEndpoints(endpoints => | |||||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | |||||
{ | { | ||||
endpoints.MapDefaultControllerRoute(); | |||||
endpoints.MapControllers(); | |||||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | |||||
{ | |||||
Predicate = _ => true, | |||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||||
}); | |||||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | |||||
{ | |||||
Predicate = r => r.Name.Contains("self") | |||||
}); | |||||
Predicate = r => r.Name.Contains("self") | |||||
}); | }); | ||||
} | |||||
}); | |||||
} | } | ||||
} | |||||
public static class ServiceCollectionExtensions | |||||
public static class ServiceCollectionExtensions | |||||
{ | |||||
public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) | |||||
{ | { | ||||
public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
services.AddOptions(); | |||||
services.Configure<UrlsConfig>(configuration.GetSection("urls")); | |||||
services.AddOptions(); | |||||
services.Configure<UrlsConfig>(configuration.GetSection("urls")); | |||||
services.AddControllers() | |||||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||||
services.AddControllers() | |||||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||||
services.AddSwaggerGen(options => | |||||
services.AddSwaggerGen(options => | |||||
{ | |||||
options.DescribeAllEnumsAsStrings(); | |||||
options.SwaggerDoc("v1", new OpenApiInfo | |||||
{ | { | ||||
options.DescribeAllEnumsAsStrings(); | |||||
options.SwaggerDoc("v1", new OpenApiInfo | |||||
{ | |||||
Title = "Shopping Aggregator for Mobile Clients", | |||||
Version = "v1", | |||||
Description = "Shopping Aggregator for Mobile Clients" | |||||
}); | |||||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |||||
Title = "Shopping Aggregator for Mobile Clients", | |||||
Version = "v1", | |||||
Description = "Shopping Aggregator for Mobile Clients" | |||||
}); | |||||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |||||
{ | |||||
Type = SecuritySchemeType.OAuth2, | |||||
Flows = new OpenApiOAuthFlows() | |||||
{ | { | ||||
Type = SecuritySchemeType.OAuth2, | |||||
Flows = new OpenApiOAuthFlows() | |||||
Implicit = new OpenApiOAuthFlow() | |||||
{ | { | ||||
Implicit = new OpenApiOAuthFlow() | |||||
{ | |||||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), | |||||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"), | |||||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), | |||||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"), | |||||
Scopes = new Dictionary<string, string>() | |||||
{ | |||||
{ "mobileshoppingagg", "Shopping Aggregator for Mobile Clients" } | |||||
} | |||||
Scopes = new Dictionary<string, string>() | |||||
{ | |||||
{ "mobileshoppingagg", "Shopping Aggregator for Mobile Clients" } | |||||
} | } | ||||
} | } | ||||
}); | |||||
options.OperationFilter<AuthorizeCheckOperationFilter>(); | |||||
} | |||||
}); | }); | ||||
services.AddCors(options => | |||||
{ | |||||
options.AddPolicy("CorsPolicy", | |||||
builder => builder | |||||
.AllowAnyMethod() | |||||
.AllowAnyHeader() | |||||
.SetIsOriginAllowed((host) => true) | |||||
.AllowCredentials()); | |||||
}); | |||||
options.OperationFilter<AuthorizeCheckOperationFilter>(); | |||||
}); | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | |||||
services.AddCors(options => | |||||
{ | { | ||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | |||||
var identityUrl = configuration.GetValue<string>("urls:identity"); | |||||
services.AddAuthentication(options => | |||||
{ | |||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
options.AddPolicy("CorsPolicy", | |||||
builder => builder | |||||
.AllowAnyMethod() | |||||
.AllowAnyHeader() | |||||
.SetIsOriginAllowed((host) => true) | |||||
.AllowCredentials()); | |||||
}); | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | |||||
}) | |||||
.AddJwtBearer(options => | |||||
{ | |||||
options.Authority = identityUrl; | |||||
options.RequireHttpsMetadata = false; | |||||
options.Audience = "mobileshoppingagg"; | |||||
}); | |||||
var identityUrl = configuration.GetValue<string>("urls:identity"); | |||||
return services; | |||||
} | |||||
services.AddAuthentication(options => | |||||
{ | |||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
public static IServiceCollection AddHttpServices(this IServiceCollection services) | |||||
}) | |||||
.AddJwtBearer(options => | |||||
{ | { | ||||
//register delegating handlers | |||||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | |||||
options.Authority = identityUrl; | |||||
options.RequireHttpsMetadata = false; | |||||
options.Audience = "mobileshoppingagg"; | |||||
}); | |||||
//register http services | |||||
return services; | |||||
} | |||||
services.AddHttpClient<IOrderApiClient, OrderApiClient>() | |||||
.AddDevspacesSupport(); | |||||
public static IServiceCollection AddHttpServices(this IServiceCollection services) | |||||
{ | |||||
//register delegating handlers | |||||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | |||||
return services; | |||||
} | |||||
//register http services | |||||
public static IServiceCollection AddGrpcServices(this IServiceCollection services) | |||||
{ | |||||
services.AddTransient<GrpcExceptionInterceptor>(); | |||||
services.AddHttpClient<IOrderApiClient, OrderApiClient>() | |||||
.AddDevspacesSupport(); | |||||
services.AddScoped<IBasketService, BasketService>(); | |||||
return services; | |||||
} | |||||
services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||||
{ | |||||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||||
options.Address = new Uri(basketApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
public static IServiceCollection AddGrpcServices(this IServiceCollection services) | |||||
{ | |||||
services.AddTransient<GrpcExceptionInterceptor>(); | |||||
services.AddScoped<ICatalogService, CatalogService>(); | |||||
services.AddScoped<IBasketService, BasketService>(); | |||||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||||
{ | |||||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||||
options.Address = new Uri(catalogApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||||
{ | |||||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||||
options.Address = new Uri(basketApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
services.AddScoped<IOrderingService, OrderingService>(); | |||||
services.AddScoped<ICatalogService, CatalogService>(); | |||||
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) => | |||||
{ | |||||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||||
options.Address = new Uri(orderingApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||||
{ | |||||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||||
options.Address = new Uri(catalogApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
return services; | |||||
} | |||||
services.AddScoped<IOrderingService, OrderingService>(); | |||||
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) => | |||||
{ | |||||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||||
options.Address = new Uri(orderingApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
return services; | |||||
} | } | ||||
} | } |
@ -1,45 +1,41 @@ | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config | |||||
public class UrlsConfig | |||||
{ | { | ||||
public class UrlsConfig | |||||
public class CatalogOperations | |||||
{ | { | ||||
// grpc call under REST must go trough port 80 | |||||
public static string GetItemById(int id) => $"/api/v1/catalog/items/{id}"; | |||||
public class CatalogOperations | |||||
{ | |||||
// grpc call under REST must go trough port 80 | |||||
public static string GetItemById(int id) => $"/api/v1/catalog/items/{id}"; | |||||
public static string GetItemById(string ids) => $"/api/v1/catalog/items/ids/{string.Join(',', ids)}"; | |||||
public static string GetItemById(string ids) => $"/api/v1/catalog/items/ids/{string.Join(',', ids)}"; | |||||
// REST call standard must go through port 5000 | |||||
public static string GetItemsById(IEnumerable<int> ids) => $":5000/api/v1/catalog/items?ids={string.Join(',', ids)}"; | |||||
} | |||||
public class BasketOperations | |||||
{ | |||||
public static string GetItemById(string id) => $"/api/v1/basket/{id}"; | |||||
// REST call standard must go through port 5000 | |||||
public static string GetItemsById(IEnumerable<int> ids) => $":5000/api/v1/catalog/items?ids={string.Join(',', ids)}"; | |||||
} | |||||
public static string UpdateBasket() => "/api/v1/basket"; | |||||
} | |||||
public class BasketOperations | |||||
{ | |||||
public static string GetItemById(string id) => $"/api/v1/basket/{id}"; | |||||
public class OrdersOperations | |||||
{ | |||||
public static string GetOrderDraft() => "/api/v1/orders/draft"; | |||||
} | |||||
public static string UpdateBasket() => "/api/v1/basket"; | |||||
} | |||||
public string Basket { get; set; } | |||||
public class OrdersOperations | |||||
{ | |||||
public static string GetOrderDraft() => "/api/v1/orders/draft"; | |||||
} | |||||
public string Catalog { get; set; } | |||||
public string Basket { get; set; } | |||||
public string Orders { get; set; } | |||||
public string Catalog { get; set; } | |||||
public string GrpcBasket { get; set; } | |||||
public string Orders { get; set; } | |||||
public string GrpcCatalog { get; set; } | |||||
public string GrpcBasket { get; set; } | |||||
public string GrpcOrdering { get; set; } | |||||
} | |||||
public string GrpcCatalog { get; set; } | |||||
public string GrpcOrdering { get; set; } | |||||
} | } | ||||
@ -1,164 +1,154 @@ | |||||
using Microsoft.AspNetCore.Authorization; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
using System; | |||||
using System.Linq; | |||||
using System.Net; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers; | |||||
[Route("api/v1/[controller]")] | |||||
[Authorize] | |||||
[ApiController] | |||||
public class BasketController : ControllerBase | |||||
{ | { | ||||
[Route("api/v1/[controller]")] | |||||
[Authorize] | |||||
[ApiController] | |||||
public class BasketController : ControllerBase | |||||
private readonly ICatalogService _catalog; | |||||
private readonly IBasketService _basket; | |||||
public BasketController(ICatalogService catalogService, IBasketService basketService) | |||||
{ | { | ||||
private readonly ICatalogService _catalog; | |||||
private readonly IBasketService _basket; | |||||
_catalog = catalogService; | |||||
_basket = basketService; | |||||
} | |||||
public BasketController(ICatalogService catalogService, IBasketService basketService) | |||||
[HttpPost] | |||||
[HttpPut] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data) | |||||
{ | |||||
if (data.Items == null || !data.Items.Any()) | |||||
{ | { | ||||
_catalog = catalogService; | |||||
_basket = basketService; | |||||
return BadRequest("Need to pass at least one basket line"); | |||||
} | } | ||||
[HttpPost] | |||||
[HttpPut] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data) | |||||
// Retrieve the current basket | |||||
var basket = await _basket.GetByIdAsync(data.BuyerId) ?? new BasketData(data.BuyerId); | |||||
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId)); | |||||
// group by product id to avoid duplicates | |||||
var itemsCalculated = data | |||||
.Items | |||||
.GroupBy(x => x.ProductId, x => x, (k, i) => new { productId = k, items = i }) | |||||
.Select(groupedItem => | |||||
{ | |||||
var item = groupedItem.items.First(); | |||||
item.Quantity = groupedItem.items.Sum(i => i.Quantity); | |||||
return item; | |||||
}); | |||||
foreach (var bitem in itemsCalculated) | |||||
{ | { | ||||
if (data.Items == null || !data.Items.Any()) | |||||
var catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId); | |||||
if (catalogItem == null) | |||||
{ | { | ||||
return BadRequest("Need to pass at least one basket line"); | |||||
return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})"); | |||||
} | } | ||||
// Retrieve the current basket | |||||
var basket = await _basket.GetById(data.BuyerId) ?? new BasketData(data.BuyerId); | |||||
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId)); | |||||
// group by product id to avoid duplicates | |||||
var itemsCalculated = data | |||||
.Items | |||||
.GroupBy(x => x.ProductId, x => x, (k, i) => new { productId = k, items = i }) | |||||
.Select(groupedItem => | |||||
{ | |||||
var item = groupedItem.items.First(); | |||||
item.Quantity = groupedItem.items.Sum(i => i.Quantity); | |||||
return item; | |||||
}); | |||||
foreach (var bitem in itemsCalculated) | |||||
var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId); | |||||
if (itemInBasket == null) | |||||
{ | { | ||||
var catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId); | |||||
if (catalogItem == null) | |||||
{ | |||||
return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})"); | |||||
} | |||||
var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId); | |||||
if (itemInBasket == null) | |||||
{ | |||||
basket.Items.Add(new BasketDataItem() | |||||
{ | |||||
Id = bitem.Id, | |||||
ProductId = catalogItem.Id, | |||||
ProductName = catalogItem.Name, | |||||
PictureUrl = catalogItem.PictureUri, | |||||
UnitPrice = catalogItem.Price, | |||||
Quantity = bitem.Quantity | |||||
}); | |||||
} | |||||
else | |||||
basket.Items.Add(new BasketDataItem() | |||||
{ | { | ||||
itemInBasket.Quantity = bitem.Quantity; | |||||
} | |||||
Id = bitem.Id, | |||||
ProductId = catalogItem.Id, | |||||
ProductName = catalogItem.Name, | |||||
PictureUrl = catalogItem.PictureUri, | |||||
UnitPrice = catalogItem.Price, | |||||
Quantity = bitem.Quantity | |||||
}); | |||||
} | } | ||||
else | |||||
{ | |||||
itemInBasket.Quantity = bitem.Quantity; | |||||
} | |||||
} | |||||
await _basket.UpdateAsync(basket); | |||||
await _basket.UpdateAsync(basket); | |||||
return basket; | |||||
} | |||||
return basket; | |||||
} | |||||
[HttpPut] | |||||
[Route("items")] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data) | |||||
[HttpPut] | |||||
[Route("items")] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data) | |||||
{ | |||||
if (!data.Updates.Any()) | |||||
{ | { | ||||
if (!data.Updates.Any()) | |||||
{ | |||||
return BadRequest("No updates sent"); | |||||
} | |||||
return BadRequest("No updates sent"); | |||||
} | |||||
// Retrieve the current basket | |||||
var currentBasket = await _basket.GetById(data.BasketId); | |||||
if (currentBasket == null) | |||||
{ | |||||
return BadRequest($"Basket with id {data.BasketId} not found."); | |||||
} | |||||
// Retrieve the current basket | |||||
var currentBasket = await _basket.GetByIdAsync(data.BasketId); | |||||
if (currentBasket == null) | |||||
{ | |||||
return BadRequest($"Basket with id {data.BasketId} not found."); | |||||
} | |||||
// Update with new quantities | |||||
foreach (var update in data.Updates) | |||||
// Update with new quantities | |||||
foreach (var update in data.Updates) | |||||
{ | |||||
var basketItem = currentBasket.Items.SingleOrDefault(bitem => bitem.Id == update.BasketItemId); | |||||
if (basketItem == null) | |||||
{ | { | ||||
var basketItem = currentBasket.Items.SingleOrDefault(bitem => bitem.Id == update.BasketItemId); | |||||
if (basketItem == null) | |||||
{ | |||||
return BadRequest($"Basket item with id {update.BasketItemId} not found"); | |||||
} | |||||
basketItem.Quantity = update.NewQty; | |||||
return BadRequest($"Basket item with id {update.BasketItemId} not found"); | |||||
} | } | ||||
basketItem.Quantity = update.NewQty; | |||||
} | |||||
// Save the updated basket | |||||
await _basket.UpdateAsync(currentBasket); | |||||
// Save the updated basket | |||||
await _basket.UpdateAsync(currentBasket); | |||||
return currentBasket; | |||||
} | |||||
return currentBasket; | |||||
} | |||||
[HttpPost] | |||||
[Route("items")] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType((int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data) | |||||
[HttpPost] | |||||
[Route("items")] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType((int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data) | |||||
{ | |||||
if (data == null || data.Quantity == 0) | |||||
{ | { | ||||
if (data == null || data.Quantity == 0) | |||||
{ | |||||
return BadRequest("Invalid payload"); | |||||
} | |||||
return BadRequest("Invalid payload"); | |||||
} | |||||
// Step 1: Get the item from catalog | |||||
var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId); | |||||
// Step 1: Get the item from catalog | |||||
var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId); | |||||
//item.PictureUri = | |||||
//item.PictureUri = | |||||
// Step 2: Get current basket status | |||||
var currentBasket = (await _basket.GetById(data.BasketId)) ?? new BasketData(data.BasketId); | |||||
// Step 3: Search if exist product into basket | |||||
var product = currentBasket.Items.SingleOrDefault(i => i.ProductId == item.Id); | |||||
if (product != null) | |||||
{ | |||||
// Step 4: Update quantity for product | |||||
product.Quantity += data.Quantity; | |||||
} | |||||
else | |||||
// Step 2: Get current basket status | |||||
var currentBasket = (await _basket.GetByIdAsync(data.BasketId)) ?? new BasketData(data.BasketId); | |||||
// Step 3: Search if exist product into basket | |||||
var product = currentBasket.Items.SingleOrDefault(i => i.ProductId == item.Id); | |||||
if (product != null) | |||||
{ | |||||
// Step 4: Update quantity for product | |||||
product.Quantity += data.Quantity; | |||||
} | |||||
else | |||||
{ | |||||
// Step 4: Merge current status with new product | |||||
currentBasket.Items.Add(new BasketDataItem() | |||||
{ | { | ||||
// Step 4: Merge current status with new product | |||||
currentBasket.Items.Add(new BasketDataItem() | |||||
{ | |||||
UnitPrice = item.Price, | |||||
PictureUrl = item.PictureUri, | |||||
ProductId = item.Id, | |||||
ProductName = item.Name, | |||||
Quantity = data.Quantity, | |||||
Id = Guid.NewGuid().ToString() | |||||
}); | |||||
} | |||||
UnitPrice = item.Price, | |||||
PictureUrl = item.PictureUri, | |||||
ProductId = item.Id, | |||||
ProductName = item.Name, | |||||
Quantity = data.Quantity, | |||||
Id = Guid.NewGuid().ToString() | |||||
}); | |||||
} | |||||
// Step 5: Update basket | |||||
await _basket.UpdateAsync(currentBasket); | |||||
// Step 5: Update basket | |||||
await _basket.UpdateAsync(currentBasket); | |||||
return Ok(); | |||||
} | |||||
return Ok(); | |||||
} | } | ||||
} | } |
@ -1,14 +1,11 @@ | |||||
using Microsoft.AspNetCore.Mvc; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers | |||||
[Route("")] | |||||
public class HomeController : Controller | |||||
{ | { | ||||
[Route("")] | |||||
public class HomeController : Controller | |||||
[HttpGet] | |||||
public IActionResult Index() | |||||
{ | { | ||||
[HttpGet()] | |||||
public IActionResult Index() | |||||
{ | |||||
return new RedirectResult("~/swagger"); | |||||
} | |||||
return new RedirectResult("~/swagger"); | |||||
} | } | ||||
} | } |
@ -1,44 +1,37 @@ | |||||
using Microsoft.AspNetCore.Authorization; | |||||
using Microsoft.AspNetCore.Mvc; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
using System.Net; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers | |||||
[Route("api/v1/[controller]")] | |||||
[Authorize] | |||||
[ApiController] | |||||
public class OrderController : ControllerBase | |||||
{ | { | ||||
[Route("api/v1/[controller]")] | |||||
[Authorize] | |||||
[ApiController] | |||||
public class OrderController : ControllerBase | |||||
private readonly IBasketService _basketService; | |||||
private readonly IOrderingService _orderingService; | |||||
public OrderController(IBasketService basketService, IOrderingService orderingService) | |||||
{ | |||||
_basketService = basketService; | |||||
_orderingService = orderingService; | |||||
} | |||||
[Route("draft/{basketId}")] | |||||
[HttpGet] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId) | |||||
{ | { | ||||
private readonly IBasketService _basketService; | |||||
private readonly IOrderingService _orderingService; | |||||
public OrderController(IBasketService basketService, IOrderingService orderingService) | |||||
if (string.IsNullOrWhiteSpace(basketId)) | |||||
{ | { | ||||
_basketService = basketService; | |||||
_orderingService = orderingService; | |||||
return BadRequest("Need a valid basketid"); | |||||
} | } | ||||
// Get the basket data and build a order draft based on it | |||||
var basket = await _basketService.GetByIdAsync(basketId); | |||||
[Route("draft/{basketId}")] | |||||
[HttpGet] | |||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||||
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)] | |||||
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId) | |||||
if (basket == null) | |||||
{ | { | ||||
if (string.IsNullOrEmpty(basketId)) | |||||
{ | |||||
return BadRequest("Need a valid basketid"); | |||||
} | |||||
// Get the basket data and build a order draft based on it | |||||
var basket = await _basketService.GetById(basketId); | |||||
if (basket == null) | |||||
{ | |||||
return BadRequest($"No basket found for id {basketId}"); | |||||
} | |||||
return await _orderingService.GetOrderDraftAsync(basket); | |||||
return BadRequest($"No basket found for id {basketId}"); | |||||
} | } | ||||
return await _orderingService.GetOrderDraftAsync(basket); | |||||
} | } | ||||
} | } |
@ -0,0 +1,41 @@ | |||||
global using CatalogApi; | |||||
global using Devspaces.Support; | |||||
global using Grpc.Core.Interceptors; | |||||
global using Grpc.Core; | |||||
global using GrpcBasket; | |||||
global using GrpcOrdering; | |||||
global using HealthChecks.UI.Client; | |||||
global using Microsoft.AspNetCore.Authentication.JwtBearer; | |||||
global using Microsoft.AspNetCore.Authentication; | |||||
global using Microsoft.AspNetCore.Authorization; | |||||
global using Microsoft.AspNetCore.Builder; | |||||
global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | |||||
global using Microsoft.AspNetCore.Hosting; | |||||
global using Microsoft.AspNetCore.Http; | |||||
global using Microsoft.AspNetCore.Mvc; | |||||
global using Microsoft.AspNetCore; | |||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; | |||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters; | |||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; | |||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; | |||||
global using Microsoft.Extensions.Configuration; | |||||
global using Microsoft.Extensions.DependencyInjection; | |||||
global using Microsoft.Extensions.Diagnostics.HealthChecks; | |||||
global using Microsoft.Extensions.Hosting; | |||||
global using Microsoft.Extensions.Logging; | |||||
global using Microsoft.Extensions.Options; | |||||
global using Microsoft.OpenApi.Models; | |||||
global using Serilog; | |||||
global using Swashbuckle.AspNetCore.SwaggerGen; | |||||
global using System.Collections.Generic; | |||||
global using System.IdentityModel.Tokens.Jwt; | |||||
global using System.Linq; | |||||
global using System.Net.Http.Headers; | |||||
global using System.Net.Http; | |||||
global using System.Net; | |||||
global using System.Text.Json; | |||||
global using System.Threading.Tasks; | |||||
global using System.Threading; | |||||
global using System; |
@ -1,41 +1,35 @@ | |||||
using Grpc.Core; | |||||
using Grpc.Core.Interceptors; | |||||
using Microsoft.Extensions.Logging; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure | |||||
public class GrpcExceptionInterceptor : Interceptor | |||||
{ | { | ||||
public class GrpcExceptionInterceptor : Interceptor | |||||
private readonly ILogger<GrpcExceptionInterceptor> _logger; | |||||
public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger) | |||||
{ | { | ||||
private readonly ILogger<GrpcExceptionInterceptor> _logger; | |||||
_logger = logger; | |||||
} | |||||
public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger) | |||||
{ | |||||
_logger = logger; | |||||
} | |||||
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( | |||||
TRequest request, | |||||
ClientInterceptorContext<TRequest, TResponse> context, | |||||
AsyncUnaryCallContinuation<TRequest, TResponse> continuation) | |||||
{ | |||||
var call = continuation(request, context); | |||||
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( | |||||
TRequest request, | |||||
ClientInterceptorContext<TRequest, TResponse> context, | |||||
AsyncUnaryCallContinuation<TRequest, TResponse> continuation) | |||||
{ | |||||
var call = continuation(request, context); | |||||
return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose); | |||||
} | |||||
return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose); | |||||
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> task) | |||||
{ | |||||
try | |||||
{ | |||||
var response = await task; | |||||
return response; | |||||
} | } | ||||
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t) | |||||
catch (RpcException e) | |||||
{ | { | ||||
try | |||||
{ | |||||
var response = await t; | |||||
return response; | |||||
} | |||||
catch (RpcException e) | |||||
{ | |||||
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); | |||||
return default; | |||||
} | |||||
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); | |||||
return default; | |||||
} | } | ||||
} | } | ||||
} | } |
@ -1,49 +1,40 @@ | |||||
using Microsoft.AspNetCore.Authentication; | |||||
using Microsoft.AspNetCore.Http; | |||||
using System.Collections.Generic; | |||||
using System.Net.Http; | |||||
using System.Net.Http.Headers; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; | |||||
public class HttpClientAuthorizationDelegatingHandler | |||||
: DelegatingHandler | |||||
{ | { | ||||
public class HttpClientAuthorizationDelegatingHandler | |||||
: DelegatingHandler | |||||
private readonly IHttpContextAccessor _httpContextAccessor; | |||||
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor) | |||||
{ | { | ||||
private readonly IHttpContextAccessor _httpContextAccessor; | |||||
_httpContextAccessor = httpContextAccessor; | |||||
} | |||||
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor) | |||||
{ | |||||
_httpContextAccessor = httpContextAccessor; | |||||
} | |||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |||||
{ | |||||
var authorizationHeader = _httpContextAccessor.HttpContext | |||||
.Request.Headers["Authorization"]; | |||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |||||
if (!string.IsNullOrWhiteSpace(authorizationHeader)) | |||||
{ | { | ||||
var authorizationHeader = _httpContextAccessor.HttpContext | |||||
.Request.Headers["Authorization"]; | |||||
if (!string.IsNullOrEmpty(authorizationHeader)) | |||||
{ | |||||
request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | |||||
} | |||||
var token = await GetToken(); | |||||
request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | |||||
} | |||||
if (token != null) | |||||
{ | |||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | |||||
} | |||||
var token = await GetTokenAsync(); | |||||
return await base.SendAsync(request, cancellationToken); | |||||
if (token != null) | |||||
{ | |||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | |||||
} | } | ||||
async Task<string> GetToken() | |||||
{ | |||||
const string ACCESS_TOKEN = "access_token"; | |||||
return await base.SendAsync(request, cancellationToken); | |||||
} | |||||
return await _httpContextAccessor.HttpContext | |||||
.GetTokenAsync(ACCESS_TOKEN); | |||||
} | |||||
Task<string> GetTokenAsync() | |||||
{ | |||||
const string ACCESS_TOKEN = "access_token"; | |||||
return _httpContextAccessor.HttpContext | |||||
.GetTokenAsync(ACCESS_TOKEN); | |||||
} | } | ||||
} | } |
@ -1,18 +1,16 @@ | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
{ | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
public class AddBasketItemRequest | |||||
{ | |||||
public int CatalogItemId { get; set; } | |||||
public class AddBasketItemRequest | |||||
{ | |||||
public int CatalogItemId { get; set; } | |||||
public string BasketId { get; set; } | |||||
public string BasketId { get; set; } | |||||
public int Quantity { get; set; } | |||||
public int Quantity { get; set; } | |||||
public AddBasketItemRequest() | |||||
{ | |||||
Quantity = 1; | |||||
} | |||||
public AddBasketItemRequest() | |||||
{ | |||||
Quantity = 1; | |||||
} | } | ||||
} | } | ||||
@ -1,22 +1,18 @@ | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
public class BasketData | |||||
{ | { | ||||
public string BuyerId { get; set; } | |||||
public class BasketData | |||||
{ | |||||
public string BuyerId { get; set; } | |||||
public List<BasketDataItem> Items { get; set; } = new List<BasketDataItem>(); | |||||
public List<BasketDataItem> Items { get; set; } = new(); | |||||
public BasketData() | |||||
{ | |||||
} | |||||
public BasketData(string buyerId) | |||||
{ | |||||
BuyerId = buyerId; | |||||
} | |||||
public BasketData() | |||||
{ | |||||
} | } | ||||
public BasketData(string buyerId) | |||||
{ | |||||
BuyerId = buyerId; | |||||
} | |||||
} | } | ||||
@ -1,21 +1,18 @@ | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
{ | |||||
public class BasketDataItem | |||||
{ | |||||
public string Id { get; set; } | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
public int ProductId { get; set; } | |||||
public class BasketDataItem | |||||
{ | |||||
public string Id { get; set; } | |||||
public string ProductName { get; set; } | |||||
public int ProductId { get; set; } | |||||
public decimal UnitPrice { get; set; } | |||||
public string ProductName { get; set; } | |||||
public decimal OldUnitPrice { get; set; } | |||||
public decimal UnitPrice { get; set; } | |||||
public int Quantity { get; set; } | |||||
public decimal OldUnitPrice { get; set; } | |||||
public string PictureUrl { get; set; } | |||||
} | |||||
public int Quantity { get; set; } | |||||
public string PictureUrl { get; set; } | |||||
} | } |
@ -1,15 +1,14 @@ | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
public class CatalogItem | |||||
{ | { | ||||
public int Id { get; set; } | |||||
public class CatalogItem | |||||
{ | |||||
public int Id { get; set; } | |||||
public string Name { get; set; } | |||||
public string Name { get; set; } | |||||
public decimal Price { get; set; } | |||||
public decimal Price { get; set; } | |||||
public string PictureUri { get; set; } | |||||
} | |||||
public string PictureUri { get; set; } | |||||
} | |||||
} |
@ -1,48 +1,43 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
public class OrderData | |||||
{ | { | ||||
public string OrderNumber { get; set; } | |||||
public class OrderData | |||||
{ | |||||
public string OrderNumber { get; set; } | |||||
public DateTime Date { get; set; } | |||||
public DateTime Date { get; set; } | |||||
public string Status { get; set; } | |||||
public string Status { get; set; } | |||||
public decimal Total { get; set; } | |||||
public decimal Total { get; set; } | |||||
public string Description { get; set; } | |||||
public string Description { get; set; } | |||||
public string City { get; set; } | |||||
public string City { get; set; } | |||||
public string Street { get; set; } | |||||
public string Street { get; set; } | |||||
public string State { get; set; } | |||||
public string State { get; set; } | |||||
public string Country { get; set; } | |||||
public string Country { get; set; } | |||||
public string ZipCode { get; set; } | |||||
public string ZipCode { get; set; } | |||||
public string CardNumber { get; set; } | |||||
public string CardNumber { get; set; } | |||||
public string CardHolderName { get; set; } | |||||
public string CardHolderName { get; set; } | |||||
public bool IsDraft { get; set; } | |||||
public bool IsDraft { get; set; } | |||||
public DateTime CardExpiration { get; set; } | |||||
public DateTime CardExpiration { get; set; } | |||||
public string CardExpirationShort { get; set; } | |||||
public string CardExpirationShort { get; set; } | |||||
public string CardSecurityNumber { get; set; } | |||||
public string CardSecurityNumber { get; set; } | |||||
public int CardTypeId { get; set; } | |||||
public int CardTypeId { get; set; } | |||||
public string Buyer { get; set; } | |||||
public List<OrderItemData> OrderItems { get; } = new List<OrderItemData>(); | |||||
} | |||||
public string Buyer { get; set; } | |||||
public List<OrderItemData> OrderItems { get; } = new(); | |||||
} | } | ||||
@ -1,19 +1,16 @@ | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
{ | |||||
public class OrderItemData | |||||
{ | |||||
public int ProductId { get; set; } | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
public string ProductName { get; set; } | |||||
public class OrderItemData | |||||
{ | |||||
public int ProductId { get; set; } | |||||
public decimal UnitPrice { get; set; } | |||||
public string ProductName { get; set; } | |||||
public decimal Discount { get; set; } | |||||
public decimal UnitPrice { get; set; } | |||||
public int Units { get; set; } | |||||
public decimal Discount { get; set; } | |||||
public string PictureUrl { get; set; } | |||||
} | |||||
public int Units { get; set; } | |||||
public string PictureUrl { get; set; } | |||||
} | } |
@ -1,16 +1,9 @@ | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
{ | |||||
public class UpdateBasketItemData | |||||
{ | |||||
public string BasketItemId { get; set; } | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
public int NewQty { get; set; } | |||||
public class UpdateBasketItemData | |||||
{ | |||||
public string BasketItemId { get; set; } | |||||
public UpdateBasketItemData() | |||||
{ | |||||
NewQty = 0; | |||||
} | |||||
} | |||||
public int NewQty { get; set; } | |||||
} | } |
@ -1,18 +1,13 @@ | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
public class UpdateBasketItemsRequest | |||||
{ | { | ||||
public string BasketId { get; set; } | |||||
public class UpdateBasketItemsRequest | |||||
{ | |||||
public string BasketId { get; set; } | |||||
public ICollection<UpdateBasketItemData> Updates { get; set; } | |||||
public ICollection<UpdateBasketItemData> Updates { get; set; } | |||||
public UpdateBasketItemsRequest() | |||||
{ | |||||
Updates = new List<UpdateBasketItemData>(); | |||||
} | |||||
public UpdateBasketItemsRequest() | |||||
{ | |||||
Updates = new List<UpdateBasketItemData>(); | |||||
} | } | ||||
} | } |
@ -1,13 +1,8 @@ | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
public class UpdateBasketRequest | |||||
{ | { | ||||
public string BuyerId { get; set; } | |||||
public class UpdateBasketRequest | |||||
{ | |||||
public string BuyerId { get; set; } | |||||
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; } | |||||
} | |||||
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; } | |||||
} | } |
@ -1,11 +1,10 @@ | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
public class UpdateBasketRequestItemData | |||||
{ | { | ||||
public class UpdateBasketRequestItemData | |||||
{ | |||||
public string Id { get; set; } // Basket id | |||||
public string Id { get; set; } // Basket id | |||||
public int ProductId { get; set; } // Catalog item id | |||||
public int ProductId { get; set; } // Catalog item id | |||||
public int Quantity { get; set; } // Quantity | |||||
} | |||||
public int Quantity { get; set; } // Quantity | |||||
} | } |
@ -1,103 +1,95 @@ | |||||
using GrpcBasket; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using Microsoft.Extensions.Logging; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services | |||||
public class BasketService : IBasketService | |||||
{ | { | ||||
public class BasketService : IBasketService | |||||
private readonly Basket.BasketClient _basketClient; | |||||
private readonly ILogger<BasketService> _logger; | |||||
public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger) | |||||
{ | |||||
_basketClient = basketClient; | |||||
_logger = logger; | |||||
} | |||||
public async Task<BasketData> GetByIdAsync(string id) | |||||
{ | { | ||||
private readonly Basket.BasketClient _basketClient; | |||||
private readonly ILogger<BasketService> _logger; | |||||
_logger.LogDebug("grpc client created, request = {@id}", id); | |||||
var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id }); | |||||
_logger.LogDebug("grpc response {@response}", response); | |||||
public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger) | |||||
{ | |||||
_basketClient = basketClient; | |||||
_logger = logger; | |||||
} | |||||
return MapToBasketData(response); | |||||
} | |||||
public async Task UpdateAsync(BasketData currentBasket) | |||||
{ | |||||
_logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket); | |||||
var request = MapToCustomerBasketRequest(currentBasket); | |||||
_logger.LogDebug("Grpc update basket request {@request}", request); | |||||
await _basketClient.UpdateBasketAsync(request); | |||||
} | |||||
public async Task<BasketData> GetById(string id) | |||||
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest) | |||||
{ | |||||
if (customerBasketRequest == null) | |||||
{ | { | ||||
_logger.LogDebug("grpc client created, request = {@id}", id); | |||||
var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id }); | |||||
_logger.LogDebug("grpc response {@response}", response); | |||||
return MapToBasketData(response); | |||||
return null; | |||||
} | } | ||||
public async Task UpdateAsync(BasketData currentBasket) | |||||
var map = new BasketData | |||||
{ | { | ||||
_logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket); | |||||
var request = MapToCustomerBasketRequest(currentBasket); | |||||
_logger.LogDebug("Grpc update basket request {@request}", request); | |||||
BuyerId = customerBasketRequest.Buyerid | |||||
}; | |||||
await _basketClient.UpdateBasketAsync(request); | |||||
} | |||||
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest) | |||||
customerBasketRequest.Items.ToList().ForEach(item => | |||||
{ | { | ||||
if (customerBasketRequest == null) | |||||
if (item.Id != null) | |||||
{ | { | ||||
return null; | |||||
map.Items.Add(new BasketDataItem | |||||
{ | |||||
Id = item.Id, | |||||
OldUnitPrice = (decimal)item.Oldunitprice, | |||||
PictureUrl = item.Pictureurl, | |||||
ProductId = item.Productid, | |||||
ProductName = item.Productname, | |||||
Quantity = item.Quantity, | |||||
UnitPrice = (decimal)item.Unitprice | |||||
}); | |||||
} | } | ||||
}); | |||||
var map = new BasketData | |||||
{ | |||||
BuyerId = customerBasketRequest.Buyerid | |||||
}; | |||||
customerBasketRequest.Items.ToList().ForEach(item => | |||||
{ | |||||
if (item.Id != null) | |||||
{ | |||||
map.Items.Add(new BasketDataItem | |||||
{ | |||||
Id = item.Id, | |||||
OldUnitPrice = (decimal)item.Oldunitprice, | |||||
PictureUrl = item.Pictureurl, | |||||
ProductId = item.Productid, | |||||
ProductName = item.Productname, | |||||
Quantity = item.Quantity, | |||||
UnitPrice = (decimal)item.Unitprice | |||||
}); | |||||
} | |||||
}); | |||||
return map; | |||||
} | |||||
return map; | |||||
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData) | |||||
{ | |||||
if (basketData == null) | |||||
{ | |||||
return null; | |||||
} | } | ||||
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData) | |||||
var map = new CustomerBasketRequest | |||||
{ | { | ||||
if (basketData == null) | |||||
{ | |||||
return null; | |||||
} | |||||
var map = new CustomerBasketRequest | |||||
{ | |||||
Buyerid = basketData.BuyerId | |||||
}; | |||||
Buyerid = basketData.BuyerId | |||||
}; | |||||
basketData.Items.ToList().ForEach(item => | |||||
basketData.Items.ToList().ForEach(item => | |||||
{ | |||||
if (item.Id != null) | |||||
{ | { | ||||
if (item.Id != null) | |||||
map.Items.Add(new BasketItemResponse | |||||
{ | { | ||||
map.Items.Add(new BasketItemResponse | |||||
{ | |||||
Id = item.Id, | |||||
Oldunitprice = (double)item.OldUnitPrice, | |||||
Pictureurl = item.PictureUrl, | |||||
Productid = item.ProductId, | |||||
Productname = item.ProductName, | |||||
Quantity = item.Quantity, | |||||
Unitprice = (double)item.UnitPrice | |||||
}); | |||||
} | |||||
}); | |||||
Id = item.Id, | |||||
Oldunitprice = (double)item.OldUnitPrice, | |||||
Pictureurl = item.PictureUrl, | |||||
Productid = item.ProductId, | |||||
Productname = item.ProductName, | |||||
Quantity = item.Quantity, | |||||
Unitprice = (double)item.UnitPrice | |||||
}); | |||||
} | |||||
}); | |||||
return map; | |||||
} | |||||
return map; | |||||
} | } | ||||
} | } |
@ -1,52 +1,44 @@ | |||||
using CatalogApi; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using Microsoft.Extensions.Logging; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services | |||||
public class CatalogService : ICatalogService | |||||
{ | { | ||||
public class CatalogService : ICatalogService | |||||
{ | |||||
private readonly Catalog.CatalogClient _client; | |||||
private readonly ILogger<CatalogService> _logger; | |||||
private readonly Catalog.CatalogClient _client; | |||||
private readonly ILogger<CatalogService> _logger; | |||||
public CatalogService(Catalog.CatalogClient client, ILogger<CatalogService> logger) | |||||
{ | |||||
_client = client; | |||||
_logger = logger; | |||||
} | |||||
public CatalogService(Catalog.CatalogClient client, ILogger<CatalogService> logger) | |||||
{ | |||||
_client = client; | |||||
_logger = logger; | |||||
} | |||||
public async Task<CatalogItem> GetCatalogItemAsync(int id) | |||||
{ | |||||
var request = new CatalogItemRequest { Id = id }; | |||||
_logger.LogInformation("grpc request {@request}", request); | |||||
var response = await _client.GetItemByIdAsync(request); | |||||
_logger.LogInformation("grpc response {@response}", response); | |||||
return MapToCatalogItemResponse(response); | |||||
public async Task<CatalogItem> GetCatalogItemAsync(int id) | |||||
{ | |||||
var request = new CatalogItemRequest { Id = id }; | |||||
_logger.LogInformation("grpc request {@request}", request); | |||||
var response = await _client.GetItemByIdAsync(request); | |||||
_logger.LogInformation("grpc response {@response}", response); | |||||
return MapToCatalogItemResponse(response); | |||||
} | |||||
} | |||||
public async Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids) | |||||
{ | |||||
var request = new CatalogItemsRequest { Ids = string.Join(",", ids), PageIndex = 1, PageSize = 10 }; | |||||
_logger.LogInformation("grpc request {@request}", request); | |||||
var response = await _client.GetItemsByIdsAsync(request); | |||||
_logger.LogInformation("grpc response {@response}", response); | |||||
return response.Data.Select(this.MapToCatalogItemResponse); | |||||
public async Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids) | |||||
{ | |||||
var request = new CatalogItemsRequest { Ids = string.Join(",", ids), PageIndex = 1, PageSize = 10 }; | |||||
_logger.LogInformation("grpc request {@request}", request); | |||||
var response = await _client.GetItemsByIdsAsync(request); | |||||
_logger.LogInformation("grpc response {@response}", response); | |||||
return response.Data.Select(this.MapToCatalogItemResponse); | |||||
} | |||||
} | |||||
private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse) | |||||
private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse) | |||||
{ | |||||
return new CatalogItem | |||||
{ | { | ||||
return new CatalogItem | |||||
{ | |||||
Id = catalogItemResponse.Id, | |||||
Name = catalogItemResponse.Name, | |||||
PictureUri = catalogItemResponse.PictureUri, | |||||
Price = (decimal)catalogItemResponse.Price | |||||
}; | |||||
} | |||||
Id = catalogItemResponse.Id, | |||||
Name = catalogItemResponse.Name, | |||||
PictureUri = catalogItemResponse.PictureUri, | |||||
Price = (decimal)catalogItemResponse.Price | |||||
}; | |||||
} | } | ||||
} | } |
@ -1,12 +1,8 @@ | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services | |||||
public interface IBasketService | |||||
{ | { | ||||
public interface IBasketService | |||||
{ | |||||
Task<BasketData> GetById(string id); | |||||
Task<BasketData> GetByIdAsync(string id); | |||||
Task UpdateAsync(BasketData currentBasket); | |||||
} | |||||
Task UpdateAsync(BasketData currentBasket); | |||||
} | } |
@ -1,13 +1,8 @@ | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using System.Collections.Generic; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services | |||||
public interface ICatalogService | |||||
{ | { | ||||
public interface ICatalogService | |||||
{ | |||||
Task<CatalogItem> GetCatalogItemAsync(int id); | |||||
Task<CatalogItem> GetCatalogItemAsync(int id); | |||||
Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids); | |||||
} | |||||
Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids); | |||||
} | } |
@ -1,10 +1,6 @@ | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services | |||||
public interface IOrderApiClient | |||||
{ | { | ||||
public interface IOrderApiClient | |||||
{ | |||||
Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket); | |||||
} | |||||
Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket); | |||||
} | } |
@ -1,10 +1,6 @@ | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services | |||||
public interface IOrderingService | |||||
{ | { | ||||
public interface IOrderingService | |||||
{ | |||||
Task<OrderData> GetOrderDraftAsync(BasketData basketData); | |||||
} | |||||
} | |||||
Task<OrderData> GetOrderDraftAsync(BasketData basketData); | |||||
} |
@ -1,40 +1,31 @@ | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using System.Net.Http; | |||||
using System.Threading.Tasks; | |||||
using System.Text.Json; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services | |||||
public class OrderApiClient : IOrderApiClient | |||||
{ | { | ||||
public class OrderApiClient : IOrderApiClient | |||||
{ | |||||
private readonly HttpClient _apiClient; | |||||
private readonly ILogger<OrderApiClient> _logger; | |||||
private readonly UrlsConfig _urls; | |||||
private readonly HttpClient _apiClient; | |||||
private readonly ILogger<OrderApiClient> _logger; | |||||
private readonly UrlsConfig _urls; | |||||
public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config) | |||||
{ | |||||
_apiClient = httpClient; | |||||
_logger = logger; | |||||
_urls = config.Value; | |||||
} | |||||
public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config) | |||||
{ | |||||
_apiClient = httpClient; | |||||
_logger = logger; | |||||
_urls = config.Value; | |||||
} | |||||
public async Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket) | |||||
{ | |||||
var url = _urls.Orders + UrlsConfig.OrdersOperations.GetOrderDraft(); | |||||
var content = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json"); | |||||
var response = await _apiClient.PostAsync(url, content); | |||||
public async Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket) | |||||
{ | |||||
var url = $"{_urls.Orders}{UrlsConfig.OrdersOperations.GetOrderDraft()}"; | |||||
var content = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json"); | |||||
var response = await _apiClient.PostAsync(url, content); | |||||
response.EnsureSuccessStatusCode(); | |||||
response.EnsureSuccessStatusCode(); | |||||
var ordersDraftResponse = await response.Content.ReadAsStringAsync(); | |||||
var ordersDraftResponse = await response.Content.ReadAsStringAsync(); | |||||
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions | |||||
{ | |||||
PropertyNameCaseInsensitive = true | |||||
}); | |||||
} | |||||
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions | |||||
{ | |||||
PropertyNameCaseInsensitive = true | |||||
}); | |||||
} | } | ||||
} | } |
@ -1,79 +1,72 @@ | |||||
using GrpcOrdering; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||||
using Microsoft.Extensions.Logging; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services | |||||
public class OrderingService : IOrderingService | |||||
{ | { | ||||
public class OrderingService : IOrderingService | |||||
private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | |||||
private readonly ILogger<OrderingService> _logger; | |||||
public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | |||||
{ | { | ||||
private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | |||||
private readonly ILogger<OrderingService> _logger; | |||||
_orderingGrpcClient = orderingGrpcClient; | |||||
_logger = logger; | |||||
} | |||||
public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | |||||
{ | |||||
_orderingGrpcClient = orderingGrpcClient; | |||||
_logger = logger; | |||||
} | |||||
public async Task<OrderData> GetOrderDraftAsync(BasketData basketData) | |||||
{ | |||||
_logger.LogDebug(" grpc client created, basketData={@basketData}", basketData); | |||||
public async Task<OrderData> GetOrderDraftAsync(BasketData basketData) | |||||
{ | |||||
_logger.LogDebug(" grpc client created, basketData={@basketData}", basketData); | |||||
var command = MapToOrderDraftCommand(basketData); | |||||
var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command); | |||||
_logger.LogDebug(" grpc response: {@response}", response); | |||||
var command = MapToOrderDraftCommand(basketData); | |||||
var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command); | |||||
_logger.LogDebug(" grpc response: {@response}", response); | |||||
return MapToResponse(response, basketData); | |||||
} | |||||
return MapToResponse(response, basketData); | |||||
private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData) | |||||
{ | |||||
if (orderDraft == null) | |||||
{ | |||||
return null; | |||||
} | } | ||||
private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData) | |||||
var data = new OrderData | |||||
{ | { | ||||
if (orderDraft == null) | |||||
{ | |||||
return null; | |||||
} | |||||
Buyer = basketData.BuyerId, | |||||
Total = (decimal)orderDraft.Total, | |||||
}; | |||||
var data = new OrderData | |||||
{ | |||||
Buyer = basketData.BuyerId, | |||||
Total = (decimal)orderDraft.Total, | |||||
}; | |||||
orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData | |||||
{ | |||||
Discount = (decimal)o.Discount, | |||||
PictureUrl = o.PictureUrl, | |||||
ProductId = o.ProductId, | |||||
ProductName = o.ProductName, | |||||
UnitPrice = (decimal)o.UnitPrice, | |||||
Units = o.Units, | |||||
})); | |||||
orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData | |||||
{ | |||||
Discount = (decimal)o.Discount, | |||||
PictureUrl = o.PictureUrl, | |||||
ProductId = o.ProductId, | |||||
ProductName = o.ProductName, | |||||
UnitPrice = (decimal)o.UnitPrice, | |||||
Units = o.Units, | |||||
})); | |||||
return data; | |||||
} | |||||
return data; | |||||
} | |||||
private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | |||||
private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | |||||
{ | |||||
var command = new CreateOrderDraftCommand | |||||
{ | { | ||||
var command = new CreateOrderDraftCommand | |||||
{ | |||||
BuyerId = basketData.BuyerId, | |||||
}; | |||||
BuyerId = basketData.BuyerId, | |||||
}; | |||||
basketData.Items.ForEach(i => command.Items.Add(new BasketItem | |||||
{ | |||||
Id = i.Id, | |||||
OldUnitPrice = (double)i.OldUnitPrice, | |||||
PictureUrl = i.PictureUrl, | |||||
ProductId = i.ProductId, | |||||
ProductName = i.ProductName, | |||||
Quantity = i.Quantity, | |||||
UnitPrice = (double)i.UnitPrice, | |||||
})); | |||||
return command; | |||||
} | |||||
basketData.Items.ForEach(i => command.Items.Add(new BasketItem | |||||
{ | |||||
Id = i.Id, | |||||
OldUnitPrice = (double)i.OldUnitPrice, | |||||
PictureUrl = i.PictureUrl, | |||||
ProductId = i.ProductId, | |||||
ProductName = i.ProductName, | |||||
Quantity = i.Quantity, | |||||
UnitPrice = (double)i.UnitPrice, | |||||
})); | |||||
return command; | |||||
} | } | ||||
} | } |
@ -1,225 +1,199 @@ | |||||
using CatalogApi; | |||||
using Devspaces.Support; | |||||
using GrpcBasket; | |||||
using GrpcOrdering; | |||||
using HealthChecks.UI.Client; | |||||
using Microsoft.AspNetCore.Authentication.JwtBearer; | |||||
using Microsoft.AspNetCore.Builder; | |||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks; | |||||
using Microsoft.AspNetCore.Hosting; | |||||
using Microsoft.AspNetCore.Http; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; | |||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||||
using Microsoft.Extensions.Configuration; | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
using Microsoft.Extensions.Diagnostics.HealthChecks; | |||||
using Microsoft.Extensions.Hosting; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using Microsoft.OpenApi.Models; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IdentityModel.Tokens.Jwt; | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator | |||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; | |||||
public class Startup | |||||
{ | { | ||||
public class Startup | |||||
public Startup(IConfiguration configuration) | |||||
{ | |||||
Configuration = configuration; | |||||
} | |||||
public IConfiguration Configuration { get; } | |||||
// This method gets called by the runtime. Use this method to add services to the container. | |||||
public void ConfigureServices(IServiceCollection services) | |||||
{ | |||||
services.AddHealthChecks() | |||||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||||
.AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); | |||||
services.AddCustomMvc(Configuration) | |||||
.AddCustomAuthentication(Configuration) | |||||
.AddDevspaces() | |||||
.AddApplicationServices() | |||||
.AddGrpcServices(); | |||||
} | |||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | |||||
{ | { | ||||
public Startup(IConfiguration configuration) | |||||
var pathBase = Configuration["PATH_BASE"]; | |||||
if (!string.IsNullOrEmpty(pathBase)) | |||||
{ | { | ||||
Configuration = configuration; | |||||
loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | |||||
app.UsePathBase(pathBase); | |||||
} | } | ||||
public IConfiguration Configuration { get; } | |||||
// This method gets called by the runtime. Use this method to add services to the container. | |||||
public void ConfigureServices(IServiceCollection services) | |||||
if (env.IsDevelopment()) | |||||
{ | { | ||||
services.AddHealthChecks() | |||||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||||
.AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) | |||||
.AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); | |||||
services.AddCustomMvc(Configuration) | |||||
.AddCustomAuthentication(Configuration) | |||||
.AddDevspaces() | |||||
.AddApplicationServices() | |||||
.AddGrpcServices(); | |||||
app.UseDeveloperExceptionPage(); | |||||
} | } | ||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | |||||
app.UseHttpsRedirection(); | |||||
app.UseSwagger().UseSwaggerUI(c => | |||||
{ | { | ||||
var pathBase = Configuration["PATH_BASE"]; | |||||
if (!string.IsNullOrEmpty(pathBase)) | |||||
{ | |||||
loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | |||||
app.UsePathBase(pathBase); | |||||
} | |||||
c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Purchase BFF V1"); | |||||
if (env.IsDevelopment()) | |||||
{ | |||||
app.UseDeveloperExceptionPage(); | |||||
} | |||||
c.OAuthClientId("webshoppingaggswaggerui"); | |||||
c.OAuthClientSecret(string.Empty); | |||||
c.OAuthRealm(string.Empty); | |||||
c.OAuthAppName("web shopping bff Swagger UI"); | |||||
}); | |||||
app.UseHttpsRedirection(); | |||||
app.UseRouting(); | |||||
app.UseCors("CorsPolicy"); | |||||
app.UseAuthentication(); | |||||
app.UseAuthorization(); | |||||
app.UseSwagger().UseSwaggerUI(c => | |||||
app.UseEndpoints(endpoints => | |||||
{ | |||||
endpoints.MapDefaultControllerRoute(); | |||||
endpoints.MapControllers(); | |||||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | |||||
{ | { | ||||
c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Purchase BFF V1"); | |||||
c.OAuthClientId("webshoppingaggswaggerui"); | |||||
c.OAuthClientSecret(string.Empty); | |||||
c.OAuthRealm(string.Empty); | |||||
c.OAuthAppName("web shopping bff Swagger UI"); | |||||
Predicate = _ => true, | |||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||||
}); | }); | ||||
app.UseRouting(); | |||||
app.UseCors("CorsPolicy"); | |||||
app.UseAuthentication(); | |||||
app.UseAuthorization(); | |||||
app.UseEndpoints(endpoints => | |||||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | |||||
{ | { | ||||
endpoints.MapDefaultControllerRoute(); | |||||
endpoints.MapControllers(); | |||||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | |||||
{ | |||||
Predicate = _ => true, | |||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||||
}); | |||||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | |||||
{ | |||||
Predicate = r => r.Name.Contains("self") | |||||
}); | |||||
Predicate = r => r.Name.Contains("self") | |||||
}); | }); | ||||
} | |||||
}); | |||||
} | } | ||||
} | |||||
public static class ServiceCollectionExtensions | |||||
public static class ServiceCollectionExtensions | |||||
{ | |||||
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | |||||
{ | { | ||||
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | |||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | |||||
var identityUrl = configuration.GetValue<string>("urls:identity"); | |||||
services.AddAuthentication(options => | |||||
{ | { | ||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | |||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
var identityUrl = configuration.GetValue<string>("urls:identity"); | |||||
services.AddAuthentication(options => | |||||
{ | |||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | |||||
}) | |||||
.AddJwtBearer(options => | |||||
{ | |||||
options.Authority = identityUrl; | |||||
options.RequireHttpsMetadata = false; | |||||
options.Audience = "webshoppingagg"; | |||||
}); | |||||
}) | |||||
.AddJwtBearer(options => | |||||
{ | |||||
options.Authority = identityUrl; | |||||
options.RequireHttpsMetadata = false; | |||||
options.Audience = "webshoppingagg"; | |||||
}); | |||||
return services; | |||||
} | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
services.AddOptions(); | |||||
services.Configure<UrlsConfig>(configuration.GetSection("urls")); | |||||
public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) | |||||
{ | |||||
services.AddOptions(); | |||||
services.Configure<UrlsConfig>(configuration.GetSection("urls")); | |||||
services.AddControllers() | |||||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||||
services.AddControllers() | |||||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||||
services.AddSwaggerGen(options => | |||||
{ | |||||
options.DescribeAllEnumsAsStrings(); | |||||
services.AddSwaggerGen(options => | |||||
options.SwaggerDoc("v1", new OpenApiInfo | |||||
{ | { | ||||
options.DescribeAllEnumsAsStrings(); | |||||
options.SwaggerDoc("v1", new OpenApiInfo | |||||
{ | |||||
Title = "Shopping Aggregator for Web Clients", | |||||
Version = "v1", | |||||
Description = "Shopping Aggregator for Web Clients" | |||||
}); | |||||
Title = "Shopping Aggregator for Web Clients", | |||||
Version = "v1", | |||||
Description = "Shopping Aggregator for Web Clients" | |||||
}); | |||||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |||||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |||||
{ | |||||
Type = SecuritySchemeType.OAuth2, | |||||
Flows = new OpenApiOAuthFlows() | |||||
{ | { | ||||
Type = SecuritySchemeType.OAuth2, | |||||
Flows = new OpenApiOAuthFlows() | |||||
Implicit = new OpenApiOAuthFlow() | |||||
{ | { | ||||
Implicit = new OpenApiOAuthFlow() | |||||
{ | |||||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), | |||||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"), | |||||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), | |||||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"), | |||||
Scopes = new Dictionary<string, string>() | |||||
{ | |||||
{ "webshoppingagg", "Shopping Aggregator for Web Clients" } | |||||
} | |||||
Scopes = new Dictionary<string, string>() | |||||
{ | |||||
{ "webshoppingagg", "Shopping Aggregator for Web Clients" } | |||||
} | } | ||||
} | } | ||||
}); | |||||
options.OperationFilter<AuthorizeCheckOperationFilter>(); | |||||
} | |||||
}); | }); | ||||
services.AddCors(options => | |||||
{ | |||||
options.AddPolicy("CorsPolicy", | |||||
builder => builder | |||||
.SetIsOriginAllowed((host) => true) | |||||
.AllowAnyMethod() | |||||
.AllowAnyHeader() | |||||
.AllowCredentials()); | |||||
}); | |||||
options.OperationFilter<AuthorizeCheckOperationFilter>(); | |||||
}); | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services) | |||||
services.AddCors(options => | |||||
{ | { | ||||
//register delegating handlers | |||||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | |||||
options.AddPolicy("CorsPolicy", | |||||
builder => builder | |||||
.SetIsOriginAllowed((host) => true) | |||||
.AllowAnyMethod() | |||||
.AllowAnyHeader() | |||||
.AllowCredentials()); | |||||
}); | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services) | |||||
{ | |||||
//register delegating handlers | |||||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | |||||
//register http services | |||||
//register http services | |||||
services.AddHttpClient<IOrderApiClient, OrderApiClient>() | |||||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>() | |||||
.AddDevspacesSupport(); | |||||
services.AddHttpClient<IOrderApiClient, OrderApiClient>() | |||||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>() | |||||
.AddDevspacesSupport(); | |||||
return services; | |||||
} | |||||
return services; | |||||
} | |||||
public static IServiceCollection AddGrpcServices(this IServiceCollection services) | |||||
{ | |||||
services.AddTransient<GrpcExceptionInterceptor>(); | |||||
public static IServiceCollection AddGrpcServices(this IServiceCollection services) | |||||
{ | |||||
services.AddTransient<GrpcExceptionInterceptor>(); | |||||
services.AddScoped<IBasketService, BasketService>(); | |||||
services.AddScoped<IBasketService, BasketService>(); | |||||
services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||||
{ | |||||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||||
options.Address = new Uri(basketApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||||
{ | |||||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||||
options.Address = new Uri(basketApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
services.AddScoped<ICatalogService, CatalogService>(); | |||||
services.AddScoped<ICatalogService, CatalogService>(); | |||||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||||
{ | |||||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||||
options.Address = new Uri(catalogApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||||
{ | |||||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||||
options.Address = new Uri(catalogApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
services.AddScoped<IOrderingService, OrderingService>(); | |||||
services.AddScoped<IOrderingService, OrderingService>(); | |||||
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) => | |||||
{ | |||||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||||
options.Address = new Uri(orderingApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) => | |||||
{ | |||||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||||
options.Address = new Uri(orderingApi); | |||||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||||
return services; | |||||
} | |||||
return services; | |||||
} | } | ||||
} | } |
@ -1,11 +1,11 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>net5.0</TargetFramework> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> | <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> | ||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" /> | |||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> | |||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" /> | |||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
</Project> | </Project> |
@ -1,29 +1,22 @@ | |||||
using Microsoft.AspNetCore.Http; | |||||
using System.Collections.Generic; | |||||
using System.Net.Http; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
namespace Devspaces.Support; | |||||
namespace Devspaces.Support | |||||
public class DevspacesMessageHandler : DelegatingHandler | |||||
{ | { | ||||
public class DevspacesMessageHandler : DelegatingHandler | |||||
private const string DevspacesHeaderName = "azds-route-as"; | |||||
private readonly IHttpContextAccessor _httpContextAccessor; | |||||
public DevspacesMessageHandler(IHttpContextAccessor httpContextAccessor) | |||||
{ | { | ||||
private const string DevspacesHeaderName = "azds-route-as"; | |||||
private readonly IHttpContextAccessor _httpContextAccessor; | |||||
public DevspacesMessageHandler(IHttpContextAccessor httpContextAccessor) | |||||
{ | |||||
_httpContextAccessor = httpContextAccessor; | |||||
} | |||||
_httpContextAccessor = httpContextAccessor; | |||||
} | |||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |||||
{ | |||||
var req = _httpContextAccessor.HttpContext.Request; | |||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |||||
{ | |||||
var req = _httpContextAccessor.HttpContext.Request; | |||||
if (req.Headers.ContainsKey(DevspacesHeaderName)) | |||||
{ | |||||
request.Headers.Add(DevspacesHeaderName, req.Headers[DevspacesHeaderName] as IEnumerable<string>); | |||||
} | |||||
return base.SendAsync(request, cancellationToken); | |||||
if (req.Headers.ContainsKey(DevspacesHeaderName)) | |||||
{ | |||||
request.Headers.Add(DevspacesHeaderName, req.Headers[DevspacesHeaderName] as IEnumerable<string>); | |||||
} | } | ||||
return base.SendAsync(request, cancellationToken); | |||||
} | } | ||||
} | } |
@ -0,0 +1,6 @@ | |||||
global using Microsoft.AspNetCore.Http; | |||||
global using Microsoft.Extensions.DependencyInjection; | |||||
global using System.Collections.Generic; | |||||
global using System.Net.Http; | |||||
global using System.Threading.Tasks; | |||||
global using System.Threading; |
@ -1,13 +1,10 @@ | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
namespace Devspaces.Support; | |||||
namespace Devspaces.Support | |||||
public static class HttpClientBuilderDevspacesExtensions | |||||
{ | { | ||||
public static class HttpClientBuilderDevspacesExtensions | |||||
public static IHttpClientBuilder AddDevspacesSupport(this IHttpClientBuilder builder) | |||||
{ | { | ||||
public static IHttpClientBuilder AddDevspacesSupport(this IHttpClientBuilder builder) | |||||
{ | |||||
builder.AddHttpMessageHandler<DevspacesMessageHandler>(); | |||||
return builder; | |||||
} | |||||
builder.AddHttpMessageHandler<DevspacesMessageHandler>(); | |||||
return builder; | |||||
} | } | ||||
} | } |
@ -1,13 +1,10 @@ | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
namespace Devspaces.Support; | |||||
namespace Devspaces.Support | |||||
public static class ServiceCollectionDevspacesExtensions | |||||
{ | { | ||||
public static class ServiceCollectionDevspacesExtensions | |||||
public static IServiceCollection AddDevspaces(this IServiceCollection services) | |||||
{ | { | ||||
public static IServiceCollection AddDevspaces(this IServiceCollection services) | |||||
{ | |||||
services.AddTransient<DevspacesMessageHandler>(); | |||||
return services; | |||||
} | |||||
services.AddTransient<DevspacesMessageHandler>(); | |||||
return services; | |||||
} | } | ||||
} | } |
@ -1,9 +1,6 @@ | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions | |||||
public interface IDynamicIntegrationEventHandler | |||||
{ | { | ||||
public interface IDynamicIntegrationEventHandler | |||||
{ | |||||
Task Handle(dynamic eventData); | |||||
} | |||||
Task Handle(dynamic eventData); | |||||
} | } |
@ -1,23 +1,20 @@ | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions | |||||
public interface IEventBus | |||||
{ | { | ||||
public interface IEventBus | |||||
{ | |||||
void Publish(IntegrationEvent @event); | |||||
void Publish(IntegrationEvent @event); | |||||
void Subscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T>; | |||||
void Subscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T>; | |||||
void SubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler; | |||||
void SubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler; | |||||
void UnsubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler; | |||||
void UnsubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler; | |||||
void Unsubscribe<T, TH>() | |||||
where TH : IIntegrationEventHandler<T> | |||||
where T : IntegrationEvent; | |||||
} | |||||
void Unsubscribe<T, TH>() | |||||
where TH : IIntegrationEventHandler<T> | |||||
where T : IntegrationEvent; | |||||
} | } |
@ -1,15 +1,11 @@ | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions | |||||
public interface IIntegrationEventHandler<in TIntegrationEvent> : IIntegrationEventHandler | |||||
where TIntegrationEvent : IntegrationEvent | |||||
{ | { | ||||
public interface IIntegrationEventHandler<in TIntegrationEvent> : IIntegrationEventHandler | |||||
where TIntegrationEvent : IntegrationEvent | |||||
{ | |||||
Task Handle(TIntegrationEvent @event); | |||||
} | |||||
Task Handle(TIntegrationEvent @event); | |||||
} | |||||
public interface IIntegrationEventHandler | |||||
{ | |||||
} | |||||
public interface IIntegrationEventHandler | |||||
{ | |||||
} | } |
@ -1,27 +1,23 @@ | |||||
using System; | |||||
using System.Text.Json.Serialization; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events | |||||
{ | |||||
public record IntegrationEvent | |||||
{ | |||||
public IntegrationEvent() | |||||
{ | |||||
Id = Guid.NewGuid(); | |||||
CreationDate = DateTime.UtcNow; | |||||
} | |||||
public record IntegrationEvent | |||||
{ | |||||
public IntegrationEvent() | |||||
{ | |||||
Id = Guid.NewGuid(); | |||||
CreationDate = DateTime.UtcNow; | |||||
} | |||||
[JsonConstructor] | |||||
public IntegrationEvent(Guid id, DateTime createDate) | |||||
{ | |||||
Id = id; | |||||
CreationDate = createDate; | |||||
} | |||||
[JsonConstructor] | |||||
public IntegrationEvent(Guid id, DateTime createDate) | |||||
{ | |||||
Id = id; | |||||
CreationDate = createDate; | |||||
} | |||||
[JsonInclude] | |||||
public Guid Id { get; private init; } | |||||
[JsonInclude] | |||||
public Guid Id { get; private init; } | |||||
[JsonInclude] | |||||
public DateTime CreationDate { get; private init; } | |||||
} | |||||
[JsonInclude] | |||||
public DateTime CreationDate { get; private init; } | |||||
} | } |
@ -1,30 +1,26 @@ | |||||
using System; | |||||
using System.Linq; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions | |||||
public static class GenericTypeExtensions | |||||
{ | { | ||||
public static class GenericTypeExtensions | |||||
public static string GetGenericTypeName(this Type type) | |||||
{ | { | ||||
public static string GetGenericTypeName(this Type type) | |||||
{ | |||||
var typeName = string.Empty; | |||||
if (type.IsGenericType) | |||||
{ | |||||
var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray()); | |||||
typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>"; | |||||
} | |||||
else | |||||
{ | |||||
typeName = type.Name; | |||||
} | |||||
var typeName = string.Empty; | |||||
return typeName; | |||||
if (type.IsGenericType) | |||||
{ | |||||
var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray()); | |||||
typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>"; | |||||
} | } | ||||
public static string GetGenericTypeName(this object @object) | |||||
else | |||||
{ | { | ||||
return @object.GetType().GetGenericTypeName(); | |||||
typeName = type.Name; | |||||
} | } | ||||
return typeName; | |||||
} | |||||
public static string GetGenericTypeName(this object @object) | |||||
{ | |||||
return @object.GetType().GetGenericTypeName(); | |||||
} | } | ||||
} | } |
@ -0,0 +1,8 @@ | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
global using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager; | |||||
global using System.Collections.Generic; | |||||
global using System.Linq; | |||||
global using System.Text.Json.Serialization; | |||||
global using System.Threading.Tasks; | |||||
global using System; |
@ -1,34 +1,27 @@ | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus | |||||
public interface IEventBusSubscriptionsManager | |||||
{ | { | ||||
public interface IEventBusSubscriptionsManager | |||||
{ | |||||
bool IsEmpty { get; } | |||||
event EventHandler<string> OnEventRemoved; | |||||
void AddDynamicSubscription<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler; | |||||
bool IsEmpty { get; } | |||||
event EventHandler<string> OnEventRemoved; | |||||
void AddDynamicSubscription<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler; | |||||
void AddSubscription<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T>; | |||||
void AddSubscription<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T>; | |||||
void RemoveSubscription<T, TH>() | |||||
where TH : IIntegrationEventHandler<T> | |||||
where T : IntegrationEvent; | |||||
void RemoveDynamicSubscription<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler; | |||||
void RemoveSubscription<T, TH>() | |||||
where TH : IIntegrationEventHandler<T> | |||||
where T : IntegrationEvent; | |||||
void RemoveDynamicSubscription<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler; | |||||
bool HasSubscriptionsForEvent<T>() where T : IntegrationEvent; | |||||
bool HasSubscriptionsForEvent(string eventName); | |||||
Type GetEventTypeByName(string eventName); | |||||
void Clear(); | |||||
IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>() where T : IntegrationEvent; | |||||
IEnumerable<SubscriptionInfo> GetHandlersForEvent(string eventName); | |||||
string GetEventKey<T>(); | |||||
} | |||||
} | |||||
bool HasSubscriptionsForEvent<T>() where T : IntegrationEvent; | |||||
bool HasSubscriptionsForEvent(string eventName); | |||||
Type GetEventTypeByName(string eventName); | |||||
void Clear(); | |||||
IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>() where T : IntegrationEvent; | |||||
IEnumerable<SubscriptionInfo> GetHandlersForEvent(string eventName); | |||||
string GetEventKey<T>(); | |||||
} |
@ -1,162 +1,155 @@ | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus | |||||
public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager | |||||
{ | { | ||||
public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager | |||||
private readonly Dictionary<string, List<SubscriptionInfo>> _handlers; | |||||
private readonly List<Type> _eventTypes; | |||||
public event EventHandler<string> OnEventRemoved; | |||||
public InMemoryEventBusSubscriptionsManager() | |||||
{ | { | ||||
_handlers = new Dictionary<string, List<SubscriptionInfo>>(); | |||||
_eventTypes = new List<Type>(); | |||||
} | |||||
public bool IsEmpty => _handlers is { Count: 0 }; | |||||
public void Clear() => _handlers.Clear(); | |||||
private readonly Dictionary<string, List<SubscriptionInfo>> _handlers; | |||||
private readonly List<Type> _eventTypes; | |||||
public void AddDynamicSubscription<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
DoAddSubscription(typeof(TH), eventName, isDynamic: true); | |||||
} | |||||
public event EventHandler<string> OnEventRemoved; | |||||
public void AddSubscription<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
{ | |||||
var eventName = GetEventKey<T>(); | |||||
public InMemoryEventBusSubscriptionsManager() | |||||
DoAddSubscription(typeof(TH), eventName, isDynamic: false); | |||||
if (!_eventTypes.Contains(typeof(T))) | |||||
{ | { | ||||
_handlers = new Dictionary<string, List<SubscriptionInfo>>(); | |||||
_eventTypes = new List<Type>(); | |||||
_eventTypes.Add(typeof(T)); | |||||
} | } | ||||
} | |||||
public bool IsEmpty => !_handlers.Keys.Any(); | |||||
public void Clear() => _handlers.Clear(); | |||||
public void AddDynamicSubscription<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
private void DoAddSubscription(Type handlerType, string eventName, bool isDynamic) | |||||
{ | |||||
if (!HasSubscriptionsForEvent(eventName)) | |||||
{ | { | ||||
DoAddSubscription(typeof(TH), eventName, isDynamic: true); | |||||
_handlers.Add(eventName, new List<SubscriptionInfo>()); | |||||
} | } | ||||
public void AddSubscription<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
if (_handlers[eventName].Any(s => s.HandlerType == handlerType)) | |||||
{ | { | ||||
var eventName = GetEventKey<T>(); | |||||
DoAddSubscription(typeof(TH), eventName, isDynamic: false); | |||||
if (!_eventTypes.Contains(typeof(T))) | |||||
{ | |||||
_eventTypes.Add(typeof(T)); | |||||
} | |||||
throw new ArgumentException( | |||||
$"Handler Type {handlerType.Name} already registered for '{eventName}'", nameof(handlerType)); | |||||
} | } | ||||
private void DoAddSubscription(Type handlerType, string eventName, bool isDynamic) | |||||
if (isDynamic) | |||||
{ | { | ||||
if (!HasSubscriptionsForEvent(eventName)) | |||||
{ | |||||
_handlers.Add(eventName, new List<SubscriptionInfo>()); | |||||
} | |||||
if (_handlers[eventName].Any(s => s.HandlerType == handlerType)) | |||||
{ | |||||
throw new ArgumentException( | |||||
$"Handler Type {handlerType.Name} already registered for '{eventName}'", nameof(handlerType)); | |||||
} | |||||
if (isDynamic) | |||||
{ | |||||
_handlers[eventName].Add(SubscriptionInfo.Dynamic(handlerType)); | |||||
} | |||||
else | |||||
{ | |||||
_handlers[eventName].Add(SubscriptionInfo.Typed(handlerType)); | |||||
} | |||||
_handlers[eventName].Add(SubscriptionInfo.Dynamic(handlerType)); | |||||
} | } | ||||
public void RemoveDynamicSubscription<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
else | |||||
{ | { | ||||
var handlerToRemove = FindDynamicSubscriptionToRemove<TH>(eventName); | |||||
DoRemoveHandler(eventName, handlerToRemove); | |||||
_handlers[eventName].Add(SubscriptionInfo.Typed(handlerType)); | |||||
} | } | ||||
} | |||||
public void RemoveSubscription<T, TH>() | |||||
where TH : IIntegrationEventHandler<T> | |||||
where T : IntegrationEvent | |||||
{ | |||||
var handlerToRemove = FindSubscriptionToRemove<T, TH>(); | |||||
var eventName = GetEventKey<T>(); | |||||
DoRemoveHandler(eventName, handlerToRemove); | |||||
} | |||||
public void RemoveDynamicSubscription<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
var handlerToRemove = FindDynamicSubscriptionToRemove<TH>(eventName); | |||||
DoRemoveHandler(eventName, handlerToRemove); | |||||
} | |||||
public void RemoveSubscription<T, TH>() | |||||
where TH : IIntegrationEventHandler<T> | |||||
where T : IntegrationEvent | |||||
{ | |||||
var handlerToRemove = FindSubscriptionToRemove<T, TH>(); | |||||
var eventName = GetEventKey<T>(); | |||||
DoRemoveHandler(eventName, handlerToRemove); | |||||
} | |||||
private void DoRemoveHandler(string eventName, SubscriptionInfo subsToRemove) | |||||
private void DoRemoveHandler(string eventName, SubscriptionInfo subsToRemove) | |||||
{ | |||||
if (subsToRemove != null) | |||||
{ | { | ||||
if (subsToRemove != null) | |||||
_handlers[eventName].Remove(subsToRemove); | |||||
if (!_handlers[eventName].Any()) | |||||
{ | { | ||||
_handlers[eventName].Remove(subsToRemove); | |||||
if (!_handlers[eventName].Any()) | |||||
_handlers.Remove(eventName); | |||||
var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); | |||||
if (eventType != null) | |||||
{ | { | ||||
_handlers.Remove(eventName); | |||||
var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); | |||||
if (eventType != null) | |||||
{ | |||||
_eventTypes.Remove(eventType); | |||||
} | |||||
RaiseOnEventRemoved(eventName); | |||||
_eventTypes.Remove(eventType); | |||||
} | } | ||||
RaiseOnEventRemoved(eventName); | |||||
} | } | ||||
} | |||||
public IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>() where T : IntegrationEvent | |||||
{ | |||||
var key = GetEventKey<T>(); | |||||
return GetHandlersForEvent(key); | |||||
} | } | ||||
public IEnumerable<SubscriptionInfo> GetHandlersForEvent(string eventName) => _handlers[eventName]; | |||||
} | |||||
private void RaiseOnEventRemoved(string eventName) | |||||
{ | |||||
var handler = OnEventRemoved; | |||||
handler?.Invoke(this, eventName); | |||||
} | |||||
public IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>() where T : IntegrationEvent | |||||
{ | |||||
var key = GetEventKey<T>(); | |||||
return GetHandlersForEvent(key); | |||||
} | |||||
public IEnumerable<SubscriptionInfo> GetHandlersForEvent(string eventName) => _handlers[eventName]; | |||||
private void RaiseOnEventRemoved(string eventName) | |||||
{ | |||||
var handler = OnEventRemoved; | |||||
handler?.Invoke(this, eventName); | |||||
} | |||||
private SubscriptionInfo FindDynamicSubscriptionToRemove<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
return DoFindSubscriptionToRemove(eventName, typeof(TH)); | |||||
} | |||||
private SubscriptionInfo FindDynamicSubscriptionToRemove<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
return DoFindSubscriptionToRemove(eventName, typeof(TH)); | |||||
} | |||||
private SubscriptionInfo FindSubscriptionToRemove<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
{ | |||||
var eventName = GetEventKey<T>(); | |||||
return DoFindSubscriptionToRemove(eventName, typeof(TH)); | |||||
} | |||||
private SubscriptionInfo DoFindSubscriptionToRemove(string eventName, Type handlerType) | |||||
private SubscriptionInfo FindSubscriptionToRemove<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
{ | |||||
var eventName = GetEventKey<T>(); | |||||
return DoFindSubscriptionToRemove(eventName, typeof(TH)); | |||||
} | |||||
private SubscriptionInfo DoFindSubscriptionToRemove(string eventName, Type handlerType) | |||||
{ | |||||
if (!HasSubscriptionsForEvent(eventName)) | |||||
{ | { | ||||
if (!HasSubscriptionsForEvent(eventName)) | |||||
{ | |||||
return null; | |||||
} | |||||
return null; | |||||
} | |||||
return _handlers[eventName].SingleOrDefault(s => s.HandlerType == handlerType); | |||||
return _handlers[eventName].SingleOrDefault(s => s.HandlerType == handlerType); | |||||
} | |||||
} | |||||
public bool HasSubscriptionsForEvent<T>() where T : IntegrationEvent | |||||
{ | |||||
var key = GetEventKey<T>(); | |||||
return HasSubscriptionsForEvent(key); | |||||
} | |||||
public bool HasSubscriptionsForEvent(string eventName) => _handlers.ContainsKey(eventName); | |||||
public bool HasSubscriptionsForEvent<T>() where T : IntegrationEvent | |||||
{ | |||||
var key = GetEventKey<T>(); | |||||
return HasSubscriptionsForEvent(key); | |||||
} | |||||
public bool HasSubscriptionsForEvent(string eventName) => _handlers.ContainsKey(eventName); | |||||
public Type GetEventTypeByName(string eventName) => _eventTypes.SingleOrDefault(t => t.Name == eventName); | |||||
public Type GetEventTypeByName(string eventName) => _eventTypes.SingleOrDefault(t => t.Name == eventName); | |||||
public string GetEventKey<T>() | |||||
{ | |||||
return typeof(T).Name; | |||||
} | |||||
public string GetEventKey<T>() | |||||
{ | |||||
return typeof(T).Name; | |||||
} | } | ||||
} | } |
@ -1,28 +1,22 @@ | |||||
using System; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus | |||||
public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager | |||||
{ | { | ||||
public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager | |||||
public class SubscriptionInfo | |||||
{ | { | ||||
public class SubscriptionInfo | |||||
public bool IsDynamic { get; } | |||||
public Type HandlerType { get; } | |||||
private SubscriptionInfo(bool isDynamic, Type handlerType) | |||||
{ | { | ||||
public bool IsDynamic { get; } | |||||
public Type HandlerType { get; } | |||||
IsDynamic = isDynamic; | |||||
HandlerType = handlerType; | |||||
} | |||||
private SubscriptionInfo(bool isDynamic, Type handlerType) | |||||
{ | |||||
IsDynamic = isDynamic; | |||||
HandlerType = handlerType; | |||||
} | |||||
public static SubscriptionInfo Dynamic(Type handlerType) => | |||||
new SubscriptionInfo(true, handlerType); | |||||
public static SubscriptionInfo Dynamic(Type handlerType) | |||||
{ | |||||
return new SubscriptionInfo(true, handlerType); | |||||
} | |||||
public static SubscriptionInfo Typed(Type handlerType) | |||||
{ | |||||
return new SubscriptionInfo(false, handlerType); | |||||
} | |||||
} | |||||
public static SubscriptionInfo Typed(Type handlerType) => | |||||
new SubscriptionInfo(false, handlerType); | |||||
} | } | ||||
} | } |
@ -1,131 +1,123 @@ | |||||
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 | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; | |||||
public class DefaultRabbitMQPersistentConnection | |||||
: IRabbitMQPersistentConnection | |||||
{ | { | ||||
public class DefaultRabbitMQPersistentConnection | |||||
: IRabbitMQPersistentConnection | |||||
{ | |||||
private readonly IConnectionFactory _connectionFactory; | |||||
private readonly ILogger<DefaultRabbitMQPersistentConnection> _logger; | |||||
private readonly int _retryCount; | |||||
IConnection _connection; | |||||
bool _disposed; | |||||
private readonly IConnectionFactory _connectionFactory; | |||||
private readonly ILogger<DefaultRabbitMQPersistentConnection> _logger; | |||||
private readonly int _retryCount; | |||||
IConnection _connection; | |||||
bool _disposed; | |||||
object sync_root = new object(); | |||||
object sync_root = new object(); | |||||
public DefaultRabbitMQPersistentConnection(IConnectionFactory connectionFactory, ILogger<DefaultRabbitMQPersistentConnection> logger, int retryCount = 5) | |||||
{ | |||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); | |||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||||
_retryCount = retryCount; | |||||
} | |||||
public DefaultRabbitMQPersistentConnection(IConnectionFactory connectionFactory, ILogger<DefaultRabbitMQPersistentConnection> logger, int retryCount = 5) | |||||
{ | |||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); | |||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||||
_retryCount = retryCount; | |||||
} | |||||
public bool IsConnected | |||||
public bool IsConnected | |||||
{ | |||||
get | |||||
{ | { | ||||
get | |||||
{ | |||||
return _connection != null && _connection.IsOpen && !_disposed; | |||||
} | |||||
return _connection != null && _connection.IsOpen && !_disposed; | |||||
} | } | ||||
} | |||||
public IModel CreateModel() | |||||
public IModel CreateModel() | |||||
{ | |||||
if (!IsConnected) | |||||
{ | { | ||||
if (!IsConnected) | |||||
{ | |||||
throw new InvalidOperationException("No RabbitMQ connections are available to perform this action"); | |||||
} | |||||
return _connection.CreateModel(); | |||||
throw new InvalidOperationException("No RabbitMQ connections are available to perform this action"); | |||||
} | } | ||||
public void Dispose() | |||||
{ | |||||
if (_disposed) return; | |||||
return _connection.CreateModel(); | |||||
} | |||||
_disposed = true; | |||||
public void Dispose() | |||||
{ | |||||
if (_disposed) return; | |||||
try | |||||
{ | |||||
_connection.Dispose(); | |||||
} | |||||
catch (IOException ex) | |||||
{ | |||||
_logger.LogCritical(ex.ToString()); | |||||
} | |||||
_disposed = true; | |||||
try | |||||
{ | |||||
_connection.ConnectionShutdown -= OnConnectionShutdown; | |||||
_connection.CallbackException -= OnCallbackException; | |||||
_connection.ConnectionBlocked -= OnConnectionBlocked; | |||||
_connection.Dispose(); | |||||
} | |||||
catch (IOException ex) | |||||
{ | |||||
_logger.LogCritical(ex.ToString()); | |||||
} | } | ||||
} | |||||
public bool TryConnect() | |||||
{ | |||||
_logger.LogInformation("RabbitMQ Client is trying to connect"); | |||||
public bool TryConnect() | |||||
lock (sync_root) | |||||
{ | { | ||||
_logger.LogInformation("RabbitMQ Client is trying to connect"); | |||||
var policy = RetryPolicy.Handle<SocketException>() | |||||
.Or<BrokerUnreachableException>() | |||||
.WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => | |||||
{ | |||||
_logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s ({ExceptionMessage})", $"{time.TotalSeconds:n1}", ex.Message); | |||||
} | |||||
); | |||||
lock (sync_root) | |||||
policy.Execute(() => | |||||
{ | { | ||||
var policy = RetryPolicy.Handle<SocketException>() | |||||
.Or<BrokerUnreachableException>() | |||||
.WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => | |||||
{ | |||||
_logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s ({ExceptionMessage})", $"{time.TotalSeconds:n1}", ex.Message); | |||||
} | |||||
); | |||||
policy.Execute(() => | |||||
{ | |||||
_connection = _connectionFactory | |||||
.CreateConnection(); | |||||
}); | |||||
_connection = _connectionFactory | |||||
.CreateConnection(); | |||||
}); | |||||
if (IsConnected) | |||||
{ | |||||
_connection.ConnectionShutdown += OnConnectionShutdown; | |||||
_connection.CallbackException += OnCallbackException; | |||||
_connection.ConnectionBlocked += OnConnectionBlocked; | |||||
if (IsConnected) | |||||
{ | |||||
_connection.ConnectionShutdown += OnConnectionShutdown; | |||||
_connection.CallbackException += OnCallbackException; | |||||
_connection.ConnectionBlocked += OnConnectionBlocked; | |||||
_logger.LogInformation("RabbitMQ Client acquired a persistent connection to '{HostName}' and is subscribed to failure events", _connection.Endpoint.HostName); | |||||
_logger.LogInformation("RabbitMQ Client acquired a persistent connection to '{HostName}' and is subscribed to failure events", _connection.Endpoint.HostName); | |||||
return true; | |||||
} | |||||
else | |||||
{ | |||||
_logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); | |||||
return true; | |||||
} | |||||
else | |||||
{ | |||||
_logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); | |||||
return false; | |||||
} | |||||
return false; | |||||
} | } | ||||
} | } | ||||
} | |||||
private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) | |||||
{ | |||||
if (_disposed) return; | |||||
private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) | |||||
{ | |||||
if (_disposed) return; | |||||
_logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect..."); | |||||
_logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect..."); | |||||
TryConnect(); | |||||
} | |||||
TryConnect(); | |||||
} | |||||
void OnCallbackException(object sender, CallbackExceptionEventArgs e) | |||||
{ | |||||
if (_disposed) return; | |||||
void OnCallbackException(object sender, CallbackExceptionEventArgs e) | |||||
{ | |||||
if (_disposed) return; | |||||
_logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect..."); | |||||
_logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect..."); | |||||
TryConnect(); | |||||
} | |||||
TryConnect(); | |||||
} | |||||
void OnConnectionShutdown(object sender, ShutdownEventArgs reason) | |||||
{ | |||||
if (_disposed) return; | |||||
void OnConnectionShutdown(object sender, ShutdownEventArgs reason) | |||||
{ | |||||
if (_disposed) return; | |||||
_logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect..."); | |||||
_logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect..."); | |||||
TryConnect(); | |||||
} | |||||
TryConnect(); | |||||
} | } | ||||
} | } |
@ -1,297 +1,279 @@ | |||||
using Autofac; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions; | |||||
using Microsoft.Extensions.Logging; | |||||
using Polly; | |||||
using Polly.Retry; | |||||
using RabbitMQ.Client; | |||||
using RabbitMQ.Client.Events; | |||||
using RabbitMQ.Client.Exceptions; | |||||
using System; | |||||
using System.Net.Sockets; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using System.Text.Json; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; | |||||
public class EventBusRabbitMQ : IEventBus, IDisposable | |||||
{ | { | ||||
public class EventBusRabbitMQ : IEventBus, IDisposable | |||||
{ | |||||
const string BROKER_NAME = "eshop_event_bus"; | |||||
const string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; | |||||
const string BROKER_NAME = "eshop_event_bus"; | |||||
const string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; | |||||
private readonly IRabbitMQPersistentConnection _persistentConnection; | |||||
private readonly ILogger<EventBusRabbitMQ> _logger; | |||||
private readonly IEventBusSubscriptionsManager _subsManager; | |||||
private readonly ILifetimeScope _autofac; | |||||
private readonly int _retryCount; | |||||
private readonly IRabbitMQPersistentConnection _persistentConnection; | |||||
private readonly ILogger<EventBusRabbitMQ> _logger; | |||||
private readonly IEventBusSubscriptionsManager _subsManager; | |||||
private readonly ILifetimeScope _autofac; | |||||
private readonly int _retryCount; | |||||
private IModel _consumerChannel; | |||||
private string _queueName; | |||||
private IModel _consumerChannel; | |||||
private string _queueName; | |||||
public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger<EventBusRabbitMQ> logger, | |||||
ILifetimeScope autofac, IEventBusSubscriptionsManager subsManager, string queueName = null, int retryCount = 5) | |||||
public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger<EventBusRabbitMQ> logger, | |||||
ILifetimeScope autofac, IEventBusSubscriptionsManager subsManager, string queueName = null, int retryCount = 5) | |||||
{ | |||||
_persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection)); | |||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||||
_subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); | |||||
_queueName = queueName; | |||||
_consumerChannel = CreateConsumerChannel(); | |||||
_autofac = autofac; | |||||
_retryCount = retryCount; | |||||
_subsManager.OnEventRemoved += SubsManager_OnEventRemoved; | |||||
} | |||||
private void SubsManager_OnEventRemoved(object sender, string eventName) | |||||
{ | |||||
if (!_persistentConnection.IsConnected) | |||||
{ | { | ||||
_persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection)); | |||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||||
_subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); | |||||
_queueName = queueName; | |||||
_consumerChannel = CreateConsumerChannel(); | |||||
_autofac = autofac; | |||||
_retryCount = retryCount; | |||||
_subsManager.OnEventRemoved += SubsManager_OnEventRemoved; | |||||
_persistentConnection.TryConnect(); | |||||
} | } | ||||
private void SubsManager_OnEventRemoved(object sender, string eventName) | |||||
using (var channel = _persistentConnection.CreateModel()) | |||||
{ | { | ||||
if (!_persistentConnection.IsConnected) | |||||
{ | |||||
_persistentConnection.TryConnect(); | |||||
} | |||||
channel.QueueUnbind(queue: _queueName, | |||||
exchange: BROKER_NAME, | |||||
routingKey: eventName); | |||||
using (var channel = _persistentConnection.CreateModel()) | |||||
if (_subsManager.IsEmpty) | |||||
{ | { | ||||
channel.QueueUnbind(queue: _queueName, | |||||
exchange: BROKER_NAME, | |||||
routingKey: eventName); | |||||
if (_subsManager.IsEmpty) | |||||
{ | |||||
_queueName = string.Empty; | |||||
_consumerChannel.Close(); | |||||
} | |||||
_queueName = string.Empty; | |||||
_consumerChannel.Close(); | |||||
} | } | ||||
} | } | ||||
} | |||||
public void Publish(IntegrationEvent @event) | |||||
public void Publish(IntegrationEvent @event) | |||||
{ | |||||
if (!_persistentConnection.IsConnected) | |||||
{ | { | ||||
if (!_persistentConnection.IsConnected) | |||||
_persistentConnection.TryConnect(); | |||||
} | |||||
var policy = RetryPolicy.Handle<BrokerUnreachableException>() | |||||
.Or<SocketException>() | |||||
.WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => | |||||
{ | { | ||||
_persistentConnection.TryConnect(); | |||||
} | |||||
_logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s ({ExceptionMessage})", @event.Id, $"{time.TotalSeconds:n1}", ex.Message); | |||||
}); | |||||
var policy = RetryPolicy.Handle<BrokerUnreachableException>() | |||||
.Or<SocketException>() | |||||
.WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => | |||||
{ | |||||
_logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s ({ExceptionMessage})", @event.Id, $"{time.TotalSeconds:n1}", ex.Message); | |||||
}); | |||||
var eventName = @event.GetType().Name; | |||||
var eventName = @event.GetType().Name; | |||||
_logger.LogTrace("Creating RabbitMQ channel to publish event: {EventId} ({EventName})", @event.Id, eventName); | |||||
_logger.LogTrace("Creating RabbitMQ channel to publish event: {EventId} ({EventName})", @event.Id, eventName); | |||||
using (var channel = _persistentConnection.CreateModel()) | |||||
{ | |||||
_logger.LogTrace("Declaring RabbitMQ exchange to publish event: {EventId}", @event.Id); | |||||
using (var channel = _persistentConnection.CreateModel()) | |||||
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); | |||||
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), new JsonSerializerOptions | |||||
{ | { | ||||
_logger.LogTrace("Declaring RabbitMQ exchange to publish event: {EventId}", @event.Id); | |||||
WriteIndented = true | |||||
}); | |||||
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); | |||||
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), new JsonSerializerOptions | |||||
{ | |||||
WriteIndented = true | |||||
}); | |||||
policy.Execute(() => | |||||
{ | |||||
var properties = channel.CreateBasicProperties(); | |||||
properties.DeliveryMode = 2; // persistent | |||||
policy.Execute(() => | |||||
{ | |||||
var properties = channel.CreateBasicProperties(); | |||||
properties.DeliveryMode = 2; // persistent | |||||
_logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id); | |||||
channel.BasicPublish( | |||||
exchange: BROKER_NAME, | |||||
routingKey: eventName, | |||||
mandatory: true, | |||||
basicProperties: properties, | |||||
body: body); | |||||
}); | |||||
} | |||||
_logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id); | |||||
channel.BasicPublish( | |||||
exchange: BROKER_NAME, | |||||
routingKey: eventName, | |||||
mandatory: true, | |||||
basicProperties: properties, | |||||
body: body); | |||||
}); | |||||
} | } | ||||
} | |||||
public void SubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
_logger.LogInformation("Subscribing to dynamic event {EventName} with {EventHandler}", eventName, typeof(TH).GetGenericTypeName()); | |||||
public void SubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
_logger.LogInformation("Subscribing to dynamic event {EventName} with {EventHandler}", eventName, typeof(TH).GetGenericTypeName()); | |||||
DoInternalSubscription(eventName); | |||||
_subsManager.AddDynamicSubscription<TH>(eventName); | |||||
StartBasicConsume(); | |||||
} | |||||
DoInternalSubscription(eventName); | |||||
_subsManager.AddDynamicSubscription<TH>(eventName); | |||||
StartBasicConsume(); | |||||
} | |||||
public void Subscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
{ | |||||
var eventName = _subsManager.GetEventKey<T>(); | |||||
DoInternalSubscription(eventName); | |||||
public void Subscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
{ | |||||
var eventName = _subsManager.GetEventKey<T>(); | |||||
DoInternalSubscription(eventName); | |||||
_logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).GetGenericTypeName()); | |||||
_logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).GetGenericTypeName()); | |||||
_subsManager.AddSubscription<T, TH>(); | |||||
StartBasicConsume(); | |||||
} | |||||
_subsManager.AddSubscription<T, TH>(); | |||||
StartBasicConsume(); | |||||
} | |||||
private void DoInternalSubscription(string eventName) | |||||
private void DoInternalSubscription(string eventName) | |||||
{ | |||||
var containsKey = _subsManager.HasSubscriptionsForEvent(eventName); | |||||
if (!containsKey) | |||||
{ | { | ||||
var containsKey = _subsManager.HasSubscriptionsForEvent(eventName); | |||||
if (!containsKey) | |||||
if (!_persistentConnection.IsConnected) | |||||
{ | { | ||||
if (!_persistentConnection.IsConnected) | |||||
{ | |||||
_persistentConnection.TryConnect(); | |||||
} | |||||
_consumerChannel.QueueBind(queue: _queueName, | |||||
exchange: BROKER_NAME, | |||||
routingKey: eventName); | |||||
_persistentConnection.TryConnect(); | |||||
} | } | ||||
_consumerChannel.QueueBind(queue: _queueName, | |||||
exchange: BROKER_NAME, | |||||
routingKey: eventName); | |||||
} | } | ||||
} | |||||
public void Unsubscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
{ | |||||
var eventName = _subsManager.GetEventKey<T>(); | |||||
public void Unsubscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
{ | |||||
var eventName = _subsManager.GetEventKey<T>(); | |||||
_logger.LogInformation("Unsubscribing from event {EventName}", eventName); | |||||
_logger.LogInformation("Unsubscribing from event {EventName}", eventName); | |||||
_subsManager.RemoveSubscription<T, TH>(); | |||||
} | |||||
_subsManager.RemoveSubscription<T, TH>(); | |||||
} | |||||
public void UnsubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
public void UnsubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
_subsManager.RemoveDynamicSubscription<TH>(eventName); | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
if (_consumerChannel != null) | |||||
{ | { | ||||
_subsManager.RemoveDynamicSubscription<TH>(eventName); | |||||
_consumerChannel.Dispose(); | |||||
} | } | ||||
public void Dispose() | |||||
{ | |||||
if (_consumerChannel != null) | |||||
{ | |||||
_consumerChannel.Dispose(); | |||||
} | |||||
_subsManager.Clear(); | |||||
} | |||||
_subsManager.Clear(); | |||||
} | |||||
private void StartBasicConsume() | |||||
{ | |||||
_logger.LogTrace("Starting RabbitMQ basic consume"); | |||||
private void StartBasicConsume() | |||||
if (_consumerChannel != null) | |||||
{ | { | ||||
_logger.LogTrace("Starting RabbitMQ basic consume"); | |||||
var consumer = new AsyncEventingBasicConsumer(_consumerChannel); | |||||
if (_consumerChannel != null) | |||||
{ | |||||
var consumer = new AsyncEventingBasicConsumer(_consumerChannel); | |||||
consumer.Received += Consumer_Received; | |||||
consumer.Received += Consumer_Received; | |||||
_consumerChannel.BasicConsume( | |||||
queue: _queueName, | |||||
autoAck: false, | |||||
consumer: consumer); | |||||
} | |||||
else | |||||
{ | |||||
_logger.LogError("StartBasicConsume can't call on _consumerChannel == null"); | |||||
} | |||||
_consumerChannel.BasicConsume( | |||||
queue: _queueName, | |||||
autoAck: false, | |||||
consumer: consumer); | |||||
} | } | ||||
private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventArgs) | |||||
else | |||||
{ | { | ||||
var eventName = eventArgs.RoutingKey; | |||||
var message = Encoding.UTF8.GetString(eventArgs.Body.Span); | |||||
_logger.LogError("StartBasicConsume can't call on _consumerChannel == null"); | |||||
} | |||||
} | |||||
try | |||||
{ | |||||
if (message.ToLowerInvariant().Contains("throw-fake-exception")) | |||||
{ | |||||
throw new InvalidOperationException($"Fake exception requested: \"{message}\""); | |||||
} | |||||
private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventArgs) | |||||
{ | |||||
var eventName = eventArgs.RoutingKey; | |||||
var message = Encoding.UTF8.GetString(eventArgs.Body.Span); | |||||
await ProcessEvent(eventName, message); | |||||
} | |||||
catch (Exception ex) | |||||
try | |||||
{ | |||||
if (message.ToLowerInvariant().Contains("throw-fake-exception")) | |||||
{ | { | ||||
_logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", message); | |||||
throw new InvalidOperationException($"Fake exception requested: \"{message}\""); | |||||
} | } | ||||
// Even on exception we take the message off the queue. | |||||
// in a REAL WORLD app this should be handled with a Dead Letter Exchange (DLX). | |||||
// For more information see: https://www.rabbitmq.com/dlx.html | |||||
_consumerChannel.BasicAck(eventArgs.DeliveryTag, multiple: false); | |||||
await ProcessEvent(eventName, message); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", message); | |||||
} | } | ||||
private IModel CreateConsumerChannel() | |||||
// Even on exception we take the message off the queue. | |||||
// in a REAL WORLD app this should be handled with a Dead Letter Exchange (DLX). | |||||
// For more information see: https://www.rabbitmq.com/dlx.html | |||||
_consumerChannel.BasicAck(eventArgs.DeliveryTag, multiple: false); | |||||
} | |||||
private IModel CreateConsumerChannel() | |||||
{ | |||||
if (!_persistentConnection.IsConnected) | |||||
{ | { | ||||
if (!_persistentConnection.IsConnected) | |||||
{ | |||||
_persistentConnection.TryConnect(); | |||||
} | |||||
_persistentConnection.TryConnect(); | |||||
} | |||||
_logger.LogTrace("Creating RabbitMQ consumer channel"); | |||||
_logger.LogTrace("Creating RabbitMQ consumer channel"); | |||||
var channel = _persistentConnection.CreateModel(); | |||||
var channel = _persistentConnection.CreateModel(); | |||||
channel.ExchangeDeclare(exchange: BROKER_NAME, | |||||
type: "direct"); | |||||
channel.ExchangeDeclare(exchange: BROKER_NAME, | |||||
type: "direct"); | |||||
channel.QueueDeclare(queue: _queueName, | |||||
durable: true, | |||||
exclusive: false, | |||||
autoDelete: false, | |||||
arguments: null); | |||||
channel.QueueDeclare(queue: _queueName, | |||||
durable: true, | |||||
exclusive: false, | |||||
autoDelete: false, | |||||
arguments: null); | |||||
channel.CallbackException += (sender, ea) => | |||||
{ | |||||
_logger.LogWarning(ea.Exception, "Recreating RabbitMQ consumer channel"); | |||||
channel.CallbackException += (sender, ea) => | |||||
{ | |||||
_logger.LogWarning(ea.Exception, "Recreating RabbitMQ consumer channel"); | |||||
_consumerChannel.Dispose(); | |||||
_consumerChannel = CreateConsumerChannel(); | |||||
StartBasicConsume(); | |||||
}; | |||||
_consumerChannel.Dispose(); | |||||
_consumerChannel = CreateConsumerChannel(); | |||||
StartBasicConsume(); | |||||
}; | |||||
return channel; | |||||
} | |||||
return channel; | |||||
} | |||||
private async Task ProcessEvent(string eventName, string message) | |||||
{ | |||||
_logger.LogTrace("Processing RabbitMQ event: {EventName}", eventName); | |||||
private async Task ProcessEvent(string eventName, string message) | |||||
{ | |||||
_logger.LogTrace("Processing RabbitMQ event: {EventName}", eventName); | |||||
if (_subsManager.HasSubscriptionsForEvent(eventName)) | |||||
if (_subsManager.HasSubscriptionsForEvent(eventName)) | |||||
{ | |||||
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) | |||||
{ | { | ||||
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) | |||||
var subscriptions = _subsManager.GetHandlersForEvent(eventName); | |||||
foreach (var subscription in subscriptions) | |||||
{ | { | ||||
var subscriptions = _subsManager.GetHandlersForEvent(eventName); | |||||
foreach (var subscription in subscriptions) | |||||
if (subscription.IsDynamic) | |||||
{ | { | ||||
if (subscription.IsDynamic) | |||||
{ | |||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler; | |||||
if (handler == null) continue; | |||||
using dynamic eventData = JsonDocument.Parse(message); | |||||
await Task.Yield(); | |||||
await handler.Handle(eventData); | |||||
} | |||||
else | |||||
{ | |||||
var handler = scope.ResolveOptional(subscription.HandlerType); | |||||
if (handler == null) continue; | |||||
var eventType = _subsManager.GetEventTypeByName(eventName); | |||||
var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive= true}); | |||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); | |||||
await Task.Yield(); | |||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); | |||||
} | |||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler; | |||||
if (handler == null) continue; | |||||
using dynamic eventData = JsonDocument.Parse(message); | |||||
await Task.Yield(); | |||||
await handler.Handle(eventData); | |||||
} | |||||
else | |||||
{ | |||||
var handler = scope.ResolveOptional(subscription.HandlerType); | |||||
if (handler == null) continue; | |||||
var eventType = _subsManager.GetEventTypeByName(eventName); | |||||
var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive= true}); | |||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); | |||||
await Task.Yield(); | |||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
else | |||||
{ | |||||
_logger.LogWarning("No subscription for RabbitMQ event: {EventName}", eventName); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
_logger.LogWarning("No subscription for RabbitMQ event: {EventName}", eventName); | |||||
} | } | ||||
} | } | ||||
} | } |
@ -0,0 +1,17 @@ | |||||
global using Microsoft.Extensions.Logging; | |||||
global using Polly; | |||||
global using Polly.Retry; | |||||
global using RabbitMQ.Client; | |||||
global using RabbitMQ.Client.Events; | |||||
global using RabbitMQ.Client.Exceptions; | |||||
global using System; | |||||
global using System.IO; | |||||
global using System.Net.Sockets; | |||||
global using Autofac; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions; | |||||
global using System.Text; | |||||
global using System.Threading.Tasks; | |||||
global using System.Text.Json; |
@ -1,15 +1,11 @@ | |||||
using RabbitMQ.Client; | |||||
using System; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ | |||||
public interface IRabbitMQPersistentConnection | |||||
: IDisposable | |||||
{ | { | ||||
public interface IRabbitMQPersistentConnection | |||||
: IDisposable | |||||
{ | |||||
bool IsConnected { get; } | |||||
bool IsConnected { get; } | |||||
bool TryConnect(); | |||||
bool TryConnect(); | |||||
IModel CreateModel(); | |||||
} | |||||
IModel CreateModel(); | |||||
} | } |
@ -1,60 +1,55 @@ | |||||
using Azure.Messaging.ServiceBus; | |||||
using Azure.Messaging.ServiceBus.Administration; | |||||
using System; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus | |||||
public class DefaultServiceBusPersisterConnection : IServiceBusPersisterConnection | |||||
{ | { | ||||
public class DefaultServiceBusPersisterConnection : IServiceBusPersisterConnection | |||||
{ | |||||
private readonly string _serviceBusConnectionString; | |||||
private ServiceBusClient _topicClient; | |||||
private ServiceBusAdministrationClient _subscriptionClient; | |||||
private readonly string _serviceBusConnectionString; | |||||
private ServiceBusClient _topicClient; | |||||
private ServiceBusAdministrationClient _subscriptionClient; | |||||
bool _disposed; | |||||
bool _disposed; | |||||
public DefaultServiceBusPersisterConnection(string serviceBusConnectionString) | |||||
{ | |||||
_serviceBusConnectionString = serviceBusConnectionString; | |||||
_subscriptionClient = new ServiceBusAdministrationClient(_serviceBusConnectionString); | |||||
_topicClient = new ServiceBusClient(_serviceBusConnectionString); | |||||
} | |||||
public DefaultServiceBusPersisterConnection(string serviceBusConnectionString) | |||||
{ | |||||
_serviceBusConnectionString = serviceBusConnectionString; | |||||
_subscriptionClient = new ServiceBusAdministrationClient(_serviceBusConnectionString); | |||||
_topicClient = new ServiceBusClient(_serviceBusConnectionString); | |||||
} | |||||
public ServiceBusClient TopicClient | |||||
public ServiceBusClient TopicClient | |||||
{ | |||||
get | |||||
{ | { | ||||
get | |||||
if (_topicClient.IsClosed) | |||||
{ | { | ||||
if (_topicClient.IsClosed) | |||||
{ | |||||
_topicClient = new ServiceBusClient(_serviceBusConnectionString); | |||||
} | |||||
return _topicClient; | |||||
_topicClient = new ServiceBusClient(_serviceBusConnectionString); | |||||
} | } | ||||
return _topicClient; | |||||
} | } | ||||
} | |||||
public ServiceBusAdministrationClient AdministrationClient | |||||
public ServiceBusAdministrationClient AdministrationClient | |||||
{ | |||||
get | |||||
{ | { | ||||
get | |||||
{ | |||||
return _subscriptionClient; | |||||
} | |||||
return _subscriptionClient; | |||||
} | } | ||||
} | |||||
public ServiceBusClient CreateModel() | |||||
public ServiceBusClient CreateModel() | |||||
{ | |||||
if (_topicClient.IsClosed) | |||||
{ | { | ||||
if (_topicClient.IsClosed) | |||||
{ | |||||
_topicClient = new ServiceBusClient(_serviceBusConnectionString); | |||||
} | |||||
return _topicClient; | |||||
_topicClient = new ServiceBusClient(_serviceBusConnectionString); | |||||
} | } | ||||
public void Dispose() | |||||
{ | |||||
if (_disposed) return; | |||||
return _topicClient; | |||||
} | |||||
_disposed = true; | |||||
_topicClient.DisposeAsync().GetAwaiter().GetResult(); | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
if (_disposed) return; | |||||
_disposed = true; | |||||
_topicClient.DisposeAsync().GetAwaiter().GetResult(); | |||||
} | } | ||||
} | } |
@ -1,215 +1,202 @@ | |||||
using Azure.Messaging.ServiceBus; | |||||
using Azure.Messaging.ServiceBus.Administration; | |||||
using Autofac; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using Microsoft.Extensions.Logging; | |||||
using System; | |||||
using System.Text.Json; | |||||
using System.Threading.Tasks; | |||||
using System.Text; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | |||||
public class EventBusServiceBus : IEventBus, IDisposable | |||||
{ | { | ||||
public class EventBusServiceBus : IEventBus, IDisposable | |||||
private readonly IServiceBusPersisterConnection _serviceBusPersisterConnection; | |||||
private readonly ILogger<EventBusServiceBus> _logger; | |||||
private readonly IEventBusSubscriptionsManager _subsManager; | |||||
private readonly ILifetimeScope _autofac; | |||||
private readonly string _topicName = "eshop_event_bus"; | |||||
private readonly string _subscriptionName; | |||||
private ServiceBusSender _sender; | |||||
private ServiceBusProcessor _processor; | |||||
private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; | |||||
private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; | |||||
public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, | |||||
ILogger<EventBusServiceBus> logger, IEventBusSubscriptionsManager subsManager, ILifetimeScope autofac, string subscriptionClientName) | |||||
{ | { | ||||
private readonly IServiceBusPersisterConnection _serviceBusPersisterConnection; | |||||
private readonly ILogger<EventBusServiceBus> _logger; | |||||
private readonly IEventBusSubscriptionsManager _subsManager; | |||||
private readonly ILifetimeScope _autofac; | |||||
private readonly string _topicName = "eshop_event_bus"; | |||||
private readonly string _subscriptionName; | |||||
private ServiceBusSender _sender; | |||||
private ServiceBusProcessor _processor; | |||||
private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; | |||||
private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; | |||||
public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, | |||||
ILogger<EventBusServiceBus> logger, IEventBusSubscriptionsManager subsManager, ILifetimeScope autofac, string subscriptionClientName) | |||||
{ | |||||
_serviceBusPersisterConnection = serviceBusPersisterConnection; | |||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||||
_subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); | |||||
_autofac = autofac; | |||||
_subscriptionName = subscriptionClientName; | |||||
_sender = _serviceBusPersisterConnection.TopicClient.CreateSender(_topicName); | |||||
ServiceBusProcessorOptions options = new ServiceBusProcessorOptions { MaxConcurrentCalls = 10, AutoCompleteMessages = false }; | |||||
_processor = _serviceBusPersisterConnection.TopicClient.CreateProcessor(_topicName, _subscriptionName, options); | |||||
RemoveDefaultRule(); | |||||
RegisterSubscriptionClientMessageHandlerAsync().GetAwaiter().GetResult(); | |||||
} | |||||
_serviceBusPersisterConnection = serviceBusPersisterConnection; | |||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | |||||
_subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); | |||||
_autofac = autofac; | |||||
_subscriptionName = subscriptionClientName; | |||||
_sender = _serviceBusPersisterConnection.TopicClient.CreateSender(_topicName); | |||||
ServiceBusProcessorOptions options = new ServiceBusProcessorOptions { MaxConcurrentCalls = 10, AutoCompleteMessages = false }; | |||||
_processor = _serviceBusPersisterConnection.TopicClient.CreateProcessor(_topicName, _subscriptionName, options); | |||||
RemoveDefaultRule(); | |||||
RegisterSubscriptionClientMessageHandlerAsync().GetAwaiter().GetResult(); | |||||
} | |||||
public void Publish(IntegrationEvent @event) | |||||
{ | |||||
var eventName = @event.GetType().Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | |||||
var jsonMessage = JsonSerializer.Serialize(@event, @event.GetType()); | |||||
var body = Encoding.UTF8.GetBytes(jsonMessage); | |||||
public void Publish(IntegrationEvent @event) | |||||
{ | |||||
var eventName = @event.GetType().Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | |||||
var jsonMessage = JsonSerializer.Serialize(@event, @event.GetType()); | |||||
var body = Encoding.UTF8.GetBytes(jsonMessage); | |||||
var message = new ServiceBusMessage | |||||
{ | |||||
MessageId = Guid.NewGuid().ToString(), | |||||
Body = new BinaryData(body), | |||||
Subject = eventName, | |||||
}; | |||||
var message = new ServiceBusMessage | |||||
{ | |||||
MessageId = Guid.NewGuid().ToString(), | |||||
Body = new BinaryData(body), | |||||
Subject = eventName, | |||||
}; | |||||
_sender.SendMessageAsync(message) | |||||
.GetAwaiter() | |||||
.GetResult(); | |||||
} | |||||
_sender.SendMessageAsync(message) | |||||
.GetAwaiter() | |||||
.GetResult(); | |||||
} | |||||
public void SubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
_logger.LogInformation("Subscribing to dynamic event {EventName} with {EventHandler}", eventName, typeof(TH).Name); | |||||
public void SubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
_logger.LogInformation("Subscribing to dynamic event {EventName} with {EventHandler}", eventName, typeof(TH).Name); | |||||
_subsManager.AddDynamicSubscription<TH>(eventName); | |||||
} | |||||
_subsManager.AddDynamicSubscription<TH>(eventName); | |||||
} | |||||
public void Subscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
{ | |||||
var eventName = typeof(T).Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | |||||
public void Subscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
var containsKey = _subsManager.HasSubscriptionsForEvent<T>(); | |||||
if (!containsKey) | |||||
{ | { | ||||
var eventName = typeof(T).Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | |||||
var containsKey = _subsManager.HasSubscriptionsForEvent<T>(); | |||||
if (!containsKey) | |||||
try | |||||
{ | { | ||||
try | |||||
_serviceBusPersisterConnection.AdministrationClient.CreateRuleAsync(_topicName, _subscriptionName, new CreateRuleOptions | |||||
{ | { | ||||
_serviceBusPersisterConnection.AdministrationClient.CreateRuleAsync(_topicName, _subscriptionName, new CreateRuleOptions | |||||
{ | |||||
Filter = new CorrelationRuleFilter() { Subject = eventName }, | |||||
Name = eventName | |||||
}).GetAwaiter().GetResult(); | |||||
} | |||||
catch (ServiceBusException) | |||||
{ | |||||
_logger.LogWarning("The messaging entity {eventName} already exists.", eventName); | |||||
} | |||||
Filter = new CorrelationRuleFilter() { Subject = eventName }, | |||||
Name = eventName | |||||
}).GetAwaiter().GetResult(); | |||||
} | |||||
catch (ServiceBusException) | |||||
{ | |||||
_logger.LogWarning("The messaging entity {eventName} already exists.", eventName); | |||||
} | } | ||||
} | |||||
_logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).Name); | |||||
_logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).Name); | |||||
_subsManager.AddSubscription<T, TH>(); | |||||
} | |||||
_subsManager.AddSubscription<T, TH>(); | |||||
} | |||||
public void Unsubscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
{ | |||||
var eventName = typeof(T).Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | |||||
public void Unsubscribe<T, TH>() | |||||
where T : IntegrationEvent | |||||
where TH : IIntegrationEventHandler<T> | |||||
try | |||||
{ | |||||
_serviceBusPersisterConnection | |||||
.AdministrationClient | |||||
.DeleteRuleAsync(_topicName, _subscriptionName, eventName) | |||||
.GetAwaiter() | |||||
.GetResult(); | |||||
} | |||||
catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityNotFound) | |||||
{ | { | ||||
var eventName = typeof(T).Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | |||||
_logger.LogWarning("The messaging entity {eventName} Could not be found.", eventName); | |||||
} | |||||
try | |||||
{ | |||||
_serviceBusPersisterConnection | |||||
.AdministrationClient | |||||
.DeleteRuleAsync(_topicName, _subscriptionName, eventName) | |||||
.GetAwaiter() | |||||
.GetResult(); | |||||
} | |||||
catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityNotFound) | |||||
{ | |||||
_logger.LogWarning("The messaging entity {eventName} Could not be found.", eventName); | |||||
} | |||||
_logger.LogInformation("Unsubscribing from event {EventName}", eventName); | |||||
_logger.LogInformation("Unsubscribing from event {EventName}", eventName); | |||||
_subsManager.RemoveSubscription<T, TH>(); | |||||
} | |||||
_subsManager.RemoveSubscription<T, TH>(); | |||||
} | |||||
public void UnsubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
_logger.LogInformation("Unsubscribing from dynamic event {EventName}", eventName); | |||||
public void UnsubscribeDynamic<TH>(string eventName) | |||||
where TH : IDynamicIntegrationEventHandler | |||||
{ | |||||
_logger.LogInformation("Unsubscribing from dynamic event {EventName}", eventName); | |||||
_subsManager.RemoveDynamicSubscription<TH>(eventName); | |||||
} | |||||
_subsManager.RemoveDynamicSubscription<TH>(eventName); | |||||
} | |||||
private async Task RegisterSubscriptionClientMessageHandlerAsync() | |||||
{ | |||||
_processor.ProcessMessageAsync += | |||||
async (args) => | |||||
{ | |||||
var eventName = $"{args.Message.Subject}{INTEGRATION_EVENT_SUFFIX}"; | |||||
string messageData = args.Message.Body.ToString(); | |||||
private async Task RegisterSubscriptionClientMessageHandlerAsync() | |||||
{ | |||||
_processor.ProcessMessageAsync += | |||||
async (args) => | |||||
// Complete the message so that it is not received again. | |||||
if (await ProcessEvent(eventName, messageData)) | |||||
{ | { | ||||
var eventName = $"{args.Message.Subject}{INTEGRATION_EVENT_SUFFIX}"; | |||||
string messageData = args.Message.Body.ToString(); | |||||
// Complete the message so that it is not received again. | |||||
if (await ProcessEvent(eventName, messageData)) | |||||
{ | |||||
await args.CompleteMessageAsync(args.Message); | |||||
} | |||||
}; | |||||
await args.CompleteMessageAsync(args.Message); | |||||
} | |||||
}; | |||||
_processor.ProcessErrorAsync += ErrorHandler; | |||||
await _processor.StartProcessingAsync(); | |||||
} | |||||
_processor.ProcessErrorAsync += ErrorHandler; | |||||
await _processor.StartProcessingAsync(); | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
_subsManager.Clear(); | |||||
_processor.CloseAsync().GetAwaiter().GetResult(); | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
_subsManager.Clear(); | |||||
_processor.CloseAsync().GetAwaiter().GetResult(); | |||||
} | |||||
private Task ErrorHandler(ProcessErrorEventArgs args) | |||||
{ | |||||
var ex = args.Exception; | |||||
var context = args.ErrorSource; | |||||
private Task ErrorHandler(ProcessErrorEventArgs args) | |||||
{ | |||||
var ex = args.Exception; | |||||
var context = args.ErrorSource; | |||||
_logger.LogError(ex, "ERROR handling message: {ExceptionMessage} - Context: {@ExceptionContext}", ex.Message, context); | |||||
_logger.LogError(ex, "ERROR handling message: {ExceptionMessage} - Context: {@ExceptionContext}", ex.Message, context); | |||||
return Task.CompletedTask; | |||||
} | |||||
return Task.CompletedTask; | |||||
} | |||||
private async Task<bool> ProcessEvent(string eventName, string message) | |||||
private async Task<bool> ProcessEvent(string eventName, string message) | |||||
{ | |||||
var processed = false; | |||||
if (_subsManager.HasSubscriptionsForEvent(eventName)) | |||||
{ | { | ||||
var processed = false; | |||||
if (_subsManager.HasSubscriptionsForEvent(eventName)) | |||||
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) | |||||
{ | { | ||||
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) | |||||
var subscriptions = _subsManager.GetHandlersForEvent(eventName); | |||||
foreach (var subscription in subscriptions) | |||||
{ | { | ||||
var subscriptions = _subsManager.GetHandlersForEvent(eventName); | |||||
foreach (var subscription in subscriptions) | |||||
if (subscription.IsDynamic) | |||||
{ | |||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler; | |||||
if (handler == null) continue; | |||||
using dynamic eventData = JsonDocument.Parse(message); | |||||
await handler.Handle(eventData); | |||||
} | |||||
else | |||||
{ | { | ||||
if (subscription.IsDynamic) | |||||
{ | |||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler; | |||||
if (handler == null) continue; | |||||
using dynamic eventData = JsonDocument.Parse(message); | |||||
await handler.Handle(eventData); | |||||
} | |||||
else | |||||
{ | |||||
var handler = scope.ResolveOptional(subscription.HandlerType); | |||||
if (handler == null) continue; | |||||
var eventType = _subsManager.GetEventTypeByName(eventName); | |||||
var integrationEvent = JsonSerializer.Deserialize(message, eventType); | |||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); | |||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); | |||||
} | |||||
var handler = scope.ResolveOptional(subscription.HandlerType); | |||||
if (handler == null) continue; | |||||
var eventType = _subsManager.GetEventTypeByName(eventName); | |||||
var integrationEvent = JsonSerializer.Deserialize(message, eventType); | |||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); | |||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); | |||||
} | } | ||||
} | } | ||||
processed = true; | |||||
} | } | ||||
return processed; | |||||
processed = true; | |||||
} | } | ||||
return processed; | |||||
} | |||||
private void RemoveDefaultRule() | |||||
private void RemoveDefaultRule() | |||||
{ | |||||
try | |||||
{ | { | ||||
try | |||||
{ | |||||
_serviceBusPersisterConnection | |||||
.AdministrationClient | |||||
.DeleteRuleAsync(_topicName, _subscriptionName, RuleProperties.DefaultRuleName) | |||||
.GetAwaiter() | |||||
.GetResult(); | |||||
} | |||||
catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityNotFound) | |||||
{ | |||||
_logger.LogWarning("The messaging entity {DefaultRuleName} Could not be found.", RuleProperties.DefaultRuleName); | |||||
} | |||||
_serviceBusPersisterConnection | |||||
.AdministrationClient | |||||
.DeleteRuleAsync(_topicName, _subscriptionName, RuleProperties.DefaultRuleName) | |||||
.GetAwaiter() | |||||
.GetResult(); | |||||
} | |||||
catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityNotFound) | |||||
{ | |||||
_logger.LogWarning("The messaging entity {DefaultRuleName} Could not be found.", RuleProperties.DefaultRuleName); | |||||
} | } | ||||
} | } | ||||
} | |||||
} |
@ -0,0 +1,23 @@ | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
global using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager; | |||||
global using System.Collections.Generic; | |||||
global using System.Linq; | |||||
global using System.Text.Json.Serialization; | |||||
global using System.Threading.Tasks; | |||||
global using System; | |||||
global using Autofac; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||||
global using Microsoft.Extensions.Logging; | |||||
global using System.Text; | |||||
global using System.Text.Json; | |||||
global using Azure.Messaging.ServiceBus; | |||||
global using Azure.Messaging.ServiceBus.Administration; | |||||
global using System; | |||||
@ -1,12 +1,7 @@ | |||||
using Azure.Messaging.ServiceBus; | |||||
using Azure.Messaging.ServiceBus.Administration; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus | |||||
{ | |||||
using System; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | |||||
public interface IServiceBusPersisterConnection : IDisposable | |||||
{ | |||||
ServiceBusClient TopicClient { get; } | |||||
ServiceBusAdministrationClient AdministrationClient { get; } | |||||
} | |||||
public interface IServiceBusPersisterConnection : IDisposable | |||||
{ | |||||
ServiceBusClient TopicClient { get; } | |||||
ServiceBusAdministrationClient AdministrationClient { get; } | |||||
} | } |
@ -1,10 +1,10 @@ | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; | |||||
public enum EventStateEnum | |||||
{ | { | ||||
public enum EventStateEnum | |||||
{ | |||||
NotPublished = 0, | |||||
InProgress = 1, | |||||
Published = 2, | |||||
PublishedFailed = 3 | |||||
} | |||||
NotPublished = 0, | |||||
InProgress = 1, | |||||
Published = 2, | |||||
PublishedFailed = 3 | |||||
} | } | ||||
@ -0,0 +1,12 @@ | |||||
global using Microsoft.EntityFrameworkCore; | |||||
global using Microsoft.EntityFrameworkCore.Metadata.Builders; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
global using System; | |||||
global using System.Text.Json; | |||||
global using System.ComponentModel.DataAnnotations.Schema; | |||||
global using System.Linq; | |||||
global using System.Threading.Tasks; | |||||
global using Microsoft.EntityFrameworkCore.Storage; | |||||
global using System.Collections.Generic; | |||||
global using System.Data.Common; | |||||
global using System.Reflection; |
@ -1,45 +1,41 @@ | |||||
using Microsoft.EntityFrameworkCore; | |||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF | |||||
public class IntegrationEventLogContext : DbContext | |||||
{ | { | ||||
public class IntegrationEventLogContext : DbContext | |||||
public IntegrationEventLogContext(DbContextOptions<IntegrationEventLogContext> options) : base(options) | |||||
{ | { | ||||
public IntegrationEventLogContext(DbContextOptions<IntegrationEventLogContext> options) : base(options) | |||||
{ | |||||
} | |||||
} | |||||
public DbSet<IntegrationEventLogEntry> IntegrationEventLogs { get; set; } | |||||
public DbSet<IntegrationEventLogEntry> IntegrationEventLogs { get; set; } | |||||
protected override void OnModelCreating(ModelBuilder builder) | |||||
{ | |||||
builder.Entity<IntegrationEventLogEntry>(ConfigureIntegrationEventLogEntry); | |||||
} | |||||
protected override void OnModelCreating(ModelBuilder builder) | |||||
{ | |||||
builder.Entity<IntegrationEventLogEntry>(ConfigureIntegrationEventLogEntry); | |||||
} | |||||
void ConfigureIntegrationEventLogEntry(EntityTypeBuilder<IntegrationEventLogEntry> builder) | |||||
{ | |||||
builder.ToTable("IntegrationEventLog"); | |||||
void ConfigureIntegrationEventLogEntry(EntityTypeBuilder<IntegrationEventLogEntry> builder) | |||||
{ | |||||
builder.ToTable("IntegrationEventLog"); | |||||
builder.HasKey(e => e.EventId); | |||||
builder.HasKey(e => e.EventId); | |||||
builder.Property(e => e.EventId) | |||||
.IsRequired(); | |||||
builder.Property(e => e.EventId) | |||||
.IsRequired(); | |||||
builder.Property(e => e.Content) | |||||
.IsRequired(); | |||||
builder.Property(e => e.Content) | |||||
.IsRequired(); | |||||
builder.Property(e => e.CreationTime) | |||||
.IsRequired(); | |||||
builder.Property(e => e.CreationTime) | |||||
.IsRequired(); | |||||
builder.Property(e => e.State) | |||||
.IsRequired(); | |||||
builder.Property(e => e.State) | |||||
.IsRequired(); | |||||
builder.Property(e => e.TimesSent) | |||||
.IsRequired(); | |||||
builder.Property(e => e.TimesSent) | |||||
.IsRequired(); | |||||
builder.Property(e => e.EventTypeName) | |||||
.IsRequired(); | |||||
builder.Property(e => e.EventTypeName) | |||||
.IsRequired(); | |||||
} | |||||
} | } | ||||
} | } |
@ -1,43 +1,36 @@ | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using System; | |||||
using System.Text.Json; | |||||
using System.ComponentModel.DataAnnotations.Schema; | |||||
using System.Linq; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF | |||||
public class IntegrationEventLogEntry | |||||
{ | { | ||||
public class IntegrationEventLogEntry | |||||
private IntegrationEventLogEntry() { } | |||||
public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId) | |||||
{ | { | ||||
private IntegrationEventLogEntry() { } | |||||
public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId) | |||||
EventId = @event.Id; | |||||
CreationTime = @event.CreationDate; | |||||
EventTypeName = @event.GetType().FullName; | |||||
Content = JsonSerializer.Serialize(@event, @event.GetType(), new JsonSerializerOptions | |||||
{ | { | ||||
EventId = @event.Id; | |||||
CreationTime = @event.CreationDate; | |||||
EventTypeName = @event.GetType().FullName; | |||||
Content = JsonSerializer.Serialize(@event, @event.GetType(), new JsonSerializerOptions | |||||
{ | |||||
WriteIndented = true | |||||
}); | |||||
State = EventStateEnum.NotPublished; | |||||
TimesSent = 0; | |||||
TransactionId = transactionId.ToString(); | |||||
} | |||||
public Guid EventId { get; private set; } | |||||
public string EventTypeName { get; private set; } | |||||
[NotMapped] | |||||
public string EventTypeShortName => EventTypeName.Split('.')?.Last(); | |||||
[NotMapped] | |||||
public IntegrationEvent IntegrationEvent { get; private set; } | |||||
public EventStateEnum State { get; set; } | |||||
public int TimesSent { get; set; } | |||||
public DateTime CreationTime { get; private set; } | |||||
public string Content { get; private set; } | |||||
public string TransactionId { get; private set; } | |||||
WriteIndented = true | |||||
}); | |||||
State = EventStateEnum.NotPublished; | |||||
TimesSent = 0; | |||||
TransactionId = transactionId.ToString(); | |||||
} | |||||
public Guid EventId { get; private set; } | |||||
public string EventTypeName { get; private set; } | |||||
[NotMapped] | |||||
public string EventTypeShortName => EventTypeName.Split('.')?.Last(); | |||||
[NotMapped] | |||||
public IntegrationEvent IntegrationEvent { get; private set; } | |||||
public EventStateEnum State { get; set; } | |||||
public int TimesSent { get; set; } | |||||
public DateTime CreationTime { get; private set; } | |||||
public string Content { get; private set; } | |||||
public string TransactionId { get; private set; } | |||||
public IntegrationEventLogEntry DeserializeJsonContent(Type type) | |||||
{ | |||||
IntegrationEvent = JsonSerializer.Deserialize(Content, type, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }) as IntegrationEvent; | |||||
return this; | |||||
} | |||||
public IntegrationEventLogEntry DeserializeJsonContent(Type type) | |||||
{ | |||||
IntegrationEvent = JsonSerializer.Deserialize(Content, type, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }) as IntegrationEvent; | |||||
return this; | |||||
} | } | ||||
} | } |
@ -1,17 +1,10 @@ | |||||
using Microsoft.EntityFrameworkCore.Storage; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services | |||||
public interface IIntegrationEventLogService | |||||
{ | { | ||||
public interface IIntegrationEventLogService | |||||
{ | |||||
Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync(Guid transactionId); | |||||
Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction); | |||||
Task MarkEventAsPublishedAsync(Guid eventId); | |||||
Task MarkEventAsInProgressAsync(Guid eventId); | |||||
Task MarkEventAsFailedAsync(Guid eventId); | |||||
} | |||||
Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync(Guid transactionId); | |||||
Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction); | |||||
Task MarkEventAsPublishedAsync(Guid eventId); | |||||
Task MarkEventAsInProgressAsync(Guid eventId); | |||||
Task MarkEventAsFailedAsync(Guid eventId); | |||||
} | } |
@ -1,110 +1,99 @@ | |||||
using Microsoft.EntityFrameworkCore; | |||||
using Microsoft.EntityFrameworkCore.Storage; | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Data.Common; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services | |||||
{ | |||||
public class IntegrationEventLogService : IIntegrationEventLogService, IDisposable | |||||
{ | |||||
private readonly IntegrationEventLogContext _integrationEventLogContext; | |||||
private readonly DbConnection _dbConnection; | |||||
private readonly List<Type> _eventTypes; | |||||
private volatile bool disposedValue; | |||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; | |||||
public IntegrationEventLogService(DbConnection dbConnection) | |||||
{ | |||||
_dbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection)); | |||||
_integrationEventLogContext = new IntegrationEventLogContext( | |||||
new DbContextOptionsBuilder<IntegrationEventLogContext>() | |||||
.UseSqlServer(_dbConnection) | |||||
.Options); | |||||
_eventTypes = Assembly.Load(Assembly.GetEntryAssembly().FullName) | |||||
.GetTypes() | |||||
.Where(t => t.Name.EndsWith(nameof(IntegrationEvent))) | |||||
.ToList(); | |||||
} | |||||
public class IntegrationEventLogService : IIntegrationEventLogService, IDisposable | |||||
{ | |||||
private readonly IntegrationEventLogContext _integrationEventLogContext; | |||||
private readonly DbConnection _dbConnection; | |||||
private readonly List<Type> _eventTypes; | |||||
private volatile bool _disposedValue; | |||||
public async Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync(Guid transactionId) | |||||
{ | |||||
var tid = transactionId.ToString(); | |||||
public IntegrationEventLogService(DbConnection dbConnection) | |||||
{ | |||||
_dbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection)); | |||||
_integrationEventLogContext = new IntegrationEventLogContext( | |||||
new DbContextOptionsBuilder<IntegrationEventLogContext>() | |||||
.UseSqlServer(_dbConnection) | |||||
.Options); | |||||
_eventTypes = Assembly.Load(Assembly.GetEntryAssembly().FullName) | |||||
.GetTypes() | |||||
.Where(t => t.Name.EndsWith(nameof(IntegrationEvent))) | |||||
.ToList(); | |||||
} | |||||
var result = await _integrationEventLogContext.IntegrationEventLogs | |||||
.Where(e => e.TransactionId == tid && e.State == EventStateEnum.NotPublished).ToListAsync(); | |||||
public async Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync(Guid transactionId) | |||||
{ | |||||
var tid = transactionId.ToString(); | |||||
if (result != null && result.Any()) | |||||
{ | |||||
return result.OrderBy(o => o.CreationTime) | |||||
.Select(e => e.DeserializeJsonContent(_eventTypes.Find(t => t.Name == e.EventTypeShortName))); | |||||
} | |||||
var result = await _integrationEventLogContext.IntegrationEventLogs | |||||
.Where(e => e.TransactionId == tid && e.State == EventStateEnum.NotPublished).ToListAsync(); | |||||
return new List<IntegrationEventLogEntry>(); | |||||
if (result != null && result.Any()) | |||||
{ | |||||
return result.OrderBy(o => o.CreationTime) | |||||
.Select(e => e.DeserializeJsonContent(_eventTypes.Find(t => t.Name == e.EventTypeShortName))); | |||||
} | } | ||||
public Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction) | |||||
{ | |||||
if (transaction == null) throw new ArgumentNullException(nameof(transaction)); | |||||
return new List<IntegrationEventLogEntry>(); | |||||
} | |||||
var eventLogEntry = new IntegrationEventLogEntry(@event, transaction.TransactionId); | |||||
public Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction) | |||||
{ | |||||
if (transaction == null) throw new ArgumentNullException(nameof(transaction)); | |||||
_integrationEventLogContext.Database.UseTransaction(transaction.GetDbTransaction()); | |||||
_integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry); | |||||
var eventLogEntry = new IntegrationEventLogEntry(@event, transaction.TransactionId); | |||||
return _integrationEventLogContext.SaveChangesAsync(); | |||||
} | |||||
_integrationEventLogContext.Database.UseTransaction(transaction.GetDbTransaction()); | |||||
_integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry); | |||||
public Task MarkEventAsPublishedAsync(Guid eventId) | |||||
{ | |||||
return UpdateEventStatus(eventId, EventStateEnum.Published); | |||||
} | |||||
return _integrationEventLogContext.SaveChangesAsync(); | |||||
} | |||||
public Task MarkEventAsInProgressAsync(Guid eventId) | |||||
{ | |||||
return UpdateEventStatus(eventId, EventStateEnum.InProgress); | |||||
} | |||||
public Task MarkEventAsPublishedAsync(Guid eventId) | |||||
{ | |||||
return UpdateEventStatus(eventId, EventStateEnum.Published); | |||||
} | |||||
public Task MarkEventAsFailedAsync(Guid eventId) | |||||
{ | |||||
return UpdateEventStatus(eventId, EventStateEnum.PublishedFailed); | |||||
} | |||||
public Task MarkEventAsInProgressAsync(Guid eventId) | |||||
{ | |||||
return UpdateEventStatus(eventId, EventStateEnum.InProgress); | |||||
} | |||||
private Task UpdateEventStatus(Guid eventId, EventStateEnum status) | |||||
{ | |||||
var eventLogEntry = _integrationEventLogContext.IntegrationEventLogs.Single(ie => ie.EventId == eventId); | |||||
eventLogEntry.State = status; | |||||
public Task MarkEventAsFailedAsync(Guid eventId) | |||||
{ | |||||
return UpdateEventStatus(eventId, EventStateEnum.PublishedFailed); | |||||
} | |||||
private Task UpdateEventStatus(Guid eventId, EventStateEnum status) | |||||
{ | |||||
var eventLogEntry = _integrationEventLogContext.IntegrationEventLogs.Single(ie => ie.EventId == eventId); | |||||
eventLogEntry.State = status; | |||||
if (status == EventStateEnum.InProgress) | |||||
eventLogEntry.TimesSent++; | |||||
if (status == EventStateEnum.InProgress) | |||||
eventLogEntry.TimesSent++; | |||||
_integrationEventLogContext.IntegrationEventLogs.Update(eventLogEntry); | |||||
_integrationEventLogContext.IntegrationEventLogs.Update(eventLogEntry); | |||||
return _integrationEventLogContext.SaveChangesAsync(); | |||||
} | |||||
return _integrationEventLogContext.SaveChangesAsync(); | |||||
} | |||||
protected virtual void Dispose(bool disposing) | |||||
protected virtual void Dispose(bool disposing) | |||||
{ | |||||
if (!_disposedValue) | |||||
{ | { | ||||
if (!disposedValue) | |||||
if (disposing) | |||||
{ | { | ||||
if (disposing) | |||||
{ | |||||
_integrationEventLogContext?.Dispose(); | |||||
} | |||||
_integrationEventLogContext?.Dispose(); | |||||
} | |||||
disposedValue = true; | |||||
} | |||||
_disposedValue = true; | |||||
} | } | ||||
} | |||||
public void Dispose() | |||||
{ | |||||
Dispose(disposing: true); | |||||
GC.SuppressFinalize(this); | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
Dispose(disposing: true); | |||||
GC.SuppressFinalize(this); | |||||
} | } | ||||
} | } |