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:
Sumit Ghosh 2021-10-20 18:53:30 +05:30 committed by GitHub
parent ab1d9cc897
commit c37320b3e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
329 changed files with 10962 additions and 12342 deletions

View File

@ -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; }
} }

View File

@ -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();
} }
} }

View File

@ -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");
}
} }
} }

View File

@ -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);
}
} }
} }

View File

@ -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

View 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.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;

View File

@ -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;
}
} }
} }
} }

View File

@ -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);
} }
} }

View File

@ -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;
}
} }
} }

View File

@ -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;
}
} }

View File

@ -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; }
} }

View File

@ -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; }
}
} }

View File

@ -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();
} }

View File

@ -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; }
} }

View File

@ -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;
}
} }
} }

View File

@ -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>();
}
} }
} }

View File

@ -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; }
}
}

View File

@ -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
} }

View File

@ -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)

View File

@ -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;
}
} }
} }

View File

@ -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
};
}
} }
} }

View File

@ -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);
}
} }

View File

@ -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);
}
} }

View File

@ -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);
}
} }

View File

@ -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);
}
}

View File

@ -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
});
}
} }
} }

View File

@ -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;
}
} }

View File

@ -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;
}
}

View File

@ -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; }
} }

View File

@ -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();
} }
} }

View File

@ -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");
}
} }
} }

View File

@ -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);
}
} }
} }

View File

@ -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" } };
}
};
} }
} }
} }
}
}

View 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;

View File

@ -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;
}
} }
} }
} }

View File

@ -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);
} }
} }

View File

@ -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;
}
} }
} }

View File

@ -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;
}
} }

View File

@ -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; }
} }

View File

@ -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; }
} }

View File

@ -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();
} }

View File

@ -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; }
} }

View File

@ -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;
}
}
} }

View File

@ -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>();
}
} }
} }

View File

@ -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; }
}
} }

View File

@ -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
}
} }

View File

@ -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

View File

@ -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;
} }
} }

View File

@ -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
};
}
} }
} }

View File

@ -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);
}
} }

View File

@ -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);
}
} }

View File

@ -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);
}
} }

View File

@ -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);
}
}

View File

@ -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
});
}
} }
} }

View File

@ -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;
}
} }

View File

@ -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;
} }
} }

View File

@ -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>

View File

@ -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);
} }
} }

View 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;

View File

@ -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;
}
} }
} }

View File

@ -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;
}
} }
} }

View File

@ -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);
}
} }

View File

@ -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;
}
} }

View File

@ -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
{
}
} }

View File

@ -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; }
} }

View File

@ -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();
} }
} }

View 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;

View File

@ -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>();
} }
}

View File

@ -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;
}
} }

View File

@ -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);
} }
} }

View File

@ -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();
}
} }

View File

@ -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);
}
}
} }

View 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;

View File

@ -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();
}
} }

View File

@ -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;
}
} }

View File

@ -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);
}
} }
} }
} }

View File

@ -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;

View File

@ -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; }
} }
}

View File

@ -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
}
} }

View File

@ -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;

View File

@ -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();
}
} }
} }

View File

@ -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>

View File

@ -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;
}
} }
} }

View File

@ -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);
}
} }

View File

@ -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);
}
} }

View File

@ -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(); });
}
});
}
} }
} }

View File

@ -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;

View File

@ -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);
}
} }
} }

View File

@ -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";
}
} }

View File

@ -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
{ {

View File

@ -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;
}
} }
} }

View File

@ -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">

View File

@ -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);
}
} }

View 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;

View File

@ -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
{ {

View File

@ -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() };
}
};
}
} }
} }

View File

@ -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
};
}
} }
} }

View File

@ -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" />

View 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;

View File

@ -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