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>
This commit is contained in:
		
							parent
							
								
									ab1d9cc897
								
							
						
					
					
						commit
						c37320b3e5
					
				| @ -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 class BasketOperations | ||||
|         { | ||||
|             public static string GetItemById(string id) => $"/api/v1/basket/{id}"; | ||||
| 
 | ||||
|             public static string UpdateBasket() => "/api/v1/basket"; | ||||
|         } | ||||
| 
 | ||||
|         public class OrdersOperations | ||||
|         { | ||||
|             public static string GetOrderDraft() => "/api/v1/orders/draft"; | ||||
|         } | ||||
| 
 | ||||
|         public string Basket { get; set; } | ||||
| 
 | ||||
|         public string Catalog { get; set; } | ||||
| 
 | ||||
|         public string Orders { get; set; } | ||||
| 
 | ||||
|         public string GrpcBasket { get; set; } | ||||
| 
 | ||||
|         public string GrpcCatalog { get; set; } | ||||
| 
 | ||||
|         public string GrpcOrdering { get; set; } | ||||
|         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 static string UpdateBasket() => "/api/v1/basket"; | ||||
|     } | ||||
| 
 | ||||
|     public class OrdersOperations | ||||
|     { | ||||
|         public static string GetOrderDraft() => "/api/v1/orders/draft"; | ||||
|     } | ||||
| 
 | ||||
|     public string Basket { get; set; } | ||||
| 
 | ||||
|     public string Catalog { get; set; } | ||||
| 
 | ||||
|     public string Orders { get; set; } | ||||
| 
 | ||||
|     public string GrpcBasket { get; set; } | ||||
| 
 | ||||
|     public string GrpcCatalog { get; set; } | ||||
| 
 | ||||
|     public string GrpcOrdering { get; set; } | ||||
| } | ||||
|  | ||||
| @ -1,156 +1,146 @@ | ||||
| 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.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) | ||||
|         { | ||||
|             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) | ||||
|                 basket.Items.Add(new BasketDataItem() | ||||
|                 { | ||||
|                     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 | ||||
|                 { | ||||
|                     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); | ||||
| 
 | ||||
|             return basket; | ||||
|         } | ||||
| 
 | ||||
|         [HttpPut] | ||||
|         [Route("items")] | ||||
|         [ProducesResponseType((int)HttpStatusCode.BadRequest)] | ||||
|         [ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] | ||||
|         public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data) | ||||
|         await _basket.UpdateAsync(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()) | ||||
|         { | ||||
|             if (!data.Updates.Any()) | ||||
|             { | ||||
|                 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."); | ||||
|             } | ||||
| 
 | ||||
|             // Update with new quantities | ||||
|             foreach (var update in data.Updates) | ||||
|             { | ||||
|                 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; | ||||
|             } | ||||
| 
 | ||||
|             // Save the updated basket | ||||
|             await _basket.UpdateAsync(currentBasket); | ||||
| 
 | ||||
|             return currentBasket; | ||||
|             return BadRequest("No updates sent"); | ||||
|         } | ||||
| 
 | ||||
|         [HttpPost] | ||||
|         [Route("items")] | ||||
|         [ProducesResponseType((int)HttpStatusCode.BadRequest)] | ||||
|         [ProducesResponseType((int)HttpStatusCode.OK)] | ||||
|         public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data) | ||||
|         // Retrieve the current basket | ||||
|         var currentBasket = await _basket.GetById(data.BasketId); | ||||
|         if (currentBasket == null) | ||||
|         { | ||||
|             if (data == null || data.Quantity == 0) | ||||
|             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); | ||||
| 
 | ||||
|             if (basketItem == null) | ||||
|             { | ||||
|                 return BadRequest("Invalid payload"); | ||||
|                 return BadRequest($"Basket item with id {update.BasketItemId} not found"); | ||||
|             } | ||||
| 
 | ||||
|             // Step 1: Get the item from catalog | ||||
|             var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId); | ||||
| 
 | ||||
|             //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(); | ||||
|             basketItem.Quantity = update.NewQty; | ||||
|         } | ||||
| 
 | ||||
|         // 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) | ||||
|         { | ||||
|             return BadRequest("Invalid payload"); | ||||
|         } | ||||
| 
 | ||||
|         // Step 1: Get the item from catalog | ||||
|         var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId); | ||||
| 
 | ||||
|         //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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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.GetById(basketId); | ||||
| 
 | ||||
|         if (basket == null) | ||||
|         { | ||||
|             return BadRequest($"No basket found for id {basketId}"); | ||||
|         } | ||||
| 
 | ||||
|         [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)) | ||||
|             { | ||||
|                 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 await _orderingService.GetOrderDraftAsync(basket); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,12 +1,5 @@ | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.OpenApi.Models; | ||||
| using Swashbuckle.AspNetCore.SwaggerGen; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters | ||||
| namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters | ||||
| { | ||||
| 
 | ||||
|     namespace Basket.API.Infrastructure.Filters | ||||
|     { | ||||
|         public class AuthorizeCheckOperationFilter : IOperationFilter | ||||
|  | ||||
| @ -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) | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             _logger = logger; | ||||
|             var response = await t; | ||||
|             return response; | ||||
|         } | ||||
| 
 | ||||
|         public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( | ||||
|             TRequest request, | ||||
|             ClientInterceptorContext<TRequest, TResponse> context, | ||||
|             AsyncUnaryCallContinuation<TRequest, TResponse> continuation) | ||||
|         catch (RpcException e) | ||||
|         { | ||||
|             var call = continuation(request, context); | ||||
| 
 | ||||
|             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; | ||||
|             } | ||||
|             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; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure | ||||
| public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler | ||||
| { | ||||
|     public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler | ||||
|     private readonly IHttpContextAccessor _httpContextAccessor; | ||||
|     private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger; | ||||
| 
 | ||||
|     public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<HttpClientAuthorizationDelegatingHandler> logger) | ||||
|     { | ||||
|         private readonly IHttpContextAccessor _httpContextAccessor; | ||||
|         private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger; | ||||
|         _httpContextAccessor = httpContextAccessor; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<HttpClientAuthorizationDelegatingHandler> logger) | ||||
|     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||||
|     { | ||||
|         request.Version = new System.Version(2, 0); | ||||
|         request.Method = HttpMethod.Get; | ||||
| 
 | ||||
|         var authorizationHeader = _httpContextAccessor.HttpContext | ||||
|             .Request.Headers["Authorization"]; | ||||
| 
 | ||||
|         if (!string.IsNullOrEmpty(authorizationHeader)) | ||||
|         { | ||||
|             _httpContextAccessor = httpContextAccessor; | ||||
|             _logger = logger; | ||||
|             request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | ||||
|         } | ||||
| 
 | ||||
|         protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||||
|         var token = await GetToken(); | ||||
| 
 | ||||
|         if (token != null) | ||||
|         { | ||||
|             request.Version = new System.Version(2, 0); | ||||
|             request.Method = HttpMethod.Get; | ||||
| 
 | ||||
|             var authorizationHeader = _httpContextAccessor.HttpContext | ||||
|                 .Request.Headers["Authorization"]; | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(authorizationHeader)) | ||||
|             { | ||||
|                 request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | ||||
|             } | ||||
| 
 | ||||
|             var token = await GetToken(); | ||||
| 
 | ||||
|             if (token != null) | ||||
|             { | ||||
|                 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | ||||
|             } | ||||
| 
 | ||||
|             return await base.SendAsync(request, cancellationToken); | ||||
|             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 string BasketId { get; set; } | ||||
| 
 | ||||
|     public int Quantity { get; set; } | ||||
| 
 | ||||
|     public AddBasketItemRequest() | ||||
|     { | ||||
|         public int CatalogItemId { get; set; } | ||||
| 
 | ||||
|         public string BasketId { get; set; } | ||||
| 
 | ||||
|         public int Quantity { get; set; } | ||||
| 
 | ||||
|         public AddBasketItemRequest() | ||||
|         { | ||||
|             Quantity = 1; | ||||
|         } | ||||
|         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 List<BasketDataItem> Items { get; set; } = new(); | ||||
| 
 | ||||
|     public BasketData() | ||||
|     { | ||||
|         public string BuyerId { get; set; } | ||||
| 
 | ||||
|         public List<BasketDataItem> Items { get; set; } = new List<BasketDataItem>(); | ||||
| 
 | ||||
|         public BasketData() | ||||
|         { | ||||
|         } | ||||
| 
 | ||||
|         public BasketData(string buyerId) | ||||
|         { | ||||
|             BuyerId = buyerId; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public BasketData(string buyerId) | ||||
|     { | ||||
|         BuyerId = buyerId; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,21 +1,18 @@ | ||||
| namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models | ||||
| namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | ||||
| 
 | ||||
| public class BasketDataItem | ||||
| { | ||||
|     public string Id { get; set; } | ||||
| 
 | ||||
|     public class BasketDataItem | ||||
|     { | ||||
|         public string Id { get; set; } | ||||
|     public int ProductId { get; set; } | ||||
| 
 | ||||
|         public int ProductId { get; set; } | ||||
|     public string ProductName { get; set; } | ||||
| 
 | ||||
|         public string ProductName { get; set; } | ||||
|     public decimal UnitPrice { get; set; } | ||||
| 
 | ||||
|         public decimal UnitPrice { get; set; } | ||||
|     public decimal OldUnitPrice { get; set; } | ||||
| 
 | ||||
|         public decimal OldUnitPrice { get; set; } | ||||
| 
 | ||||
|         public int Quantity { 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 | ||||
| namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | ||||
| 
 | ||||
| public class OrderItemData | ||||
| { | ||||
|     public int ProductId { get; set; } | ||||
| 
 | ||||
|     public class OrderItemData | ||||
|     { | ||||
|         public int ProductId { get; set; } | ||||
|     public string ProductName { get; set; } | ||||
| 
 | ||||
|         public string ProductName { get; set; } | ||||
|     public decimal UnitPrice { get; set; } | ||||
| 
 | ||||
|         public decimal UnitPrice { get; set; } | ||||
|     public decimal Discount { get; set; } | ||||
| 
 | ||||
|         public decimal Discount { get; set; } | ||||
| 
 | ||||
|         public int Units { 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 int NewQty { get; set; } | ||||
| 
 | ||||
|     public UpdateBasketItemData() | ||||
|     { | ||||
|         public string BasketItemId { get; set; } | ||||
| 
 | ||||
|         public int NewQty { get; set; } | ||||
| 
 | ||||
|         public UpdateBasketItemData() | ||||
|         { | ||||
|             NewQty = 0; | ||||
|         } | ||||
|         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 ICollection<UpdateBasketItemData> Updates { get; set; } | ||||
| 
 | ||||
|     public UpdateBasketItemsRequest() | ||||
|     { | ||||
| 
 | ||||
|         public string BasketId { get; set; } | ||||
| 
 | ||||
|         public ICollection<UpdateBasketItemData> Updates { get; set; } | ||||
| 
 | ||||
|         public UpdateBasketItemsRequest() | ||||
|         { | ||||
|             Updates = new List<UpdateBasketItemData>(); | ||||
|         } | ||||
|         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 | ||||
| namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | ||||
| 
 | ||||
| public class UpdateBasketRequestItemData | ||||
| { | ||||
|     public string Id { get; set; }          // Basket id | ||||
| 
 | ||||
|     public class UpdateBasketRequestItemData | ||||
|     { | ||||
|         public string Id { get; set; }          // Basket id | ||||
| 
 | ||||
|         public int ProductId { get; set; }      // Catalog item id | ||||
| 
 | ||||
|         public int Quantity { get; set; }       // Quantity | ||||
|     } | ||||
|     public int ProductId { get; set; }      // Catalog item id | ||||
| 
 | ||||
|     public int Quantity { get; set; }       // Quantity | ||||
| } | ||||
|  | ||||
| @ -1,10 +1,4 @@ | ||||
| using Microsoft.AspNetCore; | ||||
| using Microsoft.AspNetCore.Hosting; | ||||
| using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator; | ||||
| using Serilog; | ||||
| 
 | ||||
| 
 | ||||
| BuildWebHost(args).Run(); | ||||
| await BuildWebHost(args).RunAsync(); | ||||
| IWebHost BuildWebHost(string[] args) => | ||||
|     WebHost | ||||
|         .CreateDefaultBuilder(args) | ||||
|  | ||||
| @ -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) | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest) | ||||
|     { | ||||
|         if (customerBasketRequest == null) | ||||
|         { | ||||
|             _basketClient = basketClient; | ||||
|             _logger = logger; | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         public async Task<BasketData> GetById(string id) | ||||
|         var map = new BasketData | ||||
|         { | ||||
|             _logger.LogDebug("grpc client created, request = {@id}", id); | ||||
|             var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id }); | ||||
|             _logger.LogDebug("grpc response {@response}", response); | ||||
|             BuyerId = customerBasketRequest.Buyerid | ||||
|         }; | ||||
| 
 | ||||
|             return MapToBasketData(response); | ||||
|         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; | ||||
|     } | ||||
| 
 | ||||
|     private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData) | ||||
|     { | ||||
|         if (basketData == null) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         public async Task UpdateAsync(BasketData currentBasket) | ||||
|         var map = new CustomerBasketRequest | ||||
|         { | ||||
|             _logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket); | ||||
|             var request = MapToCustomerBasketRequest(currentBasket); | ||||
|             _logger.LogDebug("Grpc update basket request {@request}", request); | ||||
|             Buyerid = basketData.BuyerId | ||||
|         }; | ||||
| 
 | ||||
|             await _basketClient.UpdateBasketAsync(request); | ||||
|         } | ||||
| 
 | ||||
|         private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest) | ||||
|         basketData.Items.ToList().ForEach(item => map.Items.Add(new BasketItemResponse | ||||
|         { | ||||
|             if (customerBasketRequest == null) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
|             Id = item.Id, | ||||
|             Oldunitprice = (double)item.OldUnitPrice, | ||||
|             Pictureurl = item.PictureUrl, | ||||
|             Productid = item.ProductId, | ||||
|             Productname = item.ProductName, | ||||
|             Quantity = item.Quantity, | ||||
|             Unitprice = (double)item.UnitPrice | ||||
|         })); | ||||
| 
 | ||||
|             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 | ||||
|             })); | ||||
| 
 | ||||
|             return map; | ||||
|         } | ||||
| 
 | ||||
|         private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData) | ||||
|         { | ||||
|             if (basketData == null) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             var map = new CustomerBasketRequest | ||||
|             { | ||||
|                 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 | ||||
|             })); | ||||
| 
 | ||||
|             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; | ||||
| 
 | ||||
|     public CatalogService(Catalog.CatalogClient client) | ||||
|     { | ||||
|         private readonly 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) | ||||
|     private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse) | ||||
|     { | ||||
|         return new CatalogItem | ||||
|         { | ||||
|             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) | ||||
|         { | ||||
|             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; | ||||
| 
 | ||||
|     public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config) | ||||
|     { | ||||
|         private readonly HttpClient _apiClient; | ||||
|         private readonly ILogger<OrderApiClient> _logger; | ||||
|         private readonly UrlsConfig _urls; | ||||
|         _apiClient = httpClient; | ||||
|         _logger = logger; | ||||
|         _urls = config.Value; | ||||
|     } | ||||
| 
 | ||||
|         public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config) | ||||
|     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(); | ||||
| 
 | ||||
|         var ordersDraftResponse = await response.Content.ReadAsStringAsync(); | ||||
| 
 | ||||
|         return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions | ||||
|         { | ||||
|             _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); | ||||
| 
 | ||||
|             response.EnsureSuccessStatusCode(); | ||||
| 
 | ||||
|             var ordersDraftResponse = await response.Content.ReadAsStringAsync(); | ||||
| 
 | ||||
|             return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions | ||||
|             { | ||||
|                 PropertyNameCaseInsensitive = true | ||||
|             }); | ||||
|         } | ||||
|             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; | ||||
| 
 | ||||
|         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); | ||||
| 
 | ||||
|             var command = MapToOrderDraftCommand(basketData); | ||||
|             var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command); | ||||
|             _logger.LogDebug(" grpc response: {@response}", response); | ||||
| 
 | ||||
|             return MapToResponse(response, basketData); | ||||
|         } | ||||
| 
 | ||||
|         private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData) | ||||
|         { | ||||
|             if (orderDraft == null) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             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, | ||||
|             })); | ||||
| 
 | ||||
|             return data; | ||||
|         } | ||||
| 
 | ||||
|         private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | ||||
|         { | ||||
|             var command = new CreateOrderDraftCommand | ||||
|             { | ||||
|                 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; | ||||
|         } | ||||
| 
 | ||||
|         _orderingGrpcClient = orderingGrpcClient; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|     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); | ||||
| 
 | ||||
|         return MapToResponse(response, basketData); | ||||
|     } | ||||
| 
 | ||||
