Updates to .NET 6.0 (#1786)
* Seeking feedback: Build and run on .Net 6 preview 6 (#1734) * Updgrade build and hosting machines to .net6 latest * Target .net 6 * ILogger is ambiguous? * More ILogger ambiguity * Use preview 6... seeing errors in preview 7... * Of course the SDK version is different :) * downgrade the last nonworking component * Only restore the packages we need for the one off service stuck in .net 5 * Downgrade development docker files to use the preview 6 sdk * Updates `basket-api` to .NET 6 (#1742) * Use global usings * Use file-scoped namespaces * Updates docker images to preview 7 * Created a new migration plan * Included global usings for identity project * Updated docker file to preview version to 7 * Updated dockerfiles * Merged conent from Startup.cs to Program.cs * Removed Starup.cs * Removed unnecessary files * Revert "Removed unnecessary files" This reverts commit 536bddcd96b54673401cedbe802520dce12b3472. * Revert "Removed Starup.cs" This reverts commit 46175d7aa97475d88ec46bce39ed498c7037d924. * Revert "Merged conent from Startup.cs to Program.cs" This reverts commit 2766ea86dfef9220fe3f0c27a37a9a6c18153078. * Removed extra spaces * Updated basket-api project file * Update src/Services/Basket/Basket.API/Grpc/BasketService.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Moved the fully qualified namespace on top * Updated relevant packages in basket.api project * Updated relevant packages in identity.api project Co-authored-by: David Pine <david.pine@microsoft.com> * Updates all the services to .NET 6.0 (#1770) * Created global using file for catalog.api * Moved individual usings statements to globalusing * Updated catalog.api project * Fixed local run bug for catalog.api * Included globalusing for payment.api * Refactored namespace statement for payment.api * Moved namespaces to ordering.domain project * Included globalusing for ordering.domain project * Included globalusings for ordering.infrastructure project * Refactored namespaces for ordering.infrastructure project * Updated relevant packages in ordering.infrastructure project * Included globalusings for ordering.signalrHub project * Moved all the namespace to globalusings * Updated packages in ordering.signalrHub csproj file * Refactored namespace statements in catalog.api project * Fixed namespace name in ordering.domain * Included global usings for ordering.api project * Moved all usings to globalusing file * Updated ordering.api csproj project * Fixed bug in statup.cs * Updated ordering.unittests.csproj file * Included globalusings in webhooks.api project * Moved using statements to globalusing file in webhooks.api * Included globalusing for web.bff.shoppping aggregator project * Moved namespaces to globalusing shopping aggregator * Included globalusing mobile.bff.shoppping project * Moved namespaces to globalusing file * Included globalusing for eventbus project * Moved namespaces to global usings for eventbus * Included globalusing for EventBusRabbitMQ project * Moved using statements to EventBusRabbitMQ project * Included global using in EventBusServiceBus project * Moved using statements to globalusing for EventBusServiceBus * Included globalusing file for IntegrationEventLogEF project * Move using statements to globalusing file * Updated packages of IntegrationEventLogEF project * Included globalusing to Devspaces.Support project * Moved using statements to globalusing Devspaces * Updated dependent packages for Devspaces.Support.csproj * Fixed bug in Basket API * Fixed bug in catalog.api * Fixed bug Identity.API * Included globalusing to Basket.UnitTest project * Moved namespaces to Basket.UnitTest project * Updated packages of Basket.UnitTest csproj * Included globalusing for Basket.FunctionalTests project * Included file-scoped namespaces Basket.FunctionalTests * Updated packages of Basket.FunctionalTests.csproj file * Updated catalog unit test project to Net 6.0 * Included global usings for Catalog.FunctionalTests * Included file-scope namespace catalog.functionaltests * Updated packages of catalog.functionaltest csproj * Included MigrateDbContext method in HostExtensions * Included globalusing for ordering.UnitTests project * Included file-scope statement for Ordering.UnitTest project * Included globalusing for Ordering.FunctionalTests * Included file-scope namespace statement for using * Updated packages in Ordering.FunctionalTests.csproj * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.API/Startup.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToSubmittedIntegrationEventHandler.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> Co-authored-by: David Pine <david.pine@microsoft.com> * Updates WebMVC to .NET 6.0 (#1773) * Included globalusing WebMVC * Included file scope namespaces for all files * Updated dockerfile * Updated packages to WebMVC * Fixes few bugs in Net 6.0 service migration (#1774) * Created global using file for catalog.api * Moved individual usings statements to globalusing * Updated catalog.api project * Fixed local run bug for catalog.api * Included globalusing for payment.api * Refactored namespace statement for payment.api * Moved namespaces to ordering.domain project * Included globalusing for ordering.domain project * Included globalusings for ordering.infrastructure project * Refactored namespaces for ordering.infrastructure project * Updated relevant packages in ordering.infrastructure project * Included globalusings for ordering.signalrHub project * Moved all the namespace to globalusings * Updated packages in ordering.signalrHub csproj file * Refactored namespace statements in catalog.api project * Fixed namespace name in ordering.domain * Included global usings for ordering.api project * Moved all usings to globalusing file * Updated ordering.api csproj project * Fixed bug in statup.cs * Updated ordering.unittests.csproj file * Included globalusings in webhooks.api project * Moved using statements to globalusing file in webhooks.api * Included globalusing for web.bff.shoppping aggregator project * Moved namespaces to globalusing shopping aggregator * Included globalusing mobile.bff.shoppping project * Moved namespaces to globalusing file * Included globalusing for eventbus project * Moved namespaces to global usings for eventbus * Included globalusing for EventBusRabbitMQ project * Moved using statements to EventBusRabbitMQ project * Included global using in EventBusServiceBus project * Moved using statements to globalusing for EventBusServiceBus * Included globalusing file for IntegrationEventLogEF project * Move using statements to globalusing file * Updated packages of IntegrationEventLogEF project * Included globalusing to Devspaces.Support project * Moved using statements to globalusing Devspaces * Updated dependent packages for Devspaces.Support.csproj * Fixed bug in Basket API * Fixed bug in catalog.api * Fixed bug Identity.API * Included globalusing to Basket.UnitTest project * Moved namespaces to Basket.UnitTest project * Updated packages of Basket.UnitTest csproj * Included globalusing for Basket.FunctionalTests project * Included file-scoped namespaces Basket.FunctionalTests * Updated packages of Basket.FunctionalTests.csproj file * Updated catalog unit test project to Net 6.0 * Included global usings for Catalog.FunctionalTests * Included file-scope namespace catalog.functionaltests * Updated packages of catalog.functionaltest csproj * Included MigrateDbContext method in HostExtensions * Included globalusing for ordering.UnitTests project * Included file-scope statement for Ordering.UnitTest project * Included globalusing for Ordering.FunctionalTests * Included file-scope namespace statement for using * Updated packages in Ordering.FunctionalTests.csproj * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.API/Startup.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToSubmittedIntegrationEventHandler.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Update src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Apply suggestions from code review Co-authored-by: David Pine <david.pine@microsoft.com> * Fixed bugs in Mobile.BFF.Shopping project * Fixed bugs in Web.Bff.Shopping aggregator project * Fixed bugs in EventBusServiceBus project * Fixed bug in Mobile.Bff.Shopping project Co-authored-by: David Pine <david.pine@microsoft.com> * Updates webhook client project to .NET 6.0 (#1777) * Included globalusing file for webhookclient * Included file scope namespaces for Webhookclient * Updated packages in WebHookClient project * Updates webspa project to Net 6.0 (#1778) * Included globalusing in webspa project * Included file scoped namespace for webspa project * Updated packages in WebSPA project * Updates the Application.FunctionalTests project to .NET 6.0 (#1781) * Included globalusing in Application.FunctionalTests project * Included file scoped namespace * Renamed Azure.Messaging.ServiceBus namespace * Updates .NET version of Dockerfile to 6.0 (#1785) * Updatated package versions to RC2 * Updated package versions to RC2 * Updated Dockerfiles to .NET 6 RC2 * Changed docker file tag to 6.0 * Updated Program class * Updated globalusing file * Removed preview tag reference from Dockerfile.develop file * Updated dotnet version to .NET 6.0 * Updated all packages to the .NET 6.0 * Removed RC tag from dockerfile * Fixed bundleconfig json * Updated readme files * Fixed ingress yaml indentation * Included globalusing for WebStatus project * Updated WebStatus project to .NET 6.0 * Included scoped namespace * Updated Dockerfile of WebStatus to .NET 6.0 Co-authored-by: Josh Coleman <83677148+JcolemanNR@users.noreply.github.com> Co-authored-by: David Pine <david.pine@microsoft.com>
This commit is contained in:
parent
42abcad37e
commit
630cb35cfb
2
.github/workflows/catalog-api.yml
vendored
2
.github/workflows/catalog-api.yml
vendored
@ -22,7 +22,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
SERVICE: catalog-api
|
SERVICE: catalog-api
|
||||||
IMAGE: catalog.api
|
IMAGE: catalog.api
|
||||||
DOTNET_VERSION: 5.0.x
|
DOTNET_VERSION: 6.0.x
|
||||||
PROJECT_PATH: Services/Catalog/Catalog.API
|
PROJECT_PATH: Services/Catalog/Catalog.API
|
||||||
TESTS_PATH: Services/Catalog/Catalog.UnitTests
|
TESTS_PATH: Services/Catalog/Catalog.UnitTests
|
||||||
|
|
||||||
|
2
.github/workflows/ordering-api.yml
vendored
2
.github/workflows/ordering-api.yml
vendored
@ -22,7 +22,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
SERVICE: ordering-api
|
SERVICE: ordering-api
|
||||||
IMAGE: ordering.api
|
IMAGE: ordering.api
|
||||||
DOTNET_VERSION: 5.0.x
|
DOTNET_VERSION: 6.0.x
|
||||||
PROJECT_PATH: Services/Ordering/Ordering.API
|
PROJECT_PATH: Services/Ordering/Ordering.API
|
||||||
TESTS_PATH: Services/Ordering/Ordering.UnitTests
|
TESTS_PATH: Services/Ordering/Ordering.UnitTests
|
||||||
|
|
||||||
|
@ -72,9 +72,9 @@ In the future, more features will be implemented in the advanced scenario.
|
|||||||
**NEWS / ANNOUNCEMENTS**
|
**NEWS / ANNOUNCEMENTS**
|
||||||
Do you want to be up-to-date on .NET Architecture guidance and reference apps like eShopOnContainers? --> Subscribe by "WATCHING" this new GitHub repo: https://github.com/dotnet-architecture/News
|
Do you want to be up-to-date on .NET Architecture guidance and reference apps like eShopOnContainers? --> Subscribe by "WATCHING" this new GitHub repo: https://github.com/dotnet-architecture/News
|
||||||
|
|
||||||
## Updated for .NET 5
|
## Updated for .NET 6
|
||||||
|
|
||||||
eShopOnContainers is updated to .NET 5 "wave" of technologies. Not just compilation but also new recommended code in EF Core, ASP.NET Core, and other new related versions with several significant changes.
|
eShopOnContainers is updated to .NET 6 "wave" of technologies. Not just compilation but also new recommended code in EF Core, ASP.NET Core, and other new related versions with several significant changes.
|
||||||
|
|
||||||
**See more details in the [Release notes](https://github.com/dotnet-architecture/eShopOnContainers/wiki/Release-notes) wiki page**.
|
**See more details in the [Release notes](https://github.com/dotnet-architecture/eShopOnContainers/wiki/Release-notes) wiki page**.
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ eShopOnContainers is updated to .NET 5 "wave" of technologies. Not just compilat
|
|||||||
|
|
||||||
### Architecture overview
|
### Architecture overview
|
||||||
|
|
||||||
This reference application is cross-platform at the server and client-side, thanks to .NET 5 services capable of running on Linux or Windows containers depending on your Docker host, and to Xamarin for mobile apps running on Android, iOS, or Windows/UWP plus any browser for the client web apps.
|
This reference application is cross-platform at the server and client-side, thanks to .NET 6 services capable of running on Linux or Windows containers depending on your Docker host, and to Xamarin for mobile apps running on Android, iOS, or Windows/UWP plus any browser for the client web apps.
|
||||||
The architecture proposes a microservice oriented architecture implementation with multiple autonomous microservices (each one owning its own data/db) and implementing different approaches within each microservice (simple CRUD vs. DDD/CQRS patterns) using HTTP as the communication protocol between the client apps and the microservices and supports asynchronous communication for data updates propagation across multiple services based on Integration Events and an Event Bus (a light message broker, to choose between RabbitMQ or Azure Service Bus, underneath) plus other features defined at the [roadmap](https://github.com/dotnet-architecture/eShopOnContainers/wiki/Roadmap).
|
The architecture proposes a microservice oriented architecture implementation with multiple autonomous microservices (each one owning its own data/db) and implementing different approaches within each microservice (simple CRUD vs. DDD/CQRS patterns) using HTTP as the communication protocol between the client apps and the microservices and supports asynchronous communication for data updates propagation across multiple services based on Integration Events and an Event Bus (a light message broker, to choose between RabbitMQ or Azure Service Bus, underneath) plus other features defined at the [roadmap](https://github.com/dotnet-architecture/eShopOnContainers/wiki/Roadmap).
|
||||||
|
|
||||||

|

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