Compare commits
133 Commits
rebond/dev
...
dev
Author | SHA1 | Date | |
---|---|---|---|
|
47643eb2d0 | ||
|
7a5e502428 | ||
|
47d4d3cff6 | ||
|
becce91a7c | ||
|
06d5164532 | ||
|
67d4c06b6d | ||
|
508fa593d7 | ||
|
3b9560c26e | ||
|
c7c2d1ca2f | ||
|
5d13c097d3 | ||
|
42e161d1f9 | ||
|
fd76f390ef | ||
|
3169a93344 | ||
|
df4eb4c124 | ||
|
e4b94decb2 | ||
|
6f4ae509f1 | ||
|
dbef61fdaf | ||
|
6ebde3fb5c | ||
|
8f9f9d168f | ||
|
f5218e8087 | ||
|
72c8167ec0 | ||
|
8c9524c771 | ||
|
9c2b972cc9 | ||
|
3858d7ccf7 | ||
|
a87efdef32 | ||
|
52cdd1645a | ||
|
89a42e6c8f | ||
|
d5533c0ff9 | ||
|
8debda5c67 | ||
|
6ad5e8fb09 | ||
|
bdebee70dc | ||
|
184ed15ef6 | ||
|
af42aab95e | ||
|
6933d9997d | ||
|
992b58d4bc | ||
|
a7cdb1df68 | ||
|
0a07aea4ff | ||
|
8c2ca8dbd9 | ||
|
40315c69d7 | ||
|
62a6f17595 | ||
|
64a2d69e92 | ||
|
69476a3175 | ||
|
758d4bbe88 | ||
|
bd745121b2 | ||
|
84c6b11c69 | ||
|
561d48bc62 | ||
|
6a2fceb57c | ||
|
c3efea0293 | ||
|
30ed0011cb | ||
|
06d74d1658 | ||
|
50952bed10 | ||
|
f76a8c61db | ||
|
5a2d38575e | ||
|
670a9452e1 | ||
|
69b28c6add | ||
|
d0c710ebdc | ||
|
e1ec790ddf | ||
|
6b8992153a | ||
|
d086d278e8 | ||
|
3c00be38f9 | ||
|
031996d87c | ||
|
5ea034106c | ||
|
acd9a6d04b | ||
|
7027967568 | ||
|
e166b28a0a | ||
|
bcb1374d1e | ||
|
57d9baf106 | ||
|
a37b0430b2 | ||
|
2a4a6abf9b | ||
|
c41cd3830c | ||
|
0cb6b08300 | ||
|
9743c83221 | ||
|
6e69a2472e | ||
|
3357c70bc1 | ||
|
909f08675b | ||
|
a41560544c | ||
|
08e7c3424d | ||
|
7681405eaf | ||
|
ccaad9dc20 | ||
|
cf02e90aad | ||
|
5397e8d5c8 | ||
|
0fd20ee962 | ||
|
34fc9496fd | ||
|
a381a6923c | ||
|
3056418c92 | ||
|
917764273b | ||
|
e2d8590a26 | ||
|
f8abb36bc6 | ||
|
16b63001df | ||
|
83200f9331 | ||
|
fea08c78bb | ||
|
b9f48faf99 | ||
|
02c163246b | ||
|
da024f9812 | ||
|
8da0a81514 | ||
|
5342c86af0 | ||
|
3f5f0b94ed | ||
|
7d28625959 | ||
|
d96e4db08c | ||
|
c59e66861f | ||
|
56d47db91e | ||
|
b48ba7b74b | ||
|
41056e54d8 | ||
|
45a04e4a6d | ||
|
bff808016e | ||
|
48f640088b | ||
|
4e743ef666 | ||
|
366019aaa3 | ||
|
d4319bdd47 | ||
|
c565a8f799 | ||
|
794c546d2e | ||
|
f46b03cb36 | ||
|
d1372cba64 | ||
|
7da7e98a55 | ||
|
9d52426a49 | ||
|
57a93f63f0 | ||
|
8a40e9fb48 | ||
|
3fee612e68 | ||
|
00fc3d8a65 | ||
|
9af6d6342d | ||
|
e7e0eed9cc | ||
|
c7edd50b38 | ||
|
233b6e56c1 | ||
|
d4c2f17c36 | ||
|
746e5da7fa | ||
|
f3d2843166 | ||
|
91247ec52e | ||
|
d62ebcb791 | ||
|
109853983d | ||
|
d91da03665 | ||
|
746363bfd3 | ||
|
3ae0cefbcf | ||
|
451d79f7b9 |
BIN
gothrough docs.docx
Normal file
BIN
gothrough docs.docx
Normal file
Binary file not shown.
6
src/.env
6
src/.env
@ -6,15 +6,15 @@
|
||||
|
||||
# Use this values to run the app locally in Windows
|
||||
ESHOP_EXTERNAL_DNS_NAME_OR_IP=host.docker.internal
|
||||
ESHOP_STORAGE_CATALOG_URL=http://host.docker.internal:5202/c/api/v1/catalog/items/[0]/pic/
|
||||
ESHOP_STORAGE_CATALOG_URL=http://host.docker.internal:5121/c/api/v1/catalog/items/[0]/pic/
|
||||
|
||||
# Use this values to run the app locally in Mac
|
||||
# ESHOP_EXTERNAL_DNS_NAME_OR_IP=docker.for.mac.localhost
|
||||
# ESHOP_STORAGE_CATALOG_URL=http://docker.for.mac.localhost:5202/c/api/v1/catalog/items/[0]/pic/
|
||||
# ESHOP_STORAGE_CATALOG_URL=http://docker.for.mac.localhost:5121/c/api/v1/catalog/items/[0]/pic/
|
||||
|
||||
# Use this values to run the app locally in Linux
|
||||
# ESHOP_EXTERNAL_DNS_NAME_OR_IP=docker.for.linux.localhost
|
||||
# ESHOP_STORAGE_CATALOG_URL=http://docker.for.linux.localhost:5202/c/api/v1/catalog/items/[0]/pic/
|
||||
# ESHOP_STORAGE_CATALOG_URL=http://docker.for.linux.localhost:5121/c/api/v1/catalog/items/[0]/pic/
|
||||
|
||||
# Configure this values to the cloud storage locations
|
||||
# ESHOP_STORAGE_CATALOG_URL=<YourAzureStorage_Catalog_BLOB_URL>
|
||||
|
@ -1,139 +0,0 @@
|
||||
admin:
|
||||
access_log_path: "/dev/null"
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 8001
|
||||
static_resources:
|
||||
listeners:
|
||||
- address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 80
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.http_connection_manager
|
||||
config:
|
||||
codec_type: auto
|
||||
stat_prefix: ingress_http
|
||||
route_config:
|
||||
name: eshop_backend_route
|
||||
virtual_hosts:
|
||||
- name: eshop_backend
|
||||
domains:
|
||||
- "*"
|
||||
routes:
|
||||
- name: "c-short"
|
||||
match:
|
||||
prefix: "/c/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
prefix_rewrite: "/catalog-api/"
|
||||
cluster: catalog
|
||||
- name: "c-long"
|
||||
match:
|
||||
prefix: "/catalog-api/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
cluster: catalog
|
||||
- name: "o-short"
|
||||
match:
|
||||
prefix: "/o/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
prefix_rewrite: "/ordering-api/"
|
||||
cluster: ordering
|
||||
- name: "o-long"
|
||||
match:
|
||||
prefix: "/ordering-api/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
cluster: ordering
|
||||
- name: "h-long"
|
||||
match:
|
||||
prefix: "/hub/notificationhub"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
cluster: signalr-hub
|
||||
timeout: 300s
|
||||
- name: "b-short"
|
||||
match:
|
||||
prefix: "/b/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
prefix_rewrite: "/basket-api/"
|
||||
cluster: basket
|
||||
- name: "b-long"
|
||||
match:
|
||||
prefix: "/basket-api/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
cluster: basket
|
||||
- name: "agg"
|
||||
match:
|
||||
prefix: "/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
prefix_rewrite: "/"
|
||||
cluster: shoppingagg
|
||||
http_filters:
|
||||
- name: envoy.router
|
||||
access_log:
|
||||
- name: envoy.file_access_log
|
||||
filter:
|
||||
not_health_check_filter: {}
|
||||
config:
|
||||
json_format:
|
||||
time: "%START_TIME%"
|
||||
protocol: "%PROTOCOL%"
|
||||
duration: "%DURATION%"
|
||||
request_method: "%REQ(:METHOD)%"
|
||||
request_host: "%REQ(HOST)%"
|
||||
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
|
||||
response_flags: "%RESPONSE_FLAGS%"
|
||||
route_name: "%ROUTE_NAME%"
|
||||
upstream_host: "%UPSTREAM_HOST%"
|
||||
upstream_cluster: "%UPSTREAM_CLUSTER%"
|
||||
upstream_local_address: "%UPSTREAM_LOCAL_ADDRESS%"
|
||||
path: "/tmp/access.log"
|
||||
clusters:
|
||||
- name: shoppingagg
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: mobileshoppingagg
|
||||
port_value: 80
|
||||
- name: catalog
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: catalog-api
|
||||
port_value: 80
|
||||
- name: basket
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: basket-api
|
||||
port_value: 80
|
||||
- name: ordering
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: ordering-api
|
||||
port_value: 80
|
||||
- name: signalr-hub
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: ordering-signalrhub
|
||||
port_value: 80
|
@ -1,142 +0,0 @@
|
||||
admin:
|
||||
access_log_path: "/dev/null"
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 8001
|
||||
static_resources:
|
||||
listeners:
|
||||
- address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 80
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.http_connection_manager
|
||||
config:
|
||||
codec_type: auto
|
||||
stat_prefix: ingress_http
|
||||
route_config:
|
||||
name: eshop_backend_route
|
||||
virtual_hosts:
|
||||
- name: eshop_backend
|
||||
domains:
|
||||
- "*"
|
||||
routes:
|
||||
- name: "c-short"
|
||||
match:
|
||||
prefix: "/c/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
prefix_rewrite: "/catalog-api/"
|
||||
cluster: catalog
|
||||
- name: "c-long"
|
||||
match:
|
||||
prefix: "/catalog-api/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
cluster: catalog
|
||||
- name: "o-short"
|
||||
match:
|
||||
prefix: "/o/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
prefix_rewrite: "/ordering-api/"
|
||||
cluster: ordering
|
||||
- name: "o-long"
|
||||
match:
|
||||
prefix: "/ordering-api/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
cluster: ordering
|
||||
- name: "h-long"
|
||||
match:
|
||||
prefix: "/hub/notificationhub"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
cluster: signalr-hub
|
||||
timeout: 300s
|
||||
upgrade_configs:
|
||||
upgrade_type: "websocket"
|
||||
enabled: true
|
||||
- name: "b-short"
|
||||
match:
|
||||
prefix: "/b/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
prefix_rewrite: "/basket-api/"
|
||||
cluster: basket
|
||||
- name: "b-long"
|
||||
match:
|
||||
prefix: "/basket-api/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
cluster: basket
|
||||
- name: "agg"
|
||||
match:
|
||||
prefix: "/"
|
||||
route:
|
||||
auto_host_rewrite: true
|
||||
prefix_rewrite: "/"
|
||||
cluster: shoppingagg
|
||||
http_filters:
|
||||
- name: envoy.router
|
||||
access_log:
|
||||
- name: envoy.file_access_log
|
||||
filter:
|
||||
not_health_check_filter: {}
|
||||
config:
|
||||
json_format:
|
||||
time: "%START_TIME%"
|
||||
protocol: "%PROTOCOL%"
|
||||
duration: "%DURATION%"
|
||||
request_method: "%REQ(:METHOD)%"
|
||||
request_host: "%REQ(HOST)%"
|
||||
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
|
||||
response_flags: "%RESPONSE_FLAGS%"
|
||||
route_name: "%ROUTE_NAME%"
|
||||
upstream_host: "%UPSTREAM_HOST%"
|
||||
upstream_cluster: "%UPSTREAM_CLUSTER%"
|
||||
upstream_local_address: "%UPSTREAM_LOCAL_ADDRESS%"
|
||||
path: "/tmp/access.log"
|
||||
clusters:
|
||||
- name: shoppingagg
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: webshoppingagg
|
||||
port_value: 80
|
||||
- name: catalog
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: catalog-api
|
||||
port_value: 80
|
||||
- name: basket
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: basket-api
|
||||
port_value: 80
|
||||
- name: ordering
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: ordering-api
|
||||
port_value: 80
|
||||
- name: signalr-hub
|
||||
connect_timeout: 0.25s
|
||||
type: strict_dns
|
||||
lb_policy: round_robin
|
||||
hosts:
|
||||
- socket_address:
|
||||
address: ordering-signalrhub
|
||||
port_value: 80
|
@ -16,8 +16,7 @@ public class BasketController : ControllerBase
|
||||
|
||||
[HttpPost]
|
||||
[HttpPut]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
|
||||
{
|
||||
if (data.Items == null || !data.Items.Any())
|
||||
@ -73,8 +72,7 @@ public class BasketController : ControllerBase
|
||||
|
||||
[HttpPut]
|
||||
[Route("items")]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
|
||||
{
|
||||
if (!data.Updates.Any())
|
||||
@ -110,8 +108,8 @@ public class BasketController : ControllerBase
|
||||
|
||||
[HttpPost]
|
||||
[Route("items")]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data)
|
||||
{
|
||||
if (data == null || data.Quantity == 0)
|
||||
|
@ -1,11 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers;
|
||||
|
||||
[Route("")]
|
||||
public class HomeController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return new RedirectResult("~/swagger");
|
||||
}
|
||||
}
|
@ -16,8 +16,7 @@ public class OrderController : ControllerBase
|
||||
|
||||
[Route("draft/{basketId}")]
|
||||
[HttpGet]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(basketId))
|
||||
|
@ -32,6 +32,8 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj"
|
||||
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj"
|
||||
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj"
|
||||
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj"
|
||||
COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj"
|
||||
COPY "Services/Contact/Contact.API/Contact.API.csproj" "Services/Contact/Contact.API/Contact.API.csproj"
|
||||
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
|
||||
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj"
|
||||
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj"
|
||||
|
@ -0,0 +1,62 @@
|
||||
internal static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy"));
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" })
|
||||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" })
|
||||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" })
|
||||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" });
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
||||
{
|
||||
// Register delegating handlers
|
||||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
|
||||
|
||||
// Register http services
|
||||
services.AddHttpClient<IOrderApiClient, OrderApiClient>()
|
||||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
|
||||
|
||||
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<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) =>
|
||||
{
|
||||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
|
||||
options.Address = new Uri(orderingApi);
|
||||
}).AddInterceptor<GrpcExceptionInterceptor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters
|
||||
{
|
||||
namespace Basket.API.Infrastructure.Filters
|
||||
{
|
||||
public class AuthorizeCheckOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
// Check for authorize attribute
|
||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
||||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
|
||||
|
||||
if (!hasAuthorize) return;
|
||||
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
var oAuthScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +1,13 @@
|
||||
global using CatalogApi;
|
||||
global using Grpc.Core.Interceptors;
|
||||
global using System.Text.Json;
|
||||
global using CatalogApi;
|
||||
global using Grpc.Core;
|
||||
global using Grpc.Core.Interceptors;
|
||||
global using GrpcBasket;
|
||||
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;
|
||||
global using Microsoft.IdentityModel.Tokens;
|
||||
global using Services.Common;
|
||||
|
@ -28,7 +28,7 @@ public class GrpcExceptionInterceptor : Interceptor
|
||||
}
|
||||
catch (RpcException e)
|
||||
{
|
||||
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message);
|
||||
_logger.LogError(e, "Error calling via gRPC: {Status}", e.Status);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure;
|
||||
|
||||
public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger;
|
||||
|
||||
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<HttpClientAuthorizationDelegatingHandler> logger)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = 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))
|
||||
{
|
||||
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()
|
||||
{
|
||||
const string ACCESS_TOKEN = "access_token";
|
||||
|
||||
return await _httpContextAccessor.HttpContext
|
||||
.GetTokenAsync(ACCESS_TOKEN);
|
||||
}
|
||||
}
|
@ -5,29 +5,19 @@
|
||||
<AssemblyName>Mobile.Shopping.HttpAggregator</AssemblyName>
|
||||
<RootNamespace>Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator</RootNamespace>
|
||||
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
|
||||
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" />
|
||||
<PackageReference Include="Google.Protobuf" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" />
|
||||
<PackageReference Include="Grpc.Core" />
|
||||
<PackageReference Include="Grpc.Net.ClientFactory" />
|
||||
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
public class UpdateBasketItemsRequest
|
||||
{
|
||||
|
||||
public string BasketId { get; set; }
|
||||
|
||||
public ICollection<UpdateBasketItemData> Updates { get; set; }
|
||||
|
@ -1,23 +1,24 @@
|
||||
await BuildWebHost(args).RunAsync();
|
||||
IWebHost BuildWebHost(string[] args) =>
|
||||
WebHost
|
||||
.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration(cb =>
|
||||
{
|
||||
var sources = cb.Sources;
|
||||
sources.Insert(3, new Microsoft.Extensions.Configuration.Json.JsonConfigurationSource()
|
||||
{
|
||||
Optional = true,
|
||||
Path = "appsettings.localhost.json",
|
||||
ReloadOnChange = false
|
||||
});
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
.UseSerilog((builderContext, config) =>
|
||||
{
|
||||
config
|
||||
.MinimumLevel.Information()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console();
|
||||
})
|
||||
.Build();
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
builder.Services.AddReverseProxy(builder.Configuration);
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddHealthChecks(builder.Configuration);
|
||||
|
||||
builder.Services.AddApplicationServices();
|
||||
builder.Services.AddGrpcServices();
|
||||
|
||||
builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls"));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseServiceDefaults();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.MapControllers();
|
||||
app.MapReverseProxy();
|
||||
|
||||
await app.RunAsync();
|
||||
|
@ -5,5 +5,4 @@ public interface IBasketService
|
||||
Task<BasketData> GetByIdAsync(string id);
|
||||
|
||||
Task UpdateAsync(BasketData currentBasket);
|
||||
|
||||
}
|
||||
|
@ -23,9 +23,6 @@ public class OrderApiClient : IOrderApiClient
|
||||
|
||||
var ordersDraftResponse = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, JsonDefaults.CaseInsensitiveOptions);
|
||||
}
|
||||
}
|
||||
|
@ -1,210 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("self", () => HealthCheckResult.Healthy())
|
||||
.AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" })
|
||||
.AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" })
|
||||
.AddUrlGroup(new Uri(Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" })
|
||||
.AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" })
|
||||
.AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" });
|
||||
|
||||
services.AddCustomMvc(Configuration)
|
||||
.AddCustomAuthentication(Configuration)
|
||||
.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 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";
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateAudience = false
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
public static IServiceCollection AddCustomAuthorization(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("ApiScope", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim("scope", "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>();
|
||||
|
||||
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<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) =>
|
||||
{
|
||||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
|
||||
options.Address = new Uri(orderingApi);
|
||||
}).AddInterceptor<GrpcExceptionInterceptor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +1,138 @@
|
||||
{
|
||||
"Logging": {
|
||||
"Debug": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"System.Net.Http": "Warning"
|
||||
}
|
||||
},
|
||||
"OpenApi": {
|
||||
"Endpoint": {
|
||||
"Name": "Purchase BFF V1"
|
||||
},
|
||||
"Document": {
|
||||
"Description": "Shopping Aggregator for Mobile Clients",
|
||||
"Title": "Shopping Aggregator for Mobile Clients",
|
||||
"Version": "v1"
|
||||
},
|
||||
"Auth": {
|
||||
"ClientId": "mobileshoppingaggswaggerui",
|
||||
"AppName": "Mobile shopping BFF Swagger UI"
|
||||
}
|
||||
},
|
||||
"Identity": {
|
||||
"Url": "http://localhost:5223",
|
||||
"Audience": "mobileshoppingagg",
|
||||
"Scopes": {
|
||||
"webshoppingagg": "Shopping Aggregator for Mobile Clients"
|
||||
}
|
||||
},
|
||||
"ReverseProxy": {
|
||||
"Routes": {
|
||||
"c-short": {
|
||||
"ClusterId": "catalog",
|
||||
"Match": {
|
||||
"Path": "c/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/c" }
|
||||
]
|
||||
},
|
||||
"c-long": {
|
||||
"ClusterId": "catalog",
|
||||
"Match": {
|
||||
"Path": "catalog-api/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/catalog-api" }
|
||||
]
|
||||
},
|
||||
"b-short": {
|
||||
"ClusterId": "basket",
|
||||
"Match": {
|
||||
"Path": "b/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/b" }
|
||||
]
|
||||
},
|
||||
"b-long": {
|
||||
"ClusterId": "basket",
|
||||
"Match": {
|
||||
"Path": "basket-api/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/basket-api" }
|
||||
]
|
||||
},
|
||||
"o-short": {
|
||||
"ClusterId": "orders",
|
||||
"Match": {
|
||||
"Path": "o/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/o" }
|
||||
]
|
||||
},
|
||||
"o-long": {
|
||||
"ClusterId": "orders",
|
||||
"Match": {
|
||||
"Path": "ordering-api/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/ordering-api" }
|
||||
]
|
||||
},
|
||||
"h-long": {
|
||||
"ClusterId": "signalr",
|
||||
"Match": {
|
||||
"Path": "hub/notificationhub/{**catch-all}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
"Clusters": {
|
||||
"basket": {
|
||||
"Destinations": {
|
||||
"destination0": {
|
||||
"Address": "http://localhost:5221"
|
||||
}
|
||||
}
|
||||
},
|
||||
"catalog": {
|
||||
"Destinations": {
|
||||
"destination0": {
|
||||
"Address": "http://localhost:5222"
|
||||
}
|
||||
}
|
||||
},
|
||||
"orders": {
|
||||
"Destinations": {
|
||||
"destination0": {
|
||||
"Address": "http://localhost:5224"
|
||||
}
|
||||
}
|
||||
},
|
||||
"signalr": {
|
||||
"Destinations": {
|
||||
"destination0": {
|
||||
"Address": "http://localhost:5225"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Urls": {
|
||||
"Basket": "http://localhost:5221",
|
||||
"Catalog": "http://localhost:5222",
|
||||
"Orders": "http://localhost:5224",
|
||||
"Identity": "http://localhost:5223",
|
||||
"Signalr": "http://localhost:5225",
|
||||
"GrpcBasket": "http://localhost:6221",
|
||||
"GrpcCatalog": "http://localhost:6222",
|
||||
"GrpcOrdering": "http://localhost:6224"
|
||||
},
|
||||
"CatalogUrlHC": "http://localhost:5222/hc",
|
||||
"OrderingUrlHC": "http://localhost:5224/hc",
|
||||
"BasketUrlHC": "http://localhost:5221/hc",
|
||||
"IdentityUrlHC": "http://localhost:5223/hc"
|
||||
}
|
||||
|
@ -8,8 +8,10 @@
|
||||
"grpcCatalog": "http://localhost:81",
|
||||
"grpcOrdering": "http://localhost:5581"
|
||||
},
|
||||
"IdentityUrlExternal": "http://localhost:5105",
|
||||
"IdentityUrl": "http://localhost:5105",
|
||||
"Identity": {
|
||||
"ExternalUrl": "http://localhost:5105",
|
||||
"Url": "http://localhost:5105",
|
||||
},
|
||||
"Logging": {
|
||||
"Debug": {
|
||||
"IncludeScopes": false,
|
||||
|
@ -16,8 +16,7 @@ public class BasketController : ControllerBase
|
||||
|
||||
[HttpPost]
|
||||
[HttpPut]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
|
||||
{
|
||||
if (data.Items == null || !data.Items.Any())
|
||||
@ -74,8 +73,7 @@ public class BasketController : ControllerBase
|
||||
|
||||
[HttpPut]
|
||||
[Route("items")]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
|
||||
{
|
||||
if (!data.Updates.Any())
|
||||
@ -109,8 +107,8 @@ public class BasketController : ControllerBase
|
||||
|
||||
[HttpPost]
|
||||
[Route("items")]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data)
|
||||
{
|
||||
if (data == null || data.Quantity == 0)
|
||||
|
@ -1,11 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers;
|
||||
|
||||
[Route("")]
|
||||
public class HomeController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return new RedirectResult("~/swagger");
|
||||
}
|
||||
}
|
@ -16,8 +16,7 @@ public class OrderController : ControllerBase
|
||||
|
||||
[Route("draft/{basketId}")]
|
||||
[HttpGet]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(basketId))
|
||||
|
@ -32,6 +32,8 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj"
|
||||
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj"
|
||||
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj"
|
||||
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj"
|
||||
COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj"
|
||||
COPY "Services/Contact/Contact.API/Contact.API.csproj" "Services/Contact/Contact.API/Contact.API.csproj"
|
||||
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
|
||||
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj"
|
||||
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj"
|
||||
|
@ -0,0 +1,63 @@
|
||||
internal static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy"));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" })
|
||||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" })
|
||||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" })
|
||||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" });
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
||||
{
|
||||
// Register delegating handlers
|
||||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
|
||||
|
||||
// Register http services
|
||||
services.AddHttpClient<IOrderApiClient, OrderApiClient>()
|
||||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
|
||||
|
||||
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<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) =>
|
||||
{
|
||||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
|
||||
options.Address = new Uri(orderingApi);
|
||||
}).AddInterceptor<GrpcExceptionInterceptor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters
|
||||
{
|
||||
namespace Basket.API.Infrastructure.Filters
|
||||
{
|
||||
public class AuthorizeCheckOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
// Check for authorize attribute
|
||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
||||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
|
||||
|
||||
if (!hasAuthorize) return;
|
||||
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
var oAuthScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
[ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,40 +1,13 @@
|
||||
global using CatalogApi;
|
||||
global using Grpc.Core.Interceptors;
|
||||
global using System.Text.Json;
|
||||
global using CatalogApi;
|
||||
global using Grpc.Core;
|
||||
global using Grpc.Core.Interceptors;
|
||||
global using GrpcBasket;
|
||||
global using HealthChecks.UI.Client;
|
||||
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;
|
||||
global using Microsoft.IdentityModel.Tokens;
|
||||
global using Serilog.Context;
|
||||
global using Services.Common;
|
||||
|
@ -28,7 +28,7 @@ public class GrpcExceptionInterceptor : Interceptor
|
||||
}
|
||||
catch (RpcException e)
|
||||
{
|
||||
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message);
|
||||
_logger.LogError(e, "Error calling via gRPC: {Status}", e.Status);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure;
|
||||
|
||||
public class HttpClientAuthorizationDelegatingHandler
|
||||
: DelegatingHandler
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var authorizationHeader = _httpContextAccessor.HttpContext
|
||||
.Request.Headers["Authorization"];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(authorizationHeader))
|
||||
{
|
||||
request.Headers.Add("Authorization", new List<string>() { authorizationHeader });
|
||||
}
|
||||
|
||||
var token = await GetTokenAsync();
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
Task<string> GetTokenAsync()
|
||||
{
|
||||
const string ACCESS_TOKEN = "access_token";
|
||||
|
||||
return _httpContextAccessor.HttpContext
|
||||
.GetTokenAsync(ACCESS_TOKEN);
|
||||
}
|
||||
}
|
@ -1,207 +1,38 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration));
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck("self", () => HealthCheckResult.Healthy())
|
||||
.AddUrlGroup(new Uri(builder.Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" })
|
||||
.AddUrlGroup(new Uri(builder.Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" })
|
||||
.AddUrlGroup(new Uri(builder.Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" })
|
||||
.AddUrlGroup(new Uri(builder.Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" })
|
||||
.AddUrlGroup(new Uri(builder.Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" });
|
||||
builder.Services.AddCustomMvc(builder.Configuration)
|
||||
.AddCustomAuthentication(builder.Configuration)
|
||||
.AddApplicationServices()
|
||||
.AddGrpcServices();
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
builder.Services.AddReverseProxy(builder.Configuration);
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddHealthChecks(builder.Configuration);
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
// TODO: Read allowed origins from configuration
|
||||
options.AddPolicy("CorsPolicy",
|
||||
builder => builder
|
||||
.SetIsOriginAllowed((host) => true)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials());
|
||||
});
|
||||
|
||||
builder.Services.AddApplicationServices();
|
||||
builder.Services.AddGrpcServices();
|
||||
|
||||
builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls"));
|
||||
|
||||
var app = builder.Build();
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Home/Error");
|
||||
}
|
||||
var pathBase = builder.Configuration["PATH_BASE"];
|
||||
if (!string.IsNullOrEmpty(pathBase))
|
||||
{
|
||||
app.UsePathBase(pathBase);
|
||||
}
|
||||
|
||||
app.UseServiceDefaults();
|
||||
|
||||
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.MapDefaultControllerRoute();
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/hc", new HealthCheckOptions()
|
||||
{
|
||||
Predicate = _ => true,
|
||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
||||
});
|
||||
app.MapHealthChecks("/liveness", new HealthCheckOptions
|
||||
{
|
||||
Predicate = r => r.Name.Contains("self")
|
||||
});
|
||||
app.MapReverseProxy();
|
||||
|
||||
try
|
||||
{
|
||||
Log.Information("Starts Web Application ({ApplicationContext})...", Program.AppName);
|
||||
await app.RunAsync();
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
|
||||
Serilog.ILogger CreateSerilogLogger(IConfiguration configuration)
|
||||
{
|
||||
var seqServerUrl = configuration["Serilog:SeqServerUrl"];
|
||||
var logstashUrl = configuration["Serilog:LogstashgUrl"];
|
||||
return new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.Enrich.WithProperty("ApplicationContext", Program.AppName)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
}
|
||||
public partial class Program
|
||||
{
|
||||
|
||||
public static string Namespace = typeof(Program).Assembly.GetName().Name;
|
||||
public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1);
|
||||
}
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
|
||||
|
||||
var identityUrl = configuration.GetValue<string>("urls:identity");
|
||||
services.AddAuthentication("Bearer")
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = identityUrl;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.Audience = "webshoppingagg";
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateAudience = false
|
||||
};
|
||||
});
|
||||
|
||||
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.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "Shopping Aggregator for Web Clients",
|
||||
Version = "v1",
|
||||
Description = "Shopping Aggregator for Web Clients"
|
||||
});
|
||||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows()
|
||||
{
|
||||
Implicit = new OpenApiOAuthFlow()
|
||||
{
|
||||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
|
||||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>()
|
||||
{
|
||||
{ "webshoppingagg", "Shopping Aggregator for Web Clients" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
||||
});
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("CorsPolicy",
|
||||
builder => builder
|
||||
.SetIsOriginAllowed((host) => true)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials());
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
||||
{
|
||||
//register delegating handlers
|
||||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
|
||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
|
||||
//register http services
|
||||
|
||||
services.AddHttpClient<IOrderApiClient, OrderApiClient>()
|
||||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
|
||||
|
||||
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<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) =>
|
||||
{
|
||||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
|
||||
options.Address = new Uri(orderingApi);
|
||||
}).AddInterceptor<GrpcExceptionInterceptor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
await app.RunAsync();
|
||||
|
@ -1,29 +1,12 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:57425/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"Web.Shopping.HttpAggregator": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "api/values",
|
||||
"applicationUrl": "http://localhost:5229/",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"PurchaseForMvc": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "api/values",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:61632/"
|
||||
}
|
||||
}
|
||||
}
|
@ -23,9 +23,6 @@ public class OrderApiClient : IOrderApiClient
|
||||
|
||||
var ordersDraftResponse = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, JsonDefaults.CaseInsensitiveOptions);
|
||||
}
|
||||
}
|
||||
|
@ -4,31 +4,20 @@
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<AssemblyName>Web.Shopping.HttpAggregator</AssemblyName>
|
||||
<RootNamespace>Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
|
||||
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" />
|
||||
<PackageReference Include="Google.Protobuf" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" />
|
||||
<PackageReference Include="Grpc.Core" />
|
||||
<PackageReference Include="Grpc.Net.Client" />
|
||||
<PackageReference Include="Grpc.Net.ClientFactory" />
|
||||
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,16 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"Debug": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug"
|
||||
}
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug"
|
||||
}
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,138 @@
|
||||
{
|
||||
"Logging": {
|
||||
"Debug": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"System.Net.Http": "Warning"
|
||||
}
|
||||
},
|
||||
"OpenApi": {
|
||||
"Endpoint": {
|
||||
"Name": "Purchase BFF V1"
|
||||
},
|
||||
"Document": {
|
||||
"Description": "Shopping Aggregator for Web Clients",
|
||||
"Title": "Shopping Aggregator for Web Clients",
|
||||
"Version": "v1"
|
||||
},
|
||||
"Auth": {
|
||||
"ClientId": "webshoppingaggswaggerui",
|
||||
"AppName": "Web Shopping BFF Swagger UI"
|
||||
}
|
||||
},
|
||||
"Identity": {
|
||||
"Url": "http://localhost:5223",
|
||||
"Audience": "webshoppingagg",
|
||||
"Scopes": {
|
||||
"webshoppingagg": "Shopping Aggregator for Web Clients"
|
||||
}
|
||||
},
|
||||
"ReverseProxy": {
|
||||
"Routes": {
|
||||
"c-short": {
|
||||
"ClusterId": "catalog",
|
||||
"Match": {
|
||||
"Path": "c/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/c" }
|
||||
]
|
||||
},
|
||||
"c-long": {
|
||||
"ClusterId": "catalog",
|
||||
"Match": {
|
||||
"Path": "catalog-api/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/catalog-api" }
|
||||
]
|
||||
},
|
||||
"b-short": {
|
||||
"ClusterId": "basket",
|
||||
"Match": {
|
||||
"Path": "b/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/b" }
|
||||
]
|
||||
},
|
||||
"b-long": {
|
||||
"ClusterId": "basket",
|
||||
"Match": {
|
||||
"Path": "basket-api/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/basket-api" }
|
||||
]
|
||||
},
|
||||
"o-short": {
|
||||
"ClusterId": "orders",
|
||||
"Match": {
|
||||
"Path": "o/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/o" }
|
||||
]
|
||||
},
|
||||
"o-long": {
|
||||
"ClusterId": "orders",
|
||||
"Match": {
|
||||
"Path": "ordering-api/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathRemovePrefix": "/ordering-api" }
|
||||
]
|
||||
},
|
||||
"h-long": {
|
||||
"ClusterId": "signalr",
|
||||
"Match": {
|
||||
"Path": "hub/notificationhub/{**catch-all}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
"Clusters": {
|
||||
"basket": {
|
||||
"Destinations": {
|
||||
"destination0": {
|
||||
"Address": "http://localhost:5221"
|
||||
}
|
||||
}
|
||||
},
|
||||
"catalog": {
|
||||
"Destinations": {
|
||||
"destination0": {
|
||||
"Address": "http://localhost:5222"
|
||||
}
|
||||
}
|
||||
},
|
||||
"orders": {
|
||||
"Destinations": {
|
||||
"destination0": {
|
||||
"Address": "http://localhost:5224"
|
||||
}
|
||||
}
|
||||
},
|
||||
"signalr": {
|
||||
"Destinations": {
|
||||
"destination0": {
|
||||
"Address": "http://localhost:5225"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Urls": {
|
||||
"Basket": "http://localhost:5221",
|
||||
"Catalog": "http://localhost:5222",
|
||||
"Orders": "http://localhost:5224",
|
||||
"Identity": "http://localhost:5223",
|
||||
"Signalr": "http://localhost:5225",
|
||||
"GrpcBasket": "http://localhost:6221",
|
||||
"GrpcCatalog": "http://localhost:6222",
|
||||
"GrpcOrdering": "http://localhost:6224"
|
||||
},
|
||||
"CatalogUrlHC": "http://localhost:5222/hc",
|
||||
"OrderingUrlHC": "http://localhost:5224/hc",
|
||||
"BasketUrlHC": "http://localhost:5221/hc",
|
||||
"IdentityUrlHC": "http://localhost:5223/hc"
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"urls": {
|
||||
"basket": "http://localhost:55103",
|
||||
"catalog": "http://localhost:55101",
|
||||
"orders": "http://localhost:55102",
|
||||
"identity": "http://localhost:55105",
|
||||
"grpcBasket": "http://localhost:5580",
|
||||
"grpcCatalog": "http://localhost:81",
|
||||
"grpcOrdering": "http://localhost:5581"
|
||||
}
|
||||
}
|
@ -12,9 +12,10 @@ namespace EventBus.Tests
|
||||
Handled = false;
|
||||
}
|
||||
|
||||
public async Task Handle(TestIntegrationEvent @event)
|
||||
public Task Handle(TestIntegrationEvent @event)
|
||||
{
|
||||
Handled = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,10 @@ namespace EventBus.Tests
|
||||
Handled = false;
|
||||
}
|
||||
|
||||
public async Task Handle(TestIntegrationEvent @event)
|
||||
public Task Handle(TestIntegrationEvent @event)
|
||||
{
|
||||
Handled = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,6 @@ public interface IEventBus
|
||||
where T : IntegrationEvent
|
||||
where TH : IIntegrationEventHandler<T>;
|
||||
|
||||
void SubscribeDynamic<TH>(string eventName)
|
||||
where TH : IDynamicIntegrationEventHandler;
|
||||
|
||||
void UnsubscribeDynamic<TH>(string eventName)
|
||||
where TH : IDynamicIntegrationEventHandler;
|
||||
|
||||
void Unsubscribe<T, TH>()
|
||||
where TH : IIntegrationEventHandler<T>
|
||||
where T : IntegrationEvent;
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBus</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -1,8 +1,4 @@
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||
global using System.Text.Json.Serialization;
|
||||
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;
|
||||
|
@ -59,7 +59,7 @@ public class DefaultRabbitMQPersistentConnection
|
||||
.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);
|
||||
_logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s", $"{time.TotalSeconds:n1}");
|
||||
}
|
||||
);
|
||||
|
||||
@ -81,7 +81,7 @@ public class DefaultRabbitMQPersistentConnection
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened");
|
||||
_logger.LogCritical("Fatal error: RabbitMQ connections could not be created and opened");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
public class EventBusRabbitMQ : IEventBus, IDisposable
|
||||
{
|
||||
const string BROKER_NAME = "eshop_event_bus";
|
||||
const string AUTOFAC_SCOPE_NAME = "eshop_event_bus";
|
||||
|
||||
private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true };
|
||||
private static readonly JsonSerializerOptions s_caseInsensitiveOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
private readonly IRabbitMQPersistentConnection _persistentConnection;
|
||||
private readonly ILogger<EventBusRabbitMQ> _logger;
|
||||
@ -58,7 +60,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
|
||||
.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);
|
||||
_logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s", @event.Id, $"{time.TotalSeconds:n1}");
|
||||
});
|
||||
|
||||
var eventName = @event.GetType().Name;
|
||||
@ -70,10 +72,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
|
||||
|
||||
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct");
|
||||
|
||||
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), s_indentedOptions);
|
||||
|
||||
policy.Execute(() =>
|
||||
{
|
||||
@ -194,7 +193,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", message);
|
||||
_logger.LogWarning(ex, "Error Processing message \"{Message}\"", message);
|
||||
}
|
||||
|
||||
// Even on exception we take the message off the queue.
|
||||
@ -257,7 +256,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
|
||||
var handler = scope.ServiceProvider.GetService(subscription.HandlerType);
|
||||
if (handler == null) continue;
|
||||
var eventType = _subsManager.GetEventTypeByName(eventName);
|
||||
var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
|
||||
var integrationEvent = JsonSerializer.Deserialize(message, eventType, s_caseInsensitiveOptions);
|
||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
||||
|
||||
await Task.Yield();
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" />
|
||||
<PackageReference Include="Microsoft.CSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Polly" />
|
||||
|
@ -1,17 +1,13 @@
|
||||
global using Microsoft.Extensions.Logging;
|
||||
global using System.Net.Sockets;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
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 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,4 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
|
||||
|
||||
@ -12,7 +12,6 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable
|
||||
private readonly string _subscriptionName;
|
||||
private readonly ServiceBusSender _sender;
|
||||
private readonly ServiceBusProcessor _processor;
|
||||
private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus";
|
||||
private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent";
|
||||
|
||||
public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection,
|
||||
@ -141,7 +140,7 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable
|
||||
var ex = args.Exception;
|
||||
var context = args.ErrorSource;
|
||||
|
||||
_logger.LogError(ex, "ERROR handling message: {ExceptionMessage} - Context: {@ExceptionContext}", ex.Message, context);
|
||||
_logger.LogError(ex, "Error handling message - Context: {@ExceptionContext}", context);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" />
|
||||
<PackageReference Include="Azure.Messaging.ServiceBus" />
|
||||
<PackageReference Include="Microsoft.CSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
|
@ -1,14 +1,11 @@
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||
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;
|
||||
global using System.Text.Json;
|
||||
global using Azure.Messaging.ServiceBus;
|
||||
global using Azure.Messaging.ServiceBus.Administration;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||
global using Microsoft.Extensions.Logging;
|
||||
|
||||
|
||||
|
||||
|
@ -1,12 +1,8 @@
|
||||
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.ComponentModel.DataAnnotations.Schema;
|
||||
global using System.Data.Common;
|
||||
global using System.Reflection;
|
||||
global using System.Text.Json;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
global using Microsoft.EntityFrameworkCore.Storage;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -2,16 +2,16 @@
|
||||
|
||||
public class IntegrationEventLogEntry
|
||||
{
|
||||
private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true };
|
||||
private static readonly JsonSerializerOptions s_caseInsensitiveOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
private IntegrationEventLogEntry() { }
|
||||
public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId)
|
||||
{
|
||||
EventId = @event.Id;
|
||||
CreationTime = @event.CreationDate;
|
||||
EventTypeName = @event.GetType().FullName;
|
||||
Content = JsonSerializer.Serialize(@event, @event.GetType(), new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
Content = JsonSerializer.Serialize(@event, @event.GetType(), s_indentedOptions);
|
||||
State = EventStateEnum.NotPublished;
|
||||
TimesSent = 0;
|
||||
TransactionId = transactionId.ToString();
|
||||
@ -30,7 +30,7 @@ public class IntegrationEventLogEntry
|
||||
|
||||
public IntegrationEventLogEntry DeserializeJsonContent(Type type)
|
||||
{
|
||||
IntegrationEvent = JsonSerializer.Deserialize(Content, type, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }) as IntegrationEvent;
|
||||
IntegrationEvent = JsonSerializer.Deserialize(Content, type, s_caseInsensitiveOptions) as IntegrationEvent;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -2,23 +2,15 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.NETCore.Platforms" />
|
||||
<PackageReference Include="Polly" />
|
||||
<PackageReference Include="System.Data.SqlClient" />
|
||||
</ItemGroup>
|
||||
|
@ -1,77 +1,75 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Polly;
|
||||
using System;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
namespace Microsoft.AspNetCore.Hosting
|
||||
namespace Microsoft.AspNetCore.Hosting;
|
||||
|
||||
public static class IWebHostExtensions
|
||||
{
|
||||
public static class IWebHostExtensions
|
||||
public static bool IsInKubernetes(this IServiceProvider services)
|
||||
{
|
||||
public static bool IsInKubernetes(this IServiceProvider services)
|
||||
var cfg = services.GetService<IConfiguration>();
|
||||
var orchestratorType = cfg.GetValue<string>("OrchestratorType");
|
||||
return orchestratorType?.ToUpper() == "K8S";
|
||||
}
|
||||
|
||||
public static IServiceProvider MigrateDbContext<TContext>(this IServiceProvider services, Action<TContext, IServiceProvider> seeder) where TContext : DbContext
|
||||
{
|
||||
var underK8s = services.IsInKubernetes();
|
||||
|
||||
using var scope = services.CreateScope();
|
||||
var scopeServices = scope.ServiceProvider;
|
||||
var logger = scopeServices.GetRequiredService<ILogger<TContext>>();
|
||||
var context = scopeServices.GetService<TContext>();
|
||||
|
||||
try
|
||||
{
|
||||
var cfg = services.GetService<IConfiguration>();
|
||||
var orchestratorType = cfg.GetValue<string>("OrchestratorType");
|
||||
return orchestratorType?.ToUpper() == "K8S";
|
||||
}
|
||||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
||||
|
||||
public static IServiceProvider MigrateDbContext<TContext>(this IServiceProvider services, Action<TContext, IServiceProvider> seeder) where TContext : DbContext
|
||||
{
|
||||
var underK8s = services.IsInKubernetes();
|
||||
|
||||
using var scope = services.CreateScope();
|
||||
var scopeServices = scope.ServiceProvider;
|
||||
var logger = scopeServices.GetRequiredService<ILogger<TContext>>();
|
||||
var context = scopeServices.GetService<TContext>();
|
||||
|
||||
try
|
||||
if (underK8s)
|
||||
{
|
||||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
||||
|
||||
if (underK8s)
|
||||
{
|
||||
InvokeSeeder(seeder, context, scopeServices);
|
||||
}
|
||||
else
|
||||
{
|
||||
var retries = 10;
|
||||
var retry = Policy.Handle<SqlException>()
|
||||
.WaitAndRetry(
|
||||
retryCount: retries,
|
||||
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
||||
onRetry: (exception, timeSpan, retry, ctx) =>
|
||||
{
|
||||
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
|
||||
});
|
||||
|
||||
//if the sql server container is not created on run docker compose this
|
||||
//migration can't fail for network related exception. The retry options for DbContext only
|
||||
//apply to transient exceptions
|
||||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
||||
retry.Execute(() => InvokeSeeder(seeder, context, scopeServices));
|
||||
}
|
||||
|
||||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
||||
InvokeSeeder(seeder, context, scopeServices);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
||||
if (underK8s)
|
||||
{
|
||||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
||||
}
|
||||
var retries = 10;
|
||||
var retry = Policy.Handle<SqlException>()
|
||||
.WaitAndRetry(
|
||||
retryCount: retries,
|
||||
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
||||
onRetry: (exception, timeSpan, retry, ctx) =>
|
||||
{
|
||||
logger.LogWarning(exception, "[{prefix}] Error migrating database (attempt {retry} of {retries})", nameof(TContext), retry, retries);
|
||||
});
|
||||
|
||||
//if the sql server container is not created on run docker compose this
|
||||
//migration can't fail for network related exception. The retry options for DbContext only
|
||||
//apply to transient exceptions
|
||||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
||||
retry.Execute(() => InvokeSeeder(seeder, context, scopeServices));
|
||||
}
|
||||
|
||||
return services;
|
||||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
||||
if (underK8s)
|
||||
{
|
||||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
||||
}
|
||||
}
|
||||
|
||||
private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services)
|
||||
where TContext : DbContext
|
||||
{
|
||||
context.Database.Migrate();
|
||||
seeder(context, services);
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services)
|
||||
where TContext : DbContext
|
||||
{
|
||||
context.Database.Migrate();
|
||||
seeder(context, services);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" />
|
||||
<PackageVersion Include="AspNetCore.HealthChecks.AzureStorage" Version="6.1.2" />
|
||||
@ -14,8 +13,6 @@
|
||||
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5" />
|
||||
<PackageVersion Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="6.0.5" />
|
||||
<PackageVersion Include="AspNetCore.HealthChecks.Uris" Version="6.0.3" />
|
||||
<PackageVersion Include="Autofac" Version="6.5.0" />
|
||||
<PackageVersion Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.8.2" />
|
||||
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.12.0" />
|
||||
@ -29,6 +26,7 @@
|
||||
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.2.2" />
|
||||
<PackageVersion Include="Google.Protobuf" Version="3.22.0" />
|
||||
<PackageVersion Include="Grpc.AspNetCore.Server" Version="2.51.0" />
|
||||
<PackageVersion Include="Grpc.AspNetCore" Version="2.51.0" />
|
||||
<PackageVersion Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.51.0" />
|
||||
<PackageVersion Include="Grpc.Core" Version="2.46.6" />
|
||||
<PackageVersion Include="Grpc.Net.Client" Version="2.51.0" />
|
||||
@ -51,6 +49,7 @@
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="7.0.3" />
|
||||
@ -75,19 +74,13 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.AzureAppServices" Version="7.0.3" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageVersion Include="Microsoft.NETCore.Platforms" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
|
||||
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
|
||||
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.4" />
|
||||
<PackageVersion Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageVersion Include="Polly" Version="7.2.3" />
|
||||
<PackageVersion Include="RabbitMQ.Client" Version="6.4.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Environment" Version="2.2.1-dev-00787" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.5.0-dev-00359" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.1-dev-00896" />
|
||||
<PackageVersion Include="Serilog.Sinks.Http" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Seq" Version="5.2.3-dev-00260" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
|
||||
<PackageVersion Include="System.Data.SqlClient" Version="4.8.5" />
|
||||
@ -95,5 +88,6 @@
|
||||
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
||||
<PackageVersion Include="xunit" Version="2.4.2" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
|
||||
<PackageVersion Include="Yarp.ReverseProxy" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -1,28 +0,0 @@
|
||||
(function ($, swaggerUi) {
|
||||
$(function () {
|
||||
var settings = {
|
||||
authority: 'https://localhost:5105',
|
||||
client_id: 'js',
|
||||
popup_redirect_uri: window.location.protocol
|
||||
+ '//'
|
||||
+ window.location.host
|
||||
+ '/tokenclient/popup.html',
|
||||
|
||||
response_type: 'id_token token',
|
||||
scope: 'openid profile basket',
|
||||
|
||||
filter_protocol_claims: true
|
||||
},
|
||||
manager = new OidcTokenManager(settings),
|
||||
$inputApiKey = $('#input_apiKey');
|
||||
|
||||
$inputApiKey.on('dblclick', function () {
|
||||
manager.openPopupForTokenAsync()
|
||||
.then(function () {
|
||||
$inputApiKey.val(manager.access_token).change();
|
||||
}, function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
})(jQuery, window.swaggerUi);
|
File diff suppressed because one or more lines are too long
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="oidc-token-manager.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
new OidcTokenManager().processTokenPopup();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,25 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server;
|
||||
|
||||
public class AuthorizationHeaderParameterOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors;
|
||||
var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
|
||||
var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter);
|
||||
|
||||
if (isAuthorized && !allowAnonymous)
|
||||
{
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Authorization",
|
||||
In = ParameterLocation.Header,
|
||||
Description = "access token",
|
||||
Required = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
|
||||
<DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
|
||||
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<UserSecretsId>2964ec8e-0d48-4541-b305-94cab537f867</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Redis" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Google.Protobuf" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" />
|
||||
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Serilog.Enrichers.Environment" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.Http" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Redis" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Proto\basket.proto" GrpcServices="Server" Generator="MSBuild:Compile" />
|
||||
<Content Include="@(Protobuf)" />
|
||||
<None Remove="@(Protobuf)" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Proto\basket.proto" GrpcServices="Server" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" />
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" />
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Services.Common\Services.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Basket.FunctionalTests" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -1,7 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Basket.API;
|
||||
|
||||
public class BasketSettings
|
||||
{
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ public class BasketController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)]
|
||||
public async Task<ActionResult<CustomerBasket>> GetBasketByIdAsync(string id)
|
||||
{
|
||||
var basket = await _repository.GetBasketAsync(id);
|
||||
@ -32,7 +31,6 @@ public class BasketController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)]
|
||||
public async Task<ActionResult<CustomerBasket>> UpdateBasketAsync([FromBody] CustomerBasket value)
|
||||
{
|
||||
return Ok(await _repository.UpdateBasketAsync(value));
|
||||
@ -40,8 +38,8 @@ public class BasketController : ControllerBase
|
||||
|
||||
[Route("checkout")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType((int)HttpStatusCode.Accepted)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult> CheckoutAsync([FromBody] BasketCheckout basketCheckout, [FromHeader(Name = "x-requestid")] string requestId)
|
||||
{
|
||||
var userId = _identityService.GetUserIdentity();
|
||||
@ -71,7 +69,7 @@ public class BasketController : ControllerBase
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName}", eventMessage.Id, Program.AppName);
|
||||
_logger.LogError(ex, "Error Publishing integration event: {IntegrationEventId}", eventMessage.Id);
|
||||
|
||||
throw;
|
||||
}
|
||||
@ -81,7 +79,7 @@ public class BasketController : ControllerBase
|
||||
|
||||
// DELETE api/values/5
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesResponseType(typeof(void), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task DeleteBasketByIdAsync(string id)
|
||||
{
|
||||
await _repository.DeleteBasketAsync(id);
|
||||
|
@ -1,140 +0,0 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Basket.API;
|
||||
|
||||
public static class CustomExtensionMethods
|
||||
{
|
||||
public static ConfigurationManager AddKeyVault(this ConfigurationManager configuration)
|
||||
{
|
||||
if (configuration.GetValue("UseVault", false))
|
||||
{
|
||||
var credential = new ClientSecretCredential(
|
||||
configuration["Vault:TenantId"],
|
||||
configuration["Vault:ClientId"],
|
||||
configuration["Vault:ClientSecret"]);
|
||||
|
||||
configuration.AddAzureKeyVault(new Uri($"https://{configuration["Vault:Name"]}.vault.azure.net/"), credential);
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddRedis(this IServiceCollection services)
|
||||
{
|
||||
return services.AddSingleton(sp =>
|
||||
{
|
||||
var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value;
|
||||
var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);
|
||||
|
||||
return ConnectionMultiplexer.Connect(configuration);
|
||||
});
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var hcBuilder = services.AddHealthChecks();
|
||||
|
||||
hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy());
|
||||
|
||||
hcBuilder
|
||||
.AddRedis(
|
||||
configuration["ConnectionString"],
|
||||
name: "redis-check",
|
||||
tags: new string[] { "redis" });
|
||||
|
||||
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
||||
{
|
||||
hcBuilder
|
||||
.AddAzureServiceBusTopic(
|
||||
configuration["EventBusConnection"],
|
||||
topicName: "eshop_event_bus",
|
||||
name: "basket-servicebus-check",
|
||||
tags: new string[] { "servicebus" });
|
||||
}
|
||||
else
|
||||
{
|
||||
hcBuilder
|
||||
.AddRabbitMQ(
|
||||
$"amqp://{configuration["EventBusConnection"]}",
|
||||
name: "basket-rabbitmqbus-check",
|
||||
tags: new string[] { "rabbitmqbus" });
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
if (configuration.GetValue("AzureServiceBusEnabled", false))
|
||||
{
|
||||
services.AddSingleton<IServiceBusPersisterConnection>(sp =>
|
||||
{
|
||||
var serviceBusConnectionString = configuration["EventBusConnection"];
|
||||
|
||||
return new DefaultServiceBusPersisterConnection(serviceBusConnectionString);
|
||||
});
|
||||
|
||||
services.AddSingleton<IEventBus, EventBusServiceBus>(sp =>
|
||||
{
|
||||
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();
|
||||
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>();
|
||||
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
|
||||
string subscriptionName = configuration["SubscriptionClientName"];
|
||||
|
||||
return new EventBusServiceBus(serviceBusPersisterConnection, logger,
|
||||
eventBusSubscriptionsManager, sp, subscriptionName);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
|
||||
{
|
||||
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
|
||||
|
||||
var factory = new ConnectionFactory()
|
||||
{
|
||||
HostName = configuration["EventBusConnection"],
|
||||
DispatchConsumersAsync = true
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(configuration["EventBusUserName"]))
|
||||
{
|
||||
factory.UserName = configuration["EventBusUserName"];
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(configuration["EventBusPassword"]))
|
||||
{
|
||||
factory.Password = configuration["EventBusPassword"];
|
||||
}
|
||||
|
||||
var retryCount = 5;
|
||||
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"]))
|
||||
{
|
||||
retryCount = int.Parse(configuration["EventBusRetryCount"]);
|
||||
}
|
||||
|
||||
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
|
||||
});
|
||||
|
||||
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
|
||||
{
|
||||
var subscriptionClientName = configuration["SubscriptionClientName"];
|
||||
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
|
||||
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
|
||||
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
|
||||
|
||||
var retryCount = 5;
|
||||
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"]))
|
||||
{
|
||||
retryCount = int.Parse(configuration["EventBusRetryCount"]);
|
||||
}
|
||||
|
||||
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubscriptionsManager, subscriptionClientName, retryCount);
|
||||
});
|
||||
}
|
||||
|
||||
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
|
||||
|
||||
services.AddTransient<ProductPriceChangedIntegrationEventHandler>();
|
||||
services.AddTransient<OrderStartedIntegrationEventHandler>();
|
||||
return services;
|
||||
}
|
||||
}
|
@ -32,6 +32,8 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj"
|
||||
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj"
|
||||
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj"
|
||||
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj"
|
||||
COPY "Services/Contact/Contact.API/Contact.API.csproj" "Services/Contact/Contact.API/Contact.API.csproj"
|
||||
COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj"
|
||||
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
|
||||
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj"
|
||||
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj"
|
||||
|
20
src/Services/Basket/Basket.API/Extensions/Extensions.cs
Normal file
20
src/Services/Basket/Basket.API/Extensions/Extensions.cs
Normal file
@ -0,0 +1,20 @@
|
||||
public static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddRedis(_ => configuration.GetRequiredConnectionString("redis"), "redis", tags: new[] { "ready", "liveness" });
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
return services.AddSingleton(sp =>
|
||||
{
|
||||
var redisConfig = ConfigurationOptions.Parse(configuration.GetRequiredConnectionString("redis"), true);
|
||||
|
||||
return ConnectionMultiplexer.Connect(redisConfig);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,59 +1,19 @@
|
||||
global using Autofac.Extensions.DependencyInjection;
|
||||
global using Autofac;
|
||||
global using Azure.Core;
|
||||
global using Azure.Identity;
|
||||
global using Basket.API.Infrastructure.ActionResults;
|
||||
global using Basket.API.Infrastructure.Exceptions;
|
||||
global using Basket.API.Infrastructure.Filters;
|
||||
global using Basket.API.Infrastructure.Middlewares;
|
||||
global using System.ComponentModel.DataAnnotations;
|
||||
global using System.Security.Claims;
|
||||
global using System.Text.Json;
|
||||
global using Basket.API.IntegrationEvents.EventHandling;
|
||||
global using Basket.API.IntegrationEvents.Events;
|
||||
global using Basket.API.Model;
|
||||
global using Basket.API.Repositories;
|
||||
global using Grpc.Core;
|
||||
global using GrpcBasket;
|
||||
global using HealthChecks.UI.Client;
|
||||
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.Features;
|
||||
global using Microsoft.AspNetCore.Http;
|
||||
global using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
global using Microsoft.AspNetCore.Mvc.Filters;
|
||||
global using Microsoft.AspNetCore.Mvc;
|
||||
global using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
global using Microsoft.AspNetCore;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
|
||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API.Controllers;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API.Services;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API;
|
||||
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 RabbitMQ.Client;
|
||||
global using Serilog.Context;
|
||||
global using Serilog;
|
||||
global using Services.Common;
|
||||
global using StackExchange.Redis;
|
||||
global using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
global using System.Collections.Generic;
|
||||
global using System.ComponentModel.DataAnnotations;
|
||||
global using System.IdentityModel.Tokens.Jwt;
|
||||
global using System.IO;
|
||||
global using System.Linq;
|
||||
global using System.Net;
|
||||
global using System.Security.Claims;
|
||||
global using System.Text.Json;
|
||||
global using System.Threading.Tasks;
|
||||
global using System;
|
||||
|
@ -1,11 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.ActionResults;
|
||||
|
||||
public class InternalServerErrorObjectResult : ObjectResult
|
||||
{
|
||||
public InternalServerErrorObjectResult(object error)
|
||||
: base(error)
|
||||
{
|
||||
StatusCode = StatusCodes.Status500InternalServerError;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Exceptions;
|
||||
|
||||
public class BasketDomainException : Exception
|
||||
{
|
||||
public BasketDomainException()
|
||||
{ }
|
||||
|
||||
public BasketDomainException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public BasketDomainException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Middlewares;
|
||||
|
||||
public static class FailingMiddlewareAppBuilderExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder)
|
||||
{
|
||||
return UseFailingMiddleware(builder, null);
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder, Action<FailingOptions> action)
|
||||
{
|
||||
var options = new FailingOptions();
|
||||
action?.Invoke(options);
|
||||
builder.UseMiddleware<FailingMiddleware>(options);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Filters;
|
||||
|
||||
public partial class HttpGlobalExceptionFilter : IExceptionFilter
|
||||
{
|
||||
private readonly IWebHostEnvironment env;
|
||||
private readonly ILogger<HttpGlobalExceptionFilter> logger;
|
||||
|
||||
public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger<HttpGlobalExceptionFilter> logger)
|
||||
{
|
||||
this.env = env;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
logger.LogError(new EventId(context.Exception.HResult),
|
||||
context.Exception,
|
||||
context.Exception.Message);
|
||||
|
||||
if (context.Exception.GetType() == typeof(BasketDomainException))
|
||||
{
|
||||
var json = new JsonErrorResponse
|
||||
{
|
||||
Messages = new[] { context.Exception.Message }
|
||||
};
|
||||
|
||||
context.Result = new BadRequestObjectResult(json);
|
||||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
}
|
||||
else
|
||||
{
|
||||
var json = new JsonErrorResponse
|
||||
{
|
||||
Messages = new[] { "An error occurred. Try it again." }
|
||||
};
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
json.DeveloperMessage = context.Exception;
|
||||
}
|
||||
|
||||
context.Result = new InternalServerErrorObjectResult(json);
|
||||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
}
|
||||
context.ExceptionHandled = true;
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Filters;
|
||||
|
||||
public class JsonErrorResponse
|
||||
{
|
||||
public string[] Messages { get; set; }
|
||||
|
||||
public object DeveloperMessage { get; set; }
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Filters;
|
||||
|
||||
public class ValidateModelStateFilter : ActionFilterAttribute
|
||||
{
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.ModelState.IsValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var validationErrors = context.ModelState
|
||||
.Keys
|
||||
.SelectMany(k => context.ModelState[k].Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToArray();
|
||||
|
||||
var json = new JsonErrorResponse
|
||||
{
|
||||
Messages = validationErrors
|
||||
};
|
||||
|
||||
context.Result = new BadRequestObjectResult(json);
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Filters;
|
||||
|
||||
public class AuthorizeCheckOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
// Check for authorize attribute
|
||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
||||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
|
||||
|
||||
if (!hasAuthorize) return;
|
||||
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
var oAuthScheme = new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
|
||||
};
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
[ oAuthScheme ] = new [] { "basketapi" }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Middlewares;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public class FailingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private bool _mustFail;
|
||||
private readonly FailingOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public FailingMiddleware(RequestDelegate next, ILogger<FailingMiddleware> logger, FailingOptions options)
|
||||
{
|
||||
_next = next;
|
||||
_options = options;
|
||||
_mustFail = false;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path;
|
||||
if (path.Equals(_options.ConfigPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await ProcessConfigRequest(context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (MustFail(context))
|
||||
{
|
||||
_logger.LogInformation("Response for path {Path} will fail.", path);
|
||||
context.Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
|
||||
context.Response.ContentType = "text/plain";
|
||||
await context.Response.WriteAsync("Failed due to FailingMiddleware enabled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await _next.Invoke(context);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessConfigRequest(HttpContext context)
|
||||
{
|
||||
var enable = context.Request.Query.Keys.Any(k => k == "enable");
|
||||
var disable = context.Request.Query.Keys.Any(k => k == "disable");
|
||||
|
||||
if (enable && disable)
|
||||
{
|
||||
throw new ArgumentException("Must use enable or disable querystring values, but not both");
|
||||
}
|
||||
|
||||
if (disable)
|
||||
{
|
||||
_mustFail = false;
|
||||
await SendOkResponse(context, "FailingMiddleware disabled. Further requests will be processed.");
|
||||
return;
|
||||
}
|
||||
if (enable)
|
||||
{
|
||||
_mustFail = true;
|
||||
await SendOkResponse(context, "FailingMiddleware enabled. Further requests will return HTTP 500");
|
||||
return;
|
||||
}
|
||||
|
||||
// If reach here, that means that no valid parameter has been passed. Just output status
|
||||
await SendOkResponse(context, string.Format("FailingMiddleware is {0}", _mustFail ? "enabled" : "disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
private async Task SendOkResponse(HttpContext context, string message)
|
||||
{
|
||||
context.Response.StatusCode = (int)System.Net.HttpStatusCode.OK;
|
||||
context.Response.ContentType = "text/plain";
|
||||
await context.Response.WriteAsync(message);
|
||||
}
|
||||
|
||||
private bool MustFail(HttpContext context)
|
||||
{
|
||||
var rpath = context.Request.Path.Value;
|
||||
|
||||
if (_options.NotFilteredPaths.Any(p => p.Equals(rpath, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _mustFail &&
|
||||
(_options.EndpointPaths.Any(x => x == rpath)
|
||||
|| _options.EndpointPaths.Count == 0);
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Middlewares;
|
||||
|
||||
public class FailingOptions
|
||||
{
|
||||
public string ConfigPath = "/Failing";
|
||||
public List<string> EndpointPaths { get; set; } = new List<string>();
|
||||
|
||||
public List<string> NotFilteredPaths { get; set; } = new List<string>();
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Middlewares;
|
||||
|
||||
public class FailingStartupFilter : IStartupFilter
|
||||
{
|
||||
private readonly Action<FailingOptions> _options;
|
||||
public FailingStartupFilter(Action<FailingOptions> optionsAction)
|
||||
{
|
||||
_options = optionsAction;
|
||||
}
|
||||
|
||||
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
|
||||
{
|
||||
return app =>
|
||||
{
|
||||
app.UseFailingMiddleware(_options);
|
||||
next(app);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
namespace Basket.API.Infrastructure.Middlewares;
|
||||
|
||||
public static class WebHostBuildertExtensions
|
||||
{
|
||||
public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, Action<FailingOptions> options)
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<IStartupFilter>(new FailingStartupFilter(options));
|
||||
});
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ public class OrderStartedIntegrationEventHandler : IIntegrationEventHandler<Orde
|
||||
|
||||
public async Task Handle(OrderStartedIntegrationEvent @event)
|
||||
{
|
||||
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
|
||||
using (_logger.BeginScope(new List<KeyValuePair<string, object>> { new ("IntegrationEventContext", @event.Id) }))
|
||||
{
|
||||
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
|
||||
_logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event);
|
||||
|
||||
await _repository.DeleteBasketAsync(@event.UserId.ToString());
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandl
|
||||
|
||||
public async Task Handle(ProductPriceChangedIntegrationEvent @event)
|
||||
{
|
||||
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
|
||||
using (_logger.BeginScope(new List<KeyValuePair<string, object>> { new ("IntegrationEventContext", @event.Id) }))
|
||||
{
|
||||
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
|
||||
_logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event);
|
||||
|
||||
var userIds = _repository.GetUsers();
|
||||
|
||||
@ -36,7 +36,7 @@ public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandl
|
||||
|
||||
if (itemsToUpdate != null)
|
||||
{
|
||||
_logger.LogInformation("----- ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate);
|
||||
_logger.LogInformation("ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate);
|
||||
|
||||
foreach (var item in itemsToUpdate)
|
||||
{
|
||||
|
@ -1,209 +1,30 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration.AddKeyVault();
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
builder.Services.AddGrpc(options =>
|
||||
{
|
||||
options.EnableDetailedErrors = true;
|
||||
});
|
||||
builder.Services.AddGrpc();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddProblemDetails();
|
||||
|
||||
builder.Services.AddApplicationInsightsTelemetry(builder.Configuration);
|
||||
builder.Services.AddApplicationInsightsKubernetesEnricher();
|
||||
builder.Services.AddHealthChecks(builder.Configuration);
|
||||
builder.Services.AddRedis(builder.Configuration);
|
||||
|
||||
builder.Services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
|
||||
options.Filters.Add(typeof(ValidateModelStateFilter));
|
||||
});
|
||||
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "eShopOnContainers - Basket HTTP API",
|
||||
Version = "v1",
|
||||
Description = "The Basket Service HTTP API"
|
||||
});
|
||||
|
||||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows()
|
||||
{
|
||||
Implicit = new OpenApiOAuthFlow()
|
||||
{
|
||||
AuthorizationUrl = new Uri($"{builder.Configuration["IdentityUrlExternal"]}/connect/authorize"),
|
||||
TokenUrl = new Uri($"{builder.Configuration["IdentityUrlExternal"]}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>() { { "basket", "Basket API" } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
||||
});
|
||||
|
||||
// prevent from mapping "sub" claim to nameidentifier.
|
||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
|
||||
|
||||
var identityUrl = builder.Configuration["IdentityUrl"];
|
||||
|
||||
builder.Services.AddAuthentication().AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = identityUrl;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.Audience = "basket";
|
||||
options.TokenValidationParameters.ValidateAudience = false;
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("ApiScope", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim("scope", "basket");
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddCustomHealthCheck(builder.Configuration);
|
||||
|
||||
builder.Services.Configure<BasketSettings>(builder.Configuration);
|
||||
|
||||
builder.Services.AddRedis();
|
||||
|
||||
builder.Services.AddEventBus(builder.Configuration);
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddTransient<ProductPriceChangedIntegrationEventHandler>();
|
||||
builder.Services.AddTransient<OrderStartedIntegrationEventHandler>();
|
||||
|
||||
builder.Services.AddTransient<IBasketRepository, RedisBasketRepository>();
|
||||
builder.Services.AddTransient<IIdentityService, IdentityService>();
|
||||
|
||||
builder.WebHost.UseKestrel(options =>
|
||||
{
|
||||
var ports = GetDefinedPorts(builder.Configuration);
|
||||
options.Listen(IPAddress.Any, ports.httpPort, listenOptions =>
|
||||
{
|
||||
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
|
||||
});
|
||||
|
||||
options.Listen(IPAddress.Any, ports.grpcPort, listenOptions =>
|
||||
{
|
||||
listenOptions.Protocols = HttpProtocols.Http2;
|
||||
});
|
||||
});
|
||||
|
||||
builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration));
|
||||
builder.WebHost.UseFailing(options =>
|
||||
{
|
||||
options.ConfigPath = "/Failing";
|
||||
options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" });
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
app.MapGet("hello", () => "hello");
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Home/Error");
|
||||
}
|
||||
|
||||
var pathBase = app.Configuration["PATH_BASE"];
|
||||
if (!string.IsNullOrEmpty(pathBase))
|
||||
{
|
||||
app.UsePathBase(pathBase);
|
||||
}
|
||||
|
||||
app.UseSwagger();
|
||||
|
||||
app.UseSwaggerUI(setup =>
|
||||
{
|
||||
setup.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Basket.API V1");
|
||||
setup.OAuthClientId("basketswaggerui");
|
||||
setup.OAuthAppName("Basket Swagger UI");
|
||||
});
|
||||
app.UseServiceDefaults();
|
||||
|
||||
app.MapGrpcService<BasketService>();
|
||||
app.MapControllers();
|
||||
|
||||
app.MapGet("/_proto/", async ctx =>
|
||||
{
|
||||
ctx.Response.ContentType = "text/plain";
|
||||
using var fs = new FileStream(Path.Combine(app.Environment.ContentRootPath, "Proto", "basket.proto"), FileMode.Open, FileAccess.Read);
|
||||
using var sr = new StreamReader(fs);
|
||||
while (!sr.EndOfStream)
|
||||
{
|
||||
var line = await sr.ReadLineAsync();
|
||||
if (line != "/* >>" || line != "<< */")
|
||||
{
|
||||
await ctx.Response.WriteAsync(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
var eventBus = app.Services.GetRequiredService<IEventBus>();
|
||||
|
||||
app.MapHealthChecks("/hc", new HealthCheckOptions()
|
||||
{
|
||||
Predicate = _ => true,
|
||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
||||
});
|
||||
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
|
||||
eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();
|
||||
|
||||
app.MapHealthChecks("/liveness", new HealthCheckOptions
|
||||
{
|
||||
Predicate = r => r.Name.Contains("self")
|
||||
});
|
||||
|
||||
ConfigureEventBus(app);
|
||||
|
||||
try
|
||||
{
|
||||
Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName);
|
||||
|
||||
|
||||
Log.Information("Starting web host ({ApplicationContext})...", Program.AppName);
|
||||
await app.RunAsync();
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
|
||||
Serilog.ILogger CreateSerilogLogger(IConfiguration configuration)
|
||||
{
|
||||
var seqServerUrl = configuration["Serilog:SeqServerUrl"];
|
||||
var logstashUrl = configuration["Serilog:LogstashgUrl"];
|
||||
return new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.Enrich.WithProperty("ApplicationContext", Program.AppName)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl)
|
||||
.WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl, null)
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config)
|
||||
{
|
||||
var grpcPort = config.GetValue("GRPC_PORT", 5001);
|
||||
var port = config.GetValue("PORT", 80);
|
||||
return (port, grpcPort);
|
||||
}
|
||||
|
||||
void ConfigureEventBus(IApplicationBuilder app)
|
||||
{
|
||||
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
|
||||
|
||||
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
|
||||
eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();
|
||||
}
|
||||
|
||||
public partial class Program
|
||||
{
|
||||
private static string Namespace = typeof(Program).Assembly.GetName().Name;
|
||||
public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1);
|
||||
}
|
||||
await app.RunAsync();
|
||||
|
@ -1,26 +1,11 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:58017/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Microsoft.eShopOnContainers.Services.Basket.API": {
|
||||
"Basket.API": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "http://localhost:55103/",
|
||||
"applicationUrl": "http://localhost:5221",
|
||||
"environmentVariables": {
|
||||
"Identity__Url": "http://localhost:5223",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"secrets1": {
|
||||
"type": "secrets"
|
||||
},
|
||||
"rabbitmq1": {
|
||||
"type": "rabbitmq",
|
||||
"connectionId": "eventbus",
|
||||
"dynamicId": null
|
||||
},
|
||||
"redis1": {
|
||||
"type": "redis",
|
||||
"connectionId": "ConnectionStrings:Redis",
|
||||
"dynamicId": null
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"secrets1": {
|
||||
"type": "secrets.user"
|
||||
},
|
||||
"rabbitmq1": {
|
||||
"containerPorts": "5672:5672,15672:15672",
|
||||
"secretStore": "LocalSecretsFile",
|
||||
"containerName": "rabbitmq",
|
||||
"containerImage": "rabbitmq:3-management-alpine",
|
||||
"type": "rabbitmq.container",
|
||||
"connectionId": "eventbus",
|
||||
"dynamicId": null
|
||||
},
|
||||
"redis1": {
|
||||
"serviceConnectorResourceId": "",
|
||||
"containerPorts": "6379:6379",
|
||||
"secretStore": "LocalSecretsFile",
|
||||
"containerName": "basket-redis",
|
||||
"containerImage": "redis:alpine",
|
||||
"type": "redis.container",
|
||||
"connectionId": "ConnectionStrings:Redis",
|
||||
"dynamicId": null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories;
|
||||
namespace Basket.API.Repositories;
|
||||
|
||||
public class RedisBasketRepository : IBasketRepository
|
||||
{
|
||||
@ -6,9 +6,9 @@ public class RedisBasketRepository : IBasketRepository
|
||||
private readonly ConnectionMultiplexer _redis;
|
||||
private readonly IDatabase _database;
|
||||
|
||||
public RedisBasketRepository(ILoggerFactory loggerFactory, ConnectionMultiplexer redis)
|
||||
public RedisBasketRepository(ILogger<RedisBasketRepository> logger, ConnectionMultiplexer redis)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<RedisBasketRepository>();
|
||||
_logger = logger;
|
||||
_redis = redis;
|
||||
_database = redis.GetDatabase();
|
||||
}
|
||||
@ -35,15 +35,12 @@ public class RedisBasketRepository : IBasketRepository
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<CustomerBasket>(data, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
return JsonSerializer.Deserialize<CustomerBasket>(data, JsonDefaults.CaseInsensitiveOptions);
|
||||
}
|
||||
|
||||
public async Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket)
|
||||
{
|
||||
var created = await _database.StringSetAsync(basket.BuyerId, JsonSerializer.Serialize(basket));
|
||||
var created = await _database.StringSetAsync(basket.BuyerId, JsonSerializer.Serialize(basket, JsonDefaults.CaseInsensitiveOptions));
|
||||
|
||||
if (!created)
|
||||
{
|
||||
@ -51,7 +48,7 @@ public class RedisBasketRepository : IBasketRepository
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Basket item persisted succesfully.");
|
||||
_logger.LogInformation("Basket item persisted successfully.");
|
||||
|
||||
return await GetBasketAsync(basket.BuyerId);
|
||||
}
|
@ -1,16 +1,4 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.eShopOnContainers": "Debug",
|
||||
"System": "Warning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"IdentityUrlExternal": "http://localhost:5105",
|
||||
"IdentityUrl": "http://localhost:5105",
|
||||
"ConnectionString": "127.0.0.1",
|
||||
"AzureServiceBusEnabled": false,
|
||||
"EventBusConnection": "localhost"
|
||||
|
@ -1,30 +1,48 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"SeqServerUrl": null,
|
||||
"LogstashgUrl": null,
|
||||
"MinimumLevel": {
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.eShopOnContainers": "Information",
|
||||
"System": "Warning"
|
||||
}
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Kestrel": {
|
||||
"EndpointDefaults": {
|
||||
"Protocols": "Http2"
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Url": "http://localhost:5221"
|
||||
},
|
||||
"gRPC": {
|
||||
"Url": "http://localhost:6221",
|
||||
"Protocols": "Http2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SubscriptionClientName": "Basket",
|
||||
"ApplicationInsights": {
|
||||
"InstrumentationKey": ""
|
||||
"OpenApi": {
|
||||
"Endpoint": {
|
||||
"Name": "Basket.API V1"
|
||||
},
|
||||
"Document": {
|
||||
"Description": "The Basket Service HTTP API",
|
||||
"Title": "eShopOnContainers - Basket HTTP API",
|
||||
"Version": "v1"
|
||||
},
|
||||
"Auth": {
|
||||
"ClientId": "basketswaggerui",
|
||||
"AppName": "Basket Swagger UI"
|
||||
}
|
||||
},
|
||||
"EventBusRetryCount": 5,
|
||||
"UseVault": false,
|
||||
"Vault": {
|
||||
"Name": "eshop",
|
||||
"ClientId": "your-client-id",
|
||||
"ClientSecret": "your-client-secret"
|
||||
"ConnectionStrings": {
|
||||
"Redis": "localhost",
|
||||
"EventBus": "localhost"
|
||||
},
|
||||
"Identity": {
|
||||
"Audience": "basket",
|
||||
"Url": "http://localhost:5223",
|
||||
"Scopes": {
|
||||
"basket": "Basket API"
|
||||
}
|
||||
},
|
||||
"EventBus": {
|
||||
"SubscriptionClientName": "Basket",
|
||||
"RetryCount": 5
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,14 @@ using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Basket.FunctionalTests.Base;
|
||||
|
||||
public class BasketScenarioBase : WebApplicationFactory<Program>
|
||||
public class BasketScenarioBase
|
||||
{
|
||||
private const string ApiUrlBase = "api/v1/basket";
|
||||
|
||||
public TestServer CreateServer()
|
||||
{
|
||||
return Server;
|
||||
var factory = new BasketApplication();
|
||||
return factory.Server;
|
||||
}
|
||||
|
||||
public static class Get
|
||||
@ -18,6 +19,11 @@ public class BasketScenarioBase : WebApplicationFactory<Program>
|
||||
{
|
||||
return $"{ApiUrlBase}/{id}";
|
||||
}
|
||||
|
||||
public static string GetBasketByCustomer(string customerId)
|
||||
{
|
||||
return $"{ApiUrlBase}/{customerId}";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Post
|
||||
@ -26,33 +32,36 @@ public class BasketScenarioBase : WebApplicationFactory<Program>
|
||||
public static string CheckoutOrder = $"{ApiUrlBase}/checkout";
|
||||
}
|
||||
|
||||
protected override IHost CreateHost(IHostBuilder builder)
|
||||
private class BasketApplication : WebApplicationFactory<Program>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
protected override IHost CreateHost(IHostBuilder builder)
|
||||
{
|
||||
services.AddSingleton<IStartupFilter, AuthStartupFilter>();
|
||||
});
|
||||
|
||||
builder.ConfigureAppConfiguration(c =>
|
||||
{
|
||||
var directory = Path.GetDirectoryName(typeof(BasketScenarioBase).Assembly.Location)!;
|
||||
|
||||
c.AddJsonFile(Path.Combine(directory, "appsettings.json"), optional: false);
|
||||
});
|
||||
|
||||
return base.CreateHost(builder);
|
||||
}
|
||||
|
||||
private class AuthStartupFilter : IStartupFilter
|
||||
{
|
||||
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
|
||||
{
|
||||
return app =>
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
app.UseMiddleware<AutoAuthorizeMiddleware>();
|
||||
services.AddSingleton<IStartupFilter, AuthStartupFilter>();
|
||||
});
|
||||
|
||||
next(app);
|
||||
};
|
||||
builder.ConfigureAppConfiguration(c =>
|
||||
{
|
||||
var directory = Path.GetDirectoryName(typeof(BasketScenarioBase).Assembly.Location)!;
|
||||
|
||||
c.AddJsonFile(Path.Combine(directory, "appsettings.Basket.json"), optional: false);
|
||||
});
|
||||
|
||||
return base.CreateHost(builder);
|
||||
}
|
||||
|
||||
private class AuthStartupFilter : IStartupFilter
|
||||
{
|
||||
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
|
||||
{
|
||||
return app =>
|
||||
{
|
||||
app.UseMiddleware<AutoAuthorizeMiddleware>();
|
||||
|
||||
next(app);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json">
|
||||
<Content Include="appsettings.Basket.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
@ -10,7 +10,6 @@ public class BasketScenarios :
|
||||
var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
|
||||
var uri = "/api/v1/basket/";
|
||||
var response = await server.CreateClient().PostAsync(uri, content);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,19 @@
|
||||
global using Basket.FunctionalTests.Base;
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.IO;
|
||||
global using System.Net.Http;
|
||||
global using System.Security.Claims;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
global using System.Threading.Tasks;
|
||||
global using Basket.FunctionalTests.Base;
|
||||
global using Microsoft.AspNetCore.Builder;
|
||||
global using Microsoft.AspNetCore.Hosting;
|
||||
global using Microsoft.AspNetCore.Http;
|
||||
global using Microsoft.AspNetCore.Routing;
|
||||
global using Microsoft.AspNetCore.TestHost;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
||||
global using Microsoft.eShopOnContainers.Services.Basket.API;
|
||||
global using Microsoft.Extensions.Configuration;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Logging;
|
||||
global using StackExchange.Redis;
|
||||
global using System.Collections.Generic;
|
||||
global using System.IO;
|
||||
global using System.Net.Http;
|
||||
global using System.Reflection;
|
||||
global using System.Security.Claims;
|
||||
global using System.Text.Json;
|
||||
global using System.Text;
|
||||
global using System.Threading.Tasks;
|
||||
global using System;
|
||||
global using Xunit;
|
||||
|
@ -1,4 +1,4 @@
|
||||
|
||||
using Basket.API.Repositories;
|
||||
|
||||
namespace Basket.FunctionalTests
|
||||
{
|
||||
@ -9,7 +9,8 @@ namespace Basket.FunctionalTests
|
||||
[Fact]
|
||||
public async Task UpdateBasket_return_and_add_basket()
|
||||
{
|
||||
var redis = Services.GetRequiredService<ConnectionMultiplexer>();
|
||||
var server = CreateServer();
|
||||
var redis = server.Services.GetRequiredService<ConnectionMultiplexer>();
|
||||
|
||||
var redisBasketRepository = BuildBasketRepository(redis);
|
||||
|
||||
@ -26,7 +27,8 @@ namespace Basket.FunctionalTests
|
||||
[Fact]
|
||||
public async Task Delete_Basket_return_null()
|
||||
{
|
||||
var redis = Services.GetRequiredService<ConnectionMultiplexer>();
|
||||
var server = CreateServer();
|
||||
var redis = server.Services.GetRequiredService<ConnectionMultiplexer>();
|
||||
|
||||
var redisBasketRepository = BuildBasketRepository(redis);
|
||||
|
||||
@ -47,7 +49,7 @@ namespace Basket.FunctionalTests
|
||||
RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux)
|
||||
{
|
||||
var loggerFactory = new LoggerFactory();
|
||||
return new RedisBasketRepository(loggerFactory, connMux);
|
||||
return new RedisBasketRepository(loggerFactory.CreateLogger<RedisBasketRepository>(), connMux);
|
||||
}
|
||||
|
||||
List<BasketItem> BuildBasketItems()
|
||||
|
@ -9,11 +9,17 @@
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
},
|
||||
"IdentityUrl": "http://localhost:5105",
|
||||
"IdentityUrlExternal": "http://localhost:5105",
|
||||
"ConnectionString": "127.0.0.1",
|
||||
"Identity": {
|
||||
"ExternalUrl": "http://localhost:5105",
|
||||
"Url": "http://localhost:5105"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Redis": "127.0.0.1"
|
||||
},
|
||||
"EventBus": {
|
||||
"ConnectionString": "localhost",
|
||||
"SubscriptionClientName": "Basket"
|
||||
},
|
||||
"isTest": "true",
|
||||
"EventBusConnection": "localhost",
|
||||
"SubscriptionClientName": "Basket",
|
||||
"SuppressCheckForUnhandledSecurityMetadata": true
|
||||
}
|
@ -79,7 +79,7 @@ public class CartControllerTest
|
||||
//Arrange
|
||||
var fakeCatalogItem = GetFakeCatalogItem();
|
||||
|
||||
_basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny<ApplicationUser>(), It.IsAny<Int32>()))
|
||||
_basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny<ApplicationUser>(), It.IsAny<int>()))
|
||||
.Returns(Task.FromResult(1));
|
||||
|
||||
//Act
|
||||
|
42
src/Services/Catalog/Catalog.API/Apis/PicApi.cs
Normal file
42
src/Services/Catalog/Catalog.API/Apis/PicApi.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Catalog.API.Apis;
|
||||
|
||||
public static class PicApi
|
||||
{
|
||||
public static IEndpointConventionBuilder MapPicApi(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
return routes.MapGet("api/v1/catalog/items/{catalogItemId:int}/pic",
|
||||
async (int catalogItemId, CatalogContext db, IWebHostEnvironment environment) =>
|
||||
{
|
||||
var item = await db.CatalogItems.FindAsync(catalogItemId);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var path = Path.Combine(environment.ContentRootPath, "Pics", item.PictureFileName);
|
||||
|
||||
string imageFileExtension = Path.GetExtension(item.PictureFileName);
|
||||
string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension);
|
||||
|
||||
return Results.File(path, mimetype);
|
||||
})
|
||||
.WithTags("Pic")
|
||||
.Produces(404);
|
||||
|
||||
static string GetImageMimeTypeFromImageFileExtension(string extension) => extension switch
|
||||
{
|
||||
".png" => "image/png",
|
||||
".gif" => "image/gif",
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".bmp" => "image/bmp",
|
||||
".tiff" => "image/tiff",
|
||||
".wmf" => "image/wmf",
|
||||
".jp2" => "image/jp2",
|
||||
".svg" => "image/svg+xml",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
}
|
||||
}
|
@ -2,23 +2,17 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<DebugType>portable</DebugType>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<AssemblyName>Catalog.API</AssemblyName>
|
||||
<PackageId>Catalog.API</PackageId>
|
||||
<UserSecretsId>aspnet-Catalog.API-20161122013618</UserSecretsId>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
|
||||
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="wwwroot;">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="Pics\**\*;">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
@ -26,46 +20,22 @@
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<None Include="IntegrationEvents\EventHandling\AnyFutureIntegrationEventHandler.cs.txt" />
|
||||
<Content Update="web.config">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Proto\catalog.proto" GrpcServices="Server" Generator="MSBuild:Compile" />
|
||||
<Content Include="@(Protobuf)" />
|
||||
<None Remove="@(Protobuf)" />
|
||||
<Protobuf Include="Proto\catalog.proto" GrpcServices="Server" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Catalog.FunctionalTests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.AzureStorage" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Google.Protobuf" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" />
|
||||
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" />
|
||||
<PackageReference Include="Grpc.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Serilog.Enrichers.Environment" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.Http" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" />
|
||||
<PackageReference Include="System.Data.SqlClient" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
|
||||
@ -75,19 +45,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" />
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" />
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" />
|
||||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\IntegrationEventLogEF\IntegrationEventLogEF.csproj" />
|
||||
<ProjectReference Include="..\..\Services.Common\Services.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Pics\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Setup\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -20,9 +20,9 @@ public class CatalogController : ControllerBase
|
||||
// GET api/v1/[controller]/items[?pageSize=3&pageIndex=10]
|
||||
[HttpGet]
|
||||
[Route("items")]
|
||||
[ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(IEnumerable<CatalogItem>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(IEnumerable<CatalogItem>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> ItemsAsync([FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0, string ids = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ids))
|
||||
@ -74,9 +74,8 @@ public class CatalogController : ControllerBase
|
||||
|
||||
[HttpGet]
|
||||
[Route("items/{id:int}")]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType(typeof(CatalogItem), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<CatalogItem>> ItemByIdAsync(int id)
|
||||
{
|
||||
if (id <= 0)
|
||||
@ -102,7 +101,6 @@ public class CatalogController : ControllerBase
|
||||
// GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10]
|
||||
[HttpGet]
|
||||
[Route("items/withname/{name:minlength(1)}")]
|
||||
[ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)]
|
||||
public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsWithNameAsync(string name, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
|
||||
{
|
||||
var totalItems = await _catalogContext.CatalogItems
|
||||
@ -123,7 +121,6 @@ public class CatalogController : ControllerBase
|
||||
// GET api/v1/[controller]/items/type/1/brand[?pageSize=3&pageIndex=10]
|
||||
[HttpGet]
|
||||
[Route("items/type/{catalogTypeId}/brand/{catalogBrandId:int?}")]
|
||||
[ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)]
|
||||
public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsByTypeIdAndBrandIdAsync(int catalogTypeId, int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
|
||||
{
|
||||
var root = (IQueryable<CatalogItem>)_catalogContext.CatalogItems;
|
||||
@ -151,7 +148,6 @@ public class CatalogController : ControllerBase
|
||||
// GET api/v1/[controller]/items/type/all/brand[?pageSize=3&pageIndex=10]
|
||||
[HttpGet]
|
||||
[Route("items/type/all/brand/{catalogBrandId:int?}")]
|
||||
[ProducesResponseType(typeof(PaginatedItemsViewModel<CatalogItem>), (int)HttpStatusCode.OK)]
|
||||
public async Task<ActionResult<PaginatedItemsViewModel<CatalogItem>>> ItemsByBrandIdAsync(int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
|
||||
{
|
||||
var root = (IQueryable<CatalogItem>)_catalogContext.CatalogItems;
|
||||
@ -177,7 +173,6 @@ public class CatalogController : ControllerBase
|
||||
// GET api/v1/[controller]/CatalogTypes
|
||||
[HttpGet]
|
||||
[Route("catalogtypes")]
|
||||
[ProducesResponseType(typeof(List<CatalogType>), (int)HttpStatusCode.OK)]
|
||||
public async Task<ActionResult<List<CatalogType>>> CatalogTypesAsync()
|
||||
{
|
||||
return await _catalogContext.CatalogTypes.ToListAsync();
|
||||
@ -186,7 +181,6 @@ public class CatalogController : ControllerBase
|
||||
// GET api/v1/[controller]/CatalogBrands
|
||||
[HttpGet]
|
||||
[Route("catalogbrands")]
|
||||
[ProducesResponseType(typeof(List<CatalogBrand>), (int)HttpStatusCode.OK)]
|
||||
public async Task<ActionResult<List<CatalogBrand>>> CatalogBrandsAsync()
|
||||
{
|
||||
return await _catalogContext.CatalogBrands.ToListAsync();
|
||||
@ -195,8 +189,8 @@ public class CatalogController : ControllerBase
|
||||
//PUT api/v1/[controller]/items
|
||||
[Route("items")]
|
||||
[HttpPut]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.Created)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
public async Task<ActionResult> UpdateProductAsync([FromBody] CatalogItem productToUpdate)
|
||||
{
|
||||
var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id);
|
||||
@ -235,7 +229,7 @@ public class CatalogController : ControllerBase
|
||||
//POST api/v1/[controller]/items
|
||||
[Route("items")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType((int)HttpStatusCode.Created)]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
public async Task<ActionResult> CreateProductAsync([FromBody] CatalogItem product)
|
||||
{
|
||||
var item = new CatalogItem
|
||||
@ -258,8 +252,8 @@ public class CatalogController : ControllerBase
|
||||
//DELETE api/v1/[controller]/id
|
||||
[Route("{id}")]
|
||||
[HttpDelete]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteProductAsync(int id)
|
||||
{
|
||||
var product = _catalogContext.CatalogItems.SingleOrDefault(x => x.Id == id);
|
||||
|
@ -1,11 +0,0 @@
|
||||
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
|
||||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers;
|
||||
|
||||
public class HomeController : Controller
|
||||
{
|
||||
// GET: /<controller>/
|
||||
public IActionResult Index()
|
||||
{
|
||||
return new RedirectResult("~/swagger");
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
|
||||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
public class PicController : ControllerBase
|
||||
{
|
||||
private readonly IWebHostEnvironment _env;
|
||||
private readonly CatalogContext _catalogContext;
|
||||
|
||||
public PicController(IWebHostEnvironment env,
|
||||
CatalogContext catalogContext)
|
||||
{
|
||||
_env = env;
|
||||
_catalogContext = catalogContext;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("api/v1/catalog/items/{catalogItemId:int}/pic")]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
// GET: /<controller>/
|
||||
public async Task<ActionResult> GetImageAsync(int catalogItemId)
|
||||
{
|
||||
if (catalogItemId <= 0)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var item = await _catalogContext.CatalogItems
|
||||
.SingleOrDefaultAsync(ci => ci.Id == catalogItemId);
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
var webRoot = _env.WebRootPath;
|
||||
var path = Path.Combine(webRoot, item.PictureFileName);
|
||||
|
||||
string imageFileExtension = Path.GetExtension(item.PictureFileName);
|
||||
string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension);
|
||||
|
||||
var buffer = await System.IO.File.ReadAllBytesAsync(path);
|
||||
|
||||
return File(buffer, mimetype);
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
private string GetImageMimeTypeFromImageFileExtension(string extension)
|
||||
{
|
||||
string mimetype = extension switch
|
||||
{
|
||||
".png" => "image/png",
|
||||
".gif" => "image/gif",
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".bmp" => "image/bmp",
|
||||
".tiff" => "image/tiff",
|
||||
".wmf" => "image/wmf",
|
||||
".jp2" => "image/jp2",
|
||||
".svg" => "image/svg+xml",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
return mimetype;
|
||||
}
|
||||
}
|
@ -33,6 +33,8 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj"
|
||||
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj"
|
||||
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj"
|
||||
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj"
|
||||
COPY "Services/Contact/Contact.API/Contact.API.csproj" "Services/Contact/Contact.API/Contact.API.csproj"
|
||||
COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj"
|
||||
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
|
||||
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj"
|
||||
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj"
|
||||
|
92
src/Services/Catalog/Catalog.API/Extensions/Extensions.cs
Normal file
92
src/Services/Catalog/Catalog.API/Extensions/Extensions.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var hcBuilder = services.AddHealthChecks();
|
||||
|
||||
hcBuilder
|
||||
.AddSqlServer(_ => configuration.GetRequiredConnectionString("CatalogDB"),
|
||||
name: "CatalogDB-check",
|
||||
tags: new string[] { "ready" });
|
||||
|
||||
var accountName = configuration["AzureStorageAccountName"];
|
||||
var accountKey = configuration["AzureStorageAccountKey"];
|
||||
|
||||
if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey))
|
||||
{
|
||||
hcBuilder
|
||||
.AddAzureBlobStorage(
|
||||
$"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net",
|
||||
name: "catalog-storage-check",
|
||||
tags: new string[] { "ready" });
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddDbContexts(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
static void ConfigureSqlOptions(SqlServerDbContextOptionsBuilder sqlOptions)
|
||||
{
|
||||
sqlOptions.MigrationsAssembly(typeof(Program).Assembly.FullName);
|
||||
|
||||
// Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
|
||||
|
||||
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
|
||||
};
|
||||
|
||||
services.AddDbContext<CatalogContext>(options =>
|
||||
{
|
||||
var connectionString = configuration.GetRequiredConnectionString("CatalogDB");
|
||||
|
||||
options.UseSqlServer(connectionString, ConfigureSqlOptions);
|
||||
});
|
||||
|
||||
services.AddDbContext<IntegrationEventLogContext>(options =>
|
||||
{
|
||||
var connectionString = configuration.GetRequiredConnectionString("CatalogDB");
|
||||
|
||||
options.UseSqlServer(connectionString, ConfigureSqlOptions);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddApplicationOptions(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<CatalogSettings>(configuration);
|
||||
|
||||
// TODO: Move to the new problem details middleware
|
||||
services.Configure<ApiBehaviorOptions>(options =>
|
||||
{
|
||||
options.InvalidModelStateResponseFactory = context =>
|
||||
{
|
||||
var problemDetails = new ValidationProblemDetails(context.ModelState)
|
||||
{
|
||||
Instance = context.HttpContext.Request.Path,
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Detail = "Please refer to the errors property for additional details."
|
||||
};
|
||||
|
||||
return new BadRequestObjectResult(problemDetails)
|
||||
{
|
||||
ContentTypes = { "application/problem+json", "application/problem+xml" }
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddIntegrationServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
|
||||
sp => (DbConnection c) => new IntegrationEventLogService(c));
|
||||
|
||||
services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
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