|     private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData) | ||||
|     { | ||||
|         if (orderDraft == null) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         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, | ||||
|         })); | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
|     private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | ||||
|     { | ||||
|         var command = new CreateOrderDraftCommand | ||||
|         { | ||||
|             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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|         } | ||||
| 
 | ||||
|         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() | ||||
|                  .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)) | ||||
|             { | ||||
|                 loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | ||||
|                 app.UsePathBase(pathBase); | ||||
|             } | ||||
| 
 | ||||
|             if (env.IsDevelopment()) | ||||
|             { | ||||
|                 app.UseDeveloperExceptionPage(); | ||||
|             } | ||||
| 
 | ||||
|             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() | ||||
|                 { | ||||
|                     Predicate = _ => true, | ||||
|                     ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | ||||
|                 }); | ||||
|                 endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | ||||
|                 { | ||||
|                     Predicate = r => r.Name.Contains("self") | ||||
|                 }); | ||||
|             }); | ||||
|         } | ||||
|         Configuration = configuration; | ||||
|     } | ||||
| 
 | ||||
|     public static class ServiceCollectionExtensions | ||||
|     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) | ||||
|     { | ||||
|         public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) | ||||
|         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.AddOptions(); | ||||
|             services.Configure<UrlsConfig>(configuration.GetSection("urls")); | ||||
|             loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | ||||
|             app.UsePathBase(pathBase); | ||||
|         } | ||||
| 
 | ||||
|             services.AddControllers() | ||||
|                     .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | ||||
|         if (env.IsDevelopment()) | ||||
|         { | ||||
|             app.UseDeveloperExceptionPage(); | ||||
|         } | ||||
| 
 | ||||
|             services.AddSwaggerGen(options => | ||||
|         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() | ||||
|             { | ||||
|                 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 | ||||
|                 { | ||||
|                     Type = SecuritySchemeType.OAuth2, | ||||
|                     Flows = new OpenApiOAuthFlows() | ||||
|                     { | ||||
|                         Implicit = new OpenApiOAuthFlow() | ||||
|                         { | ||||
|                             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" } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 options.OperationFilter<AuthorizeCheckOperationFilter>(); | ||||
|                 Predicate = _ => true, | ||||
|                 ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | ||||
|             }); | ||||
| 
 | ||||
|             services.AddCors(options => | ||||
|             endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | ||||
|             { | ||||
|                 options.AddPolicy("CorsPolicy", | ||||
|                     builder => builder | ||||
|                     .AllowAnyMethod() | ||||
|                     .AllowAnyHeader() | ||||
|                     .SetIsOriginAllowed((host) => true) | ||||
|                     .AllowCredentials()); | ||||
|                 Predicate = r => r.Name.Contains("self") | ||||
|             }); | ||||
| 
 | ||||
|             return services; | ||||
|         } | ||||
|         public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | ||||
|         { | ||||
|             JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | ||||
| 
 | ||||
|             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 = "mobileshoppingagg"; | ||||
|             }); | ||||
| 
 | ||||
|             return services; | ||||
|         } | ||||
| 
 | ||||
|         public static IServiceCollection AddHttpServices(this IServiceCollection services) | ||||
|         { | ||||
|             //register delegating handlers | ||||
|             services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | ||||
|             services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | ||||
| 
 | ||||
|             //register http services | ||||
| 
 | ||||
|             services.AddHttpClient<IOrderApiClient, OrderApiClient>() | ||||
|                    .AddDevspacesSupport(); | ||||
| 
 | ||||
|             return services; | ||||
|         } | ||||
| 
 | ||||
|         public static IServiceCollection AddGrpcServices(this IServiceCollection services) | ||||
|         { | ||||
|             services.AddTransient<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|             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.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.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; | ||||
|         } | ||||
| 
 | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public static class ServiceCollectionExtensions | ||||
| { | ||||
|     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.AddSwaggerGen(options => | ||||
|         { | ||||
|             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 | ||||
|             { | ||||
|                 Type = SecuritySchemeType.OAuth2, | ||||
|                 Flows = new OpenApiOAuthFlows() | ||||
|                 { | ||||
|                     Implicit = new OpenApiOAuthFlow() | ||||
|                     { | ||||
|                         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" } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             options.OperationFilter<AuthorizeCheckOperationFilter>(); | ||||
|         }); | ||||
| 
 | ||||
|         services.AddCors(options => | ||||
|         { | ||||
|             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"); | ||||
| 
 | ||||
|         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 = "mobileshoppingagg"; | ||||
|         }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddHttpServices(this IServiceCollection services) | ||||
|     { | ||||
|         //register delegating handlers | ||||
|         services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | ||||
|         services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | ||||
| 
 | ||||
|         //register http services | ||||
| 
 | ||||
|         services.AddHttpClient<IOrderApiClient, OrderApiClient>() | ||||
|                 .AddDevspacesSupport(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddGrpcServices(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddTransient<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         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.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.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}"; | ||||
| 
 | ||||
|             public static string UpdateBasket() => "/api/v1/basket"; | ||||
|         } | ||||
| 
 | ||||
|         public class OrdersOperations | ||||
|         { | ||||
|             public static string GetOrderDraft() => "/api/v1/orders/draft"; | ||||
|         } | ||||
| 
 | ||||
|         public string Basket { get; set; } | ||||
| 
 | ||||
|         public string Catalog { get; set; } | ||||
| 
 | ||||
|         public string Orders { get; set; } | ||||
| 
 | ||||
|         public string GrpcBasket { get; set; } | ||||
| 
 | ||||
|         public string GrpcCatalog { get; set; } | ||||
| 
 | ||||
|         public string GrpcOrdering { get; set; } | ||||
|         // 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}"; | ||||
| 
 | ||||
|         public static string UpdateBasket() => "/api/v1/basket"; | ||||
|     } | ||||
| 
 | ||||
|     public class OrdersOperations | ||||
|     { | ||||
|         public static string GetOrderDraft() => "/api/v1/orders/draft"; | ||||
|     } | ||||
| 
 | ||||
|     public string Basket { get; set; } | ||||
| 
 | ||||
|     public string Catalog { get; set; } | ||||
| 
 | ||||
|     public string Orders { get; set; } | ||||
| 
 | ||||
|     public string GrpcBasket { 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) | ||||
|                 basket.Items.Add(new BasketDataItem() | ||||
|                 { | ||||
|                     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 | ||||
|                 { | ||||
|                     itemInBasket.Quantity = bitem.Quantity; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             await _basket.UpdateAsync(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"); | ||||
|             } | ||||
| 
 | ||||
|             // 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); | ||||
|                 if (basketItem == null) | ||||
|                 { | ||||
|                     return BadRequest($"Basket item with id {update.BasketItemId} not found"); | ||||
|                 } | ||||
|                 basketItem.Quantity = update.NewQty; | ||||
|             } | ||||
| 
 | ||||
|             // 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) | ||||
|             { | ||||
|                 return BadRequest("Invalid payload"); | ||||
|             } | ||||
| 
 | ||||
|             // Step 1: Get the item from catalog | ||||
|             var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId); | ||||
| 
 | ||||
|             //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; | ||||
|                     Id = bitem.Id, | ||||
|                     ProductId = catalogItem.Id, | ||||
|                     ProductName = catalogItem.Name, | ||||
|                     PictureUrl = catalogItem.PictureUri, | ||||
|                     UnitPrice = catalogItem.Price, | ||||
|                     Quantity = bitem.Quantity | ||||
|                 }); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 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() | ||||
|                 }); | ||||
|                 itemInBasket.Quantity = bitem.Quantity; | ||||
|             } | ||||
| 
 | ||||
|             // Step 5: Update basket | ||||
|             await _basket.UpdateAsync(currentBasket); | ||||
| 
 | ||||
|             return Ok(); | ||||
|         } | ||||
| 
 | ||||
|         await _basket.UpdateAsync(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"); | ||||
|         } | ||||
| 
 | ||||
|         // 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) | ||||
|         { | ||||
|             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; | ||||
|         } | ||||
| 
 | ||||
|         // 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) | ||||
|         { | ||||
|             return BadRequest("Invalid payload"); | ||||
|         } | ||||
| 
 | ||||
|         // Step 1: Get the item from catalog | ||||
|         var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId); | ||||
| 
 | ||||
|         //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 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() | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         // Step 5: Update basket | ||||
|         await _basket.UpdateAsync(currentBasket); | ||||
| 
 | ||||
|         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) | ||||
|     { | ||||
|         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) | ||||
|     { | ||||
|         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); | ||||
| 
 | ||||
|         if (basket == null) | ||||
|         { | ||||
|             return BadRequest($"No basket found for id {basketId}"); | ||||
|         } | ||||
| 
 | ||||
