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 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 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;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers;
|
||||||
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
|
[Route("api/v1/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
public class BasketController : ControllerBase
|
||||||
{
|
{
|
||||||
[Route("api/v1/[controller]")]
|
private readonly ICatalogService _catalog;
|
||||||
[Authorize]
|
private readonly IBasketService _basket;
|
||||||
[ApiController]
|
|
||||||
public class BasketController : ControllerBase
|
public BasketController(ICatalogService catalogService, IBasketService basketService)
|
||||||
{
|
{
|
||||||
private readonly ICatalogService _catalog;
|
_catalog = catalogService;
|
||||||
private readonly IBasketService _basket;
|
_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;
|
return BadRequest("Need to pass at least one basket line");
|
||||||
_basket = basketService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
// Retrieve the current basket
|
||||||
[HttpPut]
|
var basket = await _basket.GetById(data.BuyerId) ?? new BasketData(data.BuyerId);
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
|
||||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
|
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId));
|
||||||
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
|
// 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 itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId);
|
||||||
var basket = await _basket.GetById(data.BuyerId) ?? new BasketData(data.BuyerId);
|
if (itemInBasket == null)
|
||||||
|
|
||||||
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 catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId);
|
basket.Items.Add(new BasketDataItem()
|
||||||
if (catalogItem == null)
|
|
||||||
{
|
{
|
||||||
return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})");
|
Id = bitem.Id,
|
||||||
}
|
ProductId = catalogItem.Id,
|
||||||
|
ProductName = catalogItem.Name,
|
||||||
var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId);
|
PictureUrl = catalogItem.PictureUri,
|
||||||
if (itemInBasket == null)
|
UnitPrice = catalogItem.Price,
|
||||||
{
|
Quantity = bitem.Quantity
|
||||||
basket.Items.Add(new BasketDataItem()
|
});
|
||||||
{
|
}
|
||||||
Id = bitem.Id,
|
else
|
||||||
ProductId = catalogItem.Id,
|
{
|
||||||
ProductName = catalogItem.Name,
|
itemInBasket.Quantity = bitem.Quantity;
|
||||||
PictureUrl = catalogItem.PictureUri,
|
|
||||||
UnitPrice = catalogItem.Price,
|
|
||||||
Quantity = bitem.Quantity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
itemInBasket.Quantity = bitem.Quantity;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _basket.UpdateAsync(basket);
|
|
||||||
|
|
||||||
return basket;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut]
|
await _basket.UpdateAsync(basket);
|
||||||
[Route("items")]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
return basket;
|
||||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
|
}
|
||||||
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
|
|
||||||
|
[HttpPut]
|
||||||
|
[Route("items")]
|
||||||
|
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
|
||||||
|
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
|
||||||
|
{
|
||||||
|
if (!data.Updates.Any())
|
||||||
{
|
{
|
||||||
if (!data.Updates.Any())
|
return BadRequest("No updates sent");
|
||||||
{
|
|
||||||
return BadRequest("No updates sent");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the current basket
|
|
||||||
var currentBasket = await _basket.GetById(data.BasketId);
|
|
||||||
if (currentBasket == null)
|
|
||||||
{
|
|
||||||
return BadRequest($"Basket with id {data.BasketId} not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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]
|
// Retrieve the current basket
|
||||||
[Route("items")]
|
var currentBasket = await _basket.GetById(data.BasketId);
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
if (currentBasket == null)
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
|
||||||
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data)
|
|
||||||
{
|
{
|
||||||
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
|
basketItem.Quantity = update.NewQty;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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("")]
|
[HttpGet]
|
||||||
public class HomeController : Controller
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
[HttpGet()]
|
return new RedirectResult("~/swagger");
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
return new RedirectResult("~/swagger");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,37 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers;
|
||||||
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
|
[Route("api/v1/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
public class OrderController : ControllerBase
|
||||||
{
|
{
|
||||||
[Route("api/v1/[controller]")]
|
private readonly IBasketService _basketService;
|
||||||
[Authorize]
|
private readonly IOrderingService _orderingService;
|
||||||
[ApiController]
|
|
||||||
public class OrderController : ControllerBase
|
public OrderController(IBasketService basketService, IOrderingService orderingService)
|
||||||
{
|
{
|
||||||
private readonly IBasketService _basketService;
|
_basketService = basketService;
|
||||||
private readonly IOrderingService _orderingService;
|
_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;
|
return BadRequest("Need a valid basketid");
|
||||||
_orderingService = orderingService;
|
}
|
||||||
|
// 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}")]
|
return await _orderingService.GetOrderDraftAsync(basket);
|
||||||
[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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters
|
|
||||||
{
|
{
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.Filters
|
namespace Basket.API.Infrastructure.Filters
|
||||||
{
|
{
|
||||||
public class AuthorizeCheckOperationFilter : IOperationFilter
|
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;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure;
|
||||||
using Grpc.Core.Interceptors;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
catch (RpcException e)
|
||||||
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
|
|
||||||
TRequest request,
|
|
||||||
ClientInterceptorContext<TRequest, TResponse> context,
|
|
||||||
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
|
|
||||||
{
|
{
|
||||||
var call = continuation(request, context);
|
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message);
|
||||||
|
return default;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,54 +1,44 @@
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure
|
public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler
|
||||||
{
|
{
|
||||||
public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger;
|
||||||
|
|
||||||
|
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<HttpClientAuthorizationDelegatingHandler> logger)
|
||||||
{
|
{
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger;
|
_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;
|
request.Headers.Add("Authorization", new List<string>() { authorizationHeader });
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<string> GetToken()
|
return await base.SendAsync(request, cancellationToken);
|
||||||
{
|
}
|
||||||
const string ACCESS_TOKEN = "access_token";
|
|
||||||
|
|
||||||
return await _httpContextAccessor.HttpContext
|
async Task<string> GetToken()
|
||||||
.GetTokenAsync(ACCESS_TOKEN);
|
{
|
||||||
}
|
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; }
|
Quantity = 1;
|
||||||
|
|
||||||
public string BasketId { get; set; }
|
|
||||||
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
|
|
||||||
public AddBasketItemRequest()
|
|
||||||
{
|
|
||||||
Quantity = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
using System.Collections.Generic;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
|
public class BasketData
|
||||||
{
|
{
|
||||||
|
public string BuyerId { get; set; }
|
||||||
|
|
||||||
public class BasketData
|
public 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 int ProductId { get; set; }
|
||||||
{
|
|
||||||
public string Id { 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 int Quantity { get; set; }
|
|
||||||
|
|
||||||
public string PictureUrl { 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;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
|
public class OrderData
|
||||||
{
|
{
|
||||||
|
public string OrderNumber { get; set; }
|
||||||
|
|
||||||
public class OrderData
|
public DateTime Date { get; set; }
|
||||||
{
|
|
||||||
public string OrderNumber { 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 string Buyer { get; set; }
|
|
||||||
|
|
||||||
public List<OrderItemData> OrderItems { get; } = new List<OrderItemData>();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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 string ProductName { get; set; }
|
||||||
{
|
|
||||||
public int ProductId { 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 int Units { get; set; }
|
|
||||||
|
|
||||||
public string PictureUrl { 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; }
|
NewQty = 0;
|
||||||
|
|
||||||
public int NewQty { get; set; }
|
|
||||||
|
|
||||||
public UpdateBasketItemData()
|
|
||||||
{
|
|
||||||
NewQty = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
using System.Collections.Generic;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
|
public class UpdateBasketItemsRequest
|
||||||
{
|
{
|
||||||
|
|
||||||
public class UpdateBasketItemsRequest
|
public string BasketId { get; set; }
|
||||||
|
|
||||||
|
public ICollection<UpdateBasketItemData> Updates { get; set; }
|
||||||
|
|
||||||
|
public UpdateBasketItemsRequest()
|
||||||
{
|
{
|
||||||
|
Updates = new List<UpdateBasketItemData>();
|
||||||
public string BasketId { get; set; }
|
|
||||||
|
|
||||||
public ICollection<UpdateBasketItemData> Updates { get; set; }
|
|
||||||
|
|
||||||
public UpdateBasketItemsRequest()
|
|
||||||
{
|
|
||||||
Updates = new List<UpdateBasketItemData>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
|
public class UpdateBasketRequest
|
||||||
{
|
{
|
||||||
|
public string BuyerId { get; set; }
|
||||||
|
|
||||||
public class UpdateBasketRequest
|
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; }
|
||||||
{
|
}
|
||||||
public string BuyerId { 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 int ProductId { get; set; } // Catalog item id
|
||||||
{
|
|
||||||
public string Id { get; set; } // Basket id
|
|
||||||
|
|
||||||
public int ProductId { get; set; } // Catalog item id
|
|
||||||
|
|
||||||
public int Quantity { get; set; } // Quantity
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public int Quantity { get; set; } // Quantity
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
using Microsoft.AspNetCore;
|
await BuildWebHost(args).RunAsync();
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
|
|
||||||
BuildWebHost(args).Run();
|
|
||||||
IWebHost BuildWebHost(string[] args) =>
|
IWebHost BuildWebHost(string[] args) =>
|
||||||
WebHost
|
WebHost
|
||||||
.CreateDefaultBuilder(args)
|
.CreateDefaultBuilder(args)
|
||||||
|
@ -1,90 +1,83 @@
|
|||||||
using GrpcBasket;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
|
||||||
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
|
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;
|
_basketClient = basketClient;
|
||||||
private readonly ILogger<BasketService> _logger;
|
_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;
|
return null;
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BasketData> GetById(string id)
|
var map = new BasketData
|
||||||
{
|
{
|
||||||
_logger.LogDebug("grpc client created, request = {@id}", id);
|
BuyerId = customerBasketRequest.Buyerid
|
||||||
var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id });
|
};
|
||||||
_logger.LogDebug("grpc response {@response}", response);
|
|
||||||
|
|
||||||
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);
|
Buyerid = basketData.BuyerId
|
||||||
var request = MapToCustomerBasketRequest(currentBasket);
|
};
|
||||||
_logger.LogDebug("Grpc update basket request {@request}", request);
|
|
||||||
|
|
||||||
await _basketClient.UpdateBasketAsync(request);
|
basketData.Items.ToList().ForEach(item => map.Items.Add(new BasketItemResponse
|
||||||
}
|
|
||||||
|
|
||||||
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest)
|
|
||||||
{
|
{
|
||||||
if (customerBasketRequest == null)
|
Id = item.Id,
|
||||||
{
|
Oldunitprice = (double)item.OldUnitPrice,
|
||||||
return null;
|
Pictureurl = item.PictureUrl,
|
||||||
}
|
Productid = item.ProductId,
|
||||||
|
Productname = item.ProductName,
|
||||||
|
Quantity = item.Quantity,
|
||||||
|
Unitprice = (double)item.UnitPrice
|
||||||
|
}));
|
||||||
|
|
||||||
var map = new BasketData
|
return map;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,36 @@
|
|||||||
using CatalogApi;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
|
||||||
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
|
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)
|
public async Task<CatalogItem> GetCatalogItemAsync(int id)
|
||||||
{
|
{
|
||||||
_client = client;
|
var request = new CatalogItemRequest { Id = id };
|
||||||
}
|
var response = await _client.GetItemByIdAsync(request);
|
||||||
|
return MapToCatalogItemResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<CatalogItem> GetCatalogItemAsync(int id)
|
public async Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids)
|
||||||
{
|
{
|
||||||
var request = new CatalogItemRequest { Id = id };
|
var request = new CatalogItemsRequest { Ids = string.Join(",", ids), PageIndex = 1, PageSize = 10 };
|
||||||
var response = await _client.GetItemByIdAsync(request);
|
var response = await _client.GetItemsByIdsAsync(request);
|
||||||
return MapToCatalogItemResponse(response);
|
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 };
|
Id = catalogItemResponse.Id,
|
||||||
var response = await _client.GetItemsByIdsAsync(request);
|
Name = catalogItemResponse.Name,
|
||||||
return response.Data.Select(MapToCatalogItemResponse);
|
PictureUri = catalogItemResponse.PictureUri,
|
||||||
}
|
Price = (decimal)catalogItemResponse.Price
|
||||||
|
};
|
||||||
private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse)
|
|
||||||
{
|
|
||||||
return new CatalogItem
|
|
||||||
{
|
|
||||||
Id = catalogItemResponse.Id,
|
|
||||||
Name = catalogItemResponse.Name,
|
|
||||||
PictureUri = catalogItemResponse.PictureUri,
|
|
||||||
Price = (decimal)catalogItemResponse.Price
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services
|
public interface IBasketService
|
||||||
{
|
{
|
||||||
public interface IBasketService
|
Task<BasketData> GetByIdAsync(string id);
|
||||||
{
|
|
||||||
Task<BasketData> GetById(string id);
|
|
||||||
|
|
||||||
Task UpdateAsync(BasketData currentBasket);
|
Task UpdateAsync(BasketData currentBasket);
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
|
||||||
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
|
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;
|
_apiClient = httpClient;
|
||||||
private readonly ILogger<OrderApiClient> _logger;
|
_logger = logger;
|
||||||
private readonly UrlsConfig _urls;
|
_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;
|
PropertyNameCaseInsensitive = true
|
||||||
_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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,79 +1,72 @@
|
|||||||
using GrpcOrdering;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
|
||||||
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
|
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;
|
_orderingGrpcClient = orderingGrpcClient;
|
||||||
private readonly ILogger<OrderingService> _logger;
|
_logger = logger;
|
||||||
|
|
||||||
public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger)
|
|
||||||
{
|
|
||||||
_orderingGrpcClient = orderingGrpcClient;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<OrderData> GetOrderDraftAsync(BasketData basketData)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(" grpc client created, basketData={@basketData}", basketData);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator;
|
||||||
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
|
public class Startup
|
||||||
{
|
{
|
||||||
public class Startup
|
public Startup(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
public Startup(IConfiguration configuration)
|
Configuration = 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")
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase);
|
||||||
services.Configure<UrlsConfig>(configuration.GetSection("urls"));
|
app.UsePathBase(pathBase);
|
||||||
|
}
|
||||||
|
|
||||||
services.AddControllers()
|
if (env.IsDevelopment())
|
||||||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
|
{
|
||||||
|
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();
|
Predicate = _ => true,
|
||||||
options.SwaggerDoc("v1", new OpenApiInfo
|
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
||||||
{
|
|
||||||
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>();
|
|
||||||
});
|
});
|
||||||
|
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions
|
||||||
services.AddCors(options =>
|
|
||||||
{
|
{
|
||||||
options.AddPolicy("CorsPolicy",
|
Predicate = r => r.Name.Contains("self")
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
public static string GetItemById(string ids) => $"/api/v1/catalog/items/ids/{string.Join(',', ids)}";
|
||||||
{
|
|
||||||
// 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)}";
|
// REST call standard must go through port 5000
|
||||||
|
public static string GetItemsById(IEnumerable<int> ids) => $":5000/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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers;
|
||||||
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
|
[Route("api/v1/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
public class BasketController : ControllerBase
|
||||||
{
|
{
|
||||||
[Route("api/v1/[controller]")]
|
private readonly ICatalogService _catalog;
|
||||||
[Authorize]
|
private readonly IBasketService _basket;
|
||||||
[ApiController]
|
|
||||||
public class BasketController : ControllerBase
|
public BasketController(ICatalogService catalogService, IBasketService basketService)
|
||||||
{
|
{
|
||||||
private readonly ICatalogService _catalog;
|
_catalog = catalogService;
|
||||||
private readonly IBasketService _basket;
|
_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;
|
return BadRequest("Need to pass at least one basket line");
|
||||||
_basket = basketService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
// Retrieve the current basket
|
||||||
[HttpPut]
|
var basket = await _basket.GetByIdAsync(data.BuyerId) ?? new BasketData(data.BuyerId);
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId));
|
||||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
|
|
||||||
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
|
// 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 itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId);
|
||||||
var basket = await _basket.GetById(data.BuyerId) ?? new BasketData(data.BuyerId);
|
if (itemInBasket == null)
|
||||||
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 catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId);
|
basket.Items.Add(new BasketDataItem()
|
||||||
if (catalogItem == null)
|
|
||||||
{
|
{
|
||||||
return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})");
|
Id = bitem.Id,
|
||||||
}
|
ProductId = catalogItem.Id,
|
||||||
|
ProductName = catalogItem.Name,
|
||||||
var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId);
|
PictureUrl = catalogItem.PictureUri,
|
||||||
if (itemInBasket == null)
|
UnitPrice = catalogItem.Price,
|
||||||
{
|
Quantity = bitem.Quantity
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Step 4: Merge current status with new product
|
itemInBasket.Quantity = bitem.Quantity;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("")]
|
[HttpGet]
|
||||||
public class HomeController : Controller
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
[HttpGet()]
|
return new RedirectResult("~/swagger");
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
return new RedirectResult("~/swagger");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,44 +1,37 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers;
|
||||||
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
|
[Route("api/v1/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
public class OrderController : ControllerBase
|
||||||
{
|
{
|
||||||
[Route("api/v1/[controller]")]
|
private readonly IBasketService _basketService;
|
||||||
[Authorize]
|
private readonly IOrderingService _orderingService;
|
||||||
[ApiController]
|
|
||||||
public class OrderController : ControllerBase
|
public OrderController(IBasketService basketService, IOrderingService orderingService)
|
||||||
{
|
{
|
||||||
private readonly IBasketService _basketService;
|
_basketService = basketService;
|
||||||
private readonly IOrderingService _orderingService;
|
_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.IsNullOrWhitespace(basketId))
|
||||||
{
|
{
|
||||||
_basketService = basketService;
|
return BadRequest("Need a valid basketid");
|
||||||
_orderingService = orderingService;
|
}
|
||||||
|
// 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}")]
|
return await _orderingService.GetOrderDraftAsync(basket);
|
||||||
[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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters
|
|
||||||
{
|
{
|
||||||
namespace Basket.API.Infrastructure.Filters
|
namespace Basket.API.Infrastructure.Filters
|
||||||
{
|
{
|
||||||
@ -14,7 +8,7 @@ namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters
|
|||||||
{
|
{
|
||||||
// Check for authorize attribute
|
// Check for authorize attribute
|
||||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
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;
|
if (!hasAuthorize) return;
|
||||||
|
|
||||||
@ -27,13 +21,14 @@ namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters
|
|||||||
};
|
};
|
||||||
|
|
||||||
operation.Security = new List<OpenApiSecurityRequirement>
|
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;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure;
|
||||||
using Grpc.Core.Interceptors;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
catch (RpcException e)
|
||||||
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
|
|
||||||
TRequest request,
|
|
||||||
ClientInterceptorContext<TRequest, TResponse> context,
|
|
||||||
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
|
|
||||||
{
|
{
|
||||||
var call = continuation(request, context);
|
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message);
|
||||||
|
return default;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,49 +1,40 @@
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure;
|
||||||
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
|
public class HttpClientAuthorizationDelegatingHandler
|
||||||
|
: DelegatingHandler
|
||||||
{
|
{
|
||||||
public class HttpClientAuthorizationDelegatingHandler
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
: DelegatingHandler
|
|
||||||
|
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 = new AuthenticationHeaderValue("Bearer", token);
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<string> GetToken()
|
return await base.SendAsync(request, cancellationToken);
|
||||||
{
|
}
|
||||||
const string ACCESS_TOKEN = "access_token";
|
|
||||||
|
|
||||||
return await _httpContextAccessor.HttpContext
|
Task<string> GetTokenAsync()
|
||||||
.GetTokenAsync(ACCESS_TOKEN);
|
{
|
||||||
}
|
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; }
|
Quantity = 1;
|
||||||
|
|
||||||
public string BasketId { get; set; }
|
|
||||||
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
|
|
||||||
public AddBasketItemRequest()
|
|
||||||
{
|
|
||||||
Quantity = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,18 @@
|
|||||||
using System.Collections.Generic;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
|
public class BasketData
|
||||||
{
|
{
|
||||||
|
public string BuyerId { get; set; }
|
||||||
|
|
||||||
public class BasketData
|
public 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 int ProductId { get; set; }
|
||||||
{
|
|
||||||
public string Id { 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 int Quantity { get; set; }
|
|
||||||
|
|
||||||
public string PictureUrl { 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 string Name { get; set; }
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
|
|
||||||
public string PictureUri { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public string PictureUri { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,48 +1,43 @@
|
|||||||
using System;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
|
public class OrderData
|
||||||
{
|
{
|
||||||
|
public string OrderNumber { get; set; }
|
||||||
|
|
||||||
public class OrderData
|
public DateTime Date { get; set; }
|
||||||
{
|
|
||||||
public string OrderNumber { 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 string Buyer { get; set; }
|
|
||||||
|
|
||||||
public List<OrderItemData> OrderItems { get; } = new List<OrderItemData>();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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 string ProductName { get; set; }
|
||||||
{
|
|
||||||
public int ProductId { 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 int Units { get; set; }
|
|
||||||
|
|
||||||
public string PictureUrl { 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 int NewQty { get; set; }
|
||||||
{
|
|
||||||
public string BasketItemId { get; set; }
|
|
||||||
|
|
||||||
public int NewQty { get; set; }
|
|
||||||
|
|
||||||
public UpdateBasketItemData()
|
|
||||||
{
|
|
||||||
NewQty = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
Updates = new List<UpdateBasketItemData>();
|
||||||
|
|
||||||
public ICollection<UpdateBasketItemData> Updates { get; set; }
|
|
||||||
|
|
||||||
public UpdateBasketItemsRequest()
|
|
||||||
{
|
|
||||||
Updates = new List<UpdateBasketItemData>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
|
public class UpdateBasketRequest
|
||||||
{
|
{
|
||||||
|
public string BuyerId { get; set; }
|
||||||
|
|
||||||
public class UpdateBasketRequest
|
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; }
|
||||||
{
|
|
||||||
public string BuyerId { 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;
|
await BuildWebHost(args).RunAsync();
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
BuildWebHost(args).Run();
|
|
||||||
|
|
||||||
IWebHost BuildWebHost(string[] args) =>
|
IWebHost BuildWebHost(string[] args) =>
|
||||||
WebHost
|
WebHost
|
||||||
|
@ -1,103 +1,96 @@
|
|||||||
using GrpcBasket;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
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
|
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;
|
_basketClient = basketClient;
|
||||||
private readonly ILogger<BasketService> _logger;
|
_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;
|
return null;
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var map = new BasketData
|
||||||
public async Task<BasketData> GetById(string id)
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("grpc client created, request = {@id}", id);
|
BuyerId = customerBasketRequest.Buyerid
|
||||||
var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id });
|
};
|
||||||
_logger.LogDebug("grpc response {@response}", response);
|
|
||||||
|
|
||||||
return MapToBasketData(response);
|
customerBasketRequest.Items.ToList().ForEach(item =>
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateAsync(BasketData currentBasket)
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket);
|
if (item.Id != null)
|
||||||
var request = MapToCustomerBasketRequest(currentBasket);
|
|
||||||
_logger.LogDebug("Grpc update basket request {@request}", request);
|
|
||||||
|
|
||||||
await _basketClient.UpdateBasketAsync(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest)
|
|
||||||
{
|
|
||||||
if (customerBasketRequest == null)
|
|
||||||
{
|
{
|
||||||
return null;
|
map.Items.Add(new BasketDataItem
|
||||||
}
|
|
||||||
|
|
||||||
var map = new BasketData
|
|
||||||
{
|
|
||||||
BuyerId = customerBasketRequest.Buyerid
|
|
||||||
};
|
|
||||||
|
|
||||||
customerBasketRequest.Items.ToList().ForEach(item =>
|
|
||||||
{
|
|
||||||
if (item.Id != null)
|
|
||||||
{
|
{
|
||||||
map.Items.Add(new BasketDataItem
|
Id = item.Id,
|
||||||
{
|
OldUnitPrice = (decimal)item.Oldunitprice,
|
||||||
Id = item.Id,
|
PictureUrl = item.Pictureurl,
|
||||||
OldUnitPrice = (decimal)item.Oldunitprice,
|
ProductId = item.Productid,
|
||||||
PictureUrl = item.Pictureurl,
|
ProductName = item.Productname,
|
||||||
ProductId = item.Productid,
|
Quantity = item.Quantity,
|
||||||
ProductName = item.Productname,
|
UnitPrice = (decimal)item.Unitprice
|
||||||
Quantity = item.Quantity,
|
});
|
||||||
UnitPrice = (decimal)item.Unitprice
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData)
|
|
||||||
{
|
|
||||||
if (basketData == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var map = new CustomerBasketRequest
|
return map;
|
||||||
{
|
}
|
||||||
Buyerid = basketData.BuyerId
|
|
||||||
};
|
|
||||||
|
|
||||||
basketData.Items.ToList().ForEach(item =>
|
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData)
|
||||||
{
|
{
|
||||||
if (item.Id != null)
|
if (basketData == null)
|
||||||
{
|
{
|
||||||
map.Items.Add(new BasketItemResponse
|
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
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
|
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;
|
_client = client;
|
||||||
private readonly ILogger<CatalogService> _logger;
|
_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;
|
Id = catalogItemResponse.Id,
|
||||||
_logger = logger;
|
Name = catalogItemResponse.Name,
|
||||||
}
|
PictureUri = catalogItemResponse.PictureUri,
|
||||||
|
Price = (decimal)catalogItemResponse.Price
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services
|
public interface IBasketService
|
||||||
{
|
{
|
||||||
public interface IBasketService
|
Task<BasketData> GetByIdAsync(string id);
|
||||||
{
|
|
||||||
Task<BasketData> GetById(string id);
|
|
||||||
|
|
||||||
Task UpdateAsync(BasketData currentBasket);
|
Task UpdateAsync(BasketData currentBasket);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
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
|
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;
|
_apiClient = httpClient;
|
||||||
private readonly ILogger<OrderApiClient> _logger;
|
_logger = logger;
|
||||||
private readonly UrlsConfig _urls;
|
_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;
|
PropertyNameCaseInsensitive = true
|
||||||
_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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,79 +1,72 @@
|
|||||||
using GrpcOrdering;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
|
||||||
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
|
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;
|
_orderingGrpcClient = orderingGrpcClient;
|
||||||
private readonly ILogger<OrderingService> _logger;
|
_logger = logger;
|
||||||
|
|
||||||
public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger)
|
|
||||||
{
|
|
||||||
_orderingGrpcClient = orderingGrpcClient;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<OrderData> GetOrderDraftAsync(BasketData basketData)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(" grpc client created, basketData={@basketData}", basketData);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator;
|
||||||
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
|
public class Startup
|
||||||
{
|
{
|
||||||
public class Startup
|
public Startup(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
public Startup(IConfiguration configuration)
|
Configuration = 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")
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
if (env.IsDevelopment())
|
||||||
services.AddAuthentication(options =>
|
{
|
||||||
{
|
app.UseDeveloperExceptionPage();
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
}
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
|
|
||||||
})
|
app.UseHttpsRedirection();
|
||||||
.AddJwtBearer(options =>
|
|
||||||
|
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;
|
Predicate = _ => true,
|
||||||
options.RequireHttpsMetadata = false;
|
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
||||||
options.Audience = "webshoppingagg";
|
|
||||||
});
|
});
|
||||||
|
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions
|
||||||
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();
|
Predicate = r => r.Name.Contains("self")
|
||||||
|
|
||||||
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
|
public static class ServiceCollectionExtensions
|
||||||
.SetIsOriginAllowed((host) => true)
|
{
|
||||||
.AllowAnyMethod()
|
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
|
||||||
.AllowAnyHeader()
|
{
|
||||||
.AllowCredentials());
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
|
||||||
});
|
|
||||||
|
var identityUrl = configuration.GetValue<string>("urls:identity");
|
||||||
return services;
|
services.AddAuthentication(options =>
|
||||||
}
|
{
|
||||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
{
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
//register delegating handlers
|
|
||||||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
|
})
|
||||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
//register http services
|
options.Authority = identityUrl;
|
||||||
|
options.RequireHttpsMetadata = false;
|
||||||
services.AddHttpClient<IOrderApiClient, OrderApiClient>()
|
options.Audience = "webshoppingagg";
|
||||||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>()
|
});
|
||||||
.AddDevspacesSupport();
|
|
||||||
|
return services;
|
||||||
return services;
|
}
|
||||||
}
|
|
||||||
|
public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration)
|
||||||
public static IServiceCollection AddGrpcServices(this IServiceCollection services)
|
{
|
||||||
{
|
services.AddOptions();
|
||||||
services.AddTransient<GrpcExceptionInterceptor>();
|
services.Configure<UrlsConfig>(configuration.GetSection("urls"));
|
||||||
|
|
||||||
services.AddScoped<IBasketService, BasketService>();
|
services.AddControllers()
|
||||||
|
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
|
||||||
services.AddGrpcClient<Basket.BasketClient>((services, options) =>
|
|
||||||
{
|
services.AddSwaggerGen(options =>
|
||||||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket;
|
{
|
||||||
options.Address = new Uri(basketApi);
|
options.DescribeAllEnumsAsStrings();
|
||||||
}).AddInterceptor<GrpcExceptionInterceptor>();
|
|
||||||
|
options.SwaggerDoc("v1", new OpenApiInfo
|
||||||
services.AddScoped<ICatalogService, CatalogService>();
|
{
|
||||||
|
Title = "Shopping Aggregator for Web Clients",
|
||||||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) =>
|
Version = "v1",
|
||||||
{
|
Description = "Shopping Aggregator for Web Clients"
|
||||||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog;
|
});
|
||||||
options.Address = new Uri(catalogApi);
|
|
||||||
}).AddInterceptor<GrpcExceptionInterceptor>();
|
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
services.AddScoped<IOrderingService, OrderingService>();
|
Type = SecuritySchemeType.OAuth2,
|
||||||
|
Flows = new OpenApiOAuthFlows()
|
||||||
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) =>
|
{
|
||||||
{
|
Implicit = new OpenApiOAuthFlow()
|
||||||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
|
{
|
||||||
options.Address = new Uri(orderingApi);
|
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
|
||||||
}).AddInterceptor<GrpcExceptionInterceptor>();
|
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
|
||||||
|
|
||||||
return services;
|
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>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0-preview.7.21377.19" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-preview.7.21377.19" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,29 +1,22 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
namespace Devspaces.Support;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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";
|
_httpContextAccessor = httpContextAccessor;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
}
|
||||||
public DevspacesMessageHandler(IHttpContextAccessor httpContextAccessor)
|
|
||||||
{
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var req = _httpContextAccessor.HttpContext.Request;
|
var req = _httpContextAccessor.HttpContext.Request;
|
||||||
|
|
||||||
if (req.Headers.ContainsKey(DevspacesHeaderName))
|
if (req.Headers.ContainsKey(DevspacesHeaderName))
|
||||||
{
|
{
|
||||||
request.Headers.Add(DevspacesHeaderName, req.Headers[DevspacesHeaderName] as IEnumerable<string>);
|
request.Headers.Add(DevspacesHeaderName, req.Headers[DevspacesHeaderName] as IEnumerable<string>);
|
||||||
}
|
|
||||||
return base.SendAsync(request, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
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>()
|
void Subscribe<T, TH>()
|
||||||
where T : IntegrationEvent
|
where T : IntegrationEvent
|
||||||
where TH : IIntegrationEventHandler<T>;
|
where TH : IIntegrationEventHandler<T>;
|
||||||
|
|
||||||
void SubscribeDynamic<TH>(string eventName)
|
void SubscribeDynamic<TH>(string eventName)
|
||||||
where TH : IDynamicIntegrationEventHandler;
|
where TH : IDynamicIntegrationEventHandler;
|
||||||
|
|
||||||
void UnsubscribeDynamic<TH>(string eventName)
|
void UnsubscribeDynamic<TH>(string eventName)
|
||||||
where TH : IDynamicIntegrationEventHandler;
|
where TH : IDynamicIntegrationEventHandler;
|
||||||
|
|
||||||
void Unsubscribe<T, TH>()
|
void Unsubscribe<T, TH>()
|
||||||
where TH : IIntegrationEventHandler<T>
|
where TH : IIntegrationEventHandler<T>
|
||||||
where T : IntegrationEvent;
|
where T : IntegrationEvent;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events
|
public record IntegrationEvent
|
||||||
{
|
{
|
||||||
public record IntegrationEvent
|
public IntegrationEvent()
|
||||||
{
|
{
|
||||||
public IntegrationEvent()
|
Id = Guid.NewGuid();
|
||||||
{
|
CreationDate = DateTime.UtcNow;
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
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;
|
var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray());
|
||||||
|
typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>";
|
||||||
if (type.IsGenericType)
|
}
|
||||||
{
|
else
|
||||||
var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray());
|
{
|
||||||
typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>";
|
typeName = type.Name;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
typeName = type.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetGenericTypeName(this object @object)
|
return typeName;
|
||||||
{
|
}
|
||||||
return @object.GetType().GetGenericTypeName();
|
|
||||||
}
|
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;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus
|
public interface IEventBusSubscriptionsManager
|
||||||
{
|
{
|
||||||
public interface IEventBusSubscriptionsManager
|
bool IsEmpty { get; }
|
||||||
{
|
event EventHandler<string> OnEventRemoved;
|
||||||
bool IsEmpty { get; }
|
void AddDynamicSubscription<TH>(string eventName)
|
||||||
event EventHandler<string> OnEventRemoved;
|
where TH : IDynamicIntegrationEventHandler;
|
||||||
void AddDynamicSubscription<TH>(string eventName)
|
|
||||||
where TH : IDynamicIntegrationEventHandler;
|
|
||||||
|
|
||||||
void AddSubscription<T, TH>()
|
void AddSubscription<T, TH>()
|
||||||
where T : IntegrationEvent
|
where T : IntegrationEvent
|
||||||
where TH : IIntegrationEventHandler<T>;
|
where TH : IIntegrationEventHandler<T>;
|
||||||
|
|
||||||
void RemoveSubscription<T, TH>()
|
void RemoveSubscription<T, TH>()
|
||||||
where TH : IIntegrationEventHandler<T>
|
where TH : IIntegrationEventHandler<T>
|
||||||
where T : IntegrationEvent;
|
where T : IntegrationEvent;
|
||||||
void RemoveDynamicSubscription<TH>(string eventName)
|
void RemoveDynamicSubscription<TH>(string eventName)
|
||||||
where TH : IDynamicIntegrationEventHandler;
|
where TH : IDynamicIntegrationEventHandler;
|
||||||
|
|
||||||
bool HasSubscriptionsForEvent<T>() where T : IntegrationEvent;
|
bool HasSubscriptionsForEvent<T>() where T : IntegrationEvent;
|
||||||
bool HasSubscriptionsForEvent(string eventName);
|
bool HasSubscriptionsForEvent(string eventName);
|
||||||
Type GetEventTypeByName(string eventName);
|
Type GetEventTypeByName(string eventName);
|
||||||
void Clear();
|
void Clear();
|
||||||
IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>() where T : IntegrationEvent;
|
IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>() where T : IntegrationEvent;
|
||||||
IEnumerable<SubscriptionInfo> GetHandlersForEvent(string eventName);
|
IEnumerable<SubscriptionInfo> GetHandlersForEvent(string eventName);
|
||||||
string GetEventKey<T>();
|
string GetEventKey<T>();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -1,162 +1,155 @@
|
|||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
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;
|
public void AddDynamicSubscription<TH>(string eventName)
|
||||||
private readonly List<Type> _eventTypes;
|
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.Add(typeof(T));
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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; }
|
IsDynamic = isDynamic;
|
||||||
public Type HandlerType { get; }
|
HandlerType = handlerType;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
|
||||||
using Polly;
|
|
||||||
using Polly.Retry;
|
|
||||||
using RabbitMQ.Client;
|
|
||||||
using RabbitMQ.Client.Events;
|
|
||||||
using RabbitMQ.Client.Exceptions;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
|
public class DefaultRabbitMQPersistentConnection
|
||||||
|
: IRabbitMQPersistentConnection
|
||||||
{
|
{
|
||||||
public class DefaultRabbitMQPersistentConnection
|
private readonly IConnectionFactory _connectionFactory;
|
||||||
: IRabbitMQPersistentConnection
|
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;
|
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||||
private readonly ILogger<DefaultRabbitMQPersistentConnection> _logger;
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
private readonly int _retryCount;
|
_retryCount = retryCount;
|
||||||
IConnection _connection;
|
}
|
||||||
bool _disposed;
|
|
||||||
|
|
||||||
object sync_root = new object();
|
public bool IsConnected
|
||||||
|
{
|
||||||
public DefaultRabbitMQPersistentConnection(IConnectionFactory connectionFactory, ILogger<DefaultRabbitMQPersistentConnection> logger, int retryCount = 5)
|
get
|
||||||
{
|
{
|
||||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
return _connection != null && _connection.IsOpen && !_disposed;
|
||||||
_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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
|
||||||
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
|
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";
|
_persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection));
|
||||||
const string AUTOFAC_SCOPE_NAME = "eshop_event_bus";
|
_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 void SubsManager_OnEventRemoved(object sender, string eventName)
|
||||||
private readonly ILogger<EventBusRabbitMQ> _logger;
|
{
|
||||||
private readonly IEventBusSubscriptionsManager _subsManager;
|
if (!_persistentConnection.IsConnected)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection));
|
_persistentConnection.TryConnect();
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
||||||
_subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager();
|
|
||||||
_queueName = queueName;
|
|
||||||
_consumerChannel = CreateConsumerChannel();
|
|
||||||
_autofac = autofac;
|
|
||||||
_retryCount = retryCount;
|
|
||||||
_subsManager.OnEventRemoved += SubsManager_OnEventRemoved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SubsManager_OnEventRemoved(object sender, string eventName)
|
using (var channel = _persistentConnection.CreateModel())
|
||||||
{
|
{
|
||||||
if (!_persistentConnection.IsConnected)
|
channel.QueueUnbind(queue: _queueName,
|
||||||
|
exchange: BROKER_NAME,
|
||||||
|
routingKey: eventName);
|
||||||
|
|
||||||
|
if (_subsManager.IsEmpty)
|
||||||
{
|
{
|
||||||
_persistentConnection.TryConnect();
|
_queueName = string.Empty;
|
||||||
}
|
_consumerChannel.Close();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
|
public interface IRabbitMQPersistentConnection
|
||||||
|
: IDisposable
|
||||||
{
|
{
|
||||||
public interface IRabbitMQPersistentConnection
|
bool IsConnected { get; }
|
||||||
: IDisposable
|
|
||||||
{
|
|
||||||
bool IsConnected { get; }
|
|
||||||
|
|
||||||
bool TryConnect();
|
bool TryConnect();
|
||||||
|
|
||||||
IModel CreateModel();
|
IModel CreateModel();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,68 +1,64 @@
|
|||||||
using Microsoft.Azure.ServiceBus;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
|
||||||
using System;
|
|
||||||
|
|
||||||
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;
|
_serviceBusConnectionStringBuilder = serviceBusConnectionStringBuilder ??
|
||||||
private readonly string _subscriptionClientName;
|
throw new ArgumentNullException(nameof(serviceBusConnectionStringBuilder));
|
||||||
private SubscriptionClient _subscriptionClient;
|
_subscriptionClientName = subscriptionClientName;
|
||||||
private ITopicClient _topicClient;
|
_subscriptionClient = new SubscriptionClient(_serviceBusConnectionStringBuilder, subscriptionClientName);
|
||||||
|
_topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default);
|
||||||
|
}
|
||||||
|
|
||||||
bool _disposed;
|
public ITopicClient TopicClient
|
||||||
|
{
|
||||||
public DefaultServiceBusPersisterConnection(ServiceBusConnectionStringBuilder serviceBusConnectionStringBuilder,
|
get
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
if (_topicClient.IsClosedOrClosing)
|
if (_topicClient.IsClosedOrClosing)
|
||||||
{
|
{
|
||||||
_topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default);
|
_topicClient = new TopicClient(_serviceBusConnectionStringBuilder, RetryPolicy.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _topicClient;
|
return _topicClient;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public ISubscriptionClient SubscriptionClient
|
||||||
|
{
|
||||||
|
get
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_subscriptionClient.IsClosedOrClosing)
|
||||||
|
{
|
||||||
_disposed = true;
|
_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;
|
private readonly IServiceBusPersisterConnection _serviceBusPersisterConnection;
|
||||||
using Microsoft.Azure.ServiceBus;
|
private readonly ILogger<EventBusServiceBus> _logger;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
|
private readonly IEventBusSubscriptionsManager _subsManager;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
private readonly ILifetimeScope _autofac;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus";
|
||||||
using Microsoft.Extensions.Logging;
|
private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent";
|
||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
public class EventBusServiceBus : IEventBus
|
public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection,
|
||||||
|
ILogger<EventBusServiceBus> logger, IEventBusSubscriptionsManager subsManager, ILifetimeScope autofac)
|
||||||
{
|
{
|
||||||
private readonly IServiceBusPersisterConnection _serviceBusPersisterConnection;
|
_serviceBusPersisterConnection = serviceBusPersisterConnection;
|
||||||
private readonly ILogger<EventBusServiceBus> _logger;
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
private readonly IEventBusSubscriptionsManager _subsManager;
|
_subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager();
|
||||||
private readonly ILifetimeScope _autofac;
|
_autofac = autofac;
|
||||||
private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus";
|
|
||||||
private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent";
|
|
||||||
|
|
||||||
public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection,
|
RemoveDefaultRule();
|
||||||
ILogger<EventBusServiceBus> logger, IEventBusSubscriptionsManager subsManager, ILifetimeScope autofac)
|
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;
|
MessageId = Guid.NewGuid().ToString(),
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
Body = body,
|
||||||
_subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager();
|
Label = eventName,
|
||||||
_autofac = autofac;
|
};
|
||||||
|
|
||||||
RemoveDefaultRule();
|
_serviceBusPersisterConnection.TopicClient.SendAsync(message)
|
||||||
RegisterSubscriptionClientMessageHandler();
|
.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, "");
|
_serviceBusPersisterConnection
|
||||||
var jsonMessage = JsonSerializer.Serialize(@event);
|
.SubscriptionClient
|
||||||
var body = Encoding.UTF8.GetBytes(jsonMessage);
|
.RemoveRuleAsync(eventName)
|
||||||
|
|
||||||
var message = new Message
|
|
||||||
{
|
|
||||||
MessageId = Guid.NewGuid().ToString(),
|
|
||||||
Body = body,
|
|
||||||
Label = eventName,
|
|
||||||
};
|
|
||||||
|
|
||||||
_serviceBusPersisterConnection.TopicClient.SendAsync(message)
|
|
||||||
.GetAwaiter()
|
.GetAwaiter()
|
||||||
.GetResult();
|
.GetResult();
|
||||||
}
|
}
|
||||||
|
catch (MessagingEntityNotFoundException)
|
||||||
public void SubscribeDynamic<TH>(string eventName)
|
|
||||||
where TH : IDynamicIntegrationEventHandler
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Subscribing to dynamic event {EventName} with {EventHandler}", eventName, typeof(TH).Name);
|
_logger.LogWarning("The messaging entity {eventName} Could not be found.", eventName);
|
||||||
|
|
||||||
_subsManager.AddDynamicSubscription<TH>(eventName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Subscribe<T, TH>()
|
_logger.LogInformation("Unsubscribing from event {EventName}", eventName);
|
||||||
where T : IntegrationEvent
|
|
||||||
where TH : IIntegrationEventHandler<T>
|
|
||||||
{
|
|
||||||
var eventName = typeof(T).Name.Replace(INTEGRATION_EVENT_SUFFIX, "");
|
|
||||||
|
|
||||||
var containsKey = _subsManager.HasSubscriptionsForEvent<T>();
|
_subsManager.RemoveSubscription<T, TH>();
|
||||||
if (!containsKey)
|
}
|
||||||
|
|
||||||
|
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
|
await _serviceBusPersisterConnection.SubscriptionClient.CompleteAsync(message.SystemProperties.LockToken);
|
||||||
{
|
|
||||||
Filter = new CorrelationFilter { Label = eventName },
|
|
||||||
Name = eventName
|
|
||||||
}).GetAwaiter().GetResult();
|
|
||||||
}
|
}
|
||||||
catch (ServiceBusException)
|
},
|
||||||
{
|
new MessageHandlerOptions(ExceptionReceivedHandlerAsync) { MaxConcurrentCalls = 10, AutoComplete = false });
|
||||||
_logger.LogWarning("The messaging entity {eventName} already exists.", eventName);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_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>()
|
return Task.CompletedTask;
|
||||||
where T : IntegrationEvent
|
}
|
||||||
where TH : IIntegrationEventHandler<T>
|
|
||||||
|
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, "");
|
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME))
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
_serviceBusPersisterConnection
|
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
|
||||||
.SubscriptionClient
|
foreach (var subscription in subscriptions)
|
||||||
.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 eventName = $"{message.Label}{INTEGRATION_EVENT_SUFFIX}";
|
if (subscription.IsDynamic)
|
||||||
var messageData = Encoding.UTF8.GetString(message.Body);
|
|
||||||
|
|
||||||
// Complete the message so that it is not received again.
|
|
||||||
if (await ProcessEvent(eventName, messageData))
|
|
||||||
{
|
{
|
||||||
await _serviceBusPersisterConnection.SubscriptionClient.CompleteAsync(message.SystemProperties.LockToken);
|
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
|
||||||
}
|
if (handler == null) continue;
|
||||||
},
|
|
||||||
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;
|
|
||||||
|
|
||||||
using dynamic eventData = JsonDocument.Parse(message);
|
using dynamic eventData = JsonDocument.Parse(message);
|
||||||
await handler.Handle(eventData);
|
await handler.Handle(eventData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var handler = scope.ResolveOptional(subscription.HandlerType);
|
var handler = scope.ResolveOptional(subscription.HandlerType);
|
||||||
if (handler == null) continue;
|
if (handler == null) continue;
|
||||||
var eventType = _subsManager.GetEventTypeByName(eventName);
|
var eventType = _subsManager.GetEventTypeByName(eventName);
|
||||||
var integrationEvent = JsonSerializer.Deserialize(message, eventType);
|
var integrationEvent = JsonSerializer.Deserialize(message, eventType);
|
||||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
||||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
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
|
||||||
_serviceBusPersisterConnection
|
.RemoveRuleAsync(RuleDescription.DefaultRuleName)
|
||||||
.SubscriptionClient
|
.GetAwaiter()
|
||||||
.RemoveRuleAsync(RuleDescription.DefaultRuleName)
|
.GetResult();
|
||||||
.GetAwaiter()
|
}
|
||||||
.GetResult();
|
catch (MessagingEntityNotFoundException)
|
||||||
}
|
{
|
||||||
catch (MessagingEntityNotFoundException)
|
_logger.LogWarning("The messaging entity {DefaultRuleName} Could not be found.", RuleDescription.DefaultRuleName);
|
||||||
{
|
|
||||||
_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
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
|
||||||
{
|
|
||||||
using Microsoft.Azure.ServiceBus;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
public interface IServiceBusPersisterConnection : IDisposable
|
public interface IServiceBusPersisterConnection : IDisposable
|
||||||
{
|
{
|
||||||
ITopicClient TopicClient { get; }
|
ITopicClient TopicClient { get; }
|
||||||
ISubscriptionClient SubscriptionClient { 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,
|
||||||
NotPublished = 0,
|
Published = 2,
|
||||||
InProgress = 1,
|
PublishedFailed = 3
|
||||||
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;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
||||||
|
|
||||||
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)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Entity<IntegrationEventLogEntry>(ConfigureIntegrationEventLogEntry);
|
builder.Entity<IntegrationEventLogEntry>(ConfigureIntegrationEventLogEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureIntegrationEventLogEntry(EntityTypeBuilder<IntegrationEventLogEntry> builder)
|
void ConfigureIntegrationEventLogEntry(EntityTypeBuilder<IntegrationEventLogEntry> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("IntegrationEventLog");
|
builder.ToTable("IntegrationEventLog");
|
||||||
|
|
||||||
builder.HasKey(e => e.EventId);
|
builder.HasKey(e => e.EventId);
|
||||||
|
|
||||||
builder.Property(e => e.EventId)
|
builder.Property(e => e.EventId)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
builder.Property(e => e.Content)
|
builder.Property(e => e.Content)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
builder.Property(e => e.CreationTime)
|
builder.Property(e => e.CreationTime)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
builder.Property(e => e.State)
|
builder.Property(e => e.State)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
builder.Property(e => e.TimesSent)
|
builder.Property(e => e.TimesSent)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
builder.Property(e => e.EventTypeName)
|
builder.Property(e => e.EventTypeName)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-preview.7.21378.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0-preview.7.21378.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0-preview.7.21378.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,43 +1,36 @@
|
|||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF;
|
||||||
using System;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF
|
public class IntegrationEventLogEntry
|
||||||
{
|
{
|
||||||
public class IntegrationEventLogEntry
|
private IntegrationEventLogEntry() { }
|
||||||
|
public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId)
|
||||||
{
|
{
|
||||||
private IntegrationEventLogEntry() { }
|
EventId = @event.Id;
|
||||||
public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId)
|
CreationTime = @event.CreationDate;
|
||||||
|
EventTypeName = @event.GetType().FullName;
|
||||||
|
Content = JsonSerializer.Serialize(@event, @event.GetType(), new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
EventId = @event.Id;
|
WriteIndented = true
|
||||||
CreationTime = @event.CreationDate;
|
});
|
||||||
EventTypeName = @event.GetType().FullName;
|
State = EventStateEnum.NotPublished;
|
||||||
Content = JsonSerializer.Serialize(@event, @event.GetType(), new JsonSerializerOptions
|
TimesSent = 0;
|
||||||
{
|
TransactionId = transactionId.ToString();
|
||||||
WriteIndented = true
|
}
|
||||||
});
|
public Guid EventId { get; private set; }
|
||||||
State = EventStateEnum.NotPublished;
|
public string EventTypeName { get; private set; }
|
||||||
TimesSent = 0;
|
[NotMapped]
|
||||||
TransactionId = transactionId.ToString();
|
public string EventTypeShortName => EventTypeName.Split('.')?.Last();
|
||||||
}
|
[NotMapped]
|
||||||
public Guid EventId { get; private set; }
|
public IntegrationEvent IntegrationEvent { get; private set; }
|
||||||
public string EventTypeName { get; private set; }
|
public EventStateEnum State { get; set; }
|
||||||
[NotMapped]
|
public int TimesSent { get; set; }
|
||||||
public string EventTypeShortName => EventTypeName.Split('.')?.Last();
|
public DateTime CreationTime { get; private set; }
|
||||||
[NotMapped]
|
public string Content { get; private set; }
|
||||||
public IntegrationEvent IntegrationEvent { get; private set; }
|
public string TransactionId { 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)
|
public IntegrationEventLogEntry DeserializeJsonContent(Type type)
|
||||||
{
|
{
|
||||||
IntegrationEvent = JsonSerializer.Deserialize(Content, type, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }) as IntegrationEvent;
|
IntegrationEvent = JsonSerializer.Deserialize(Content, type, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }) as IntegrationEvent;
|
||||||
return this;
|
return this;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Storage;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync(Guid transactionId);
|
Task MarkEventAsPublishedAsync(Guid eventId);
|
||||||
Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction);
|
Task MarkEventAsInProgressAsync(Guid eventId);
|
||||||
Task MarkEventAsPublishedAsync(Guid eventId);
|
Task MarkEventAsFailedAsync(Guid eventId);
|
||||||
Task MarkEventAsInProgressAsync(Guid eventId);
|
|
||||||
Task MarkEventAsFailedAsync(Guid eventId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,110 +1,99 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services;
|
||||||
using Microsoft.EntityFrameworkCore.Storage;
|
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data.Common;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services
|
public class IntegrationEventLogService : IIntegrationEventLogService, IDisposable
|
||||||
{
|
{
|
||||||
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;
|
_dbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection));
|
||||||
private readonly DbConnection _dbConnection;
|
_integrationEventLogContext = new IntegrationEventLogContext(
|
||||||
private readonly List<Type> _eventTypes;
|
new DbContextOptionsBuilder<IntegrationEventLogContext>()
|
||||||
private volatile bool disposedValue;
|
.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));
|
return result.OrderBy(o => o.CreationTime)
|
||||||
_integrationEventLogContext = new IntegrationEventLogContext(
|
.Select(e => e.DeserializeJsonContent(_eventTypes.Find(t => t.Name == e.EventTypeShortName)));
|
||||||
new DbContextOptionsBuilder<IntegrationEventLogContext>()
|
|
||||||
.UseSqlServer(_dbConnection)
|
|
||||||
.Options);
|
|
||||||
|
|
||||||
_eventTypes = Assembly.Load(Assembly.GetEntryAssembly().FullName)
|
|
||||||
.GetTypes()
|
|
||||||
.Where(t => t.Name.EndsWith(nameof(IntegrationEvent)))
|
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
if (disposing)
|
||||||
|
|
||||||
var result = await _integrationEventLogContext.IntegrationEventLogs
|
|
||||||
.Where(e => e.TransactionId == tid && e.State == EventStateEnum.NotPublished).ToListAsync();
|
|
||||||
|
|
||||||
if (result != null && result.Any())
|
|
||||||
{
|
{
|
||||||
return result.OrderBy(o => o.CreationTime)
|
_integrationEventLogContext?.Dispose();
|
||||||
.Select(e => e.DeserializeJsonContent(_eventTypes.Find(t => t.Name == e.EventTypeShortName)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new List<IntegrationEventLogEntry>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction)
|
_disposedValue = true;
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,25 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities;
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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;
|
//Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction():
|
||||||
private ResilientTransaction(DbContext context) =>
|
//See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
|
||||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
var strategy = _context.Database.CreateExecutionStrategy();
|
||||||
|
await strategy.ExecuteAsync(async () =>
|
||||||
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():
|
using (var transaction = _context.Database.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())
|
await action();
|
||||||
{
|
transaction.Commit();
|
||||||
await action();
|
}
|
||||||
transaction.Commit();
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
namespace Basket.API.Infrastructure.Middlewares;
|
namespace Basket.API.Infrastructure.Middlewares;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
public class FailingMiddleware
|
public class FailingMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private bool _mustFail;
|
private bool _mustFail;
|
||||||
private readonly FailingOptions _options;
|
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;
|
_next = next;
|
||||||
_options = options;
|
_options = options;
|
||||||
|
@ -1,31 +1,26 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
namespace Basket.FunctionalTests.Base;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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)
|
identity.AddClaim(new Claim("sub", IDENTITY_ID));
|
||||||
{
|
identity.AddClaim(new Claim("unique_name", IDENTITY_ID));
|
||||||
_next = rd;
|
identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID));
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Invoke(HttpContext httpContext)
|
httpContext.User.AddIdentity(identity);
|
||||||
{
|
|
||||||
var identity = new ClaimsIdentity("cookies");
|
|
||||||
|
|
||||||
identity.AddClaim(new Claim("sub", IDENTITY_ID));
|
await _next.Invoke(httpContext);
|
||||||
identity.AddClaim(new Claim("unique_name", IDENTITY_ID));
|
|
||||||
identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID));
|
|
||||||
|
|
||||||
httpContext.User.AddIdentity(identity);
|
|
||||||
|
|
||||||
await _next.Invoke(httpContext);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,36 @@
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
namespace Basket.FunctionalTests.Base;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
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 hostBuilder = new WebHostBuilder()
|
||||||
{
|
.UseContentRoot(Path.GetDirectoryName(path))
|
||||||
var path = Assembly.GetAssembly(typeof(BasketScenarioBase))
|
.ConfigureAppConfiguration(cb =>
|
||||||
.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)
|
|
||||||
{
|
{
|
||||||
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}/";
|
return $"{ApiUrlBase}/{id}";
|
||||||
public static string CheckoutOrder = $"{ApiUrlBase}/checkout";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
namespace Basket.FunctionalTests.Base
|
||||||
{
|
{
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.TestHost;
|
namespace Basket.FunctionalTests.Base;
|
||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-preview.7.21378.6" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="5.0.2" />
|
<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="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="Moq" Version="4.15.2" />
|
<PackageReference Include="Moq" Version="4.15.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
|
@ -1,95 +1,85 @@
|
|||||||
using Basket.FunctionalTests.Base;
|
namespace Basket.FunctionalTests;
|
||||||
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
|
public class BasketScenarios
|
||||||
|
: BasketScenarioBase
|
||||||
{
|
{
|
||||||
public class BasketScenarios
|
[Fact]
|
||||||
: BasketScenarioBase
|
public async Task Post_basket_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
[Fact]
|
using (var server = CreateServer())
|
||||||
public async Task Post_basket_and_response_ok_status_code()
|
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
|
.PostAsync(Post.Basket, content);
|
||||||
var response = await server.CreateClient()
|
|
||||||
.PostAsync(Post.Basket, content);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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
|
namespace Basket.FunctionalTests
|
||||||
{
|
{
|
||||||
|
@ -1,155 +1,140 @@
|
|||||||
using Basket.API.IntegrationEvents.Events;
|
namespace UnitTest.Basket.Application;
|
||||||
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;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
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;
|
_basketRepositoryMock = new Mock<IBasketRepository>();
|
||||||
private readonly Mock<IBasketIdentityService> _identityServiceMock;
|
_identityServiceMock = new Mock<IBasketIdentityService>();
|
||||||
private readonly Mock<IEventBus> _serviceBusMock;
|
_serviceBusMock = new Mock<IEventBus>();
|
||||||
private readonly Mock<ILogger<BasketController>> _loggerMock;
|
_loggerMock = new Mock<ILogger<BasketController>>();
|
||||||
|
}
|
||||||
|
|
||||||
public BasketWebApiTest()
|
[Fact]
|
||||||
{
|
public async Task Get_customer_basket_success()
|
||||||
_basketRepositoryMock = new Mock<IBasketRepository>();
|
{
|
||||||
_identityServiceMock = new Mock<IBasketIdentityService>();
|
//Arrange
|
||||||
_serviceBusMock = new Mock<IEventBus>();
|
var fakeCustomerId = "1";
|
||||||
_loggerMock = new Mock<ILogger<BasketController>>();
|
var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId);
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
_basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny<string>()))
|
||||||
public async Task Get_customer_basket_success()
|
.Returns(Task.FromResult(fakeCustomerBasket));
|
||||||
{
|
_identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId);
|
||||||
//Arrange
|
|
||||||
var fakeCustomerId = "1";
|
|
||||||
var fakeCustomerBasket = GetCustomerBasketFake(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));
|
.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(
|
||||||
var basketController = new BasketController(
|
_loggerMock.Object,
|
||||||
_loggerMock.Object,
|
_basketRepositoryMock.Object,
|
||||||
_basketRepositoryMock.Object,
|
_identityServiceMock.Object,
|
||||||
_identityServiceMock.Object,
|
_serviceBusMock.Object);
|
||||||
_serviceBusMock.Object);
|
|
||||||
|
|
||||||
var actionResult = await basketController.GetBasketByIdAsync(fakeCustomerId);
|
basketController.ControllerContext = new ControllerContext()
|
||||||
|
|
||||||
//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
|
HttpContext = new DefaultHttpContext()
|
||||||
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()
|
User = new ClaimsPrincipal(
|
||||||
{
|
new ClaimsIdentity(new Claim[] {
|
||||||
User = new ClaimsPrincipal(
|
new Claim("sub", "testuser"),
|
||||||
new ClaimsIdentity(new Claim[] {
|
new Claim("unique_name", "testuser"),
|
||||||
new Claim("sub", "testuser"),
|
new Claim(ClaimTypes.Name, "testuser")
|
||||||
new Claim("unique_name", "testuser"),
|
}))
|
||||||
new Claim(ClaimTypes.Name, "testuser")
|
}
|
||||||
}))
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
var result = await basketController.CheckoutAsync(new BasketCheckout(), Guid.NewGuid().ToString()) as AcceptedResult;
|
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;
|
namespace UnitTest.Basket.Application;
|
||||||
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
|
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;
|
_catalogServiceMock = new Mock<ICatalogService>();
|
||||||
private readonly Mock<IBasketService> _basketServiceMock;
|
_basketServiceMock = new Mock<IBasketService>();
|
||||||
private readonly Mock<IIdentityParser<ApplicationUser>> _identityParserMock;
|
_identityParserMock = new Mock<IIdentityParser<ApplicationUser>>();
|
||||||
private readonly Mock<HttpContext> _contextMock;
|
_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>();
|
["fakeProdA"] = 1,
|
||||||
_basketServiceMock = new Mock<IBasketService>();
|
["fakeProdB"] = 2
|
||||||
_identityParserMock = new Mock<IIdentityParser<ApplicationUser>>();
|
};
|
||||||
_contextMock = new Mock<HttpContext>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
_basketServiceMock.Setup(x => x.SetQuantities(It.IsAny<ApplicationUser>(), It.IsAny<Dictionary<string, int>>()))
|
||||||
public async Task Post_cart_success()
|
.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
|
["fakeProdA"] = 1,
|
||||||
var fakeBuyerId = "1";
|
["fakeProdB"] = 2
|
||||||
var action = string.Empty;
|
};
|
||||||
var fakeBasket = GetFakeBasket(fakeBuyerId);
|
|
||||||
var fakeQuantities = new Dictionary<string, int>()
|
|
||||||
{
|
|
||||||
["fakeProdA"] = 1,
|
|
||||||
["fakeProdB"] = 2
|
|
||||||
};
|
|
||||||
|
|
||||||
_basketServiceMock.Setup(x => x.SetQuantities(It.IsAny<ApplicationUser>(), It.IsAny<Dictionary<string, int>>()))
|
_basketServiceMock.Setup(x => x.SetQuantities(It.IsAny<ApplicationUser>(), It.IsAny<Dictionary<string, int>>()))
|
||||||
.Returns(Task.FromResult(fakeBasket));
|
.Returns(Task.FromResult(fakeBasket));
|
||||||
|
|
||||||
_basketServiceMock.Setup(x => x.UpdateBasket(It.IsAny<BasketModel>()))
|
_basketServiceMock.Setup(x => x.UpdateBasket(It.IsAny<BasketModel>()))
|
||||||
.Returns(Task.FromResult(fakeBasket));
|
.Returns(Task.FromResult(fakeBasket));
|
||||||
|
|
||||||
//Act
|
//Act
|
||||||
var cartController = new CartController(_basketServiceMock.Object, _catalogServiceMock.Object, _identityParserMock.Object);
|
var orderController = new CartController(_basketServiceMock.Object, _catalogServiceMock.Object, _identityParserMock.Object);
|
||||||
cartController.ControllerContext.HttpContext = _contextMock.Object;
|
orderController.ControllerContext.HttpContext = _contextMock.Object;
|
||||||
var actionResult = await cartController.Index(fakeQuantities, action);
|
var actionResult = await orderController.Index(fakeQuantities, action);
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
var viewResult = Assert.IsType<ViewResult>(actionResult);
|
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(actionResult);
|
||||||
}
|
Assert.Equal("Order", redirectToActionResult.ControllerName);
|
||||||
|
Assert.Equal("Create", redirectToActionResult.ActionName);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Post_cart_checkout_success()
|
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
|
BuyerId = buyerId
|
||||||
var fakeBuyerId = "1";
|
};
|
||||||
var action = "[ Checkout ]";
|
}
|
||||||
var fakeBasket = GetFakeBasket(fakeBuyerId);
|
|
||||||
var fakeQuantities = new Dictionary<string, int>()
|
|
||||||
{
|
|
||||||
["fakeProdA"] = 1,
|
|
||||||
["fakeProdB"] = 2
|
|
||||||
};
|
|
||||||
|
|
||||||
_basketServiceMock.Setup(x => x.SetQuantities(It.IsAny<ApplicationUser>(), It.IsAny<Dictionary<string, int>>()))
|
private CatalogItem GetFakeCatalogItem()
|
||||||
.Returns(Task.FromResult(fakeBasket));
|
{
|
||||||
|
return new CatalogItem()
|
||||||
_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()
|
|
||||||
{
|
{
|
||||||
//Arrange
|
Id = 1,
|
||||||
var fakeCatalogItem = GetFakeCatalogItem();
|
Name = "fakeName",
|
||||||
|
CatalogBrand = "fakeBrand",
|
||||||
_basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny<ApplicationUser>(), It.IsAny<Int32>()))
|
CatalogType = "fakeType",
|
||||||
.Returns(Task.FromResult(1));
|
CatalogBrandId = 2,
|
||||||
|
CatalogTypeId = 5,
|
||||||
//Act
|
Price = 20
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" 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="5.0.2" />
|
<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.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="Microsoft.NETCore.Platforms" Version="5.0.0" />
|
<PackageReference Include="Microsoft.NETCore.Platforms" Version="5.0.0" />
|
||||||
<PackageReference Include="Moq" Version="4.15.2" />
|
<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>
|
||||||
|
|
||||||
<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.AzureStorage" Version="5.0.1" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" 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="AspNetCore.HealthChecks.UI.Client" Version="5.0.1" />
|
||||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.1.0" />
|
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.2.0-preview.1" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.1" />
|
|
||||||
<PackageReference Include="Azure.Identity" Version="1.4.0" />
|
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.14.0" />
|
<PackageReference Include="Google.Protobuf" Version="3.14.0" />
|
||||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.34.0" />
|
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.34.0" />
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.34.0" PrivateAssets="All" />
|
<PackageReference Include="Grpc.Tools" Version="2.34.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.16.0" />
|
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.18.0" />
|
||||||
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.16.0" />
|
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.18.0" />
|
||||||
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="1.1.3" />
|
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="2.0.2-beta2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" Version="1.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" Version="1.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="5.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.0-preview.7.21378.6" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="3.1.18" />
|
||||||
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" />
|
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" Version="6.0.0-preview.7.21378.6" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.1-dev-00216" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.11.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0-dev-00834" />
|
<PackageReference Include="Serilog.AspNetCore" Version="4.1.1-dev-00229" />
|
||||||
<PackageReference Include="Serilog.Sinks.Http" Version="7.2.0" />
|
<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="Serilog.Sinks.Seq" Version="4.1.0-dev-00166" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.1" />
|
||||||
<PackageReference Include="System.Data.SqlClient" Version="4.8.2" />
|
<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>
|
||||||
|
|
||||||
<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