Merge pull request #2107 from dotnet-architecture/davidfowl/common-services
Modernization
This commit is contained in:
		
						commit
						3169a93344
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -282,3 +282,4 @@ src/**/app.yaml | |||||||
| src/**/inf.yaml | src/**/inf.yaml | ||||||
| 
 | 
 | ||||||
| .angular/ | .angular/ | ||||||
|  | /src/Services/Identity/Identity.API/keys/*.json | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ 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.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/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/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/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.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 "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" | ||||||
| COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.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" } |  | ||||||
|                     } |  | ||||||
|                 }; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -2,32 +2,24 @@ | |||||||
| global using Grpc.Core.Interceptors; | global using Grpc.Core.Interceptors; | ||||||
| global using Grpc.Core; | global using Grpc.Core; | ||||||
| global using GrpcBasket; | global using GrpcBasket; | ||||||
| global using GrpcOrdering; |  | ||||||
| global using HealthChecks.UI.Client; | global using HealthChecks.UI.Client; | ||||||
| global using Microsoft.AspNetCore.Authentication.JwtBearer; | global using Microsoft.AspNetCore.Authentication.JwtBearer; | ||||||
| global using Microsoft.AspNetCore.Authentication; | global using Microsoft.AspNetCore.Authentication; | ||||||
| global using Microsoft.AspNetCore.Authorization; | global using Microsoft.AspNetCore.Authorization; | ||||||
| global using Microsoft.AspNetCore.Builder; | global using Microsoft.AspNetCore.Builder; | ||||||
| global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | ||||||
| global using Microsoft.AspNetCore.Hosting; |  | ||||||
| global using Microsoft.AspNetCore.Http; | global using Microsoft.AspNetCore.Http; | ||||||
| global using Microsoft.AspNetCore.Mvc; | global using Microsoft.AspNetCore.Mvc; | ||||||
| global using Microsoft.AspNetCore; |  | ||||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config; | 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.Infrastructure; | ||||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | ||||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | ||||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator; |  | ||||||
| global using Microsoft.Extensions.Configuration; | global using Microsoft.Extensions.Configuration; | ||||||
| global using Microsoft.Extensions.DependencyInjection; | global using Microsoft.Extensions.DependencyInjection; | ||||||
| global using Microsoft.Extensions.Diagnostics.HealthChecks; | global using Microsoft.Extensions.Diagnostics.HealthChecks; | ||||||
| global using Microsoft.Extensions.Hosting; |  | ||||||
| global using Microsoft.Extensions.Logging; | global using Microsoft.Extensions.Logging; | ||||||
| global using Microsoft.Extensions.Options; | global using Microsoft.Extensions.Options; | ||||||
| global using Microsoft.OpenApi.Models; | global using Microsoft.OpenApi.Models; | ||||||
| global using Serilog; |  | ||||||
| global using Swashbuckle.AspNetCore.SwaggerGen; |  | ||||||
| global using System.Collections.Generic; | global using System.Collections.Generic; | ||||||
| global using System.IdentityModel.Tokens.Jwt; | global using System.IdentityModel.Tokens.Jwt; | ||||||
| global using System.Linq; | global using System.Linq; | ||||||
| @ -39,3 +31,4 @@ global using System.Threading.Tasks; | |||||||
| global using System.Threading; | global using System.Threading; | ||||||
| global using System; | global using System; | ||||||
| global using Microsoft.IdentityModel.Tokens; | global using Microsoft.IdentityModel.Tokens; | ||||||
|  | global using Services.Common; | ||||||
|  | |||||||
| @ -28,7 +28,7 @@ public class GrpcExceptionInterceptor : Interceptor | |||||||
|         } |         } | ||||||
|         catch (RpcException e) |         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; |             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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -10,10 +10,14 @@ | |||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <Folder Include="wwwroot\" /> |     <Compile Remove="wwwroot\**" /> | ||||||
|  |     <Content Remove="wwwroot\**" /> | ||||||
|  |     <EmbeddedResource Remove="wwwroot\**" /> | ||||||
|  |     <None Remove="wwwroot\**" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|  |     <PackageReference Include="Yarp.ReverseProxy" /> | ||||||
|     <PackageReference Include="AspNetCore.HealthChecks.UI.Client" /> |     <PackageReference Include="AspNetCore.HealthChecks.UI.Client" /> | ||||||
|     <PackageReference Include="AspNetCore.HealthChecks.Uris" /> |     <PackageReference Include="AspNetCore.HealthChecks.Uris" /> | ||||||
|     <PackageReference Include="Google.Protobuf" /> |     <PackageReference Include="Google.Protobuf" /> | ||||||
| @ -25,11 +29,13 @@ | |||||||
|     <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" /> |     <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" /> | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" /> |     <PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" /> |     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" /> | ||||||
|     <PackageReference Include="Serilog.AspNetCore" /> |  | ||||||
|     <PackageReference Include="Serilog.Sinks.Console" /> |  | ||||||
|     <PackageReference Include="Swashbuckle.AspNetCore" /> |     <PackageReference Include="Swashbuckle.AspNetCore" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|  |   <ItemGroup> | ||||||
|  |     <ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" /> | ||||||
|  |   </ItemGroup> | ||||||
|  | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <Protobuf Include="..\..\..\Services\Basket\Basket.API\Proto\basket.proto" GrpcServices="Client" /> |     <Protobuf Include="..\..\..\Services\Basket\Basket.API\Proto\basket.proto" GrpcServices="Client" /> | ||||||
|     <Protobuf Include="..\..\..\Services\Catalog\Catalog.API\Proto\catalog.proto" GrpcServices="Client" /> |     <Protobuf Include="..\..\..\Services\Catalog\Catalog.API\Proto\catalog.proto" GrpcServices="Client" /> | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ | |||||||
| 
 | 
 | ||||||
| public class UpdateBasketItemsRequest | public class UpdateBasketItemsRequest | ||||||
| { | { | ||||||
| 
 |  | ||||||
|     public string BasketId { get; set; } |     public string BasketId { get; set; } | ||||||
| 
 | 
 | ||||||
|     public ICollection<UpdateBasketItemData> Updates { get; set; } |     public ICollection<UpdateBasketItemData> Updates { get; set; } | ||||||
|  | |||||||
| @ -1,23 +1,24 @@ | |||||||
| await BuildWebHost(args).RunAsync(); | var builder = WebApplication.CreateBuilder(args); | ||||||
| IWebHost BuildWebHost(string[] args) => | 
 | ||||||
|     WebHost | builder.AddServiceDefaults(); | ||||||
|         .CreateDefaultBuilder(args) | 
 | ||||||
|         .ConfigureAppConfiguration(cb => | builder.Services.AddReverseProxy(builder.Configuration); | ||||||
|         { | builder.Services.AddControllers(); | ||||||
|             var sources = cb.Sources; | 
 | ||||||
|             sources.Insert(3, new Microsoft.Extensions.Configuration.Json.JsonConfigurationSource() | builder.Services.AddHealthChecks(builder.Configuration); | ||||||
|             { | 
 | ||||||
|                 Optional = true, | builder.Services.AddApplicationServices(); | ||||||
|                 Path = "appsettings.localhost.json", | builder.Services.AddGrpcServices(); | ||||||
|                 ReloadOnChange = false | 
 | ||||||
|             }); | builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls")); | ||||||
|         }) | 
 | ||||||
|         .UseStartup<Startup>() | var app = builder.Build(); | ||||||
|         .UseSerilog((builderContext, config) => | 
 | ||||||
|         { | app.UseServiceDefaults(); | ||||||
|             config | 
 | ||||||
|                 .MinimumLevel.Information() | app.UseHttpsRedirection(); | ||||||
|                 .Enrich.FromLogContext() | 
 | ||||||
|                 .WriteTo.Console(); | app.MapControllers(); | ||||||
|         }) | app.MapReverseProxy(); | ||||||
|         .Build(); | 
 | ||||||
|  | await app.RunAsync(); | ||||||
|  | |||||||
| @ -5,5 +5,4 @@ public interface IBasketService | |||||||
|     Task<BasketData> GetByIdAsync(string id); |     Task<BasketData> GetByIdAsync(string id); | ||||||
| 
 | 
 | ||||||
|     Task UpdateAsync(BasketData currentBasket); |     Task UpdateAsync(BasketData currentBasket); | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,10 +2,10 @@ | |||||||
| 
 | 
 | ||||||
| public class OrderingService : IOrderingService | public class OrderingService : IOrderingService | ||||||
| { | { | ||||||
|     private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; |     private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | ||||||
|     private readonly ILogger<OrderingService> _logger; |     private readonly ILogger<OrderingService> _logger; | ||||||
| 
 | 
 | ||||||
|     public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) |     public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | ||||||
|     { |     { | ||||||
|         _orderingGrpcClient = orderingGrpcClient; |         _orderingGrpcClient = orderingGrpcClient; | ||||||
|         _logger = logger; |         _logger = logger; | ||||||
| @ -48,14 +48,14 @@ public class OrderingService : IOrderingService | |||||||
|         return data; |         return data; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) |     private GrpcOrdering.CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | ||||||
|     { |     { | ||||||
|         var command = new CreateOrderDraftCommand |         var command = new GrpcOrdering.CreateOrderDraftCommand | ||||||
|         { |         { | ||||||
|             BuyerId = basketData.BuyerId, |             BuyerId = basketData.BuyerId, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         basketData.Items.ForEach(i => command.Items.Add(new BasketItem |         basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem | ||||||
|         { |         { | ||||||
|             Id = i.Id, |             Id = i.Id, | ||||||
|             OldUnitPrice = (double)i.OldUnitPrice, |             OldUnitPrice = (double)i.OldUnitPrice, | ||||||
|  | |||||||
| @ -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<OrderingGrpc.OrderingGrpcClient>((services, options) => |  | ||||||
|         { |  | ||||||
|             var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; |  | ||||||
|             options.Address = new Uri(orderingApi); |  | ||||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); |  | ||||||
| 
 |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| @ -1,15 +1,138 @@ | |||||||
| { | { | ||||||
|   "Logging": { |   "Logging": { | ||||||
|     "IncludeScopes": false, |  | ||||||
|     "Debug": { |  | ||||||
|     "LogLevel": { |     "LogLevel": { | ||||||
|         "Default": "Warning" |       "Default": "Information", | ||||||
|  |       "Microsoft.AspNetCore": "Warning", | ||||||
|  |       "System.Net.Http": "Warning" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|     "Console": { |   "OpenApi": { | ||||||
|       "LogLevel": { |     "Endpoint": { | ||||||
|         "Default": "Warning" |       "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}" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "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,16 +8,19 @@ | |||||||
|     "grpcCatalog": "http://localhost:81", |     "grpcCatalog": "http://localhost:81", | ||||||
|     "grpcOrdering": "http://localhost:5581" |     "grpcOrdering": "http://localhost:5581" | ||||||
|   }, |   }, | ||||||
|   "IdentityUrlExternal": "http://localhost:5105", |   "Identity": { | ||||||
|   "IdentityUrl": "http://localhost:5105", |     "ExternalUrl": "http://localhost:5105", | ||||||
|  |     "Url": "http://localhost:5105", | ||||||
|  |   }, | ||||||
|   "Logging": { |   "Logging": { | ||||||
|     "IncludeScopes": false, |  | ||||||
|     "Debug": { |     "Debug": { | ||||||
|  |       "IncludeScopes": false, | ||||||
|       "LogLevel": { |       "LogLevel": { | ||||||
|         "Default": "Debug" |         "Default": "Debug" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "Console": { |     "Console": { | ||||||
|  |       "IncludeScopes": false, | ||||||
|       "LogLevel": { |       "LogLevel": { | ||||||
|         "Default": "Debug" |         "Default": "Debug" | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -1,11 +0,0 @@ | |||||||
| namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers; |  | ||||||
| 
 |  | ||||||
| [Route("")] |  | ||||||
| public class HomeController : Controller |  | ||||||
| { |  | ||||||
|     [HttpGet] |  | ||||||
|     public IActionResult Index() |  | ||||||
|     { |  | ||||||
|         return new RedirectResult("~/swagger"); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -32,6 +32,7 @@ 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.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/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/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/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.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 "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" | ||||||
| COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.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,41 +1,27 @@ | |||||||
| global using CatalogApi; | global using System; | ||||||
| global using Grpc.Core.Interceptors; | global using System.Collections.Generic; | ||||||
|  | global using System.Linq; | ||||||
|  | global using System.Net; | ||||||
|  | global using System.Net.Http; | ||||||
|  | global using System.Net.Http.Headers; | ||||||
|  | global using System.Text.Json; | ||||||
|  | global using System.Threading; | ||||||
|  | global using System.Threading.Tasks; | ||||||
|  | global using CatalogApi; | ||||||
| global using Grpc.Core; | global using Grpc.Core; | ||||||
|  | global using Grpc.Core.Interceptors; | ||||||
| global using GrpcBasket; | global using GrpcBasket; | ||||||
| global using GrpcOrdering; |  | ||||||
| global using HealthChecks.UI.Client; |  | ||||||
| global using Microsoft.AspNetCore.Authentication; | global using Microsoft.AspNetCore.Authentication; | ||||||
| global using Microsoft.AspNetCore.Authorization; | global using Microsoft.AspNetCore.Authorization; | ||||||
| global using Microsoft.AspNetCore.Builder; | 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.Http; | ||||||
| global using Microsoft.AspNetCore.Mvc; | global using Microsoft.AspNetCore.Mvc; | ||||||
| global using Microsoft.AspNetCore; |  | ||||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; | 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.Infrastructure; | ||||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | ||||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | ||||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; |  | ||||||
| global using Microsoft.Extensions.Configuration; | global using Microsoft.Extensions.Configuration; | ||||||
| global using Microsoft.Extensions.DependencyInjection; | 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.Logging; | ||||||
| global using Microsoft.Extensions.Options; | global using Microsoft.Extensions.Options; | ||||||
| global using Microsoft.OpenApi.Models; | global using Services.Common; | ||||||
| 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; |  | ||||||
|  | |||||||
| @ -28,7 +28,7 @@ public class GrpcExceptionInterceptor : Interceptor | |||||||
|         } |         } | ||||||
|         catch (RpcException e) |         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; |             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,156 +1,14 @@ | |||||||
| var appName = "Web.Shopping.HttpAggregator"; | var builder = WebApplication.CreateBuilder(args); | ||||||
| var builder = WebApplication.CreateBuilder(args); |  | ||||||
| 
 | 
 | ||||||
| builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration)); | builder.AddServiceDefaults(); | ||||||
| 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(); |  | ||||||
| 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.UseHttpsRedirection(); | builder.Services.AddReverseProxy(builder.Configuration); | ||||||
|  | builder.Services.AddControllers(); | ||||||
| 
 | 
 | ||||||
| app.UseSwagger().UseSwaggerUI(c => | builder.Services.AddHealthChecks(builder.Configuration); | ||||||
| { | builder.Services.AddCors(options => | ||||||
|     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") |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| 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 => |  | ||||||
| { | { | ||||||
|  |     // TODO: Read allowed origins from configuration | ||||||
|     options.AddPolicy("CorsPolicy", |     options.AddPolicy("CorsPolicy", | ||||||
|         builder => builder |         builder => builder | ||||||
|         .SetIsOriginAllowed((host) => true) |         .SetIsOriginAllowed((host) => true) | ||||||
| @ -159,50 +17,22 @@ public static class ServiceCollectionExtensions | |||||||
|         .AllowCredentials()); |         .AllowCredentials()); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|         return services; | builder.Services.AddApplicationServices(); | ||||||
|     } | builder.Services.AddGrpcServices(); | ||||||
|     public static IServiceCollection AddApplicationServices(this IServiceCollection services) |  | ||||||
|     { |  | ||||||
|         //register delegating handlers |  | ||||||
|         services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); |  | ||||||
|         services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); |  | ||||||
| 
 | 
 | ||||||
|         //register http services | builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls")); | ||||||
| 
 | 
 | ||||||
|         services.AddHttpClient<IOrderApiClient, OrderApiClient>() | var app = builder.Build(); | ||||||
|             .AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>(); |  | ||||||
| 
 | 
 | ||||||
|         return services; | app.UseServiceDefaults(); | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     public static IServiceCollection AddGrpcServices(this IServiceCollection services) | app.UseHttpsRedirection(); | ||||||
|     { |  | ||||||
|         services.AddTransient<GrpcExceptionInterceptor>(); |  | ||||||
| 
 | 
 | ||||||
|         services.AddScoped<IBasketService, BasketService>(); | app.UseCors("CorsPolicy"); | ||||||
|  | app.UseAuthentication(); | ||||||
|  | app.UseAuthorization(); | ||||||
| 
 | 
 | ||||||
|         services.AddGrpcClient<Basket.BasketClient>((services, options) => | app.MapControllers(); | ||||||
|         { | app.MapReverseProxy(); | ||||||
|             var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; |  | ||||||
|             options.Address = new Uri(basketApi); |  | ||||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); |  | ||||||
| 
 | 
 | ||||||
|         services.AddScoped<ICatalogService, CatalogService>(); | await app.RunAsync(); | ||||||
| 
 |  | ||||||
|         services.AddGrpcClient<Catalog.CatalogClient>((services, options) => |  | ||||||
|         { |  | ||||||
|             var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; |  | ||||||
|             options.Address = new Uri(catalogApi); |  | ||||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); |  | ||||||
| 
 |  | ||||||
|         services.AddScoped<IOrderingService, OrderingService>(); |  | ||||||
| 
 |  | ||||||
|         services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) => |  | ||||||
|         { |  | ||||||
|             var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; |  | ||||||
|             options.Address = new Uri(orderingApi); |  | ||||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); |  | ||||||
| 
 |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,29 +1,12 @@ | |||||||
| { | { | ||||||
|   "iisSettings": { |  | ||||||
|     "windowsAuthentication": false, |  | ||||||
|     "anonymousAuthentication": true, |  | ||||||
|     "iisExpress": { |  | ||||||
|       "applicationUrl": "http://localhost:57425/", |  | ||||||
|       "sslPort": 0 |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "profiles": { |   "profiles": { | ||||||
|     "IIS Express": { |     "Web.Shopping.HttpAggregator": { | ||||||
|       "commandName": "IISExpress", |  | ||||||
|       "launchBrowser": true, |  | ||||||
|       "launchUrl": "api/values", |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "PurchaseForMvc": { |  | ||||||
|       "commandName": "Project", |       "commandName": "Project", | ||||||
|       "launchBrowser": true, |       "launchBrowser": true, | ||||||
|       "launchUrl": "api/values", |       "applicationUrl": "http://localhost:5229/", | ||||||
|       "environmentVariables": { |       "environmentVariables": { | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |         "ASPNETCORE_ENVIRONMENT": "Development" | ||||||
|       }, |       } | ||||||
|       "applicationUrl": "http://localhost:61632/" |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -2,10 +2,10 @@ | |||||||
| 
 | 
 | ||||||
| public class OrderingService : IOrderingService | public class OrderingService : IOrderingService | ||||||
| { | { | ||||||
|     private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; |     private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | ||||||
|     private readonly ILogger<OrderingService> _logger; |     private readonly ILogger<OrderingService> _logger; | ||||||
| 
 | 
 | ||||||
|     public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) |     public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | ||||||
|     { |     { | ||||||
|         _orderingGrpcClient = orderingGrpcClient; |         _orderingGrpcClient = orderingGrpcClient; | ||||||
|         _logger = logger; |         _logger = logger; | ||||||
| @ -48,14 +48,14 @@ public class OrderingService : IOrderingService | |||||||
|         return data; |         return data; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) |     private GrpcOrdering.CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) | ||||||
|     { |     { | ||||||
|         var command = new CreateOrderDraftCommand |         var command = new GrpcOrdering.CreateOrderDraftCommand | ||||||
|         { |         { | ||||||
|             BuyerId = basketData.BuyerId, |             BuyerId = basketData.BuyerId, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         basketData.Items.ForEach(i => command.Items.Add(new BasketItem |         basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem | ||||||
|         { |         { | ||||||
|             Id = i.Id, |             Id = i.Id, | ||||||
|             OldUnitPrice = (double)i.OldUnitPrice, |             OldUnitPrice = (double)i.OldUnitPrice, | ||||||
|  | |||||||
| @ -5,30 +5,18 @@ | |||||||
|     <AssemblyName>Web.Shopping.HttpAggregator</AssemblyName> |     <AssemblyName>Web.Shopping.HttpAggregator</AssemblyName> | ||||||
|     <RootNamespace>Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator</RootNamespace> |     <RootNamespace>Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator</RootNamespace> | ||||||
|     <DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath> |     <DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | ||||||
|     <GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> |  | ||||||
|     <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>     |  | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <Folder Include="wwwroot\" /> |     <PackageReference Include="Yarp.ReverseProxy" /> | ||||||
|   </ItemGroup> |  | ||||||
| 
 |  | ||||||
|   <ItemGroup> |  | ||||||
|     <PackageReference Include="AspNetCore.HealthChecks.UI.Client" /> |  | ||||||
|     <PackageReference Include="AspNetCore.HealthChecks.Uris" /> |     <PackageReference Include="AspNetCore.HealthChecks.Uris" /> | ||||||
|     <PackageReference Include="Google.Protobuf" /> |     <PackageReference Include="Google.Protobuf" /> | ||||||
|     <PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" /> |     <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="Grpc.Tools" PrivateAssets="All" /> | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" /> |   </ItemGroup> | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" /> | 
 | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" /> |   <ItemGroup> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" /> |     <ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" /> | ||||||
|     <PackageReference Include="Serilog.AspNetCore" /> |  | ||||||
|     <PackageReference Include="Serilog.Sinks.Console" /> |  | ||||||
|     <PackageReference Include="Swashbuckle.AspNetCore" /> |  | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|  | |||||||
| @ -1,15 +1,8 @@ | |||||||
| { | { | ||||||
|   "Logging": { |   "Logging": { | ||||||
|     "IncludeScopes": false, |  | ||||||
|     "Debug": { |  | ||||||
|     "LogLevel": { |     "LogLevel": { | ||||||
|         "Default": "Debug" |       "Default": "Information", | ||||||
|       } |       "Microsoft.AspNetCore": "Warning" | ||||||
|     }, |  | ||||||
|     "Console": { |  | ||||||
|       "LogLevel": { |  | ||||||
|         "Default": "Debug" |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,15 +1,138 @@ | |||||||
| { | { | ||||||
|   "Logging": { |   "Logging": { | ||||||
|     "IncludeScopes": false, |  | ||||||
|     "Debug": { |  | ||||||
|     "LogLevel": { |     "LogLevel": { | ||||||
|         "Default": "Warning" |       "Default": "Information", | ||||||
|  |       "Microsoft.AspNetCore": "Warning", | ||||||
|  |       "System.Net.Http": "Warning" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|     "Console": { |   "OpenApi": { | ||||||
|       "LogLevel": { |     "Endpoint": { | ||||||
|         "Default": "Warning" |       "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}" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "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; |             Handled = false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public async Task Handle(TestIntegrationEvent @event) |         public Task Handle(TestIntegrationEvent @event) | ||||||
|         { |         { | ||||||
|             Handled = true; |             Handled = true; | ||||||
|  |             return Task.CompletedTask; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -12,9 +12,10 @@ namespace EventBus.Tests | |||||||
|             Handled = false; |             Handled = false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public async Task Handle(TestIntegrationEvent @event) |         public Task Handle(TestIntegrationEvent @event) | ||||||
|         { |         { | ||||||
|             Handled = true; |             Handled = true; | ||||||
|  |             return Task.CompletedTask; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -8,12 +8,6 @@ public interface IEventBus | |||||||
|         where T : IntegrationEvent |         where T : IntegrationEvent | ||||||
|         where TH : IIntegrationEventHandler<T>; |         where TH : IIntegrationEventHandler<T>; | ||||||
| 
 | 
 | ||||||
|     void SubscribeDynamic<TH>(string eventName) |  | ||||||
|         where TH : IDynamicIntegrationEventHandler; |  | ||||||
| 
 |  | ||||||
|     void UnsubscribeDynamic<TH>(string eventName) |  | ||||||
|         where TH : IDynamicIntegrationEventHandler; |  | ||||||
| 
 |  | ||||||
|     void Unsubscribe<T, TH>() |     void Unsubscribe<T, TH>() | ||||||
|         where TH : IIntegrationEventHandler<T> |         where TH : IIntegrationEventHandler<T> | ||||||
|         where T : IntegrationEvent; |         where T : IntegrationEvent; | ||||||
|  | |||||||
| @ -59,7 +59,7 @@ public class DefaultRabbitMQPersistentConnection | |||||||
|                 .Or<BrokerUnreachableException>() |                 .Or<BrokerUnreachableException>() | ||||||
|                 .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => |                 .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 |             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; |                 return false; | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ using Microsoft.Extensions.DependencyInjection; | |||||||
| public class EventBusRabbitMQ : IEventBus, IDisposable | public class EventBusRabbitMQ : IEventBus, IDisposable | ||||||
| { | { | ||||||
|     const string BROKER_NAME = "eshop_event_bus"; |     const string BROKER_NAME = "eshop_event_bus"; | ||||||
|     const string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; |  | ||||||
| 
 | 
 | ||||||
|     private readonly IRabbitMQPersistentConnection _persistentConnection; |     private readonly IRabbitMQPersistentConnection _persistentConnection; | ||||||
|     private readonly ILogger<EventBusRabbitMQ> _logger; |     private readonly ILogger<EventBusRabbitMQ> _logger; | ||||||
| @ -58,7 +57,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable | |||||||
|             .Or<SocketException>() |             .Or<SocketException>() | ||||||
|             .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => |             .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; |         var eventName = @event.GetType().Name; | ||||||
| @ -194,7 +193,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable | |||||||
|         } |         } | ||||||
|         catch (Exception ex) |         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. |         // Even on exception we take the message off the queue. | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ | |||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="Autofac" /> |  | ||||||
|     <PackageReference Include="Microsoft.CSharp" /> |     <PackageReference Include="Microsoft.CSharp" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Logging" />     |     <PackageReference Include="Microsoft.Extensions.Logging" />     | ||||||
|     <PackageReference Include="Polly" /> |     <PackageReference Include="Polly" /> | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ global using RabbitMQ.Client.Exceptions; | |||||||
| global using System; | global using System; | ||||||
| global using System.IO; | global using System.IO; | ||||||
| global using System.Net.Sockets; | global using System.Net.Sockets; | ||||||
| global using Autofac; |  | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||||
| 
 | 
 | ||||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | ||||||
| 
 | 
 | ||||||
| @ -12,7 +12,6 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable | |||||||
|     private readonly string _subscriptionName; |     private readonly string _subscriptionName; | ||||||
|     private readonly ServiceBusSender _sender; |     private readonly ServiceBusSender _sender; | ||||||
|     private readonly ServiceBusProcessor _processor; |     private readonly ServiceBusProcessor _processor; | ||||||
|     private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; |  | ||||||
|     private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; |     private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; | ||||||
| 
 | 
 | ||||||
|     public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, |     public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, | ||||||
| @ -141,7 +140,7 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable | |||||||
|         var ex = args.Exception; |         var ex = args.Exception; | ||||||
|         var context = args.ErrorSource; |         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; |         return Task.CompletedTask; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ | |||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="Autofac" /> |  | ||||||
|     <PackageReference Include="Azure.Messaging.ServiceBus" /> |     <PackageReference Include="Azure.Messaging.ServiceBus" /> | ||||||
|     <PackageReference Include="Microsoft.CSharp" /> |     <PackageReference Include="Microsoft.CSharp" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Logging" /> |     <PackageReference Include="Microsoft.Extensions.Logging" /> | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ | |||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | ||||||
| global using System.Threading.Tasks; | global using System.Threading.Tasks; | ||||||
| global using System; | global using System; | ||||||
| global using Autofac; |  | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | ||||||
| global using Microsoft.Extensions.Logging; | global using Microsoft.Extensions.Logging; | ||||||
| global using System.Text; | global using System.Text; | ||||||
|  | |||||||
| @ -1,30 +1,30 @@ | |||||||
| using Microsoft.EntityFrameworkCore; | using System; | ||||||
|  | using System.Data.SqlClient; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Configuration; | using Microsoft.Extensions.Configuration; | ||||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| using Polly; | 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 IWebHost webHost) |         public static bool IsInKubernetes(this IServiceProvider services) | ||||||
|         { |         { | ||||||
|             var cfg = webHost.Services.GetService<IConfiguration>(); |             var cfg = services.GetService<IConfiguration>(); | ||||||
|             var orchestratorType = cfg.GetValue<string>("OrchestratorType"); |             var orchestratorType = cfg.GetValue<string>("OrchestratorType"); | ||||||
|             return orchestratorType?.ToUpper() == "K8S"; |             return orchestratorType?.ToUpper() == "K8S"; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public static IWebHost MigrateDbContext<TContext>(this IWebHost webHost, Action<TContext, IServiceProvider> seeder) where TContext : DbContext |         public static IServiceProvider MigrateDbContext<TContext>(this IServiceProvider services, Action<TContext, IServiceProvider> seeder) where TContext : DbContext | ||||||
|         { |         { | ||||||
|             var underK8s = webHost.IsInKubernetes(); |             var underK8s = services.IsInKubernetes(); | ||||||
| 
 | 
 | ||||||
|             using var scope = webHost.Services.CreateScope(); |             using var scope = services.CreateScope(); | ||||||
|             var services = scope.ServiceProvider; |             var scopeServices = scope.ServiceProvider; | ||||||
|             var logger = services.GetRequiredService<ILogger<TContext>>(); |             var logger = scopeServices.GetRequiredService<ILogger<TContext>>(); | ||||||
|             var context = services.GetService<TContext>(); |             var context = scopeServices.GetService<TContext>(); | ||||||
| 
 | 
 | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
| @ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Hosting | |||||||
| 
 | 
 | ||||||
|                 if (underK8s) |                 if (underK8s) | ||||||
|                 { |                 { | ||||||
|                     InvokeSeeder(seeder, context, services); |                     InvokeSeeder(seeder, context, scopeServices); | ||||||
|                 } |                 } | ||||||
|                 else |                 else | ||||||
|                 { |                 { | ||||||
| @ -43,14 +43,14 @@ namespace Microsoft.AspNetCore.Hosting | |||||||
|                             sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), |                             sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), | ||||||
|                             onRetry: (exception, timeSpan, retry, ctx) => |                             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); |                                 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 |                     //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  |                     //migration can't fail for network related exception. The retry options for DbContext only  | ||||||
|                     //apply to transient exceptions |                     //apply to transient exceptions | ||||||
|                     // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) |                     // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) | ||||||
|                     retry.Execute(() => InvokeSeeder(seeder, context, services)); |                     retry.Execute(() => InvokeSeeder(seeder, context, scopeServices)); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); |                 logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); | ||||||
| @ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Hosting | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return webHost; |             return services; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) |         private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) | ||||||
|  | |||||||
| @ -3,7 +3,6 @@ | |||||||
|     <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> |     <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> | ||||||
|     <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled> |     <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageVersion Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" /> |     <PackageVersion Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" /> | ||||||
|     <PackageVersion Include="AspNetCore.HealthChecks.AzureStorage" Version="6.1.2" /> |     <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.Client" Version="6.0.5" /> | ||||||
|     <PackageVersion Include="AspNetCore.HealthChecks.UI.InMemory.Storage" 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="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.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" /> | ||||||
|     <PackageVersion Include="Azure.Identity" Version="1.8.2" /> |     <PackageVersion Include="Azure.Identity" Version="1.8.2" /> | ||||||
|     <PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.12.0" /> |     <PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.12.0" /> | ||||||
| @ -29,6 +26,7 @@ | |||||||
|     <PackageVersion Include="FluentValidation.AspNetCore" Version="11.2.2" /> |     <PackageVersion Include="FluentValidation.AspNetCore" Version="11.2.2" /> | ||||||
|     <PackageVersion Include="Google.Protobuf" Version="3.22.0" /> |     <PackageVersion Include="Google.Protobuf" Version="3.22.0" /> | ||||||
|     <PackageVersion Include="Grpc.AspNetCore.Server" Version="2.51.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.AspNetCore.Server.ClientFactory" Version="2.51.0" /> | ||||||
|     <PackageVersion Include="Grpc.Core" Version="2.46.6" /> |     <PackageVersion Include="Grpc.Core" Version="2.46.6" /> | ||||||
|     <PackageVersion Include="Grpc.Net.Client" Version="2.51.0" /> |     <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.Identity.UI" Version="7.0.3" /> | ||||||
|     <PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" 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.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.SignalR.StackExchangeRedis" Version="7.0.3" /> | ||||||
|     <PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.3" /> |     <PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.3" /> | ||||||
|     <PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="7.0.3" /> |     <PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="7.0.3" /> | ||||||
| @ -82,12 +81,6 @@ | |||||||
|     <PackageVersion Include="Newtonsoft.Json" Version="13.0.2" /> |     <PackageVersion Include="Newtonsoft.Json" Version="13.0.2" /> | ||||||
|     <PackageVersion Include="Polly" Version="7.2.3" /> |     <PackageVersion Include="Polly" Version="7.2.3" /> | ||||||
|     <PackageVersion Include="RabbitMQ.Client" Version="6.4.0" /> |     <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" Version="6.5.0" /> | ||||||
|     <PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" /> |     <PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" /> | ||||||
|     <PackageVersion Include="System.Data.SqlClient" Version="4.8.5" /> |     <PackageVersion Include="System.Data.SqlClient" Version="4.8.5" /> | ||||||
| @ -95,5 +88,6 @@ | |||||||
|     <PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" /> |     <PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" /> | ||||||
|     <PackageVersion Include="xunit" Version="2.4.2" /> |     <PackageVersion Include="xunit" Version="2.4.2" /> | ||||||
|     <PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" /> |     <PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" /> | ||||||
|  |     <PackageVersion Include="Yarp.ReverseProxy" Version="2.0.0" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| </Project> | </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,59 +1,25 @@ | |||||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||||
|   <PropertyGroup> |   <PropertyGroup> | ||||||
|     <TargetFramework>net7.0</TargetFramework> |     <TargetFramework>net7.0</TargetFramework> | ||||||
| 		<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |  | ||||||
|     <DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> |     <DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | ||||||
| 		<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> |     <UserSecretsId>2964ec8e-0d48-4541-b305-94cab537f867</UserSecretsId> | ||||||
| 		<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled> |  | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
| 		<Content Update="web.config"> |     <PackageReference Include="Grpc.AspNetCore" /> | ||||||
| 			<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> |  | ||||||
| 		</Content> |  | ||||||
| 	</ItemGroup> |  | ||||||
| 
 |  | ||||||
| 	<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.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="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> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
| 		<Protobuf Include="Proto\basket.proto" GrpcServices="Server" Generator="MSBuild:Compile" /> |     <Protobuf Include="Proto\basket.proto" GrpcServices="Server" /> | ||||||
| 		<Content Include="@(Protobuf)" /> |  | ||||||
| 		<None Remove="@(Protobuf)" /> |  | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
| 		<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" /> |     <ProjectReference Include="..\..\Services.Common\Services.Common.csproj" /> | ||||||
| 		<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" /> |   </ItemGroup> | ||||||
| 		<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" /> | 
 | ||||||
|  |   <ItemGroup> | ||||||
|  |     <InternalsVisibleTo Include="Basket.FunctionalTests" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| </Project> | </Project> | ||||||
|  | |||||||
| @ -56,7 +56,7 @@ public class BasketController : ControllerBase | |||||||
|             return BadRequest(); |             return BadRequest(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         var userName = this.HttpContext.User.FindFirst(x => x.Type == ClaimTypes.Name).Value; |         var userName = User.FindFirst(x => x.Type == ClaimTypes.Name).Value; | ||||||
| 
 | 
 | ||||||
|         var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street, |         var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street, | ||||||
|             basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName, |             basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName, | ||||||
| @ -71,7 +71,7 @@ public class BasketController : ControllerBase | |||||||
|         } |         } | ||||||
|         catch (Exception ex) |         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; |             throw; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -1,11 +0,0 @@ | |||||||
| namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers; |  | ||||||
| 
 |  | ||||||
| public class HomeController : Controller |  | ||||||
| { |  | ||||||
|     // GET: /<controller>/ |  | ||||||
|     public IActionResult Index() |  | ||||||
|     { |  | ||||||
|         return new RedirectResult("~/swagger"); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @ -1,37 +0,0 @@ | |||||||
| namespace Microsoft.eShopOnContainers.Services.Basket.API; |  | ||||||
| 
 |  | ||||||
| public static class CustomExtensionMethods |  | ||||||
| { |  | ||||||
|     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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -32,6 +32,7 @@ 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.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/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/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/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.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 "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" | ||||||
| COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.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,29 @@ | |||||||
| global using Autofac.Extensions.DependencyInjection; | global using System; | ||||||
| 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 Basket.API.IntegrationEvents.EventHandling; |  | ||||||
| global using Basket.API.IntegrationEvents.Events; |  | ||||||
| global using Basket.API.Model; |  | ||||||
| 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 StackExchange.Redis; |  | ||||||
| global using Swashbuckle.AspNetCore.SwaggerGen; |  | ||||||
| global using System.Collections.Generic; | global using System.Collections.Generic; | ||||||
| global using System.ComponentModel.DataAnnotations; | global using System.ComponentModel.DataAnnotations; | ||||||
| global using System.IdentityModel.Tokens.Jwt; |  | ||||||
| global using System.IO; |  | ||||||
| global using System.Linq; | global using System.Linq; | ||||||
| global using System.Net; | global using System.Net; | ||||||
| global using System.Security.Claims; | global using System.Security.Claims; | ||||||
| global using System.Text.Json; | global using System.Text.Json; | ||||||
| global using System.Threading.Tasks; | global using System.Threading.Tasks; | ||||||
| global using System; | 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 Microsoft.AspNetCore.Authorization; | ||||||
|  | global using Microsoft.AspNetCore.Builder; | ||||||
|  | global using Microsoft.AspNetCore.Http; | ||||||
|  | global using Microsoft.AspNetCore.Mvc; | ||||||
|  | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | ||||||
|  | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | ||||||
|  | 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.Extensions.Configuration; | ||||||
|  | global using Microsoft.Extensions.DependencyInjection; | ||||||
|  | global using Microsoft.Extensions.Logging; | ||||||
|  | global using Services.Common; | ||||||
|  | global using StackExchange.Redis; | ||||||
|  | |||||||
| @ -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,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) |     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()); |             await _repository.DeleteBasketAsync(@event.UserId.ToString()); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -15,9 +15,9 @@ public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandl | |||||||
| 
 | 
 | ||||||
|     public async Task Handle(ProductPriceChangedIntegrationEvent @event) |     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(); |             var userIds = _repository.GetUsers(); | ||||||
| 
 | 
 | ||||||
| @ -36,7 +36,7 @@ public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandl | |||||||
| 
 | 
 | ||||||
|         if (itemsToUpdate != null) |         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) |             foreach (var item in itemsToUpdate) | ||||||
|             { |             { | ||||||
|  | |||||||
| @ -1,333 +1,30 @@ | |||||||
| using Autofac.Core; | var builder = WebApplication.CreateBuilder(args); | ||||||
| using Microsoft.Azure.Amqp.Framing; |  | ||||||
| using Microsoft.Extensions.Configuration; |  | ||||||
| 
 | 
 | ||||||
| var appName = "Basket.API"; | builder.AddServiceDefaults(); | ||||||
| var builder = WebApplication.CreateBuilder(new WebApplicationOptions |  | ||||||
| { |  | ||||||
|     Args = args, |  | ||||||
|     ApplicationName = typeof(Program).Assembly.FullName, |  | ||||||
|     ContentRootPath = Directory.GetCurrentDirectory() |  | ||||||
| }); |  | ||||||
| if (builder.Configuration.GetValue<bool>("UseVault", false)) |  | ||||||
| { |  | ||||||
|     TokenCredential credential = new ClientSecretCredential( |  | ||||||
|            builder.Configuration["Vault:TenantId"], |  | ||||||
|         builder.Configuration["Vault:ClientId"], |  | ||||||
|         builder.Configuration["Vault:ClientSecret"]); |  | ||||||
|     builder.Configuration.AddAzureKeyVault(new Uri($"https://{builder.Configuration["Vault:Name"]}.vault.azure.net/"), credential); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| builder.Services.AddGrpc(options => | builder.Services.AddGrpc(); | ||||||
| { | builder.Services.AddControllers(); | ||||||
|     options.EnableDetailedErrors = true; | builder.Services.AddProblemDetails(); | ||||||
| }); |  | ||||||
| builder.Services.AddApplicationInsightsTelemetry(builder.Configuration); |  | ||||||
| builder.Services.AddApplicationInsightsKubernetesEnricher(); |  | ||||||
| builder.Services.AddControllers(options => |  | ||||||
| { |  | ||||||
|     options.Filters.Add(typeof(HttpGlobalExceptionFilter)); |  | ||||||
|     options.Filters.Add(typeof(ValidateModelStateFilter)); |  | ||||||
| 
 | 
 | ||||||
| }) // Added for functional tests | builder.Services.AddHealthChecks(builder.Configuration); | ||||||
|             .AddApplicationPart(typeof(BasketController).Assembly) | builder.Services.AddRedis(builder.Configuration); | ||||||
|             .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); |  | ||||||
| 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 | builder.Services.AddTransient<ProductPriceChangedIntegrationEventHandler>(); | ||||||
|     { | builder.Services.AddTransient<OrderStartedIntegrationEventHandler>(); | ||||||
|         Type = SecuritySchemeType.OAuth2, |  | ||||||
|         Flows = new OpenApiOAuthFlows() |  | ||||||
|         { |  | ||||||
|             Implicit = new OpenApiOAuthFlow() |  | ||||||
|             { |  | ||||||
|                 AuthorizationUrl = new Uri($"{builder.Configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), |  | ||||||
|                 TokenUrl = new Uri($"{builder.Configuration.GetValue<string>("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.GetValue<string>("IdentityUrl"); |  | ||||||
| 
 |  | ||||||
| builder.Services.AddAuthentication("Bearer").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.AddSingleton<ConnectionMultiplexer>(sp => |  | ||||||
| { |  | ||||||
|     var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value; |  | ||||||
|     var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true); |  | ||||||
| 
 |  | ||||||
|     return ConnectionMultiplexer.Connect(configuration); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| if (builder.Configuration.GetValue<bool>("AzureServiceBusEnabled")) |  | ||||||
| { |  | ||||||
|     builder.Services.AddSingleton<IServiceBusPersisterConnection>(sp => |  | ||||||
|     { |  | ||||||
|         var serviceBusConnectionString = builder.Configuration["EventBusConnection"]; |  | ||||||
| 
 |  | ||||||
|         return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| else |  | ||||||
| { |  | ||||||
|     builder.Services.AddSingleton<IRabbitMQPersistentConnection>(sp => |  | ||||||
|     { |  | ||||||
|         var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>(); |  | ||||||
| 
 |  | ||||||
|         var factory = new ConnectionFactory() |  | ||||||
|         { |  | ||||||
|             HostName = builder.Configuration["EventBusConnection"], |  | ||||||
|             DispatchConsumersAsync = true |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         if (!string.IsNullOrEmpty(builder.Configuration["EventBusUserName"])) |  | ||||||
|         { |  | ||||||
|             factory.UserName = builder.Configuration["EventBusUserName"]; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (!string.IsNullOrEmpty(builder.Configuration["EventBusPassword"])) |  | ||||||
|         { |  | ||||||
|             factory.Password = builder.Configuration["EventBusPassword"]; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         var retryCount = 5; |  | ||||||
|         if (!string.IsNullOrEmpty(builder.Configuration["EventBusRetryCount"])) |  | ||||||
|         { |  | ||||||
|             retryCount = int.Parse(builder.Configuration["EventBusRetryCount"]); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| builder.Services.RegisterEventBus(builder.Configuration); |  | ||||||
| builder.Services.AddCors(options => |  | ||||||
| { |  | ||||||
|     options.AddPolicy("CorsPolicy", |  | ||||||
|         builder => builder |  | ||||||
|         .SetIsOriginAllowed((host) => true) |  | ||||||
|         .AllowAnyMethod() |  | ||||||
|         .AllowAnyHeader() |  | ||||||
|         .AllowCredentials()); |  | ||||||
| }); |  | ||||||
| builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); |  | ||||||
| builder.Services.AddTransient<IBasketRepository, RedisBasketRepository>(); | builder.Services.AddTransient<IBasketRepository, RedisBasketRepository>(); | ||||||
| builder.Services.AddTransient<IIdentityService, IdentityService>(); | builder.Services.AddTransient<IIdentityService, IdentityService>(); | ||||||
| 
 | 
 | ||||||
| builder.Services.AddOptions(); |  | ||||||
| builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()); |  | ||||||
| builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); |  | ||||||
| builder.Configuration.AddEnvironmentVariables(); |  | ||||||
| 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.WebHost.CaptureStartupErrors(false); |  | ||||||
| builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration)); |  | ||||||
| builder.WebHost.UseFailing(options => |  | ||||||
| { |  | ||||||
|     options.ConfigPath = "/Failing"; |  | ||||||
|     options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" }); |  | ||||||
| }); |  | ||||||
| var app = builder.Build(); | var app = builder.Build(); | ||||||
| 
 | 
 | ||||||
| if (app.Environment.IsDevelopment()) | app.UseServiceDefaults(); | ||||||
| { |  | ||||||
|     app.UseDeveloperExceptionPage(); |  | ||||||
| } |  | ||||||
| else |  | ||||||
| { |  | ||||||
|     app.UseExceptionHandler("/Home/Error"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var pathBase = app.Configuration["PATH_BASE"]; |  | ||||||
| if (!string.IsNullOrEmpty(pathBase)) |  | ||||||
| { |  | ||||||
|     app.UsePathBase(pathBase); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| app.UseSwagger() |  | ||||||
|             .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.UseRouting(); |  | ||||||
| app.UseCors("CorsPolicy"); |  | ||||||
| app.UseAuthentication(); |  | ||||||
| app.UseAuthorization(); |  | ||||||
| app.UseStaticFiles(); |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| app.MapGrpcService<BasketService>(); | app.MapGrpcService<BasketService>(); | ||||||
| app.MapDefaultControllerRoute(); |  | ||||||
| app.MapControllers(); | 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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
| app.MapHealthChecks("/hc", new HealthCheckOptions() |  | ||||||
| { |  | ||||||
|     Predicate = _ => true, |  | ||||||
|     ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse |  | ||||||
| }); |  | ||||||
| app.MapHealthChecks("/liveness", new HealthCheckOptions |  | ||||||
| { |  | ||||||
|     Predicate = r => r.Name.Contains("self") |  | ||||||
| }); |  | ||||||
| ConfigureEventBus(app); |  | ||||||
| try |  | ||||||
| { |  | ||||||
|     Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); |  | ||||||
| 
 | 
 | ||||||
| 
 | var eventBus = app.Services.GetRequiredService<IEventBus>(); | ||||||
|     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<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>(); | ||||||
| eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>(); | eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>(); | ||||||
| } |  | ||||||
| public partial class Program |  | ||||||
| { |  | ||||||
| 
 | 
 | ||||||
|     public static string Namespace = typeof(Program).Assembly.GetName().Name; | await app.RunAsync(); | ||||||
|     public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| public static class CustomExtensionMethods |  | ||||||
| { |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     public static IServiceCollection RegisterEventBus(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         if (configuration.GetValue<bool>("AzureServiceBusEnabled")) |  | ||||||
|         { |  | ||||||
|             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<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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,26 +1,11 @@ | |||||||
| { | { | ||||||
|   "iisSettings": { |  | ||||||
|     "windowsAuthentication": false, |  | ||||||
|     "anonymousAuthentication": true, |  | ||||||
|     "iisExpress": { |  | ||||||
|       "applicationUrl": "http://localhost:58017/", |  | ||||||
|       "sslPort": 0 |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "profiles": { |   "profiles": { | ||||||
|     "IIS Express": { |     "Basket.API": { | ||||||
|       "commandName": "IISExpress", |  | ||||||
|       "launchBrowser": true, |  | ||||||
|       "launchUrl": "swagger", |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "Microsoft.eShopOnContainers.Services.Basket.API": { |  | ||||||
|       "commandName": "Project", |       "commandName": "Project", | ||||||
|       "launchBrowser": true, |       "launchBrowser": true, | ||||||
|       "launchUrl": "http://localhost:55103/", |       "applicationUrl": "http://localhost:5221", | ||||||
|       "environmentVariables": { |       "environmentVariables": { | ||||||
|  |         "Identity__Url": "http://localhost:5223", | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |         "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 | public class RedisBasketRepository : IBasketRepository | ||||||
| { | { | ||||||
| @ -6,9 +6,9 @@ public class RedisBasketRepository : IBasketRepository | |||||||
|     private readonly ConnectionMultiplexer _redis; |     private readonly ConnectionMultiplexer _redis; | ||||||
|     private readonly IDatabase _database; |     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; |         _redis = redis; | ||||||
|         _database = redis.GetDatabase(); |         _database = redis.GetDatabase(); | ||||||
|     } |     } | ||||||
| @ -1,6 +0,0 @@ | |||||||
| namespace Microsoft.eShopOnContainers.Services.Basket.API; |  | ||||||
| 
 |  | ||||||
| internal class TestHttpResponseTrailersFeature : IHttpResponseTrailersFeature |  | ||||||
| { |  | ||||||
|     public IHeaderDictionary Trailers { get; set; } |  | ||||||
| } |  | ||||||
| @ -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", |   "ConnectionString": "127.0.0.1", | ||||||
|   "AzureServiceBusEnabled": false, |   "AzureServiceBusEnabled": false, | ||||||
|   "EventBusConnection": "localhost" |   "EventBusConnection": "localhost" | ||||||
|  | |||||||
| @ -1,30 +1,48 @@ | |||||||
| { | { | ||||||
|   "Serilog": { |   "Logging": { | ||||||
|     "SeqServerUrl": null, |     "LogLevel": { | ||||||
|     "LogstashgUrl": null, |  | ||||||
|     "MinimumLevel": { |  | ||||||
|       "Default": "Information", |       "Default": "Information", | ||||||
|       "Override": { |       "Microsoft.AspNetCore": "Warning" | ||||||
|         "Microsoft": "Warning", |  | ||||||
|         "Microsoft.eShopOnContainers": "Information", |  | ||||||
|         "System": "Warning" |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "Kestrel": { |   "Kestrel": { | ||||||
|     "EndpointDefaults": { |     "Endpoints": { | ||||||
|  |       "Http": { | ||||||
|  |         "Url": "http://localhost:5221" | ||||||
|  |       }, | ||||||
|  |       "gRPC": { | ||||||
|  |         "Url": "http://localhost:6221", | ||||||
|         "Protocols": "Http2" |         "Protocols": "Http2" | ||||||
|       } |       } | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|  |   "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" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "ConnectionStrings": { | ||||||
|  |     "Redis": "localhost", | ||||||
|  |     "EventBus": "localhost" | ||||||
|  |   }, | ||||||
|  |   "Identity": { | ||||||
|  |     "Audience": "basket", | ||||||
|  |     "Url": "http://localhost:5223", | ||||||
|  |     "Scopes": { | ||||||
|  |       "basket": "Basket API" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "EventBus": { | ||||||
|     "SubscriptionClientName": "Basket", |     "SubscriptionClientName": "Basket", | ||||||
|   "ApplicationInsights": { |     "RetryCount": 5 | ||||||
|     "InstrumentationKey": "" |  | ||||||
|   }, |  | ||||||
|   "EventBusRetryCount": 5, |  | ||||||
|   "UseVault": false, |  | ||||||
|   "Vault": { |  | ||||||
|     "Name": "eshop", |  | ||||||
|     "ClientId": "your-client-id", |  | ||||||
|     "ClientSecret": "your-client-secret" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,17 +0,0 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <configuration> |  | ||||||
|   <!-- |  | ||||||
|     Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380 |  | ||||||
|   --> |  | ||||||
|   <system.webServer> |  | ||||||
|     <handlers> |  | ||||||
|       <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" /> |  | ||||||
|     </handlers> |  | ||||||
|     <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" hostingModel="InProcess"> |  | ||||||
|       <environmentVariables> |  | ||||||
|         <environmentVariable name="COMPLUS_ForceENC" value="1" /> |  | ||||||
|         <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" /> |  | ||||||
|       </environmentVariables> |  | ||||||
|     </aspNetCore> |  | ||||||
|   </system.webServer> |  | ||||||
| </configuration> |  | ||||||
| @ -18,6 +18,7 @@ class AutoAuthorizeMiddleware | |||||||
|         identity.AddClaim(new Claim("sub", IDENTITY_ID)); |         identity.AddClaim(new Claim("sub", IDENTITY_ID)); | ||||||
|         identity.AddClaim(new Claim("unique_name", IDENTITY_ID)); |         identity.AddClaim(new Claim("unique_name", IDENTITY_ID)); | ||||||
|         identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); |         identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); | ||||||
|  |         identity.AddClaim(new Claim("scope", "basket")); | ||||||
| 
 | 
 | ||||||
|         httpContext.User.AddIdentity(identity); |         httpContext.User.AddIdentity(identity); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,7 @@ | |||||||
| namespace Basket.FunctionalTests.Base; | using Microsoft.AspNetCore.Mvc.Testing; | ||||||
|  | using Microsoft.Extensions.Hosting; | ||||||
|  | 
 | ||||||
|  | namespace Basket.FunctionalTests.Base; | ||||||
| 
 | 
 | ||||||
| public class BasketScenarioBase | public class BasketScenarioBase | ||||||
| { | { | ||||||
| @ -6,18 +9,8 @@ public class BasketScenarioBase | |||||||
| 
 | 
 | ||||||
|     public TestServer CreateServer() |     public TestServer CreateServer() | ||||||
|     { |     { | ||||||
|         var path = Assembly.GetAssembly(typeof(BasketScenarioBase)) |         var factory = new BasketApplication(); | ||||||
|             .Location; |         return factory.Server; | ||||||
| 
 |  | ||||||
|         var hostBuilder = new WebHostBuilder() |  | ||||||
|             .UseContentRoot(Path.GetDirectoryName(path)) |  | ||||||
|             .ConfigureAppConfiguration(cb => |  | ||||||
|             { |  | ||||||
|                 cb.AddJsonFile("appsettings.json", optional: false) |  | ||||||
|                 .AddEnvironmentVariables(); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|         return new TestServer(hostBuilder); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static class Get |     public static class Get | ||||||
| @ -26,6 +19,11 @@ public class BasketScenarioBase | |||||||
|         { |         { | ||||||
|             return $"{ApiUrlBase}/{id}"; |             return $"{ApiUrlBase}/{id}"; | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         public static string GetBasketByCustomer(string customerId) | ||||||
|  |         { | ||||||
|  |             return $"{ApiUrlBase}/{customerId}"; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static class Post |     public static class Post | ||||||
| @ -33,4 +31,37 @@ public class BasketScenarioBase | |||||||
|         public static string Basket = $"{ApiUrlBase}/"; |         public static string Basket = $"{ApiUrlBase}/"; | ||||||
|         public static string CheckoutOrder = $"{ApiUrlBase}/checkout"; |         public static string CheckoutOrder = $"{ApiUrlBase}/checkout"; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private class BasketApplication : WebApplicationFactory<Program> | ||||||
|  |     { | ||||||
|  |         protected override IHost CreateHost(IHostBuilder builder) | ||||||
|  |         { | ||||||
|  |             builder.ConfigureServices(services => | ||||||
|  |             { | ||||||
|  |                 services.AddSingleton<IStartupFilter, AuthStartupFilter>(); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             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); | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,13 +0,0 @@ | |||||||
| namespace Basket.FunctionalTests.Base; |  | ||||||
| 
 |  | ||||||
| static class HttpClientExtensions |  | ||||||
| { |  | ||||||
|     public static HttpClient CreateIdempotentClient(this TestServer server) |  | ||||||
|     { |  | ||||||
|         var client = server.CreateClient(); |  | ||||||
| 
 |  | ||||||
|         client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); |  | ||||||
| 
 |  | ||||||
|         return client; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -7,11 +7,7 @@ | |||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <None Remove="appsettings.json" /> |     <Content Include="appsettings.Basket.json"> | ||||||
|   </ItemGroup> |  | ||||||
| 
 |  | ||||||
|   <ItemGroup> |  | ||||||
|     <Content Include="appsettings.json"> |  | ||||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> |       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||||
|     </Content> |     </Content> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|  | |||||||
| @ -1,16 +1,15 @@ | |||||||
| namespace Basket.FunctionalTests; | namespace Basket.FunctionalTests; | ||||||
| 
 | 
 | ||||||
| public class BasketScenarios | public class BasketScenarios :  | ||||||
|     : BasketScenarioBase |     BasketScenarioBase | ||||||
| { | { | ||||||
|     [Fact] |     [Fact] | ||||||
|     public async Task Post_basket_and_response_ok_status_code() |     public async Task Post_basket_and_response_ok_status_code() | ||||||
|     { |     { | ||||||
|         using var server = CreateServer(); |         using var server = CreateServer(); | ||||||
|         var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); |         var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); | ||||||
|         var response = await server.CreateClient() |         var uri = "/api/v1/basket/"; | ||||||
|             .PostAsync(Post.Basket, content); |         var response = await server.CreateClient().PostAsync(uri, content); | ||||||
| 
 |  | ||||||
|         response.EnsureSuccessStatusCode(); |         response.EnsureSuccessStatusCode(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -20,7 +19,6 @@ public class BasketScenarios | |||||||
|         using var server = CreateServer(); |         using var server = CreateServer(); | ||||||
|         var response = await server.CreateClient() |         var response = await server.CreateClient() | ||||||
|             .GetAsync(Get.GetBasket(1)); |             .GetAsync(Get.GetBasket(1)); | ||||||
| 
 |  | ||||||
|         response.EnsureSuccessStatusCode(); |         response.EnsureSuccessStatusCode(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -33,9 +31,12 @@ public class BasketScenarios | |||||||
|         await server.CreateClient() |         await server.CreateClient() | ||||||
|             .PostAsync(Post.Basket, contentBasket); |             .PostAsync(Post.Basket, contentBasket); | ||||||
| 
 | 
 | ||||||
|         var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json"); |         var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json") | ||||||
|  |         { | ||||||
|  |              Headers = { { "x-requestid", Guid.NewGuid().ToString() } } | ||||||
|  |         }; | ||||||
| 
 | 
 | ||||||
|         var response = await server.CreateIdempotentClient() |         var response = await server.CreateClient() | ||||||
|             .PostAsync(Post.CheckoutOrder, contentCheckout); |             .PostAsync(Post.CheckoutOrder, contentCheckout); | ||||||
| 
 | 
 | ||||||
|         response.EnsureSuccessStatusCode(); |         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.Builder; | ||||||
| global using Microsoft.AspNetCore.Hosting; | global using Microsoft.AspNetCore.Hosting; | ||||||
| global using Microsoft.AspNetCore.Http; | global using Microsoft.AspNetCore.Http; | ||||||
| global using Microsoft.AspNetCore.Routing; |  | ||||||
| global using Microsoft.AspNetCore.TestHost; | 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.Model; | ||||||
| global using Microsoft.eShopOnContainers.Services.Basket.API; |  | ||||||
| global using Microsoft.Extensions.Configuration; | global using Microsoft.Extensions.Configuration; | ||||||
| global using Microsoft.Extensions.DependencyInjection; | global using Microsoft.Extensions.DependencyInjection; | ||||||
| global using Microsoft.Extensions.Logging; | global using Microsoft.Extensions.Logging; | ||||||
| global using StackExchange.Redis; | 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; | global using Xunit; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
|  | using Basket.API.Repositories; | ||||||
| 
 | 
 | ||||||
| namespace Basket.FunctionalTests | namespace Basket.FunctionalTests | ||||||
| { | { | ||||||
| @ -9,8 +9,8 @@ namespace Basket.FunctionalTests | |||||||
|         [Fact] |         [Fact] | ||||||
|         public async Task UpdateBasket_return_and_add_basket() |         public async Task UpdateBasket_return_and_add_basket() | ||||||
|         { |         { | ||||||
|             using var server = CreateServer(); |             var server = CreateServer(); | ||||||
|             var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>(); |             var redis = server.Services.GetRequiredService<ConnectionMultiplexer>(); | ||||||
| 
 | 
 | ||||||
|             var redisBasketRepository = BuildBasketRepository(redis); |             var redisBasketRepository = BuildBasketRepository(redis); | ||||||
| 
 | 
 | ||||||
| @ -22,16 +22,13 @@ namespace Basket.FunctionalTests | |||||||
| 
 | 
 | ||||||
|             Assert.NotNull(basket); |             Assert.NotNull(basket); | ||||||
|             Assert.Single(basket.Items); |             Assert.Single(basket.Items); | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         [Fact] |         [Fact] | ||||||
|         public async Task Delete_Basket_return_null() |         public async Task Delete_Basket_return_null() | ||||||
|         { |         { | ||||||
| 
 |             var server = CreateServer(); | ||||||
|             using var server = CreateServer(); |             var redis = server.Services.GetRequiredService<ConnectionMultiplexer>(); | ||||||
|             var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>(); |  | ||||||
| 
 | 
 | ||||||
|             var redisBasketRepository = BuildBasketRepository(redis); |             var redisBasketRepository = BuildBasketRepository(redis); | ||||||
| 
 | 
 | ||||||
| @ -52,7 +49,7 @@ namespace Basket.FunctionalTests | |||||||
|         RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux) |         RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux) | ||||||
|         { |         { | ||||||
|             var loggerFactory = new LoggerFactory(); |             var loggerFactory = new LoggerFactory(); | ||||||
|             return new RedisBasketRepository(loggerFactory, connMux); |             return new RedisBasketRepository(loggerFactory.CreateLogger<RedisBasketRepository>(), connMux); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         List<BasketItem> BuildBasketItems() |         List<BasketItem> BuildBasketItems() | ||||||
|  | |||||||
| @ -0,0 +1,25 @@ | |||||||
|  | { | ||||||
|  |   "Logging": { | ||||||
|  |     "Console": { | ||||||
|  |       "IncludeScopes": false | ||||||
|  |     }, | ||||||
|  |     "LogLevel": { | ||||||
|  |       "Default": "Debug", | ||||||
|  |       "System": "Information", | ||||||
|  |       "Microsoft": "Information" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "Identity": { | ||||||
|  |     "ExternalUrl": "http://localhost:5105", | ||||||
|  |     "Url": "http://localhost:5105" | ||||||
|  |   }, | ||||||
|  |   "ConnectionStrings": { | ||||||
|  |     "Redis": "127.0.0.1" | ||||||
|  |   }, | ||||||
|  |   "EventBus": { | ||||||
|  |     "ConnectionString": "localhost", | ||||||
|  |     "SubscriptionClientName": "Basket" | ||||||
|  |   }, | ||||||
|  |   "isTest": "true", | ||||||
|  |   "SuppressCheckForUnhandledSecurityMetadata": true | ||||||
|  | } | ||||||
| @ -1,17 +0,0 @@ | |||||||
| { |  | ||||||
|   "Logging": { |  | ||||||
|     "IncludeScopes": false, |  | ||||||
|     "LogLevel": { |  | ||||||
|       "Default": "Debug", |  | ||||||
|       "System": "Information", |  | ||||||
|       "Microsoft": "Information" |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "IdentityUrl": "http://localhost:5105", |  | ||||||
|   "IdentityUrlExternal": "http://localhost:5105", |  | ||||||
|   "ConnectionString": "127.0.0.1", |  | ||||||
|   "isTest": "true", |  | ||||||
|   "EventBusConnection": "localhost", |  | ||||||
|   "SubscriptionClientName": "Basket", |  | ||||||
|   "SuppressCheckForUnhandledSecurityMetadata": true |  | ||||||
| } |  | ||||||
| @ -79,7 +79,7 @@ public class CartControllerTest | |||||||
|         //Arrange |         //Arrange | ||||||
|         var fakeCatalogItem = GetFakeCatalogItem(); |         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)); |             .Returns(Task.FromResult(1)); | ||||||
| 
 | 
 | ||||||
|         //Act |         //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,22 +2,15 @@ | |||||||
| 
 | 
 | ||||||
|   <PropertyGroup> |   <PropertyGroup> | ||||||
|     <TargetFramework>net7.0</TargetFramework> |     <TargetFramework>net7.0</TargetFramework> | ||||||
|     <DebugType>portable</DebugType> |  | ||||||
|     <PreserveCompilationContext>true</PreserveCompilationContext> |  | ||||||
|     <AssemblyName>Catalog.API</AssemblyName> |     <AssemblyName>Catalog.API</AssemblyName> | ||||||
|     <PackageId>Catalog.API</PackageId> |     <PackageId>Catalog.API</PackageId> | ||||||
|     <UserSecretsId>aspnet-Catalog.API-20161122013618</UserSecretsId> |     <UserSecretsId>aspnet-Catalog.API-20161122013618</UserSecretsId> | ||||||
|     <DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> |     <DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | ||||||
|     <GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> |  | ||||||
|     <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled> |  | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <Content Update="appsettings.json"> |     <Content Update="appsettings.json"> | ||||||
|       <CopyToOutputDirectory>Always</CopyToOutputDirectory> |       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||||
|     </Content> |  | ||||||
|     <Content Update="wwwroot;"> |  | ||||||
|       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> |  | ||||||
|     </Content> |     </Content> | ||||||
|     <Content Include="Pics\**\*;"> |     <Content Include="Pics\**\*;"> | ||||||
|       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> |       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | ||||||
| @ -25,49 +18,23 @@ | |||||||
|     <Content Include="Setup\**\*;"> |     <Content Include="Setup\**\*;"> | ||||||
|       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> |       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | ||||||
|     </Content> |     </Content> | ||||||
|     <Content Remove="Setup\Catalogitems - Copy.zip" /> |     <None Include="IntegrationEvents\EventHandling\AnyFutureIntegrationEventHandler.cs.txt" /> | ||||||
|     <None Remove="Setup\Catalogitems - Copy.zip" /> |  | ||||||
|     <Compile Include="IntegrationEvents\EventHandling\AnyFutureIntegrationEventHandler.cs.txt" /> |  | ||||||
|     <Content Update="web.config;"> |  | ||||||
|       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> |  | ||||||
|     </Content> |  | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <Protobuf Include="Proto\catalog.proto" GrpcServices="Server" Generator="MSBuild:Compile" /> |     <Protobuf Include="Proto\catalog.proto" GrpcServices="Server" /> | ||||||
|     <Content Include="@(Protobuf)" /> |   </ItemGroup> | ||||||
|     <None Remove="@(Protobuf)" /> | 
 | ||||||
|  |   <ItemGroup> | ||||||
|  |     <InternalsVisibleTo Include="Catalog.FunctionalTests" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|   <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.AzureStorage" /> | ||||||
|     <PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" /> |  | ||||||
|     <PackageReference Include="AspNetCore.HealthChecks.SqlServer" /> |     <PackageReference Include="AspNetCore.HealthChecks.SqlServer" /> | ||||||
|     <PackageReference Include="AspNetCore.HealthChecks.UI.Client" /> |     <PackageReference Include="AspNetCore.HealthChecks.UI.Client" /> | ||||||
|     <PackageReference Include="Autofac.Extensions.DependencyInjection" /> |     <PackageReference Include="Grpc.AspNetCore" /> | ||||||
|     <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="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" /> |     <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.Proxies" /> | ||||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" /> |     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" /> | ||||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Tools"> |     <PackageReference Include="Microsoft.EntityFrameworkCore.Tools"> | ||||||
| @ -77,19 +44,7 @@ | |||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|   <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="..\..\..\BuildingBlocks\EventBus\IntegrationEventLogEF\IntegrationEventLogEF.csproj" /> | ||||||
|  |     <ProjectReference Include="..\..\Services.Common\Services.Common.csproj" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 |  | ||||||
|   <ItemGroup> |  | ||||||
|     <None Update="Pics\*"> |  | ||||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> |  | ||||||
|     </None> |  | ||||||
|     <None Update="Setup\*"> |  | ||||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> |  | ||||||
|     </None> |  | ||||||
|   </ItemGroup> |  | ||||||
| 
 |  | ||||||
| </Project> | </Project> | ||||||
|  | |||||||
| @ -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,7 @@ 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.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/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/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/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.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 "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" | ||||||
| COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -13,7 +13,7 @@ public static class LinqSelectExtensions | |||||||
|             } |             } | ||||||
|             catch (Exception ex) |             catch (Exception ex) | ||||||
|             { |             { | ||||||
|                 returnedValue = new SelectTryResult<TSource, TResult>(element, default(TResult), ex); |                 returnedValue = new SelectTryResult<TSource, TResult>(element, default, ex); | ||||||
|             } |             } | ||||||
|             yield return returnedValue; |             yield return returnedValue; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -1,68 +0,0 @@ | |||||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; |  | ||||||
| 
 |  | ||||||
| public static class WebHostExtensions |  | ||||||
| { |  | ||||||
|     public static bool IsInKubernetes(this IWebHost host) |  | ||||||
|     { |  | ||||||
|         var cfg = host.Services.GetService<IConfiguration>(); |  | ||||||
|         var orchestratorType = cfg.GetValue<string>("OrchestratorType"); |  | ||||||
|         return orchestratorType?.ToUpper() == "K8S"; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static IWebHost MigrateDbContext<TContext>(this IWebHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext |  | ||||||
|     { |  | ||||||
|         var underK8s = host.IsInKubernetes(); |  | ||||||
| 
 |  | ||||||
|         using var scope = host.Services.CreateScope(); |  | ||||||
|         var services = scope.ServiceProvider; |  | ||||||
| 
 |  | ||||||
|         var logger = services.GetRequiredService<ILogger<TContext>>(); |  | ||||||
| 
 |  | ||||||
|         var context = services.GetService<TContext>(); |  | ||||||
| 
 |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); |  | ||||||
| 
 |  | ||||||
|             if (underK8s) |  | ||||||
|             { |  | ||||||
|                 InvokeSeeder(seeder, context, services); |  | ||||||
|             } |  | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 var retry = Policy.Handle<SqlException>() |  | ||||||
|                         .WaitAndRetry(new TimeSpan[] |  | ||||||
|                         { |  | ||||||
|                         TimeSpan.FromSeconds(3), |  | ||||||
|                         TimeSpan.FromSeconds(5), |  | ||||||
|                         TimeSpan.FromSeconds(8), |  | ||||||
|                         }); |  | ||||||
| 
 |  | ||||||
|                 //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, 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 |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return host; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) |  | ||||||
|         where TContext : DbContext |  | ||||||
|     { |  | ||||||
|         context.Database.Migrate(); |  | ||||||
|         seeder(context, services); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,63 +1,43 @@ | |||||||
| global using Azure.Core; | global using System; | ||||||
| global using Azure.Identity; |  | ||||||
| global using Autofac.Extensions.DependencyInjection; |  | ||||||
| global using Autofac; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.ActionResults; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; |  | ||||||
| global using Grpc.Core; |  | ||||||
| global using Microsoft.AspNetCore.Hosting; |  | ||||||
| global using Microsoft.AspNetCore.Http; |  | ||||||
| global using Microsoft.AspNetCore.Builder; |  | ||||||
| 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.Extensions.Logging; |  | ||||||
| global using Microsoft.EntityFrameworkCore.Design; |  | ||||||
| global using Microsoft.EntityFrameworkCore.Metadata.Builders; |  | ||||||
| global using Microsoft.EntityFrameworkCore; |  | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; |  | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; |  | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; |  | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; |  | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.Model; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; |  | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.Grpc; |  | ||||||
| global using Microsoft.Extensions.Configuration; |  | ||||||
| global using Microsoft.Extensions.DependencyInjection; |  | ||||||
| global using Microsoft.Extensions.Hosting; |  | ||||||
| global using Microsoft.Extensions.Options; |  | ||||||
| global using Polly.Retry; |  | ||||||
| global using Polly; |  | ||||||
| global using Serilog.Context; |  | ||||||
| global using Serilog; |  | ||||||
| global using System.Collections.Generic; | global using System.Collections.Generic; | ||||||
| global using System.Data.Common; | global using System.Data.Common; | ||||||
| global using System.Data.SqlClient; | global using System.Data.SqlClient; | ||||||
| global using System.Globalization; | global using System.Globalization; | ||||||
| global using System.IO.Compression; |  | ||||||
| global using System.IO; | global using System.IO; | ||||||
|  | global using System.IO.Compression; | ||||||
| global using System.Linq; | global using System.Linq; | ||||||
| global using System.Net; | global using System.Net; | ||||||
| global using System.Text.RegularExpressions; | global using System.Text.RegularExpressions; | ||||||
| global using System.Threading.Tasks; | global using System.Threading.Tasks; | ||||||
| global using System; | global using Grpc.Core; | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters; | global using Microsoft.AspNetCore.Builder; | ||||||
| global using HealthChecks.UI.Client; | global using Microsoft.AspNetCore.Hosting; | ||||||
| global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | global using Microsoft.AspNetCore.Http; | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | global using Microsoft.AspNetCore.Mvc; | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; | global using Microsoft.EntityFrameworkCore; | ||||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | global using Microsoft.EntityFrameworkCore.Design; | ||||||
|  | global using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||||||
|  | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | ||||||
|  | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | ||||||
|  | global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; | ||||||
|  | global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; | ||||||
|  | global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; | ||||||
|  | global using Microsoft.eShopOnContainers.Services.Catalog.API; | ||||||
|  | global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; | ||||||
|  | global using Microsoft.eShopOnContainers.Services.Catalog.API.Grpc; | ||||||
|  | global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; | ||||||
|  | global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; | ||||||
|  | global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions; | ||||||
|  | global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; | ||||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; | global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; | ||||||
| global using Microsoft.Extensions.Diagnostics.HealthChecks; | global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; | ||||||
| global using Microsoft.OpenApi.Models; | global using Microsoft.eShopOnContainers.Services.Catalog.API.Model; | ||||||
| global using RabbitMQ.Client; | global using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; | ||||||
| global using System.Reflection; | global using Microsoft.Extensions.Configuration; | ||||||
| global using Microsoft.Extensions.FileProviders; | global using Microsoft.Extensions.DependencyInjection; | ||||||
|  | global using Microsoft.Extensions.Logging; | ||||||
|  | global using Microsoft.Extensions.Options; | ||||||
|  | global using Polly; | ||||||
|  | global using Polly.Retry; | ||||||
|  | global using Services.Common; | ||||||
|  | global using Catalog.API.Apis; | ||||||
|  | |||||||
| @ -1,10 +0,0 @@ | |||||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.ActionResults; |  | ||||||
| 
 |  | ||||||
| public class InternalServerErrorObjectResult : ObjectResult |  | ||||||
| { |  | ||||||
|     public InternalServerErrorObjectResult(object error) |  | ||||||
|         : base(error) |  | ||||||
|     { |  | ||||||
|         StatusCode = StatusCodes.Status500InternalServerError; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -60,14 +60,14 @@ public class CatalogContextSeed | |||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|             logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); |             logger.LogError(ex, "Error reading CSV headers"); | ||||||
|             return GetPreconfiguredCatalogBrands(); |             return GetPreconfiguredCatalogBrands(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return File.ReadAllLines(csvFileCatalogBrands) |         return File.ReadAllLines(csvFileCatalogBrands) | ||||||
|                                     .Skip(1) // skip header row |                                     .Skip(1) // skip header row | ||||||
|                                     .SelectTry(x => CreateCatalogBrand(x)) |                                     .SelectTry(CreateCatalogBrand) | ||||||
|                                     .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) |                                     .OnCaughtException(ex => { logger.LogError(ex, "Error creating brand while seeding database"); return null; }) | ||||||
|                                     .Where(x => x != null); |                                     .Where(x => x != null); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -75,9 +75,9 @@ public class CatalogContextSeed | |||||||
|     { |     { | ||||||
|         brand = brand.Trim('"').Trim(); |         brand = brand.Trim('"').Trim(); | ||||||
| 
 | 
 | ||||||
|         if (String.IsNullOrEmpty(brand)) |         if (string.IsNullOrEmpty(brand)) | ||||||
|         { |         { | ||||||
|             throw new Exception("catalog Brand Name is empty"); |             throw new Exception("Catalog Brand Name is empty"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return new CatalogBrand |         return new CatalogBrand | ||||||
| @ -115,14 +115,14 @@ public class CatalogContextSeed | |||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|             logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); |             logger.LogError(ex, "Error reading CSV headers"); | ||||||
|             return GetPreconfiguredCatalogTypes(); |             return GetPreconfiguredCatalogTypes(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return File.ReadAllLines(csvFileCatalogTypes) |         return File.ReadAllLines(csvFileCatalogTypes) | ||||||
|                                     .Skip(1) // skip header row |                                     .Skip(1) // skip header row | ||||||
|                                     .SelectTry(x => CreateCatalogType(x)) |                                     .SelectTry(x => CreateCatalogType(x)) | ||||||
|                                     .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) |                                     .OnCaughtException(ex => { logger.LogError(ex, "Error creating catalog type while seeding database"); return null; }) | ||||||
|                                     .Where(x => x != null); |                                     .Where(x => x != null); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -130,7 +130,7 @@ public class CatalogContextSeed | |||||||
|     { |     { | ||||||
|         type = type.Trim('"').Trim(); |         type = type.Trim('"').Trim(); | ||||||
| 
 | 
 | ||||||
|         if (String.IsNullOrEmpty(type)) |         if (string.IsNullOrEmpty(type)) | ||||||
|         { |         { | ||||||
|             throw new Exception("catalog Type Name is empty"); |             throw new Exception("catalog Type Name is empty"); | ||||||
|         } |         } | ||||||
| @ -170,7 +170,7 @@ public class CatalogContextSeed | |||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|             logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); |             logger.LogError(ex, "Error reading CSV headers"); | ||||||
|             return GetPreconfiguredItems(); |             return GetPreconfiguredItems(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -181,11 +181,11 @@ public class CatalogContextSeed | |||||||
|                     .Skip(1) // skip header row |                     .Skip(1) // skip header row | ||||||
|                     .Select(row => Regex.Split(row, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) |                     .Select(row => Regex.Split(row, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) | ||||||
|                     .SelectTry(column => CreateCatalogItem(column, csvheaders, catalogTypeIdLookup, catalogBrandIdLookup)) |                     .SelectTry(column => CreateCatalogItem(column, csvheaders, catalogTypeIdLookup, catalogBrandIdLookup)) | ||||||
|                     .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) |                     .OnCaughtException(ex => { logger.LogError(ex, "Error creating catalog item while seeding database"); return null; }) | ||||||
|                     .Where(x => x != null); |                     .Where(x => x != null); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary<String, int> catalogTypeIdLookup, Dictionary<String, int> catalogBrandIdLookup) |     private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary<string, int> catalogTypeIdLookup, Dictionary<string, int> catalogBrandIdLookup) | ||||||
|     { |     { | ||||||
|         if (column.Count() != headers.Count()) |         if (column.Count() != headers.Count()) | ||||||
|         { |         { | ||||||
| @ -205,7 +205,7 @@ public class CatalogContextSeed | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         string priceString = column[Array.IndexOf(headers, "price")].Trim('"').Trim(); |         string priceString = column[Array.IndexOf(headers, "price")].Trim('"').Trim(); | ||||||
|         if (!Decimal.TryParse(priceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out Decimal price)) |         if (!decimal.TryParse(priceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal price)) | ||||||
|         { |         { | ||||||
|             throw new Exception($"price={priceString}is not a valid decimal number"); |             throw new Exception($"price={priceString}is not a valid decimal number"); | ||||||
|         } |         } | ||||||
| @ -224,7 +224,7 @@ public class CatalogContextSeed | |||||||
|         if (availableStockIndex != -1) |         if (availableStockIndex != -1) | ||||||
|         { |         { | ||||||
|             string availableStockString = column[availableStockIndex].Trim('"').Trim(); |             string availableStockString = column[availableStockIndex].Trim('"').Trim(); | ||||||
|             if (!String.IsNullOrEmpty(availableStockString)) |             if (!string.IsNullOrEmpty(availableStockString)) | ||||||
|             { |             { | ||||||
|                 if (int.TryParse(availableStockString, out int availableStock)) |                 if (int.TryParse(availableStockString, out int availableStock)) | ||||||
|                 { |                 { | ||||||
| @ -241,7 +241,7 @@ public class CatalogContextSeed | |||||||
|         if (restockThresholdIndex != -1) |         if (restockThresholdIndex != -1) | ||||||
|         { |         { | ||||||
|             string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); |             string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); | ||||||
|             if (!String.IsNullOrEmpty(restockThresholdString)) |             if (!string.IsNullOrEmpty(restockThresholdString)) | ||||||
|             { |             { | ||||||
|                 if (int.TryParse(restockThresholdString, out int restockThreshold)) |                 if (int.TryParse(restockThresholdString, out int restockThreshold)) | ||||||
|                 { |                 { | ||||||
| @ -258,7 +258,7 @@ public class CatalogContextSeed | |||||||
|         if (maxStockThresholdIndex != -1) |         if (maxStockThresholdIndex != -1) | ||||||
|         { |         { | ||||||
|             string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); |             string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); | ||||||
|             if (!String.IsNullOrEmpty(maxStockThresholdString)) |             if (!string.IsNullOrEmpty(maxStockThresholdString)) | ||||||
|             { |             { | ||||||
|                 if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) |                 if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) | ||||||
|                 { |                 { | ||||||
| @ -275,7 +275,7 @@ public class CatalogContextSeed | |||||||
|         if (onReorderIndex != -1) |         if (onReorderIndex != -1) | ||||||
|         { |         { | ||||||
|             string onReorderString = column[onReorderIndex].Trim('"').Trim(); |             string onReorderString = column[onReorderIndex].Trim('"').Trim(); | ||||||
|             if (!String.IsNullOrEmpty(onReorderString)) |             if (!string.IsNullOrEmpty(onReorderString)) | ||||||
|             { |             { | ||||||
|                 if (bool.TryParse(onReorderString, out bool onReorder)) |                 if (bool.TryParse(onReorderString, out bool onReorder)) | ||||||
|                 { |                 { | ||||||
| @ -361,7 +361,7 @@ public class CatalogContextSeed | |||||||
|                 sleepDurationProvider: retry => TimeSpan.FromSeconds(5), |                 sleepDurationProvider: retry => TimeSpan.FromSeconds(5), | ||||||
|                 onRetry: (exception, timeSpan, retry, ctx) => |                 onRetry: (exception, timeSpan, retry, ctx) => | ||||||
|                 { |                 { | ||||||
|                     logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", prefix, exception.GetType().Name, exception.Message, retry, retries); |                     logger.LogWarning(exception, "[{prefix}] Error seeding database (attempt {retry} of {retries})", prefix, retry, retries); | ||||||
|                 } |                 } | ||||||
|             ); |             ); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,58 +0,0 @@ | |||||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters; |  | ||||||
| 
 |  | ||||||
| public 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(CatalogDomainException)) |  | ||||||
|         { |  | ||||||
|             var problemDetails = new ValidationProblemDetails() |  | ||||||
|             { |  | ||||||
|                 Instance = context.HttpContext.Request.Path, |  | ||||||
|                 Status = StatusCodes.Status400BadRequest, |  | ||||||
|                 Detail = "Please refer to the errors property for additional details." |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); |  | ||||||
| 
 |  | ||||||
|             context.Result = new BadRequestObjectResult(problemDetails); |  | ||||||
|             context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; |  | ||||||
|         } |  | ||||||
|         else |  | ||||||
|         { |  | ||||||
|             var json = new JsonErrorResponse |  | ||||||
|             { |  | ||||||
|                 Messages = new[] { "An error ocurred." } |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             if (env.IsDevelopment()) |  | ||||||
|             { |  | ||||||
|                 json.DeveloperMessage = context.Exception; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             context.Result = new InternalServerErrorObjectResult(json); |  | ||||||
|             context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; |  | ||||||
|         } |  | ||||||
|         context.ExceptionHandled = true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private class JsonErrorResponse |  | ||||||
|     { |  | ||||||
|         public string[] Messages { get; set; } |  | ||||||
| 
 |  | ||||||
|         public object DeveloperMessage { get; set; } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -26,7 +26,7 @@ public class CatalogIntegrationEventService : ICatalogIntegrationEventService, I | |||||||
|     { |     { | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             _logger.LogInformation("----- Publishing integration event: {IntegrationEventId_published} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); |             _logger.LogInformation("Publishing integration event: {IntegrationEventId_published} - ({@IntegrationEvent})", evt.Id, evt); | ||||||
| 
 | 
 | ||||||
|             await _eventLogService.MarkEventAsInProgressAsync(evt.Id); |             await _eventLogService.MarkEventAsInProgressAsync(evt.Id); | ||||||
|             _eventBus.Publish(evt); |             _eventBus.Publish(evt); | ||||||
| @ -34,14 +34,14 @@ public class CatalogIntegrationEventService : ICatalogIntegrationEventService, I | |||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|             _logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); |             _logger.LogError(ex, "Error Publishing integration event: {IntegrationEventId} - ({@IntegrationEvent})", evt.Id, evt); | ||||||
|             await _eventLogService.MarkEventAsFailedAsync(evt.Id); |             await _eventLogService.MarkEventAsFailedAsync(evt.Id); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt) |     public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt) | ||||||
|     { |     { | ||||||
|         _logger.LogInformation("----- CatalogIntegrationEventService - Saving changes and integrationEvent: {IntegrationEventId}", evt.Id); |         _logger.LogInformation("CatalogIntegrationEventService - Saving changes and integrationEvent: {IntegrationEventId}", evt.Id); | ||||||
| 
 | 
 | ||||||
|         //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): |         //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): | ||||||
|         //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency             |         //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency             | ||||||
|  | |||||||
| @ -19,9 +19,9 @@ public class OrderStatusChangedToAwaitingValidationIntegrationEventHandler : | |||||||
| 
 | 
 | ||||||
|     public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event) |     public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @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 confirmedOrderStockItems = new List<ConfirmedOrderStockItem>(); |             var confirmedOrderStockItems = new List<ConfirmedOrderStockItem>(); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,9 +16,9 @@ public class OrderStatusChangedToPaidIntegrationEventHandler : | |||||||
| 
 | 
 | ||||||
|     public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event) |     public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @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); | ||||||
| 
 | 
 | ||||||
|             //we're not blocking stock/inventory |             //we're not blocking stock/inventory | ||||||
|             foreach (var orderStockItem in @event.OrderStockItems) |             foreach (var orderStockItem in @event.OrderStockItems) | ||||||
|  | |||||||
| @ -1,388 +1,43 @@ | |||||||
| var builder = WebApplication.CreateBuilder(new WebApplicationOptions | var builder = WebApplication.CreateBuilder(args); | ||||||
| { |  | ||||||
|     Args = args, |  | ||||||
|     ApplicationName = typeof(Program).Assembly.FullName, |  | ||||||
|     ContentRootPath = Directory.GetCurrentDirectory(), |  | ||||||
|     WebRootPath = "Pics", |  | ||||||
| }); |  | ||||||
| if (builder.Configuration.GetValue<bool>("UseVault", false)) |  | ||||||
| { |  | ||||||
|     TokenCredential credential = new ClientSecretCredential( |  | ||||||
|         builder.Configuration["Vault:TenantId"], |  | ||||||
|         builder.Configuration["Vault:ClientId"], |  | ||||||
|         builder.Configuration["Vault:ClientSecret"]); |  | ||||||
|     //builder.AddAzureKeyVault(new Uri($"https://{builder.Configuration["Vault:Name"]}.vault.azure.net/"), credential);         |  | ||||||
| } |  | ||||||
| builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); |  | ||||||
| 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.AddServiceDefaults(); | ||||||
| builder.Services.AddAppInsight(builder.Configuration); | 
 | ||||||
| builder.Services.AddGrpc().Services | builder.Services.AddGrpc(); | ||||||
|     .AddCustomMVC(builder.Configuration) | builder.Services.AddControllers(); | ||||||
|     .AddCustomDbContext(builder.Configuration) | 
 | ||||||
|     .AddCustomOptions(builder.Configuration) | // Application specific services | ||||||
|     .AddCustomHealthCheck(builder.Configuration) | builder.Services.AddHealthChecks(builder.Configuration); | ||||||
|     .AddIntegrationServices(builder.Configuration) | builder.Services.AddDbContexts(builder.Configuration); | ||||||
|     .AddEventBus(builder.Configuration) | builder.Services.AddApplicationOptions(builder.Configuration); | ||||||
|     .AddSwagger(builder.Configuration); | builder.Services.AddIntegrationServices(); | ||||||
|  | 
 | ||||||
|  | builder.Services.AddTransient<OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | ||||||
|  | builder.Services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>(); | ||||||
| 
 | 
 | ||||||
| var app = builder.Build(); | var app = builder.Build(); | ||||||
| 
 | 
 | ||||||
| if (app.Environment.IsDevelopment()) | app.UseServiceDefaults(); | ||||||
| { |  | ||||||
|     app.UseDeveloperExceptionPage(); |  | ||||||
| } |  | ||||||
| else |  | ||||||
| { |  | ||||||
|     app.UseExceptionHandler("/Home/Error"); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| var pathBase = app.Configuration["PATH_BASE"]; | app.MapPicApi(); | ||||||
| if (!string.IsNullOrEmpty(pathBase)) |  | ||||||
| { |  | ||||||
|     app.UsePathBase(pathBase); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| app.UseSwagger() |  | ||||||
|             .UseSwaggerUI(c => |  | ||||||
|             { |  | ||||||
|                 c.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Catalog.API V1"); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
| app.UseRouting(); |  | ||||||
| app.UseCors("CorsPolicy"); |  | ||||||
| app.MapDefaultControllerRoute(); |  | ||||||
| app.MapControllers(); | app.MapControllers(); | ||||||
| app.UseFileServer(new FileServerOptions |  | ||||||
| { |  | ||||||
|     FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "Pics")), |  | ||||||
|     RequestPath = "/pics" |  | ||||||
| }); |  | ||||||
| app.UseStaticFiles(new StaticFileOptions |  | ||||||
| { |  | ||||||
|     FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "Pics")), |  | ||||||
|     RequestPath = "/pics" |  | ||||||
| }); |  | ||||||
| app.MapGet("/_proto/", async ctx => |  | ||||||
| { |  | ||||||
|     ctx.Response.ContentType = "text/plain"; |  | ||||||
|     using var fs = new FileStream(Path.Combine(app.Environment.ContentRootPath, "Proto", "catalog.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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| }); |  | ||||||
| app.MapGrpcService<CatalogService>(); | app.MapGrpcService<CatalogService>(); | ||||||
| app.MapHealthChecks("/hc", new HealthCheckOptions() |  | ||||||
| { |  | ||||||
|     Predicate = _ => true, |  | ||||||
|     ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse |  | ||||||
| }); |  | ||||||
| app.MapHealthChecks("/liveness", new HealthCheckOptions |  | ||||||
| { |  | ||||||
|     Predicate = r => r.Name.Contains("self") |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| ConfigureEventBus(app); | var eventBus = app.Services.GetRequiredService<IEventBus>(); | ||||||
| 
 | 
 | ||||||
| try | eventBus.Subscribe<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | ||||||
|  | eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>(); | ||||||
|  | 
 | ||||||
|  | // REVIEW: This is done fore development east but shouldn't be here in production | ||||||
|  | using (var scope = app.Services.CreateScope()) | ||||||
| { | { | ||||||
|     Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); |  | ||||||
|     using var scope = app.Services.CreateScope(); |  | ||||||
|     var context = scope.ServiceProvider.GetRequiredService<CatalogContext>(); |     var context = scope.ServiceProvider.GetRequiredService<CatalogContext>(); | ||||||
|     var env = app.Services.GetService<IWebHostEnvironment>(); |  | ||||||
|     var settings = app.Services.GetService<IOptions<CatalogSettings>>(); |     var settings = app.Services.GetService<IOptions<CatalogSettings>>(); | ||||||
|     var logger = app.Services.GetService<ILogger<CatalogContextSeed>>(); |     var logger = app.Services.GetService<ILogger<CatalogContextSeed>>(); | ||||||
|     await context.Database.MigrateAsync(); |     await context.Database.MigrateAsync(); | ||||||
| 
 | 
 | ||||||
|     await new CatalogContextSeed().SeedAsync(context, env, settings, logger); |     await new CatalogContextSeed().SeedAsync(context, app.Environment, settings, logger); | ||||||
|     var integEventContext = scope.ServiceProvider.GetRequiredService<IntegrationEventLogContext>(); |     var integEventContext = scope.ServiceProvider.GetRequiredService<IntegrationEventLogContext>(); | ||||||
|     await integEventContext.Database.MigrateAsync(); |     await integEventContext.Database.MigrateAsync(); | ||||||
|     app.Logger.LogInformation("Starting web host ({ApplicationName})...", AppName); | } | ||||||
|  | 
 | ||||||
| await app.RunAsync(); | await app.RunAsync(); | ||||||
| 
 |  | ||||||
|     return 0; |  | ||||||
| } |  | ||||||
| catch (Exception ex) |  | ||||||
| { |  | ||||||
|     Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); |  | ||||||
|     return 1; |  | ||||||
| } |  | ||||||
| finally |  | ||||||
| { |  | ||||||
|     Log.CloseAndFlush(); |  | ||||||
| } |  | ||||||
| void ConfigureEventBus(IApplicationBuilder app) |  | ||||||
| { |  | ||||||
|     var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); |  | ||||||
|     eventBus.Subscribe<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); |  | ||||||
|     eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| (int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) |  | ||||||
| { |  | ||||||
|     var grpcPort = config.GetValue("GRPC_PORT", 81); |  | ||||||
|     var port = config.GetValue("PORT", 80); |  | ||||||
|     return (port, grpcPort); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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 CustomExtensionMethods |  | ||||||
| { |  | ||||||
|     public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         services.AddApplicationInsightsTelemetry(configuration); |  | ||||||
|         services.AddApplicationInsightsKubernetesEnricher(); |  | ||||||
| 
 |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         services.AddControllers(options => |  | ||||||
|         { |  | ||||||
|             options.Filters.Add(typeof(HttpGlobalExceptionFilter)); |  | ||||||
|         }) |  | ||||||
|         .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); |  | ||||||
| 
 |  | ||||||
|         services.AddCors(options => |  | ||||||
|         { |  | ||||||
|             options.AddPolicy("CorsPolicy", |  | ||||||
|                 builder => builder |  | ||||||
|                 .SetIsOriginAllowed((host) => true) |  | ||||||
|                 .AllowAnyMethod() |  | ||||||
|                 .AllowAnyHeader() |  | ||||||
|                 .AllowCredentials()); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         var accountName = configuration.GetValue<string>("AzureStorageAccountName"); |  | ||||||
|         var accountKey = configuration.GetValue<string>("AzureStorageAccountKey"); |  | ||||||
| 
 |  | ||||||
|         var hcBuilder = services.AddHealthChecks(); |  | ||||||
| 
 |  | ||||||
|         hcBuilder |  | ||||||
|             .AddCheck("self", () => HealthCheckResult.Healthy()) |  | ||||||
|             .AddSqlServer( |  | ||||||
|                 configuration["ConnectionString"], |  | ||||||
|                 name: "CatalogDB-check", |  | ||||||
|                 tags: new string[] { "catalogdb" }); |  | ||||||
| 
 |  | ||||||
|         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[] { "catalogstorage" }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (configuration.GetValue<bool>("AzureServiceBusEnabled")) |  | ||||||
|         { |  | ||||||
|             hcBuilder |  | ||||||
|                 .AddAzureServiceBusTopic( |  | ||||||
|                     configuration["EventBusConnection"], |  | ||||||
|                     topicName: "eshop_event_bus", |  | ||||||
|                     name: "catalog-servicebus-check", |  | ||||||
|                     tags: new string[] { "servicebus" }); |  | ||||||
|         } |  | ||||||
|         else |  | ||||||
|         { |  | ||||||
|             hcBuilder |  | ||||||
|                 .AddRabbitMQ( |  | ||||||
|                     $"amqp://{configuration["EventBusConnection"]}", |  | ||||||
|                     name: "catalog-rabbitmqbus-check", |  | ||||||
|                     tags: new string[] { "rabbitmqbus" }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         services.AddEntityFrameworkSqlServer() |  | ||||||
|             .AddDbContext<CatalogContext>(options => |  | ||||||
|             { |  | ||||||
|                 options.UseSqlServer(configuration["ConnectionString"], |  | ||||||
|                                         sqlServerOptionsAction: sqlOptions => |  | ||||||
|                                         { |  | ||||||
|                                             sqlOptions.MigrationsAssembly(typeof(Program).GetTypeInfo().Assembly.GetName().Name); |  | ||||||
|                                             //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<IntegrationEventLogContext>(options => |  | ||||||
|         { |  | ||||||
|             options.UseSqlServer(configuration["ConnectionString"], |  | ||||||
|                                     sqlServerOptionsAction: sqlOptions => |  | ||||||
|                                     { |  | ||||||
|                                         sqlOptions.MigrationsAssembly(typeof(Program).GetTypeInfo().Assembly.GetName().Name); |  | ||||||
|                                         //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency  |  | ||||||
|                                         sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); |  | ||||||
|                                     }); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static IServiceCollection AddCustomOptions(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         services.Configure<CatalogSettings>(configuration); |  | ||||||
|         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 AddSwagger(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         services.AddSwaggerGen(options => |  | ||||||
|         { |  | ||||||
|             options.SwaggerDoc("v1", new OpenApiInfo |  | ||||||
|             { |  | ||||||
|                 Title = "eShopOnContainers - Catalog HTTP API", |  | ||||||
|                 Version = "v1", |  | ||||||
|                 Description = "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample" |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         return services; |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>( |  | ||||||
|             sp => (DbConnection c) => new IntegrationEventLogService(c)); |  | ||||||
| 
 |  | ||||||
|         services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>(); |  | ||||||
| 
 |  | ||||||
|         if (configuration.GetValue<bool>("AzureServiceBusEnabled")) |  | ||||||
|         { |  | ||||||
|             services.AddSingleton<IServiceBusPersisterConnection>(sp => |  | ||||||
|             { |  | ||||||
|                 var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; |  | ||||||
|                 var serviceBusConnection = settings.EventBusConnection; |  | ||||||
| 
 |  | ||||||
|                 return new DefaultServiceBusPersisterConnection(serviceBusConnection); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|         else |  | ||||||
|         { |  | ||||||
|             services.AddSingleton<IRabbitMQPersistentConnection>(sp => |  | ||||||
|             { |  | ||||||
|                 var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; |  | ||||||
|                 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); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         if (configuration.GetValue<bool>("AzureServiceBusEnabled")) |  | ||||||
|         { |  | ||||||
|             services.AddSingleton<IEventBus, EventBusServiceBus>(sp => |  | ||||||
|             { |  | ||||||
|                 var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>(); |  | ||||||
|                 var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>(); |  | ||||||
|                 var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); |  | ||||||
|                 string subscriptionName = configuration["SubscriptionClientName"]; |  | ||||||
| 
 |  | ||||||
|                 return new EventBusServiceBus(serviceBusPersisterConnection, logger, |  | ||||||
|                     eventBusSubcriptionsManager, sp, subscriptionName); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
|         else |  | ||||||
|         { |  | ||||||
|             services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp => |  | ||||||
|             { |  | ||||||
|                 var subscriptionClientName = configuration["SubscriptionClientName"]; |  | ||||||
|                 var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>(); |  | ||||||
|                 var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>(); |  | ||||||
|                 var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); |  | ||||||
| 
 |  | ||||||
|                 var retryCount = 5; |  | ||||||
|                 if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) |  | ||||||
|                 { |  | ||||||
|                     retryCount = int.Parse(configuration["EventBusRetryCount"]); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubcriptionsManager, subscriptionClientName, retryCount); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>(); |  | ||||||
|         services.AddTransient<OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); |  | ||||||
|         services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>(); |  | ||||||
| 
 |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,29 +1,9 @@ | |||||||
| { | { | ||||||
|   "iisSettings": { |  | ||||||
|     "windowsAuthentication": false, |  | ||||||
|     "anonymousAuthentication": true, |  | ||||||
|     "iisExpress": { |  | ||||||
|       "applicationUrl": "http://localhost:57424/", |  | ||||||
|       "sslPort": 0 |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   "profiles": { |   "profiles": { | ||||||
|     "IIS Express": { |     "Catalog.API": { | ||||||
|       "commandName": "IISExpress", |  | ||||||
|       "launchBrowser": true, |  | ||||||
|       "launchUrl": "/swagger", |  | ||||||
|       "environmentVariables": { |  | ||||||
|         "ConnectionString": "server=localhost,5433;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word", |  | ||||||
|         "Serilog:LogstashgUrl": "http://locahost:8080", |  | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development", |  | ||||||
|         "EventBusConnection": "localhost", |  | ||||||
|         "Serilog:SeqServerUrl": "http://locahost:5340" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "Microsoft.eShopOnContainers.Services.Catalog.API": { |  | ||||||
|       "commandName": "Project", |       "commandName": "Project", | ||||||
|       "launchBrowser": true, |       "launchBrowser": true, | ||||||
|       "launchUrl": "http://localhost:55101/", |       "applicationUrl": "http://localhost:5222/", | ||||||
|       "environmentVariables": { |       "environmentVariables": { | ||||||
|         "ASPNETCORE_ENVIRONMENT": "Development" |         "ASPNETCORE_ENVIRONMENT": "Development" | ||||||
|       } |       } | ||||||
|  | |||||||
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