|         [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)) | ||||
|             { | ||||
|                 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 await _orderingService.GetOrderDraftAsync(basket); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,10 +1,4 @@ | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.OpenApi.Models; | ||||
| using Swashbuckle.AspNetCore.SwaggerGen; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters | ||||
| namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters | ||||
| { | ||||
|     namespace Basket.API.Infrastructure.Filters | ||||
|     { | ||||
| @ -14,7 +8,7 @@ namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters | ||||
|             { | ||||
|                 // Check for authorize attribute | ||||
|                 var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() || | ||||
|                                    context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any(); | ||||
|                                     context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any(); | ||||
| 
 | ||||
|                 if (!hasAuthorize) return; | ||||
| 
 | ||||
| @ -27,13 +21,14 @@ namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters | ||||
|                 }; | ||||
| 
 | ||||
|                 operation.Security = new List<OpenApiSecurityRequirement> | ||||
|             { | ||||
|                 new OpenApiSecurityRequirement | ||||
|                 { | ||||
|                     new OpenApiSecurityRequirement | ||||
|                     { | ||||
|                         [ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" } | ||||
|                     } | ||||
|                 }; | ||||
|                     [ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" } | ||||
|                 } | ||||
|             }; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/ApiGateways/Web.Bff.Shopping/aggregator/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/ApiGateways/Web.Bff.Shopping/aggregator/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> task) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             _logger = logger; | ||||
|             var response = await task; | ||||
|             return response; | ||||
|         } | ||||
| 
 | ||||
|         public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( | ||||
|             TRequest request, | ||||
|             ClientInterceptorContext<TRequest, TResponse> context, | ||||
|             AsyncUnaryCallContinuation<TRequest, TResponse> continuation) | ||||
|         catch (RpcException e) | ||||
|         { | ||||
|             var call = continuation(request, context); | ||||
| 
 | ||||
|             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; | ||||
|             } | ||||
|             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) | ||||
|     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||||
|     { | ||||
|         var authorizationHeader = _httpContextAccessor.HttpContext | ||||
|             .Request.Headers["Authorization"]; | ||||
| 
 | ||||
|         if (!string.IsNullOrWhitespace(authorizationHeader)) | ||||
|         { | ||||
|             _httpContextAccessor = httpContextAccessor; | ||||
|             request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | ||||
|         } | ||||
| 
 | ||||
|         protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||||
|         var token = await GetTokenAsync(); | ||||
| 
 | ||||
|         if (token != null) | ||||
|         { | ||||
|             var authorizationHeader = _httpContextAccessor.HttpContext | ||||
|                 .Request.Headers["Authorization"]; | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(authorizationHeader)) | ||||
|             { | ||||
|                 request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | ||||
|             } | ||||
| 
 | ||||
|             var token = await GetToken(); | ||||
| 
 | ||||
|             if (token != null) | ||||
|             { | ||||
|                 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | ||||
|             } | ||||
| 
 | ||||
|             return await base.SendAsync(request, cancellationToken); | ||||
|             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 string BasketId { get; set; } | ||||
| 
 | ||||
|     public int Quantity { get; set; } | ||||
| 
 | ||||
|     public AddBasketItemRequest() | ||||
|     { | ||||
|         public int CatalogItemId { get; set; } | ||||
| 
 | ||||
|         public string BasketId { get; set; } | ||||
| 
 | ||||
|         public int Quantity { get; set; } | ||||
| 
 | ||||
|         public AddBasketItemRequest() | ||||
|         { | ||||
|             Quantity = 1; | ||||
|         } | ||||
|         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 List<BasketDataItem> Items { get; set; } = new(); | ||||
| 
 | ||||
|     public BasketData() | ||||
|     { | ||||
|         public string BuyerId { get; set; } | ||||
| 
 | ||||
|         public List<BasketDataItem> Items { get; set; } = new List<BasketDataItem>(); | ||||
| 
 | ||||
|         public BasketData() | ||||
|         { | ||||
|         } | ||||
| 
 | ||||
|         public BasketData(string buyerId) | ||||
|         { | ||||
|             BuyerId = buyerId; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public BasketData(string buyerId) | ||||
|     { | ||||
|         BuyerId = buyerId; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,21 +1,18 @@ | ||||
| namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models | ||||
| namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | ||||
| 
 | ||||
| public class BasketDataItem | ||||
| { | ||||
|     public string Id { get; set; } | ||||
| 
 | ||||
|     public class BasketDataItem | ||||
|     { | ||||
|         public string Id { get; set; } | ||||
|     public int ProductId { get; set; } | ||||
| 
 | ||||
|         public int ProductId { get; set; } | ||||
|     public string ProductName { get; set; } | ||||
| 
 | ||||
|         public string ProductName { get; set; } | ||||
|     public decimal UnitPrice { get; set; } | ||||
| 
 | ||||
|         public decimal UnitPrice { get; set; } | ||||
|     public decimal OldUnitPrice { get; set; } | ||||
| 
 | ||||
|         public decimal OldUnitPrice { get; set; } | ||||
| 
 | ||||
|         public int Quantity { 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 string PictureUri { get; set; } | ||||
|     } | ||||
|     public decimal Price { 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 | ||||
| namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | ||||
| 
 | ||||
| public class OrderItemData | ||||
| { | ||||
|     public int ProductId { get; set; } | ||||
| 
 | ||||
|     public class OrderItemData | ||||
|     { | ||||
|         public int ProductId { get; set; } | ||||
|     public string ProductName { get; set; } | ||||
| 
 | ||||
|         public string ProductName { get; set; } | ||||
|     public decimal UnitPrice { get; set; } | ||||
| 
 | ||||
|         public decimal UnitPrice { get; set; } | ||||
|     public decimal Discount { get; set; } | ||||
| 
 | ||||
|         public decimal Discount { get; set; } | ||||
| 
 | ||||
|         public int Units { 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 | ||||
| namespace Microsoft.eShopOnContainers.Web.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 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 ICollection<UpdateBasketItemData> Updates { get; set; } | ||||
| 
 | ||||
|     public UpdateBasketItemsRequest() | ||||
|     { | ||||
|         public string BasketId { get; set; } | ||||
| 
 | ||||
|         public ICollection<UpdateBasketItemData> Updates { get; set; } | ||||
| 
 | ||||
|         public UpdateBasketItemsRequest() | ||||
|         { | ||||
|             Updates = new List<UpdateBasketItemData>(); | ||||
|         } | ||||
|         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,9 +1,4 @@ | ||||
| using Microsoft.AspNetCore; | ||||
| using Microsoft.AspNetCore.Hosting; | ||||
| using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; | ||||
| using Serilog; | ||||
| 
 | ||||
| BuildWebHost(args).Run(); | ||||
| await BuildWebHost(args).RunAsync(); | ||||
| 
 | ||||
| IWebHost BuildWebHost(string[] args) => | ||||
|     WebHost | ||||
|  | ||||
| @ -1,103 +1,96 @@ | ||||
| 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) | ||||
|     { | ||||
|         private readonly Basket.BasketClient _basketClient; | ||||
|         private readonly ILogger<BasketService> _logger; | ||||
|         _basketClient = basketClient; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger) | ||||
| 
 | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest) | ||||
|     { | ||||
|         if (customerBasketRequest == null) | ||||
|         { | ||||
|             _basketClient = basketClient; | ||||
|             _logger = logger; | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public async Task<BasketData> GetById(string id) | ||||
|         var map = new BasketData | ||||
|         { | ||||
|             _logger.LogDebug("grpc client created, request = {@id}", id); | ||||
|             var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id }); | ||||
|             _logger.LogDebug("grpc response {@response}", response); | ||||
|             BuyerId = customerBasketRequest.Buyerid | ||||
|         }; | ||||
| 
 | ||||
|             return MapToBasketData(response); | ||||
|         } | ||||
| 
 | ||||
|         public async Task UpdateAsync(BasketData currentBasket) | ||||
|         customerBasketRequest.Items.ToList().ForEach(item => | ||||
|         { | ||||
|             _logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket); | ||||
|             var request = MapToCustomerBasketRequest(currentBasket); | ||||
|             _logger.LogDebug("Grpc update basket request {@request}", request); | ||||
| 
 | ||||
|             await _basketClient.UpdateBasketAsync(request); | ||||
|         } | ||||
| 
 | ||||
|         private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest) | ||||
|         { | ||||
|             if (customerBasketRequest == null) | ||||
|             if (item.Id != null) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             var map = new BasketData | ||||
|             { | ||||
|                 BuyerId = customerBasketRequest.Buyerid | ||||
|             }; | ||||
| 
 | ||||
|             customerBasketRequest.Items.ToList().ForEach(item => | ||||
|             { | ||||
|                 if (item.Id != null) | ||||
|                 map.Items.Add(new BasketDataItem | ||||
|                 { | ||||
|                     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; | ||||
|         } | ||||
| 
 | ||||
|         private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData) | ||||
|         { | ||||
|             if (basketData == null) | ||||
|             { | ||||
|                 return null; | ||||
|                     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 CustomerBasketRequest | ||||
|             { | ||||
|                 Buyerid = basketData.BuyerId | ||||
|             }; | ||||
|         return map; | ||||
|     } | ||||
| 
 | ||||
|             basketData.Items.ToList().ForEach(item => | ||||
|             { | ||||
|                 if (item.Id != null) | ||||
|                 { | ||||
|                     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; | ||||
|     private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData) | ||||
|     { | ||||
|         if (basketData == null) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         var map = new CustomerBasketRequest | ||||
|         { | ||||
|             Buyerid = basketData.BuyerId | ||||
|         }; | ||||
| 
 | ||||
|         basketData.Items.ToList().ForEach(item => | ||||
|         { | ||||
|             if (item.Id != null) | ||||
|             { | ||||
|                 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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
|     public CatalogService(Catalog.CatalogClient client, ILogger<CatalogService> logger) | ||||
|     { | ||||
|         private readonly Catalog.CatalogClient _client; | ||||
|         private readonly ILogger<CatalogService> _logger; | ||||
|         _client = client; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         public CatalogService(Catalog.CatalogClient client, ILogger<CatalogService> 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<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) | ||||
|     { | ||||
|         return new CatalogItem | ||||
|         { | ||||
|             _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<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) | ||||
|         { | ||||
|             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; | ||||
| 
 | ||||
|     public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config) | ||||
|     { | ||||
|         private readonly HttpClient _apiClient; | ||||
|         private readonly ILogger<OrderApiClient> _logger; | ||||
|         private readonly UrlsConfig _urls; | ||||
|         _apiClient = httpClient; | ||||
|         _logger = logger; | ||||
|         _urls = config.Value; | ||||
|     } | ||||
| 
 | ||||
|         public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config) | ||||
|     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(); | ||||
| 
 | ||||
|         var ordersDraftResponse = await response.Content.ReadAsStringAsync(); | ||||
| 
 | ||||
|         return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions | ||||
|         { | ||||
|             _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); | ||||
| 
 | ||||
|             response.EnsureSuccessStatusCode(); | ||||
| 
 | ||||
|             var ordersDraftResponse = await response.Content.ReadAsStringAsync(); | ||||
| 
 | ||||
|             return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions | ||||
|             { | ||||
|                 PropertyNameCaseInsensitive = true | ||||
|             }); | ||||
|         } | ||||
|             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; | ||||
| 
 | ||||
|         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); | ||||
| 
 | ||||
|             var command = MapToOrderDraftCommand(basketData); | ||||
|             var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command); | ||||
|             _logger.LogDebug(" grpc response: {@response}", response); | ||||
| 
 | ||||
|             return MapToResponse(response, basketData); | ||||
|         } | ||||
| 
 | ||||
|         private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData) | ||||
|         { | ||||
|             if (orderDraft == null) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             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, | ||||
|             })); | ||||
| 
 | ||||
|             return data; | ||||
|         } | ||||
| 
 | ||||
|         private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | ||||
|         { | ||||
|             var command = new CreateOrderDraftCommand | ||||
|             { | ||||
|                 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; | ||||
|         } | ||||
| 
 | ||||
|         _orderingGrpcClient = orderingGrpcClient; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|     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); | ||||
| 
 | ||||
|         return MapToResponse(response, basketData); | ||||
|     } | ||||
| 
 | ||||
|     private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData) | ||||
|     { | ||||
|         if (orderDraft == null) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         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, | ||||
|         })); | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
|     private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | ||||
|     { | ||||
|         var command = new CreateOrderDraftCommand | ||||
|         { | ||||
|             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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
|     { | ||||
|         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) | ||||
|         { | ||||
|             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.UseHttpsRedirection(); | ||||
| 
 | ||||
|             app.UseSwagger().UseSwaggerUI(c => | ||||
|             { | ||||
|                 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"); | ||||
|             }); | ||||
| 
 | ||||
|             app.UseRouting(); | ||||
|             app.UseCors("CorsPolicy"); | ||||
|             app.UseAuthentication(); | ||||
|             app.UseAuthorization(); | ||||
| 
 | ||||
|             app.UseEndpoints(endpoints => | ||||
|             { | ||||
|                 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") | ||||
|                 }); | ||||
|             }); | ||||
|         } | ||||
|         Configuration = configuration; | ||||
|     } | ||||
| 
 | ||||
|     public static class ServiceCollectionExtensions | ||||
|     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) | ||||
|     { | ||||
|         public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | ||||
|         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) | ||||
|     { | ||||
|         var pathBase = Configuration["PATH_BASE"]; | ||||
|         if (!string.IsNullOrEmpty(pathBase)) | ||||
|         { | ||||
|             JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | ||||
|             loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | ||||
|             app.UsePathBase(pathBase); | ||||
|         } | ||||
| 
 | ||||
|             var identityUrl = configuration.GetValue<string>("urls:identity"); | ||||
|             services.AddAuthentication(options => | ||||
|             { | ||||
|                 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | ||||
|                 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | ||||
|         if (env.IsDevelopment()) | ||||
|         { | ||||
|             app.UseDeveloperExceptionPage(); | ||||
|         } | ||||
| 
 | ||||
|             }) | ||||
|             .AddJwtBearer(options => | ||||
|         app.UseHttpsRedirection(); | ||||
| 
 | ||||
|         app.UseSwagger().UseSwaggerUI(c => | ||||
|         { | ||||
|             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"); | ||||
|         }); | ||||
| 
 | ||||
|         app.UseRouting(); | ||||
|         app.UseCors("CorsPolicy"); | ||||
|         app.UseAuthentication(); | ||||
|         app.UseAuthorization(); | ||||
| 
 | ||||
|         app.UseEndpoints(endpoints => | ||||
|         { | ||||
|             endpoints.MapDefaultControllerRoute(); | ||||
|             endpoints.MapControllers(); | ||||
|             endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | ||||
|             { | ||||
|                 options.Authority = identityUrl; | ||||
|                 options.RequireHttpsMetadata = false; | ||||
|                 options.Audience = "webshoppingagg"; | ||||
|                 Predicate = _ => true, | ||||
|                 ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | ||||
|             }); | ||||
| 
 | ||||
|             return services; | ||||
|         } | ||||
| 
 | ||||
|         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.AddSwaggerGen(options => | ||||
|             endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | ||||
|             { | ||||
|                 options.DescribeAllEnumsAsStrings(); | ||||
| 
 | ||||
|                 options.SwaggerDoc("v1", new OpenApiInfo | ||||
|                 { | ||||
|                     Title = "Shopping Aggregator for Web Clients", | ||||
|                     Version = "v1", | ||||
|                     Description = "Shopping Aggregator for Web Clients" | ||||
|                 }); | ||||
| 
 | ||||
|                 options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | ||||
|                 { | ||||
|                     Type = SecuritySchemeType.OAuth2, | ||||
|                     Flows = new OpenApiOAuthFlows() | ||||
|                     { | ||||
|                         Implicit = new OpenApiOAuthFlow() | ||||
|                         { | ||||
|                             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" } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 options.OperationFilter<AuthorizeCheckOperationFilter>(); | ||||
|                 Predicate = r => r.Name.Contains("self") | ||||
|             }); | ||||
| 
 | ||||
|             services.AddCors(options => | ||||
|             { | ||||
|                 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 | ||||
| 
 | ||||
|             services.AddHttpClient<IOrderApiClient, OrderApiClient>() | ||||
|                 .AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>() | ||||
|                 .AddDevspacesSupport(); | ||||
| 
 | ||||
|             return services; | ||||
|         } | ||||
| 
 | ||||
|         public static IServiceCollection AddGrpcServices(this IServiceCollection services) | ||||
|         { | ||||
|             services.AddTransient<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|             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.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.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; | ||||
|         } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public static class ServiceCollectionExtensions | ||||
| { | ||||
|     public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | ||||
| 
 | ||||
|         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"; | ||||
|         }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     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.AddSwaggerGen(options => | ||||
|         { | ||||
|             options.DescribeAllEnumsAsStrings(); | ||||
| 
 | ||||
|             options.SwaggerDoc("v1", new OpenApiInfo | ||||
|             { | ||||
|                 Title = "Shopping Aggregator for Web Clients", | ||||
|                 Version = "v1", | ||||
|                 Description = "Shopping Aggregator for Web Clients" | ||||
|             }); | ||||
| 
 | ||||
|             options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | ||||
|             { | ||||
|                 Type = SecuritySchemeType.OAuth2, | ||||
|                 Flows = new OpenApiOAuthFlows() | ||||
|                 { | ||||
|                     Implicit = new OpenApiOAuthFlow() | ||||
|                     { | ||||
|                         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" } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             options.OperationFilter<AuthorizeCheckOperationFilter>(); | ||||
|         }); | ||||
| 
 | ||||
|         services.AddCors(options => | ||||
|         { | ||||
|             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 | ||||
| 
 | ||||
|         services.AddHttpClient<IOrderApiClient, OrderApiClient>() | ||||
|             .AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>() | ||||
|             .AddDevspacesSupport(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddGrpcServices(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddTransient<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         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.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.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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
|   </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-preview.7.21377.19" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-preview.7.21377.19" /> | ||||
|   </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); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										6
									
								
								src/BuildingBlocks/Devspaces.Support/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/BuildingBlocks/Devspaces.Support/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| { | ||||
|     Task Handle(TIntegrationEvent @event); | ||||
| } | ||||
| 
 | ||||
| public interface IIntegrationEventHandler | ||||
| { | ||||
|     public interface IIntegrationEventHandler<in TIntegrationEvent> : IIntegrationEventHandler | ||||
|         where TIntegrationEvent : IntegrationEvent | ||||
|     { | ||||
|         Task Handle(TIntegrationEvent @event); | ||||
|     } | ||||
| 
 | ||||
|     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; | ||||
|         } | ||||
| 
 | ||||
|         [JsonConstructor] | ||||
|         public IntegrationEvent(Guid id, DateTime createDate) | ||||
|         { | ||||
|             Id = id; | ||||
|             CreationDate = createDate; | ||||
|         } | ||||
| 
 | ||||
|         [JsonInclude] | ||||
|         public Guid Id { get; private init; } | ||||
| 
 | ||||
|         [JsonInclude] | ||||
|         public DateTime CreationDate { get; private init; } | ||||
| public record IntegrationEvent | ||||
| {         | ||||
|     public IntegrationEvent() | ||||
|     { | ||||
|         Id = Guid.NewGuid(); | ||||
|         CreationDate = DateTime.UtcNow; | ||||
|     } | ||||
| 
 | ||||
|     [JsonConstructor] | ||||
|     public IntegrationEvent(Guid id, DateTime createDate) | ||||
|     { | ||||
|         Id = id; | ||||
|         CreationDate = createDate; | ||||
|     } | ||||
| 
 | ||||
|     [JsonInclude] | ||||
|     public Guid Id { 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 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; | ||||
|             } | ||||
| 
 | ||||
|             return typeName; | ||||
|             var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray()); | ||||
|             typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>"; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             typeName = type.Name; | ||||
|         } | ||||
| 
 | ||||
|         public static string GetGenericTypeName(this object @object) | ||||
|         { | ||||
|             return @object.GetType().GetGenericTypeName(); | ||||
|         } | ||||
|         return typeName; | ||||
|     } | ||||
| 
 | ||||
|     public static string GetGenericTypeName(this object @object) | ||||
|     { | ||||
|         return @object.GetType().GetGenericTypeName(); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										8
									
								
								src/BuildingBlocks/EventBus/EventBus/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/BuildingBlocks/EventBus/EventBus/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @ -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>(); | ||||
|         } | ||||
| 
 | ||||
|         public bool IsEmpty => !_handlers.Keys.Any(); | ||||
|         public void Clear() => _handlers.Clear(); | ||||
| 
 | ||||
|         public void AddDynamicSubscription<TH>(string eventName) | ||||
|             where TH : IDynamicIntegrationEventHandler | ||||
|         { | ||||
|             DoAddSubscription(typeof(TH), eventName, isDynamic: true); | ||||
|         } | ||||
| 
 | ||||
|         public void AddSubscription<T, TH>() | ||||
|             where T : IntegrationEvent | ||||
|             where TH : IIntegrationEventHandler<T> | ||||
|         { | ||||
|             var eventName = GetEventKey<T>(); | ||||
| 
 | ||||
|             DoAddSubscription(typeof(TH), eventName, isDynamic: false); | ||||
| 
 | ||||
|             if (!_eventTypes.Contains(typeof(T))) | ||||
|             { | ||||
|                 _eventTypes.Add(typeof(T)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private void DoAddSubscription(Type handlerType, string eventName, bool 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)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         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) | ||||
|         { | ||||
|             if (subsToRemove != null) | ||||
|             { | ||||
|                 _handlers[eventName].Remove(subsToRemove); | ||||
|                 if (!_handlers[eventName].Any()) | ||||
|                 { | ||||
|                     _handlers.Remove(eventName); | ||||
|                     var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); | ||||
|                     if (eventType != null) | ||||
|                     { | ||||
|                         _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); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         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) | ||||
|         { | ||||
|             if (!HasSubscriptionsForEvent(eventName)) | ||||
|             { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             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 Type GetEventTypeByName(string eventName) => _eventTypes.SingleOrDefault(t => t.Name == eventName); | ||||
| 
 | ||||
|         public string GetEventKey<T>() | ||||
|         { | ||||
|             return typeof(T).Name; | ||||
|             _eventTypes.Add(typeof(T)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void DoAddSubscription(Type handlerType, string eventName, bool 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)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     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) | ||||
|     { | ||||
|         if (subsToRemove != null) | ||||
|         { | ||||
|             _handlers[eventName].Remove(subsToRemove); | ||||
|             if (!_handlers[eventName].Any()) | ||||
|             { | ||||
|                 _handlers.Remove(eventName); | ||||
|                 var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); | ||||
|                 if (eventType != null) | ||||
|                 { | ||||
|                     _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); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     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) | ||||
|     { | ||||
|         if (!HasSubscriptionsForEvent(eventName)) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         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 Type GetEventTypeByName(string eventName) => _eventTypes.SingleOrDefault(t => t.Name == eventName); | ||||
| 
 | ||||
|     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; } | ||||
| 
 | ||||
|             private SubscriptionInfo(bool isDynamic, Type handlerType) | ||||
|             { | ||||
|                 IsDynamic = isDynamic; | ||||
|                 HandlerType = handlerType; | ||||
|             } | ||||
| 
 | ||||
|             public static SubscriptionInfo Dynamic(Type handlerType) | ||||
|             { | ||||
|                 return new SubscriptionInfo(true, handlerType); | ||||
|             } | ||||
|             public static SubscriptionInfo Typed(Type handlerType) | ||||
|             { | ||||
|                 return new SubscriptionInfo(false, handlerType); | ||||
|             } | ||||
|             IsDynamic = isDynamic; | ||||
|             HandlerType = handlerType; | ||||
|         } | ||||
| 
 | ||||
|         public static SubscriptionInfo Dynamic(Type handlerType) => | ||||
|             new SubscriptionInfo(true, 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; | ||||
| 
 | ||||
|     object sync_root = new object(); | ||||
| 
 | ||||
|     public DefaultRabbitMQPersistentConnection(IConnectionFactory connectionFactory, ILogger<DefaultRabbitMQPersistentConnection> logger, int retryCount = 5) | ||||
|     { | ||||
|         private readonly IConnectionFactory _connectionFactory; | ||||
|         private readonly ILogger<DefaultRabbitMQPersistentConnection> _logger; | ||||
|         private readonly int _retryCount; | ||||
|         IConnection _connection; | ||||
|         bool _disposed; | ||||
|         _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); | ||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|         _retryCount = retryCount; | ||||
|     } | ||||
| 
 | ||||
|         object sync_root = new object(); | ||||
| 
 | ||||
|         public DefaultRabbitMQPersistentConnection(IConnectionFactory connectionFactory, ILogger<DefaultRabbitMQPersistentConnection> logger, int retryCount = 5) | ||||
|     public bool IsConnected | ||||
|     { | ||||
|         get | ||||
|         { | ||||
|             _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); | ||||
|             _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|             _retryCount = retryCount; | ||||
|         } | ||||
| 
 | ||||
|         public bool IsConnected | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 return _connection != null && _connection.IsOpen && !_disposed; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public IModel CreateModel() | ||||
|         { | ||||
|             if (!IsConnected) | ||||
|             { | ||||
|                 throw new InvalidOperationException("No RabbitMQ connections are available to perform this action"); | ||||
|             } | ||||
| 
 | ||||
|             return _connection.CreateModel(); | ||||
|         } | ||||
| 
 | ||||
|         public void Dispose() | ||||
|         { | ||||
|             if (_disposed) return; | ||||
| 
 | ||||
|             _disposed = true; | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 _connection.Dispose(); | ||||
|             } | ||||
|             catch (IOException ex) | ||||
|             { | ||||
|                 _logger.LogCritical(ex.ToString()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public bool TryConnect() | ||||
|         { | ||||
|             _logger.LogInformation("RabbitMQ Client is trying to connect"); | ||||
| 
 | ||||
|             lock (sync_root) | ||||
|             { | ||||
|                 var policy = RetryPolicy.Handle<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(); | ||||
|                 }); | ||||
| 
 | ||||
|                 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); | ||||
| 
 | ||||
|                     return true; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     _logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); | ||||
| 
 | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) | ||||
|         { | ||||
|             if (_disposed) return; | ||||
| 
 | ||||
|             _logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect..."); | ||||
| 
 | ||||
|             TryConnect(); | ||||
|         } | ||||
| 
 | ||||
|         void OnCallbackException(object sender, CallbackExceptionEventArgs e) | ||||
|         { | ||||
|             if (_disposed) return; | ||||
| 
 | ||||
|             _logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect..."); | ||||
| 
 | ||||
|             TryConnect(); | ||||
|         } | ||||
| 
 | ||||
|         void OnConnectionShutdown(object sender, ShutdownEventArgs reason) | ||||
|         { | ||||
|             if (_disposed) return; | ||||
| 
 | ||||
|             _logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect..."); | ||||
| 
 | ||||
|             TryConnect(); | ||||
|             return _connection != null && _connection.IsOpen && !_disposed; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public IModel CreateModel() | ||||
|     { | ||||
|         if (!IsConnected) | ||||
|         { | ||||
|             throw new InvalidOperationException("No RabbitMQ connections are available to perform this action"); | ||||
|         } | ||||
| 
 | ||||
|         return _connection.CreateModel(); | ||||
|     } | ||||
| 
 | ||||
|     public void Dispose() | ||||
|     { | ||||
|         if (_disposed) return; | ||||
| 
 | ||||
|         _disposed = true; | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             _connection.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"); | ||||
| 
 | ||||
|         lock (sync_root) | ||||
|         { | ||||
|             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(); | ||||
|             }); | ||||
| 
 | ||||
|             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); | ||||
| 
 | ||||
|                 return true; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); | ||||
| 
 | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) | ||||
|     { | ||||
|         if (_disposed) return; | ||||
| 
 | ||||
|         _logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect..."); | ||||
| 
 | ||||
|         TryConnect(); | ||||
|     } | ||||
| 
 | ||||
|     void OnCallbackException(object sender, CallbackExceptionEventArgs e) | ||||
|     { | ||||
|         if (_disposed) return; | ||||
| 
 | ||||
|         _logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect..."); | ||||
| 
 | ||||
|         TryConnect(); | ||||
|     } | ||||
| 
 | ||||
|     void OnConnectionShutdown(object sender, ShutdownEventArgs reason) | ||||
|     { | ||||
|         if (_disposed) return; | ||||
| 
 | ||||
|         _logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect..."); | ||||
| 
 | ||||
|         TryConnect(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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"; | ||||
| 
 | ||||
|     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; | ||||
| 
 | ||||
|     public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger<EventBusRabbitMQ> logger, | ||||
|         ILifetimeScope autofac, IEventBusSubscriptionsManager subsManager, string queueName = null, int retryCount = 5) | ||||
|     { | ||||
|         const string BROKER_NAME = "eshop_event_bus"; | ||||
|         const string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; | ||||
|         _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 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; | ||||
| 
 | ||||
|         public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger<EventBusRabbitMQ> logger, | ||||
|             ILifetimeScope autofac, IEventBusSubscriptionsManager subsManager, string queueName = null, int retryCount = 5) | ||||
|     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) | ||||
|             channel.QueueUnbind(queue: _queueName, | ||||
|                 exchange: BROKER_NAME, | ||||
|                 routingKey: eventName); | ||||
| 
 | ||||
|             if (_subsManager.IsEmpty) | ||||
|             { | ||||
|                 _persistentConnection.TryConnect(); | ||||
|             } | ||||
| 
 | ||||
|             using (var channel = _persistentConnection.CreateModel()) | ||||
|             { | ||||
|                 channel.QueueUnbind(queue: _queueName, | ||||
|                     exchange: BROKER_NAME, | ||||
|                     routingKey: eventName); | ||||
| 
 | ||||
|                 if (_subsManager.IsEmpty) | ||||
|                 { | ||||
|                     _queueName = string.Empty; | ||||
|                     _consumerChannel.Close(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public void Publish(IntegrationEvent @event) | ||||
|         { | ||||
|             if (!_persistentConnection.IsConnected) | ||||
|             { | ||||
|                 _persistentConnection.TryConnect(); | ||||
|             } | ||||
| 
 | ||||
|             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; | ||||
| 
 | ||||
|             _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); | ||||
| 
 | ||||
|                 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 | ||||
| 
 | ||||
|                     _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()); | ||||
| 
 | ||||
|             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); | ||||
| 
 | ||||
|             _logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).GetGenericTypeName()); | ||||
| 
 | ||||
|             _subsManager.AddSubscription<T, TH>(); | ||||
|             StartBasicConsume(); | ||||
|         } | ||||
| 
 | ||||
|         private void DoInternalSubscription(string eventName) | ||||
|         { | ||||
|             var containsKey = _subsManager.HasSubscriptionsForEvent(eventName); | ||||
|             if (!containsKey) | ||||
|             { | ||||
|                 if (!_persistentConnection.IsConnected) | ||||
|                 { | ||||
|                     _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>(); | ||||
| 
 | ||||
|             _logger.LogInformation("Unsubscribing from event {EventName}", eventName); | ||||
| 
 | ||||
|             _subsManager.RemoveSubscription<T, TH>(); | ||||
|         } | ||||
| 
 | ||||
|         public void UnsubscribeDynamic<TH>(string eventName) | ||||
|             where TH : IDynamicIntegrationEventHandler | ||||
|         { | ||||
|             _subsManager.RemoveDynamicSubscription<TH>(eventName); | ||||
|         } | ||||
| 
 | ||||
|         public void Dispose() | ||||
|         { | ||||
|             if (_consumerChannel != null) | ||||
|             { | ||||
|                 _consumerChannel.Dispose(); | ||||
|             } | ||||
| 
 | ||||
|             _subsManager.Clear(); | ||||
|         } | ||||
| 
 | ||||
|         private void StartBasicConsume() | ||||
|         { | ||||
|             _logger.LogTrace("Starting RabbitMQ basic consume"); | ||||
| 
 | ||||
|             if (_consumerChannel != null) | ||||
|             { | ||||
|                 var consumer = new AsyncEventingBasicConsumer(_consumerChannel); | ||||
| 
 | ||||
|                 consumer.Received += Consumer_Received; | ||||
| 
 | ||||
|                 _consumerChannel.BasicConsume( | ||||
|                     queue: _queueName, | ||||
|                     autoAck: false, | ||||
|                     consumer: consumer); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _logger.LogError("StartBasicConsume can't call on _consumerChannel == null"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventArgs) | ||||
|         { | ||||
|             var eventName = eventArgs.RoutingKey; | ||||
|             var message = Encoding.UTF8.GetString(eventArgs.Body.Span); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 if (message.ToLowerInvariant().Contains("throw-fake-exception")) | ||||
|                 { | ||||
|                     throw new InvalidOperationException($"Fake exception requested: \"{message}\""); | ||||
|                 } | ||||
| 
 | ||||
|                 await ProcessEvent(eventName, message); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", 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); | ||||
|         } | ||||
| 
 | ||||
|         private IModel CreateConsumerChannel() | ||||
|         { | ||||
|             if (!_persistentConnection.IsConnected) | ||||
|             { | ||||
|                 _persistentConnection.TryConnect(); | ||||
|             } | ||||
| 
 | ||||
|             _logger.LogTrace("Creating RabbitMQ consumer channel"); | ||||
| 
 | ||||
|             var channel = _persistentConnection.CreateModel(); | ||||
| 
 | ||||
|             channel.ExchangeDeclare(exchange: BROKER_NAME, | ||||
|                                     type: "direct"); | ||||
| 
 | ||||
|             channel.QueueDeclare(queue: _queueName, | ||||
|                                  durable: true, | ||||
|                                  exclusive: false, | ||||
|                                  autoDelete: false, | ||||
|                                  arguments: null); | ||||
| 
 | ||||
|             channel.CallbackException += (sender, ea) => | ||||
|             { | ||||
|                 _logger.LogWarning(ea.Exception, "Recreating RabbitMQ consumer channel"); | ||||
| 
 | ||||
|                 _consumerChannel.Dispose(); | ||||
|                 _consumerChannel = CreateConsumerChannel(); | ||||
|                 StartBasicConsume(); | ||||
|             }; | ||||
| 
 | ||||
|             return channel; | ||||
|         } | ||||
| 
 | ||||
|         private async Task ProcessEvent(string eventName, string message) | ||||
|         { | ||||
|             _logger.LogTrace("Processing RabbitMQ event: {EventName}", eventName); | ||||
| 
 | ||||
|             if (_subsManager.HasSubscriptionsForEvent(eventName)) | ||||
|             { | ||||
|                 using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) | ||||
|                 { | ||||
|                     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 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); | ||||
|                 _queueName = string.Empty; | ||||
|                 _consumerChannel.Close(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void Publish(IntegrationEvent @event) | ||||
|     { | ||||
|         if (!_persistentConnection.IsConnected) | ||||
|         { | ||||
|             _persistentConnection.TryConnect(); | ||||
|         } | ||||
| 
 | ||||
|         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; | ||||
| 
 | ||||
|         _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); | ||||
| 
 | ||||
|             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 | ||||
| 
 | ||||
|                 _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()); | ||||
| 
 | ||||
|         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); | ||||
| 
 | ||||
|         _logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).GetGenericTypeName()); | ||||
| 
 | ||||
|         _subsManager.AddSubscription<T, TH>(); | ||||
|         StartBasicConsume(); | ||||
|     } | ||||
| 
 | ||||
|     private void DoInternalSubscription(string eventName) | ||||
|     { | ||||
|         var containsKey = _subsManager.HasSubscriptionsForEvent(eventName); | ||||
|         if (!containsKey) | ||||
|         { | ||||
|             if (!_persistentConnection.IsConnected) | ||||
|             { | ||||
|                 _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>(); | ||||
| 
 | ||||
|         _logger.LogInformation("Unsubscribing from event {EventName}", eventName); | ||||
| 
 | ||||
|         _subsManager.RemoveSubscription<T, TH>(); | ||||
|     } | ||||
| 
 | ||||
|     public void UnsubscribeDynamic<TH>(string eventName) | ||||
|         where TH : IDynamicIntegrationEventHandler | ||||
|     { | ||||
|         _subsManager.RemoveDynamicSubscription<TH>(eventName); | ||||
|     } | ||||
| 
 | ||||
|     public void Dispose() | ||||
|     { | ||||
|         if (_consumerChannel != null) | ||||
|         { | ||||
|             _consumerChannel.Dispose(); | ||||
|         } | ||||
| 
 | ||||
|         _subsManager.Clear(); | ||||
|     } | ||||
| 
 | ||||
|     private void StartBasicConsume() | ||||
|     { | ||||
|         _logger.LogTrace("Starting RabbitMQ basic consume"); | ||||
| 
 | ||||
|         if (_consumerChannel != null) | ||||
|         { | ||||
|             var consumer = new AsyncEventingBasicConsumer(_consumerChannel); | ||||
| 
 | ||||
|             consumer.Received += Consumer_Received; | ||||
| 
 | ||||
|             _consumerChannel.BasicConsume( | ||||
|                 queue: _queueName, | ||||
|                 autoAck: false, | ||||
|                 consumer: consumer); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             _logger.LogError("StartBasicConsume can't call on _consumerChannel == null"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventArgs) | ||||
|     { | ||||
|         var eventName = eventArgs.RoutingKey; | ||||
|         var message = Encoding.UTF8.GetString(eventArgs.Body.Span); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             if (message.ToLowerInvariant().Contains("throw-fake-exception")) | ||||
|             { | ||||
|                 throw new InvalidOperationException($"Fake exception requested: \"{message}\""); | ||||
|             } | ||||
| 
 | ||||
|             await ProcessEvent(eventName, message); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             _logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", 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); | ||||
|     } | ||||
| 
 | ||||
|     private IModel CreateConsumerChannel() | ||||
|     { | ||||
|         if (!_persistentConnection.IsConnected) | ||||
|         { | ||||
|             _persistentConnection.TryConnect(); | ||||
|         } | ||||
| 
 | ||||
|         _logger.LogTrace("Creating RabbitMQ consumer channel"); | ||||
| 
 | ||||
|         var channel = _persistentConnection.CreateModel(); | ||||
| 
 | ||||
|         channel.ExchangeDeclare(exchange: BROKER_NAME, | ||||
|                                 type: "direct"); | ||||
| 
 | ||||
|         channel.QueueDeclare(queue: _queueName, | ||||
|                                 durable: true, | ||||
|                                 exclusive: false, | ||||
|                                 autoDelete: false, | ||||
|                                 arguments: null); | ||||
| 
 | ||||
|         channel.CallbackException += (sender, ea) => | ||||
|         { | ||||
|             _logger.LogWarning(ea.Exception, "Recreating RabbitMQ consumer channel"); | ||||
| 
 | ||||
|             _consumerChannel.Dispose(); | ||||
|             _consumerChannel = CreateConsumerChannel(); | ||||
|             StartBasicConsume(); | ||||
|         }; | ||||
| 
 | ||||
|         return channel; | ||||
|     } | ||||
| 
 | ||||
|     private async Task ProcessEvent(string eventName, string message) | ||||
|     { | ||||
|         _logger.LogTrace("Processing RabbitMQ event: {EventName}", eventName); | ||||
| 
 | ||||
|         if (_subsManager.HasSubscriptionsForEvent(eventName)) | ||||
|         { | ||||
|             using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) | ||||
|             { | ||||
|                 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 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										17
									
								
								src/BuildingBlocks/EventBus/EventBusRabbitMQ/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/BuildingBlocks/EventBus/EventBusRabbitMQ/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @ -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,68 +1,64 @@ | ||||
| using Microsoft.Azure.ServiceBus; | ||||
| using System; | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus | ||||
| public class DefaultServiceBusPersisterConnection : IServiceBusPersisterConnection | ||||
| { | ||||
|     public class DefaultServiceBusPersisterConnection : IServiceBusPersisterConnection | ||||
|     private readonly ServiceBusConnectionStringBuilder _serviceBusConnectionStringBuilder; | ||||
|     private readonly string _subscriptionClientName; | ||||
|     private SubscriptionClient _subscriptionClient; | ||||
|     private ITopicClient _topicClient; | ||||
| 
 | ||||
|     bool _disposed; | ||||
| 
 | ||||
|     public DefaultServiceBusPersisterConnection(ServiceBusConnectionStringBuilder serviceBusConnectionStringBuilder, | ||||
|         string subscriptionClientName) | ||||
|     { | ||||
|         private readonly ServiceBusConnectionStringBuilder _serviceBusConnectionStringBuilder; | ||||
|         private readonly string _subscriptionClientName; | ||||
|         private SubscriptionClient _subscriptionClient; | ||||
|         private ITopicClient _topicClient; | ||||
|         _serviceBusConnectionStringBuilder = serviceBusConnectionStringBuilder ?? | ||||
|             throw new ArgumentNullException(nameof(serviceBusConnectionStringBuilder)); | ||||
|         _subscriptionClientName = subscriptionClientName; | ||||
|         _subscriptionClient = new SubscriptionClient(_serviceBusConnectionStringBuilder, subscriptionClientName); | ||||
|         _topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default); | ||||
|     } | ||||
| 
 | ||||
|         bool _disposed; | ||||
| 
 | ||||
|         public DefaultServiceBusPersisterConnection(ServiceBusConnectionStringBuilder serviceBusConnectionStringBuilder, | ||||
|             string subscriptionClientName) | ||||
|         { | ||||
|             _serviceBusConnectionStringBuilder = serviceBusConnectionStringBuilder ?? | ||||
|                 throw new ArgumentNullException(nameof(serviceBusConnectionStringBuilder)); | ||||
|             _subscriptionClientName = subscriptionClientName; | ||||
|             _subscriptionClient = new SubscriptionClient(_serviceBusConnectionStringBuilder, subscriptionClientName); | ||||
|             _topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default); | ||||
|         } | ||||
| 
 | ||||
|         public ITopicClient TopicClient | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if (_topicClient.IsClosedOrClosing) | ||||
|                 { | ||||
|                     _topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default); | ||||
|                 } | ||||
|                 return _topicClient; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public ISubscriptionClient SubscriptionClient | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if (_subscriptionClient.IsClosedOrClosing) | ||||
|                 { | ||||
|                     _subscriptionClient = new SubscriptionClient(_serviceBusConnectionStringBuilder, _subscriptionClientName); | ||||
|                 } | ||||
|                 return _subscriptionClient; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public ServiceBusConnectionStringBuilder ServiceBusConnectionStringBuilder => _serviceBusConnectionStringBuilder; | ||||
| 
 | ||||
|         public ITopicClient CreateModel() | ||||
|     public ITopicClient TopicClient | ||||
|     { | ||||
|         get | ||||
|         { | ||||
|             if (_topicClient.IsClosedOrClosing) | ||||
|             { | ||||
|                 _topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default); | ||||
|             } | ||||
| 
 | ||||
|             return _topicClient; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|         public void Dispose() | ||||
|     public ISubscriptionClient SubscriptionClient | ||||
|     { | ||||
|         get | ||||
|         { | ||||
|             if (_disposed) return; | ||||
| 
 | ||||
|             _disposed = true; | ||||
|             if (_subscriptionClient.IsClosedOrClosing) | ||||
|             { | ||||
|                 _subscriptionClient = new SubscriptionClient(_serviceBusConnectionStringBuilder, _subscriptionClientName); | ||||
|             } | ||||
|             return _subscriptionClient; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public ServiceBusConnectionStringBuilder ServiceBusConnectionStringBuilder => _serviceBusConnectionStringBuilder; | ||||
| 
 | ||||
|     public ITopicClient CreateModel() | ||||
|     { | ||||
|         if (_topicClient.IsClosedOrClosing) | ||||
|         { | ||||
|             _topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default); | ||||
|         } | ||||
| 
 | ||||
|         return _topicClient; | ||||
|     } | ||||
| 
 | ||||
|     public void Dispose() | ||||
|     { | ||||
|         if (_disposed) return; | ||||
| 
 | ||||
|         _disposed = true; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,203 +1,191 @@ | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | ||||
| 
 | ||||
| public class EventBusServiceBus : IEventBus | ||||
| { | ||||
|     using Autofac; | ||||
|     using Microsoft.Azure.ServiceBus; | ||||
|     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; | ||||
|     using System.Text.Json; | ||||
|     using System.Threading.Tasks; | ||||
|     private readonly IServiceBusPersisterConnection _serviceBusPersisterConnection; | ||||
|     private readonly ILogger<EventBusServiceBus> _logger; | ||||
|     private readonly IEventBusSubscriptionsManager _subsManager; | ||||
|     private readonly ILifetimeScope _autofac; | ||||
|     private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; | ||||
|     private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; | ||||
| 
 | ||||
|     public class EventBusServiceBus : IEventBus | ||||
|     public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, | ||||
|         ILogger<EventBusServiceBus> logger, IEventBusSubscriptionsManager subsManager, ILifetimeScope autofac) | ||||
|     { | ||||
|         private readonly IServiceBusPersisterConnection _serviceBusPersisterConnection; | ||||
|         private readonly ILogger<EventBusServiceBus> _logger; | ||||
|         private readonly IEventBusSubscriptionsManager _subsManager; | ||||
|         private readonly ILifetimeScope _autofac; | ||||
|         private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; | ||||
|         private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; | ||||
|         _serviceBusPersisterConnection = serviceBusPersisterConnection; | ||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|         _subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); | ||||
|         _autofac = autofac; | ||||
| 
 | ||||
|         public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, | ||||
|             ILogger<EventBusServiceBus> logger, IEventBusSubscriptionsManager subsManager, ILifetimeScope autofac) | ||||
|         RemoveDefaultRule(); | ||||
|         RegisterSubscriptionClientMessageHandler(); | ||||
|     } | ||||
| 
 | ||||
|     public void Publish(IntegrationEvent @event) | ||||
|     { | ||||
|         var eventName = @event.GetType().Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | ||||
|         var jsonMessage = JsonSerializer.Serialize(@event); | ||||
|         var body = Encoding.UTF8.GetBytes(jsonMessage); | ||||
| 
 | ||||
|         var message = new Message | ||||
|         { | ||||
|             _serviceBusPersisterConnection = serviceBusPersisterConnection; | ||||
|             _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|             _subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); | ||||
|             _autofac = autofac; | ||||
|             MessageId = Guid.NewGuid().ToString(), | ||||
|             Body = body, | ||||
|             Label = eventName, | ||||
|         }; | ||||
| 
 | ||||
|             RemoveDefaultRule(); | ||||
|             RegisterSubscriptionClientMessageHandler(); | ||||
|         _serviceBusPersisterConnection.TopicClient.SendAsync(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); | ||||
| 
 | ||||
|         _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, ""); | ||||
| 
 | ||||
|         var containsKey = _subsManager.HasSubscriptionsForEvent<T>(); | ||||
|         if (!containsKey) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 _serviceBusPersisterConnection.SubscriptionClient.AddRuleAsync(new RuleDescription | ||||
|                 { | ||||
|                     Filter = new CorrelationFilter { Label = eventName }, | ||||
|                     Name = eventName | ||||
|                 }).GetAwaiter().GetResult(); | ||||
|             } | ||||
|             catch (ServiceBusException) | ||||
|             { | ||||
|                 _logger.LogWarning("The messaging entity {eventName} already exists.", eventName); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public void Publish(IntegrationEvent @event) | ||||
|         _logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).Name); | ||||
| 
 | ||||
|         _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, ""); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             var eventName = @event.GetType().Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | ||||
|             var jsonMessage = JsonSerializer.Serialize(@event); | ||||
|             var body = Encoding.UTF8.GetBytes(jsonMessage); | ||||
| 
 | ||||
|             var message = new Message | ||||
|             { | ||||
|                 MessageId = Guid.NewGuid().ToString(), | ||||
|                 Body = body, | ||||
|                 Label = eventName, | ||||
|             }; | ||||
| 
 | ||||
|             _serviceBusPersisterConnection.TopicClient.SendAsync(message) | ||||
|             _serviceBusPersisterConnection | ||||
|                 .SubscriptionClient | ||||
|                 .RemoveRuleAsync(eventName) | ||||
|                 .GetAwaiter() | ||||
|                 .GetResult(); | ||||
|         } | ||||
| 
 | ||||
|         public void SubscribeDynamic<TH>(string eventName) | ||||
|             where TH : IDynamicIntegrationEventHandler | ||||
|         catch (MessagingEntityNotFoundException) | ||||
|         { | ||||
|             _logger.LogInformation("Subscribing to dynamic event {EventName} with {EventHandler}", eventName, typeof(TH).Name); | ||||
| 
 | ||||
|             _subsManager.AddDynamicSubscription<TH>(eventName); | ||||
|             _logger.LogWarning("The messaging entity {eventName} Could not be found.", eventName); | ||||
|         } | ||||
| 
 | ||||
|         public void Subscribe<T, TH>() | ||||
|             where T : IntegrationEvent | ||||
|             where TH : IIntegrationEventHandler<T> | ||||
|         { | ||||
|             var eventName = typeof(T).Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | ||||
|         _logger.LogInformation("Unsubscribing from event {EventName}", eventName); | ||||
| 
 | ||||
|             var containsKey = _subsManager.HasSubscriptionsForEvent<T>(); | ||||
|             if (!containsKey) | ||||
|         _subsManager.RemoveSubscription<T, TH>(); | ||||
|     } | ||||
| 
 | ||||
|     public void UnsubscribeDynamic<TH>(string eventName) | ||||
|         where TH : IDynamicIntegrationEventHandler | ||||
|     { | ||||
|         _logger.LogInformation("Unsubscribing from dynamic event {EventName}", eventName); | ||||
| 
 | ||||
|         _subsManager.RemoveDynamicSubscription<TH>(eventName); | ||||
|     } | ||||
| 
 | ||||
|     public void Dispose() | ||||
|     { | ||||
|         _subsManager.Clear(); | ||||
|     } | ||||
| 
 | ||||
|     private void RegisterSubscriptionClientMessageHandler() | ||||
|     { | ||||
|         _serviceBusPersisterConnection.SubscriptionClient.RegisterMessageHandler( | ||||
|             async (message, token) => | ||||
|             { | ||||
|                 try | ||||
|                 var eventName = $"{message.Label}{INTEGRATION_EVENT_SUFFIX}"; | ||||
|                 var messageData = Encoding.UTF8.GetString(message.Body); | ||||
| 
 | ||||
|                 // Complete the message so that it is not received again. | ||||
|                 if (await ProcessEventAsync(eventName, messageData)) | ||||
|                 { | ||||
|                     _serviceBusPersisterConnection.SubscriptionClient.AddRuleAsync(new RuleDescription | ||||
|                     { | ||||
|                         Filter = new CorrelationFilter { Label = eventName }, | ||||
|                         Name = eventName | ||||
|                     }).GetAwaiter().GetResult(); | ||||
|                     await _serviceBusPersisterConnection.SubscriptionClient.CompleteAsync(message.SystemProperties.LockToken); | ||||
|                 } | ||||
|                 catch (ServiceBusException) | ||||
|                 { | ||||
|                     _logger.LogWarning("The messaging entity {eventName} already exists.", eventName); | ||||
|                 } | ||||
|             } | ||||
|             }, | ||||
|             new MessageHandlerOptions(ExceptionReceivedHandlerAsync) { MaxConcurrentCalls = 10, AutoComplete = false }); | ||||
|     } | ||||
| 
 | ||||
|             _logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).Name); | ||||
|     private Task ExceptionReceivedHandlerAsync(ExceptionReceivedEventArgs exceptionReceivedEventArgs) | ||||
|     { | ||||
|         var ex = exceptionReceivedEventArgs.Exception; | ||||
|         var context = exceptionReceivedEventArgs.ExceptionReceivedContext; | ||||
| 
 | ||||
|             _subsManager.AddSubscription<T, TH>(); | ||||
|         } | ||||
|         _logger.LogError(ex, "ERROR handling message: {ExceptionMessage} - Context: {@ExceptionContext}", ex.Message, context); | ||||
| 
 | ||||
|         public void Unsubscribe<T, TH>() | ||||
|             where T : IntegrationEvent | ||||
|             where TH : IIntegrationEventHandler<T> | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     private async Task<bool> ProcessEventAsync(string eventName, string message) | ||||
|     { | ||||
|         var processed = false; | ||||
|         if (_subsManager.HasSubscriptionsForEvent(eventName)) | ||||
|         { | ||||
|             var eventName = typeof(T).Name.Replace(INTEGRATION_EVENT_SUFFIX, ""); | ||||
| 
 | ||||
|             try | ||||
|             using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) | ||||
|             { | ||||
|                 _serviceBusPersisterConnection | ||||
|                     .SubscriptionClient | ||||
|                     .RemoveRuleAsync(eventName) | ||||
|                     .GetAwaiter() | ||||
|                     .GetResult(); | ||||
|             } | ||||
|             catch (MessagingEntityNotFoundException) | ||||
|             { | ||||
|                 _logger.LogWarning("The messaging entity {eventName} Could not be found.", eventName); | ||||
|             } | ||||
| 
 | ||||
|             _logger.LogInformation("Unsubscribing from event {EventName}", eventName); | ||||
| 
 | ||||
|             _subsManager.RemoveSubscription<T, TH>(); | ||||
|         } | ||||
| 
 | ||||
|         public void UnsubscribeDynamic<TH>(string eventName) | ||||
|             where TH : IDynamicIntegrationEventHandler | ||||
|         { | ||||
|             _logger.LogInformation("Unsubscribing from dynamic event {EventName}", eventName); | ||||
| 
 | ||||
|             _subsManager.RemoveDynamicSubscription<TH>(eventName); | ||||
|         } | ||||
| 
 | ||||
|         public void Dispose() | ||||
|         { | ||||
|             _subsManager.Clear(); | ||||
|         } | ||||
| 
 | ||||
|         private void RegisterSubscriptionClientMessageHandler() | ||||
|         { | ||||
|             _serviceBusPersisterConnection.SubscriptionClient.RegisterMessageHandler( | ||||
|                 async (message, token) => | ||||
|                 var subscriptions = _subsManager.GetHandlersForEvent(eventName); | ||||
|                 foreach (var subscription in subscriptions) | ||||
|                 { | ||||
|                     var eventName = $"{message.Label}{INTEGRATION_EVENT_SUFFIX}"; | ||||
|                     var messageData = Encoding.UTF8.GetString(message.Body); | ||||
| 
 | ||||
|                     // Complete the message so that it is not received again. | ||||
|                     if (await ProcessEvent(eventName, messageData)) | ||||
|                     if (subscription.IsDynamic) | ||||
|                     { | ||||
|                         await _serviceBusPersisterConnection.SubscriptionClient.CompleteAsync(message.SystemProperties.LockToken); | ||||
|                     } | ||||
|                 }, | ||||
|                 new MessageHandlerOptions(ExceptionReceivedHandler) { MaxConcurrentCalls = 10, AutoComplete = false }); | ||||
|         } | ||||
| 
 | ||||
|         private Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs) | ||||
|         { | ||||
|             var ex = exceptionReceivedEventArgs.Exception; | ||||
|             var context = exceptionReceivedEventArgs.ExceptionReceivedContext; | ||||
| 
 | ||||
|             _logger.LogError(ex, "ERROR handling message: {ExceptionMessage} - Context: {@ExceptionContext}", ex.Message, context); | ||||
| 
 | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
| 
 | ||||
|         private async Task<bool> ProcessEvent(string eventName, string message) | ||||
|         { | ||||
|             var processed = false; | ||||
|             if (_subsManager.HasSubscriptionsForEvent(eventName)) | ||||
|             { | ||||
|                 using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) | ||||
|                 { | ||||
|                     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; | ||||
|                         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 }); | ||||
|                         } | ||||
|                         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 }); | ||||
|                     } | ||||
|                 } | ||||
|                 processed = true; | ||||
|             } | ||||
|             return processed; | ||||
|             processed = true; | ||||
|         } | ||||
|         return processed; | ||||
|     } | ||||
| 
 | ||||
|         private void RemoveDefaultRule() | ||||
|     private void RemoveDefaultRule() | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 _serviceBusPersisterConnection | ||||
|                     .SubscriptionClient | ||||
|                     .RemoveRuleAsync(RuleDescription.DefaultRuleName) | ||||
|                     .GetAwaiter() | ||||
|                     .GetResult(); | ||||
|             } | ||||
|             catch (MessagingEntityNotFoundException) | ||||
|             { | ||||
|                 _logger.LogWarning("The messaging entity {DefaultRuleName} Could not be found.", RuleDescription.DefaultRuleName); | ||||
|             } | ||||
|             _serviceBusPersisterConnection | ||||
|                 .SubscriptionClient | ||||
|                 .RemoveRuleAsync(RuleDescription.DefaultRuleName) | ||||
|                 .GetAwaiter() | ||||
|                 .GetResult(); | ||||
|         } | ||||
|         catch (MessagingEntityNotFoundException) | ||||
|         { | ||||
|             _logger.LogWarning("The messaging entity {DefaultRuleName} Could not be found.", RuleDescription.DefaultRuleName); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,22 @@ | ||||
| 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 Microsoft.Azure.ServiceBus; | ||||
| global using Autofac; | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | ||||
| global using Microsoft.Extensions.Logging; | ||||
| global using System.Text; | ||||
| global using System.Text.Json; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @ -1,11 +1,7 @@ | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus | ||||
| { | ||||
|     using Microsoft.Azure.ServiceBus; | ||||
|     using System; | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | ||||
| 
 | ||||
|     public interface IServiceBusPersisterConnection : IDisposable | ||||
|     { | ||||
|         ITopicClient TopicClient { get; } | ||||
|         ISubscriptionClient SubscriptionClient { get; } | ||||
|     } | ||||
| } | ||||
| public interface IServiceBusPersisterConnection : IDisposable | ||||
| { | ||||
|     ITopicClient TopicClient { get; } | ||||
|     ISubscriptionClient SubscriptionClient { 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(); | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -6,13 +6,13 @@ | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.2"> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0-preview.7.21378.4"> | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|     </PackageReference> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.2" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.2" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.2" />     | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-preview.7.21378.4" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0-preview.7.21378.4" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0-preview.7.21378.4" />     | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services | ||||
| public class IntegrationEventLogService : IIntegrationEventLogService, IDisposable | ||||
| { | ||||
|     public class IntegrationEventLogService : IIntegrationEventLogService, IDisposable | ||||
|     private readonly IntegrationEventLogContext _integrationEventLogContext; | ||||
|     private readonly DbConnection _dbConnection; | ||||
|     private readonly List<Type> _eventTypes; | ||||
|     private volatile bool _disposedValue; | ||||
| 
 | ||||
|     public IntegrationEventLogService(DbConnection dbConnection) | ||||
|     { | ||||
|         private readonly IntegrationEventLogContext _integrationEventLogContext; | ||||
|         private readonly DbConnection _dbConnection; | ||||
|         private readonly List<Type> _eventTypes; | ||||
|         private volatile bool disposedValue; | ||||
|         _dbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection)); | ||||
|         _integrationEventLogContext = new IntegrationEventLogContext( | ||||
|             new DbContextOptionsBuilder<IntegrationEventLogContext>() | ||||
|                 .UseSqlServer(_dbConnection) | ||||
|                 .Options); | ||||
| 
 | ||||
|         public IntegrationEventLogService(DbConnection dbConnection) | ||||
|         _eventTypes = Assembly.Load(Assembly.GetEntryAssembly().FullName) | ||||
|             .GetTypes() | ||||
|             .Where(t => t.Name.EndsWith(nameof(IntegrationEvent))) | ||||
|             .ToList(); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync(Guid transactionId) | ||||
|     { | ||||
|         var tid = transactionId.ToString(); | ||||
| 
 | ||||
|         var result = await _integrationEventLogContext.IntegrationEventLogs | ||||
|             .Where(e => e.TransactionId == tid && e.State == EventStateEnum.NotPublished).ToListAsync(); | ||||
| 
 | ||||
|         if (result != null && result.Any()) | ||||
|         { | ||||
|             _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(); | ||||
|             return result.OrderBy(o => o.CreationTime) | ||||
|                 .Select(e => e.DeserializeJsonContent(_eventTypes.Find(t => t.Name == e.EventTypeShortName))); | ||||
|         } | ||||
| 
 | ||||
|         public async Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync(Guid transactionId) | ||||
|         return new List<IntegrationEventLogEntry>(); | ||||
|     } | ||||
| 
 | ||||
|     public Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction) | ||||
|     { | ||||
|         if (transaction == null) throw new ArgumentNullException(nameof(transaction)); | ||||
| 
 | ||||
|         var eventLogEntry = new IntegrationEventLogEntry(@event, transaction.TransactionId); | ||||
| 
 | ||||
|         _integrationEventLogContext.Database.UseTransaction(transaction.GetDbTransaction()); | ||||
|         _integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry); | ||||
| 
 | ||||
|         return _integrationEventLogContext.SaveChangesAsync(); | ||||
|     } | ||||
| 
 | ||||
|     public Task MarkEventAsPublishedAsync(Guid eventId) | ||||
|     { | ||||
|         return UpdateEventStatus(eventId, EventStateEnum.Published); | ||||
|     } | ||||
| 
 | ||||
|     public Task MarkEventAsInProgressAsync(Guid eventId) | ||||
|     { | ||||
|         return UpdateEventStatus(eventId, EventStateEnum.InProgress); | ||||
|     } | ||||
| 
 | ||||
|     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++; | ||||
| 
 | ||||
|         _integrationEventLogContext.IntegrationEventLogs.Update(eventLogEntry); | ||||
| 
 | ||||
|         return _integrationEventLogContext.SaveChangesAsync(); | ||||
|     } | ||||
| 
 | ||||
|     protected virtual void Dispose(bool disposing) | ||||
|     { | ||||
|         if (!_disposedValue) | ||||
|         { | ||||
|             var tid = transactionId.ToString(); | ||||
| 
 | ||||
|             var result = await _integrationEventLogContext.IntegrationEventLogs | ||||
|                 .Where(e => e.TransactionId == tid && e.State == EventStateEnum.NotPublished).ToListAsync(); | ||||
| 
 | ||||
|             if (result != null && result.Any()) | ||||
|             if (disposing) | ||||
|             { | ||||
|                 return result.OrderBy(o => o.CreationTime) | ||||
|                     .Select(e => e.DeserializeJsonContent(_eventTypes.Find(t => t.Name == e.EventTypeShortName))); | ||||
|                 _integrationEventLogContext?.Dispose(); | ||||
|             } | ||||
| 
 | ||||
|             return new List<IntegrationEventLogEntry>(); | ||||
|         } | ||||
| 
 | ||||
|         public Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction) | ||||
|         { | ||||
|             if (transaction == null) throw new ArgumentNullException(nameof(transaction)); | ||||
| 
 | ||||
|             var eventLogEntry = new IntegrationEventLogEntry(@event, transaction.TransactionId); | ||||
| 
 | ||||
|             _integrationEventLogContext.Database.UseTransaction(transaction.GetDbTransaction()); | ||||
|             _integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry); | ||||
| 
 | ||||
|             return _integrationEventLogContext.SaveChangesAsync(); | ||||
|         } | ||||
| 
 | ||||
|         public Task MarkEventAsPublishedAsync(Guid eventId) | ||||
|         { | ||||
|             return UpdateEventStatus(eventId, EventStateEnum.Published); | ||||
|         } | ||||
| 
 | ||||
|         public Task MarkEventAsInProgressAsync(Guid eventId) | ||||
|         { | ||||
|             return UpdateEventStatus(eventId, EventStateEnum.InProgress); | ||||
|         } | ||||
| 
 | ||||
|         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++; | ||||
| 
 | ||||
|             _integrationEventLogContext.IntegrationEventLogs.Update(eventLogEntry); | ||||
| 
 | ||||
|             return _integrationEventLogContext.SaveChangesAsync(); | ||||
|         } | ||||
| 
 | ||||
|         protected virtual void Dispose(bool disposing) | ||||
|         { | ||||
|             if (!disposedValue) | ||||
|             { | ||||
|                 if (disposing) | ||||
|                 { | ||||
|                     _integrationEventLogContext?.Dispose(); | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 disposedValue = true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public void Dispose() | ||||
|         { | ||||
|             Dispose(disposing: true); | ||||
|             GC.SuppressFinalize(this); | ||||
|             _disposedValue = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void Dispose() | ||||
|     { | ||||
|         Dispose(disposing: true); | ||||
|         GC.SuppressFinalize(this); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,31 +1,25 @@ | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities | ||||
| public class ResilientTransaction | ||||
| { | ||||
|     public class ResilientTransaction | ||||
|     private DbContext _context; | ||||
|     private ResilientTransaction(DbContext context) => | ||||
|         _context = context ?? throw new ArgumentNullException(nameof(context)); | ||||
| 
 | ||||
|     public static ResilientTransaction New(DbContext context) => new(context); | ||||
| 
 | ||||
|     public async Task ExecuteAsync(Func<Task> action) | ||||
|     { | ||||
|         private DbContext _context; | ||||
|         private ResilientTransaction(DbContext context) => | ||||
|             _context = context ?? throw new ArgumentNullException(nameof(context)); | ||||
| 
 | ||||
|         public static ResilientTransaction New(DbContext context) => | ||||
|             new ResilientTransaction(context); | ||||
| 
 | ||||
|         public async Task ExecuteAsync(Func<Task> action) | ||||
|         //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): | ||||
|         //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | ||||
|         var strategy = _context.Database.CreateExecutionStrategy(); | ||||
|         await strategy.ExecuteAsync(async () => | ||||
|         { | ||||
|             //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): | ||||
|             //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency | ||||
|             var strategy = _context.Database.CreateExecutionStrategy(); | ||||
|             await strategy.ExecuteAsync(async () => | ||||
|             using (var transaction = _context.Database.BeginTransaction()) | ||||
|             { | ||||
|                 using (var transaction = _context.Database.BeginTransaction()) | ||||
|                 { | ||||
|                     await action(); | ||||
|                     transaction.Commit(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|                 await action(); | ||||
|                 transaction.Commit(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| namespace Basket.API.Infrastructure.Middlewares; | ||||
| 
 | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| public class FailingMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
|     private bool _mustFail; | ||||
|     private readonly FailingOptions _options; | ||||
|     private readonly Microsoft.Extensions.Logging.ILogger _logger; | ||||
|     private readonly ILogger _logger; | ||||
| 
 | ||||
|     public FailingMiddleware(RequestDelegate next, Microsoft.Extensions.Logging.ILogger<FailingMiddleware> logger, FailingOptions options) | ||||
|     public FailingMiddleware(RequestDelegate next, ILogger<FailingMiddleware> logger, FailingOptions options) | ||||
|     { | ||||
|         _next = next; | ||||
|         _options = options; | ||||
|  | ||||
| @ -1,31 +1,26 @@ | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using System.Security.Claims; | ||||
| using System.Threading.Tasks; | ||||
| namespace Basket.FunctionalTests.Base; | ||||
| 
 | ||||
| namespace Basket.FunctionalTests.Base | ||||
| class AutoAuthorizeMiddleware | ||||
| { | ||||
|     class AutoAuthorizeMiddleware | ||||
|     public const string IDENTITY_ID = "9e3163b9-1ae6-4652-9dc6-7898ab7b7a00"; | ||||
| 
 | ||||
|     private readonly RequestDelegate _next; | ||||
| 
 | ||||
|     public AutoAuthorizeMiddleware(RequestDelegate rd) | ||||
|     { | ||||
|         public const string IDENTITY_ID = "9e3163b9-1ae6-4652-9dc6-7898ab7b7a00"; | ||||
|         _next = rd; | ||||
|     } | ||||
| 
 | ||||
|         private readonly RequestDelegate _next; | ||||
|     public async Task Invoke(HttpContext httpContext) | ||||
|     { | ||||
|         var identity = new ClaimsIdentity("cookies"); | ||||
| 
 | ||||
|         public AutoAuthorizeMiddleware(RequestDelegate rd) | ||||
|         { | ||||
|             _next = rd; | ||||
|         } | ||||
|         identity.AddClaim(new Claim("sub", IDENTITY_ID)); | ||||
|         identity.AddClaim(new Claim("unique_name", IDENTITY_ID)); | ||||
|         identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); | ||||
| 
 | ||||
|         public async Task Invoke(HttpContext httpContext) | ||||
|         { | ||||
|             var identity = new ClaimsIdentity("cookies"); | ||||
|         httpContext.User.AddIdentity(identity); | ||||
| 
 | ||||
|             identity.AddClaim(new Claim("sub", IDENTITY_ID)); | ||||
|             identity.AddClaim(new Claim("unique_name", IDENTITY_ID)); | ||||
|             identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); | ||||
| 
 | ||||
|             httpContext.User.AddIdentity(identity); | ||||
| 
 | ||||
|             await _next.Invoke(httpContext); | ||||
|         } | ||||
|         await _next.Invoke(httpContext); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,43 +1,36 @@ | ||||
| using Microsoft.AspNetCore.Hosting; | ||||
| using Microsoft.AspNetCore.TestHost; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using System.IO; | ||||
| using System.Reflection; | ||||
| namespace Basket.FunctionalTests.Base; | ||||
| 
 | ||||
| namespace Basket.FunctionalTests.Base | ||||
| public class BasketScenarioBase | ||||
| { | ||||
|     public class BasketScenarioBase | ||||
|     private const string ApiUrlBase = "api/v1/basket"; | ||||
| 
 | ||||
|     public TestServer CreateServer() | ||||
|     { | ||||
|         private const string ApiUrlBase = "api/v1/basket"; | ||||
|         var path = Assembly.GetAssembly(typeof(BasketScenarioBase)) | ||||
|             .Location; | ||||
| 
 | ||||
|         public TestServer CreateServer() | ||||
|         { | ||||
|             var path = Assembly.GetAssembly(typeof(BasketScenarioBase)) | ||||
|                .Location; | ||||
| 
 | ||||
|             var hostBuilder = new WebHostBuilder() | ||||
|                 .UseContentRoot(Path.GetDirectoryName(path)) | ||||
|                 .ConfigureAppConfiguration(cb => | ||||
|                 { | ||||
|                     cb.AddJsonFile("appsettings.json", optional: false) | ||||
|                     .AddEnvironmentVariables(); | ||||
|                 }).UseStartup<BasketTestsStartup>(); | ||||
| 
 | ||||
|             return new TestServer(hostBuilder); | ||||
|         } | ||||
| 
 | ||||
|         public static class Get | ||||
|         { | ||||
|             public static string GetBasket(int id) | ||||
|         var hostBuilder = new WebHostBuilder() | ||||
|             .UseContentRoot(Path.GetDirectoryName(path)) | ||||
|             .ConfigureAppConfiguration(cb => | ||||
|             { | ||||
|                 return $"{ApiUrlBase}/{id}"; | ||||
|             } | ||||
|         } | ||||
|                 cb.AddJsonFile("appsettings.json", optional: false) | ||||
|                 .AddEnvironmentVariables(); | ||||
|             }).UseStartup<BasketTestsStartup>(); | ||||
| 
 | ||||
|         public static class Post | ||||
|         return new TestServer(hostBuilder); | ||||
|     } | ||||
| 
 | ||||
|     public static class Get | ||||
|     { | ||||
|         public static string GetBasket(int id) | ||||
|         { | ||||
|             public static string Basket = $"{ApiUrlBase}/"; | ||||
|             public static string CheckoutOrder = $"{ApiUrlBase}/checkout"; | ||||
|             return $"{ApiUrlBase}/{id}"; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Post | ||||
|     { | ||||
|         public static string Basket = $"{ApiUrlBase}/"; | ||||
|         public static string CheckoutOrder = $"{ApiUrlBase}/checkout"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,9 +1,4 @@ | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.AspNetCore.Routing; | ||||
| using Microsoft.eShopOnContainers.Services.Basket.API; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using System; | ||||
|  | ||||
| 
 | ||||
| namespace Basket.FunctionalTests.Base | ||||
| { | ||||
|  | ||||
| @ -1,18 +1,13 @@ | ||||
| using Microsoft.AspNetCore.TestHost; | ||||
| using System; | ||||
| using System.Net.Http; | ||||
| namespace Basket.FunctionalTests.Base; | ||||
| 
 | ||||
| namespace Basket.FunctionalTests.Base | ||||
| static class HttpClientExtensions | ||||
| { | ||||
|     static class HttpClientExtensions | ||||
|     public static HttpClient CreateIdempotentClient(this TestServer server) | ||||
|     { | ||||
|         public static HttpClient CreateIdempotentClient(this TestServer server) | ||||
|         { | ||||
|             var client = server.CreateClient(); | ||||
|         var client = server.CreateClient(); | ||||
| 
 | ||||
|             client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); | ||||
|         client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); | ||||
| 
 | ||||
|             return client; | ||||
|         } | ||||
|         return client; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -16,8 +16,8 @@ | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.2" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="5.0.2" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-preview.7.21378.6" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="6.0.0-preview.7.21378.6" /> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> | ||||
|     <PackageReference Include="Moq" Version="4.15.2" /> | ||||
|     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> | ||||
|  | ||||
| @ -1,95 +1,85 @@ | ||||
| using Basket.FunctionalTests.Base; | ||||
| using Microsoft.eShopOnContainers.Services.Basket.API.Model; | ||||
| using System; | ||||
| using System.Net.Http; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using System.Threading.Tasks; | ||||
| using Xunit; | ||||
| namespace Basket.FunctionalTests; | ||||
| 
 | ||||
| namespace Basket.FunctionalTests | ||||
| public class BasketScenarios | ||||
|     : BasketScenarioBase | ||||
| { | ||||
|     public class BasketScenarios | ||||
|         : BasketScenarioBase | ||||
|     [Fact] | ||||
|     public async Task Post_basket_and_response_ok_status_code() | ||||
|     { | ||||
|         [Fact] | ||||
|         public async Task Post_basket_and_response_ok_status_code() | ||||
|         using (var server = CreateServer()) | ||||
|         { | ||||
|             using (var server = CreateServer()) | ||||
|             { | ||||
|                 var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); | ||||
|                 var response = await server.CreateClient() | ||||
|                    .PostAsync(Post.Basket, content); | ||||
|             var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); | ||||
|             var response = await server.CreateClient() | ||||
|                 .PostAsync(Post.Basket, content); | ||||
| 
 | ||||
|                 response.EnsureSuccessStatusCode(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Get_basket_and_response_ok_status_code() | ||||
|         { | ||||
|             using (var server = CreateServer()) | ||||
|             { | ||||
|                 var response = await server.CreateClient() | ||||
|                    .GetAsync(Get.GetBasket(1)); | ||||
| 
 | ||||
|                 response.EnsureSuccessStatusCode(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Send_Checkout_basket_and_response_ok_status_code() | ||||
|         { | ||||
|             using (var server = CreateServer()) | ||||
|             { | ||||
|                 var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); | ||||
| 
 | ||||
|                 await server.CreateClient() | ||||
|                    .PostAsync(Post.Basket, contentBasket); | ||||
| 
 | ||||
|                 var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json"); | ||||
| 
 | ||||
|                 var response = await server.CreateIdempotentClient() | ||||
|                    .PostAsync(Post.CheckoutOrder, contentCheckout); | ||||
| 
 | ||||
|                 response.EnsureSuccessStatusCode(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         string BuildBasket() | ||||
|         { | ||||
|             var order = new CustomerBasket(AutoAuthorizeMiddleware.IDENTITY_ID); | ||||
| 
 | ||||
|             order.Items.Add(new BasketItem | ||||
|             { | ||||
|                 ProductId = 1, | ||||
|                 ProductName = ".NET Bot Black Hoodie", | ||||
|                 UnitPrice = 10, | ||||
|                 Quantity = 1 | ||||
|             }); | ||||
| 
 | ||||
|             return JsonSerializer.Serialize(order); | ||||
|         } | ||||
| 
 | ||||
|         string BuildCheckout() | ||||
|         { | ||||
|             var checkoutBasket = new | ||||
|             { | ||||
|                 City = "city", | ||||
|                 Street = "street", | ||||
|                 State = "state", | ||||
|                 Country = "coutry", | ||||
|                 ZipCode = "zipcode", | ||||
|                 CardNumber = "1234567890123456", | ||||
|                 CardHolderName = "CardHolderName", | ||||
|                 CardExpiration = DateTime.UtcNow.AddDays(1), | ||||
|                 CardSecurityNumber = "123", | ||||
|                 CardTypeId = 1, | ||||
|                 Buyer = "Buyer", | ||||
|                 RequestId = Guid.NewGuid() | ||||
|             }; | ||||
| 
 | ||||
|             return JsonSerializer.Serialize(checkoutBasket); | ||||
|             response.EnsureSuccessStatusCode(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|     public async Task Get_basket_and_response_ok_status_code() | ||||
|     { | ||||
|         using (var server = CreateServer()) | ||||
|         { | ||||
|             var response = await server.CreateClient() | ||||
|                 .GetAsync(Get.GetBasket(1)); | ||||
| 
 | ||||
|             response.EnsureSuccessStatusCode(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|     public async Task Send_Checkout_basket_and_response_ok_status_code() | ||||
|     { | ||||
|         using (var server = CreateServer()) | ||||
|         { | ||||
|             var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); | ||||
| 
 | ||||
|             await server.CreateClient() | ||||
|                 .PostAsync(Post.Basket, contentBasket); | ||||
| 
 | ||||
|             var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json"); | ||||
| 
 | ||||
|             var response = await server.CreateIdempotentClient() | ||||
|                 .PostAsync(Post.CheckoutOrder, contentCheckout); | ||||
| 
 | ||||
|             response.EnsureSuccessStatusCode(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     string BuildBasket() | ||||
|     { | ||||
|         var order = new CustomerBasket(AutoAuthorizeMiddleware.IDENTITY_ID); | ||||
| 
 | ||||
|         order.Items.Add(new BasketItem | ||||
|         { | ||||
|             ProductId = 1, | ||||
|             ProductName = ".NET Bot Black Hoodie", | ||||
|             UnitPrice = 10, | ||||
|             Quantity = 1 | ||||
|         }); | ||||
| 
 | ||||
|         return JsonSerializer.Serialize(order); | ||||
|     } | ||||
| 
 | ||||
|     string BuildCheckout() | ||||
|     { | ||||
|         var checkoutBasket = new | ||||
|         { | ||||
|             City = "city", | ||||
|             Street = "street", | ||||
|             State = "state", | ||||
|             Country = "coutry", | ||||
|             ZipCode = "zipcode", | ||||
|             CardNumber = "1234567890123456", | ||||
|             CardHolderName = "CardHolderName", | ||||
|             CardExpiration = DateTime.UtcNow.AddDays(1), | ||||
|             CardSecurityNumber = "123", | ||||
|             CardTypeId = 1, | ||||
|             Buyer = "Buyer", | ||||
|             RequestId = Guid.NewGuid() | ||||
|         }; | ||||
| 
 | ||||
|         return JsonSerializer.Serialize(checkoutBasket); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										23
									
								
								src/Services/Basket/Basket.FunctionalTests/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/Services/Basket/Basket.FunctionalTests/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| global using Basket.FunctionalTests.Base; | ||||
| global using Microsoft.AspNetCore.Builder; | ||||
| global using Microsoft.AspNetCore.Hosting; | ||||
| global using Microsoft.AspNetCore.Http; | ||||
| global using Microsoft.AspNetCore.Routing; | ||||
| global using Microsoft.AspNetCore.TestHost; | ||||
| global using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; | ||||
| global using Microsoft.eShopOnContainers.Services.Basket.API.Model; | ||||
| global using Microsoft.eShopOnContainers.Services.Basket.API; | ||||
| global using Microsoft.Extensions.Configuration; | ||||
| global using Microsoft.Extensions.DependencyInjection; | ||||
| global using Microsoft.Extensions.Logging; | ||||
| global using StackExchange.Redis; | ||||
| global using System.Collections.Generic; | ||||
| global using System.IO; | ||||
| global using System.Net.Http; | ||||
| global using System.Reflection; | ||||
| global using System.Security.Claims; | ||||
| global using System.Text.Json; | ||||
| global using System.Text; | ||||
| global using System.Threading.Tasks; | ||||
| global using System; | ||||
| global using Xunit; | ||||
| @ -1,12 +1,4 @@ | ||||
| using Basket.FunctionalTests.Base; | ||||
| using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; | ||||
| using Microsoft.eShopOnContainers.Services.Basket.API.Model; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using StackExchange.Redis; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using Xunit; | ||||
|  | ||||
| 
 | ||||
| namespace Basket.FunctionalTests | ||||
| { | ||||
|  | ||||
| @ -1,155 +1,140 @@ | ||||
| using Basket.API.IntegrationEvents.Events; | ||||
| using Basket.API.Model; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | ||||
| using Microsoft.eShopOnContainers.Services.Basket.API.Controllers; | ||||
| namespace UnitTest.Basket.Application; | ||||
| 
 | ||||
| using Microsoft.eShopOnContainers.Services.Basket.API.Model; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Moq; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Security.Claims; | ||||
| using System.Threading.Tasks; | ||||
| using Xunit; | ||||
| using IBasketIdentityService = Microsoft.eShopOnContainers.Services.Basket.API.Services.IIdentityService; | ||||
| 
 | ||||
| namespace UnitTest.Basket.Application | ||||
| public class BasketWebApiTest | ||||
| { | ||||
|     public class BasketWebApiTest | ||||
|     private readonly Mock<IBasketRepository> _basketRepositoryMock; | ||||
|     private readonly Mock<IBasketIdentityService> _identityServiceMock; | ||||
|     private readonly Mock<IEventBus> _serviceBusMock; | ||||
|     private readonly Mock<ILogger<BasketController>> _loggerMock; | ||||
| 
 | ||||
|     public BasketWebApiTest() | ||||
|     { | ||||
|         private readonly Mock<IBasketRepository> _basketRepositoryMock; | ||||
|         private readonly Mock<IBasketIdentityService> _identityServiceMock; | ||||
|         private readonly Mock<IEventBus> _serviceBusMock; | ||||
|         private readonly Mock<ILogger<BasketController>> _loggerMock; | ||||
|         _basketRepositoryMock = new Mock<IBasketRepository>(); | ||||
|         _identityServiceMock = new Mock<IBasketIdentityService>(); | ||||
|         _serviceBusMock = new Mock<IEventBus>(); | ||||
|         _loggerMock = new Mock<ILogger<BasketController>>(); | ||||
|     } | ||||
| 
 | ||||
|         public BasketWebApiTest() | ||||
|         { | ||||
|             _basketRepositoryMock = new Mock<IBasketRepository>(); | ||||
|             _identityServiceMock = new Mock<IBasketIdentityService>(); | ||||
|             _serviceBusMock = new Mock<IEventBus>(); | ||||
|             _loggerMock = new Mock<ILogger<BasketController>>(); | ||||
|         } | ||||
|     [Fact] | ||||
|     public async Task Get_customer_basket_success() | ||||
|     { | ||||
|         //Arrange | ||||
|         var fakeCustomerId = "1"; | ||||
|         var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Get_customer_basket_success() | ||||
|         { | ||||
|             //Arrange | ||||
|             var fakeCustomerId = "1"; | ||||
|             var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); | ||||
|         _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny<string>())) | ||||
|             .Returns(Task.FromResult(fakeCustomerBasket)); | ||||
|         _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); | ||||
| 
 | ||||
|             _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny<string>())) | ||||
|         _serviceBusMock.Setup(x => x.Publish(It.IsAny<UserCheckoutAcceptedIntegrationEvent>())); | ||||
| 
 | ||||
|         //Act | ||||
|         var basketController = new BasketController( | ||||
|             _loggerMock.Object, | ||||
|             _basketRepositoryMock.Object, | ||||
|             _identityServiceMock.Object, | ||||
|             _serviceBusMock.Object); | ||||
| 
 | ||||
|         var actionResult = await basketController.GetBasketByIdAsync(fakeCustomerId); | ||||
| 
 | ||||
|         //Assert | ||||
|         Assert.Equal((actionResult.Result as OkObjectResult).StatusCode, (int)System.Net.HttpStatusCode.OK); | ||||
|         Assert.Equal((((ObjectResult)actionResult.Result).Value as CustomerBasket).BuyerId, fakeCustomerId); | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|     public async Task Post_customer_basket_success() | ||||
|     { | ||||
|         //Arrange | ||||
|         var fakeCustomerId = "1"; | ||||
|         var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); | ||||
| 
 | ||||
|         _basketRepositoryMock.Setup(x => x.UpdateBasketAsync(It.IsAny<CustomerBasket>())) | ||||
|             .Returns(Task.FromResult(fakeCustomerBasket)); | ||||
|         _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); | ||||
|         _serviceBusMock.Setup(x => x.Publish(It.IsAny<UserCheckoutAcceptedIntegrationEvent>())); | ||||
| 
 | ||||
|         //Act | ||||
|         var basketController = new BasketController( | ||||
|             _loggerMock.Object, | ||||
|             _basketRepositoryMock.Object, | ||||
|             _identityServiceMock.Object, | ||||
|             _serviceBusMock.Object); | ||||
| 
 | ||||
|         var actionResult = await basketController.UpdateBasketAsync(fakeCustomerBasket); | ||||
| 
 | ||||
|         //Assert | ||||
|         Assert.Equal((actionResult.Result as OkObjectResult).StatusCode, (int)System.Net.HttpStatusCode.OK); | ||||
|         Assert.Equal((((ObjectResult)actionResult.Result).Value as CustomerBasket).BuyerId, fakeCustomerId); | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|     public async Task Doing_Checkout_Without_Basket_Should_Return_Bad_Request() | ||||
|     { | ||||
|         var fakeCustomerId = "2"; | ||||
|         _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny<string>())) | ||||
|             .Returns(Task.FromResult((CustomerBasket)null)); | ||||
|         _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); | ||||
| 
 | ||||
|         //Act | ||||
|         var basketController = new BasketController( | ||||
|             _loggerMock.Object, | ||||
|             _basketRepositoryMock.Object, | ||||
|             _identityServiceMock.Object, | ||||
|             _serviceBusMock.Object); | ||||
| 
 | ||||
|         var result = await basketController.CheckoutAsync(new BasketCheckout(), Guid.NewGuid().ToString()) as BadRequestResult; | ||||
|         Assert.NotNull(result); | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|     public async Task Doing_Checkout_Wit_Basket_Should_Publish_UserCheckoutAccepted_Integration_Event() | ||||
|     { | ||||
|         var fakeCustomerId = "1"; | ||||
|         var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); | ||||
| 
 | ||||
|         _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny<string>())) | ||||
|                 .Returns(Task.FromResult(fakeCustomerBasket)); | ||||
|             _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); | ||||
| 
 | ||||
|             _serviceBusMock.Setup(x => x.Publish(It.IsAny<UserCheckoutAcceptedIntegrationEvent>())); | ||||
|         _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); | ||||
| 
 | ||||
|             //Act | ||||
|             var basketController = new BasketController( | ||||
|                 _loggerMock.Object, | ||||
|                 _basketRepositoryMock.Object, | ||||
|                 _identityServiceMock.Object, | ||||
|                 _serviceBusMock.Object); | ||||
|         var basketController = new BasketController( | ||||
|             _loggerMock.Object, | ||||
|             _basketRepositoryMock.Object, | ||||
|             _identityServiceMock.Object, | ||||
|             _serviceBusMock.Object); | ||||
| 
 | ||||
|             var actionResult = await basketController.GetBasketByIdAsync(fakeCustomerId); | ||||
| 
 | ||||
|             //Assert | ||||
|             Assert.Equal((actionResult.Result as OkObjectResult).StatusCode, (int)System.Net.HttpStatusCode.OK); | ||||
|             Assert.Equal((((ObjectResult)actionResult.Result).Value as CustomerBasket).BuyerId, fakeCustomerId); | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Post_customer_basket_success() | ||||
|         basketController.ControllerContext = new ControllerContext() | ||||
|         { | ||||
|             //Arrange | ||||
|             var fakeCustomerId = "1"; | ||||
|             var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); | ||||
| 
 | ||||
|             _basketRepositoryMock.Setup(x => x.UpdateBasketAsync(It.IsAny<CustomerBasket>())) | ||||
|                 .Returns(Task.FromResult(fakeCustomerBasket)); | ||||
|             _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); | ||||
|             _serviceBusMock.Setup(x => x.Publish(It.IsAny<UserCheckoutAcceptedIntegrationEvent>())); | ||||
| 
 | ||||
|             //Act | ||||
|             var basketController = new BasketController( | ||||
|                 _loggerMock.Object, | ||||
|                 _basketRepositoryMock.Object, | ||||
|                 _identityServiceMock.Object, | ||||
|                 _serviceBusMock.Object); | ||||
| 
 | ||||
|             var actionResult = await basketController.UpdateBasketAsync(fakeCustomerBasket); | ||||
| 
 | ||||
|             //Assert | ||||
|             Assert.Equal((actionResult.Result as OkObjectResult).StatusCode, (int)System.Net.HttpStatusCode.OK); | ||||
|             Assert.Equal((((ObjectResult)actionResult.Result).Value as CustomerBasket).BuyerId, fakeCustomerId); | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Doing_Checkout_Without_Basket_Should_Return_Bad_Request() | ||||
|         { | ||||
|             var fakeCustomerId = "2"; | ||||
|             _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny<string>())) | ||||
|                 .Returns(Task.FromResult((CustomerBasket)null)); | ||||
|             _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); | ||||
| 
 | ||||
|             //Act | ||||
|             var basketController = new BasketController( | ||||
|                 _loggerMock.Object, | ||||
|                 _basketRepositoryMock.Object, | ||||
|                 _identityServiceMock.Object, | ||||
|                 _serviceBusMock.Object); | ||||
| 
 | ||||
|             var result = await basketController.CheckoutAsync(new BasketCheckout(), Guid.NewGuid().ToString()) as BadRequestResult; | ||||
|             Assert.NotNull(result); | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Doing_Checkout_Wit_Basket_Should_Publish_UserCheckoutAccepted_Integration_Event() | ||||
|         { | ||||
|             var fakeCustomerId = "1"; | ||||
|             var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); | ||||
| 
 | ||||
|             _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny<string>())) | ||||
|                  .Returns(Task.FromResult(fakeCustomerBasket)); | ||||
| 
 | ||||
|             _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); | ||||
| 
 | ||||
|             var basketController = new BasketController( | ||||
|                 _loggerMock.Object, | ||||
|                 _basketRepositoryMock.Object, | ||||
|                 _identityServiceMock.Object, | ||||
|                 _serviceBusMock.Object); | ||||
| 
 | ||||
|             basketController.ControllerContext = new ControllerContext() | ||||
|             HttpContext = new DefaultHttpContext() | ||||
|             { | ||||
|                 HttpContext = new DefaultHttpContext() | ||||
|                 { | ||||
|                     User = new ClaimsPrincipal( | ||||
|                         new ClaimsIdentity(new Claim[] { | ||||
|                             new Claim("sub", "testuser"), | ||||
|                             new Claim("unique_name", "testuser"), | ||||
|                             new Claim(ClaimTypes.Name, "testuser") | ||||
|                              })) | ||||
|                 } | ||||
|             }; | ||||
|                 User = new ClaimsPrincipal( | ||||
|                     new ClaimsIdentity(new Claim[] { | ||||
|                         new Claim("sub", "testuser"), | ||||
|                         new Claim("unique_name", "testuser"), | ||||
|                         new Claim(ClaimTypes.Name, "testuser") | ||||
|                             })) | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|             //Act | ||||
|             var result = await basketController.CheckoutAsync(new BasketCheckout(), Guid.NewGuid().ToString()) as AcceptedResult; | ||||
|         //Act | ||||
|         var result = await basketController.CheckoutAsync(new BasketCheckout(), Guid.NewGuid().ToString()) as AcceptedResult; | ||||
| 
 | ||||
|             _serviceBusMock.Verify(mock => mock.Publish(It.IsAny<UserCheckoutAcceptedIntegrationEvent>()), Times.Once); | ||||
|         _serviceBusMock.Verify(mock => mock.Publish(It.IsAny<UserCheckoutAcceptedIntegrationEvent>()), Times.Once); | ||||
| 
 | ||||
|             Assert.NotNull(result); | ||||
|         } | ||||
|         Assert.NotNull(result); | ||||
|     } | ||||
| 
 | ||||
|         private CustomerBasket GetCustomerBasketFake(string fakeCustomerId) | ||||
|     private CustomerBasket GetCustomerBasketFake(string fakeCustomerId) | ||||
|     { | ||||
|         return new CustomerBasket(fakeCustomerId) | ||||
|         { | ||||
|             return new CustomerBasket(fakeCustomerId) | ||||
|             Items = new List<BasketItem>() | ||||
|             { | ||||
|                 Items = new List<BasketItem>() | ||||
|                 { | ||||
|                     new BasketItem() | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|                 new BasketItem() | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,130 +1,117 @@ | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.eShopOnContainers.WebMVC.Controllers; | ||||
| using Microsoft.eShopOnContainers.WebMVC.Services; | ||||
| using Microsoft.eShopOnContainers.WebMVC.ViewModels; | ||||
| using Moq; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using Xunit; | ||||
| using BasketModel = Microsoft.eShopOnContainers.WebMVC.ViewModels.Basket; | ||||
| namespace UnitTest.Basket.Application; | ||||
| 
 | ||||
| namespace UnitTest.Basket.Application | ||||
| public class CartControllerTest | ||||
| { | ||||
|     public class CartControllerTest | ||||
|     private readonly Mock<ICatalogService> _catalogServiceMock; | ||||
|     private readonly Mock<IBasketService> _basketServiceMock; | ||||
|     private readonly Mock<IIdentityParser<ApplicationUser>> _identityParserMock; | ||||
|     private readonly Mock<HttpContext> _contextMock; | ||||
| 
 | ||||
|     public CartControllerTest() | ||||
|     { | ||||
|         private readonly Mock<ICatalogService> _catalogServiceMock; | ||||
|         private readonly Mock<IBasketService> _basketServiceMock; | ||||
|         private readonly Mock<IIdentityParser<ApplicationUser>> _identityParserMock; | ||||
|         private readonly Mock<HttpContext> _contextMock; | ||||
|         _catalogServiceMock = new Mock<ICatalogService>(); | ||||
|         _basketServiceMock = new Mock<IBasketService>(); | ||||
|         _identityParserMock = new Mock<IIdentityParser<ApplicationUser>>(); | ||||
|         _contextMock = new Mock<HttpContext>(); | ||||
|     } | ||||
| 
 | ||||
|         public CartControllerTest() | ||||
|     [Fact] | ||||
|     public async Task Post_cart_success() | ||||
|     { | ||||
|         //Arrange | ||||
|         var fakeBuyerId = "1"; | ||||
|         var action = string.Empty; | ||||
|         var fakeBasket = GetFakeBasket(fakeBuyerId); | ||||
|         var fakeQuantities = new Dictionary<string, int>() | ||||
|         { | ||||
|             _catalogServiceMock = new Mock<ICatalogService>(); | ||||
|             _basketServiceMock = new Mock<IBasketService>(); | ||||
|             _identityParserMock = new Mock<IIdentityParser<ApplicationUser>>(); | ||||
|             _contextMock = new Mock<HttpContext>(); | ||||
|         } | ||||
|             ["fakeProdA"] = 1, | ||||
|             ["fakeProdB"] = 2 | ||||
|         }; | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Post_cart_success() | ||||
|         _basketServiceMock.Setup(x => x.SetQuantities(It.IsAny<ApplicationUser>(), It.IsAny<Dictionary<string, int>>())) | ||||
|             .Returns(Task.FromResult(fakeBasket)); | ||||
| 
 | ||||
|         _basketServiceMock.Setup(x => x.UpdateBasket(It.IsAny<BasketModel>())) | ||||
|             .Returns(Task.FromResult(fakeBasket)); | ||||
| 
 | ||||
|         //Act | ||||
|         var cartController = new CartController(_basketServiceMock.Object, _catalogServiceMock.Object, _identityParserMock.Object); | ||||
|         cartController.ControllerContext.HttpContext = _contextMock.Object; | ||||
|         var actionResult = await cartController.Index(fakeQuantities, action); | ||||
| 
 | ||||
|         //Assert | ||||
|         var viewResult = Assert.IsType<ViewResult>(actionResult); | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|     public async Task Post_cart_checkout_success() | ||||
|     { | ||||
|         //Arrange | ||||
|         var fakeBuyerId = "1"; | ||||
|         var action = "[ Checkout ]"; | ||||
|         var fakeBasket = GetFakeBasket(fakeBuyerId); | ||||
|         var fakeQuantities = new Dictionary<string, int>() | ||||
|         { | ||||
|             //Arrange | ||||
|             var fakeBuyerId = "1"; | ||||
|             var action = string.Empty; | ||||
|             var fakeBasket = GetFakeBasket(fakeBuyerId); | ||||
|             var fakeQuantities = new Dictionary<string, int>() | ||||
|             { | ||||
|                 ["fakeProdA"] = 1, | ||||
|                 ["fakeProdB"] = 2 | ||||
|             }; | ||||
|             ["fakeProdA"] = 1, | ||||
|             ["fakeProdB"] = 2 | ||||
|         }; | ||||
| 
 | ||||
|             _basketServiceMock.Setup(x => x.SetQuantities(It.IsAny<ApplicationUser>(), It.IsAny<Dictionary<string, int>>())) | ||||
|                 .Returns(Task.FromResult(fakeBasket)); | ||||
|         _basketServiceMock.Setup(x => x.SetQuantities(It.IsAny<ApplicationUser>(), It.IsAny<Dictionary<string, int>>())) | ||||
|             .Returns(Task.FromResult(fakeBasket)); | ||||
| 
 | ||||
|             _basketServiceMock.Setup(x => x.UpdateBasket(It.IsAny<BasketModel>())) | ||||
|                 .Returns(Task.FromResult(fakeBasket)); | ||||
|         _basketServiceMock.Setup(x => x.UpdateBasket(It.IsAny<BasketModel>())) | ||||
|             .Returns(Task.FromResult(fakeBasket)); | ||||
| 
 | ||||
|             //Act | ||||
|             var cartController = new CartController(_basketServiceMock.Object, _catalogServiceMock.Object, _identityParserMock.Object); | ||||
|             cartController.ControllerContext.HttpContext = _contextMock.Object; | ||||
|             var actionResult = await cartController.Index(fakeQuantities, action); | ||||
|         //Act | ||||
|         var orderController = new CartController(_basketServiceMock.Object, _catalogServiceMock.Object, _identityParserMock.Object); | ||||
|         orderController.ControllerContext.HttpContext = _contextMock.Object; | ||||
|         var actionResult = await orderController.Index(fakeQuantities, action); | ||||
| 
 | ||||
|             //Assert | ||||
|             var viewResult = Assert.IsType<ViewResult>(actionResult); | ||||
|         } | ||||
|         //Assert | ||||
|         var redirectToActionResult = Assert.IsType<RedirectToActionResult>(actionResult); | ||||
|         Assert.Equal("Order", redirectToActionResult.ControllerName); | ||||
|         Assert.Equal("Create", redirectToActionResult.ActionName); | ||||
|     } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Post_cart_checkout_success() | ||||
|     [Fact] | ||||
|     public async Task Add_to_cart_success() | ||||
|     { | ||||
|         //Arrange | ||||
|         var fakeCatalogItem = GetFakeCatalogItem(); | ||||
| 
 | ||||
|         _basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny<ApplicationUser>(), It.IsAny<Int32>())) | ||||
|             .Returns(Task.FromResult(1)); | ||||
| 
 | ||||
|         //Act | ||||
|         var orderController = new CartController(_basketServiceMock.Object, _catalogServiceMock.Object, _identityParserMock.Object); | ||||
|         orderController.ControllerContext.HttpContext = _contextMock.Object; | ||||
|         var actionResult = await orderController.AddToCart(fakeCatalogItem); | ||||
| 
 | ||||
|         //Assert | ||||
|         var redirectToActionResult = Assert.IsType<RedirectToActionResult>(actionResult); | ||||
|         Assert.Equal("Catalog", redirectToActionResult.ControllerName); | ||||
|         Assert.Equal("Index", redirectToActionResult.ActionName); | ||||
|     } | ||||
| 
 | ||||
|     private BasketModel GetFakeBasket(string buyerId) | ||||
|     { | ||||
|         return new BasketModel() | ||||
|         { | ||||
|             //Arrange | ||||
|             var fakeBuyerId = "1"; | ||||
|             var action = "[ Checkout ]"; | ||||
|             var fakeBasket = GetFakeBasket(fakeBuyerId); | ||||
|             var fakeQuantities = new Dictionary<string, int>() | ||||
|             { | ||||
|                 ["fakeProdA"] = 1, | ||||
|                 ["fakeProdB"] = 2 | ||||
|             }; | ||||
|             BuyerId = buyerId | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|             _basketServiceMock.Setup(x => x.SetQuantities(It.IsAny<ApplicationUser>(), It.IsAny<Dictionary<string, int>>())) | ||||
|                 .Returns(Task.FromResult(fakeBasket)); | ||||
| 
 | ||||
|             _basketServiceMock.Setup(x => x.UpdateBasket(It.IsAny<BasketModel>())) | ||||
|                 .Returns(Task.FromResult(fakeBasket)); | ||||
| 
 | ||||
|             //Act | ||||
|             var orderController = new CartController(_basketServiceMock.Object, _catalogServiceMock.Object, _identityParserMock.Object); | ||||
|             orderController.ControllerContext.HttpContext = _contextMock.Object; | ||||
|             var actionResult = await orderController.Index(fakeQuantities, action); | ||||
| 
 | ||||
|             //Assert | ||||
|             var redirectToActionResult = Assert.IsType<RedirectToActionResult>(actionResult); | ||||
|             Assert.Equal("Order", redirectToActionResult.ControllerName); | ||||
|             Assert.Equal("Create", redirectToActionResult.ActionName); | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Add_to_cart_success() | ||||
|     private CatalogItem GetFakeCatalogItem() | ||||
|     { | ||||
|         return new CatalogItem() | ||||
|         { | ||||
|             //Arrange | ||||
|             var fakeCatalogItem = GetFakeCatalogItem(); | ||||
| 
 | ||||
|             _basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny<ApplicationUser>(), It.IsAny<Int32>())) | ||||
|                 .Returns(Task.FromResult(1)); | ||||
| 
 | ||||
|             //Act | ||||
|             var orderController = new CartController(_basketServiceMock.Object, _catalogServiceMock.Object, _identityParserMock.Object); | ||||
|             orderController.ControllerContext.HttpContext = _contextMock.Object; | ||||
|             var actionResult = await orderController.AddToCart(fakeCatalogItem); | ||||
| 
 | ||||
|             //Assert | ||||
|             var redirectToActionResult = Assert.IsType<RedirectToActionResult>(actionResult); | ||||
|             Assert.Equal("Catalog", redirectToActionResult.ControllerName); | ||||
|             Assert.Equal("Index", redirectToActionResult.ActionName); | ||||
|         } | ||||
| 
 | ||||
|         private BasketModel GetFakeBasket(string buyerId) | ||||
|         { | ||||
|             return new BasketModel() | ||||
|             { | ||||
|                 BuyerId = buyerId | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         private CatalogItem GetFakeCatalogItem() | ||||
|         { | ||||
|             return new CatalogItem() | ||||
|             { | ||||
|                 Id = 1, | ||||
|                 Name = "fakeName", | ||||
|                 CatalogBrand = "fakeBrand", | ||||
|                 CatalogType = "fakeType", | ||||
|                 CatalogBrandId = 2, | ||||
|                 CatalogTypeId = 5, | ||||
|                 Price = 20 | ||||
|             }; | ||||
|         } | ||||
|             Id = 1, | ||||
|             Name = "fakeName", | ||||
|             CatalogBrand = "fakeBrand", | ||||
|             CatalogType = "fakeType", | ||||
|             CatalogBrandId = 2, | ||||
|             CatalogTypeId = 5, | ||||
|             Price = 20 | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,8 +8,8 @@ | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="MediatR" Version="9.0.0" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.2" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="5.0.2" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-preview.7.21378.6" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.0-preview.7.21378.6" /> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> | ||||
|     <PackageReference Include="Microsoft.NETCore.Platforms" Version="5.0.0" /> | ||||
|     <PackageReference Include="Moq" Version="4.15.2" /> | ||||
|  | ||||
							
								
								
									
										19
									
								
								src/Services/Basket/Basket.UnitTests/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/Services/Basket/Basket.UnitTests/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| global using Basket.API.IntegrationEvents.Events; | ||||
| global using Basket.API.Model; | ||||
| global using Microsoft.AspNetCore.Http; | ||||
| global using Microsoft.AspNetCore.Mvc; | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | ||||
| global using Microsoft.eShopOnContainers.Services.Basket.API.Controllers; | ||||
| global using Microsoft.eShopOnContainers.Services.Basket.API.Model; | ||||
| global using Microsoft.Extensions.Logging; | ||||
| global using Moq; | ||||
| global using System; | ||||
| global using System.Collections.Generic; | ||||
| global using System.Security.Claims; | ||||
| global using System.Threading.Tasks; | ||||
| global using Xunit; | ||||
| global using IBasketIdentityService = Microsoft.eShopOnContainers.Services.Basket.API.Services.IIdentityService; | ||||
| global using Microsoft.eShopOnContainers.WebMVC.Controllers; | ||||
| global using Microsoft.eShopOnContainers.WebMVC.Services; | ||||
| global using Microsoft.eShopOnContainers.WebMVC.ViewModels; | ||||
| global using BasketModel = Microsoft.eShopOnContainers.WebMVC.ViewModels.Basket; | ||||
| @ -42,30 +42,37 @@ | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" Version="5.0.1" /> | ||||
|     <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.12.2" /> | ||||
| 	  <PackageReference Include="Azure.Identity" Version="1.5.0-beta.3" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" Version="5.1.1" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.AzureStorage" Version="5.0.1" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="5.0.1" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="5.0.1" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="5.0.3" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="5.0.1" /> | ||||
|     <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.1.0" /> | ||||
|     <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.1" /> | ||||
|     <PackageReference Include="Azure.Identity" Version="1.4.0" /> | ||||
|     <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.2.0-preview.1" /> | ||||
|     <PackageReference Include="Google.Protobuf" Version="3.14.0" /> | ||||
|     <PackageReference Include="Grpc.AspNetCore.Server" Version="2.34.0" /> | ||||
|     <PackageReference Include="Grpc.Tools" Version="2.34.0" PrivateAssets="All" />     | ||||
|     <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.16.0" /> | ||||
|     <PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.16.0" /> | ||||
|     <PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="1.1.3" /> | ||||
|     <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.18.0" /> | ||||
|     <PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.18.0" /> | ||||
|     <PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="2.0.2-beta2" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.HealthChecks" Version="1.0.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="5.0.2" /> | ||||
|     <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> | ||||
|     <PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" /> | ||||
|     <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.1-dev-00216" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" Version="4.0.0-dev-00834" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Http" Version="7.2.0" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.0-preview.7.21378.6" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="3.1.18" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="6.0.0-preview.7.21378.6" /> | ||||
|     <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.11.1" /> | ||||
|     <PackageReference Include="Serilog.AspNetCore" Version="4.1.1-dev-00229" /> | ||||
|     <PackageReference Include="Serilog.Enrichers.Environment" Version="2.2.1-dev-00787" /> | ||||
|     <PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0-dev-00291" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1-dev-00876" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Http" Version="8.0.0-beta.9" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Seq" Version="4.1.0-dev-00166" /> | ||||
|     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> | ||||
|     <PackageReference Include="System.Data.SqlClient" Version="4.8.2" /> | ||||
|     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.1" /> | ||||
|     <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.2.1" /> | ||||
|     <PackageReference Include="System.Data.SqlClient" version="4.8.2"/> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="6.0.0-preview.7.21378.4" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0-preview.7.21378.4" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0-preview.7.21378.4"/> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user