* 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"> | |||
<PropertyGroup> | |||
<TargetFramework>net5.0</TargetFramework> | |||
<TargetFramework>net6.0</TargetFramework> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<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> | |||
</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); | |||
} | |||
} |