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 | ||||
| 
 | ||||
| .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.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" | ||||
| COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" | ||||
| COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" | ||||
| COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" | ||||
| COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" | ||||
| COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" | ||||
|  | ||||
| @ -0,0 +1,62 @@ | ||||
| internal static class Extensions | ||||
| { | ||||
|     public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy")); | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddHealthChecks() | ||||
|             .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | ||||
|             .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | ||||
|             .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" }) | ||||
|             .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddApplicationServices(this IServiceCollection services) | ||||
|     { | ||||
|         // Register delegating handlers | ||||
|         services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | ||||
| 
 | ||||
|         // Register http services | ||||
|         services.AddHttpClient<IOrderApiClient, OrderApiClient>() | ||||
|             .AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddGrpcServices(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddTransient<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         services.AddScoped<IBasketService, BasketService>(); | ||||
| 
 | ||||
|         services.AddGrpcClient<Basket.BasketClient>((services, options) => | ||||
|         { | ||||
|             var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | ||||
|             options.Address = new Uri(basketApi); | ||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         services.AddScoped<ICatalogService, CatalogService>(); | ||||
| 
 | ||||
|         services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | ||||
|         { | ||||
|             var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | ||||
|             options.Address = new Uri(catalogApi); | ||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         services.AddScoped<IOrderingService, OrderingService>(); | ||||
| 
 | ||||
|         services.AddGrpcClient<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) => | ||||
|         { | ||||
|             var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | ||||
|             options.Address = new Uri(orderingApi); | ||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
| @ -1,33 +0,0 @@ | ||||
| namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters | ||||
| { | ||||
|     namespace Basket.API.Infrastructure.Filters | ||||
|     { | ||||
|         public class AuthorizeCheckOperationFilter : IOperationFilter | ||||
|         { | ||||
|             public void Apply(OpenApiOperation operation, OperationFilterContext context) | ||||
|             { | ||||
|                 // Check for authorize attribute | ||||
|                 var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() || | ||||
|                                    context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any(); | ||||
| 
 | ||||
|                 if (!hasAuthorize) return; | ||||
| 
 | ||||
|                 operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); | ||||
|                 operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); | ||||
| 
 | ||||
|                 var oAuthScheme = new OpenApiSecurityScheme | ||||
|                 { | ||||
|                     Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } | ||||
|                 }; | ||||
| 
 | ||||
|                 operation.Security = new List<OpenApiSecurityRequirement> | ||||
|                 { | ||||
|                     new() | ||||
|                     { | ||||
|                         [ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" } | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -2,32 +2,24 @@ | ||||
| global using Grpc.Core.Interceptors; | ||||
| global using Grpc.Core; | ||||
| global using GrpcBasket; | ||||
| global using GrpcOrdering; | ||||
| global using HealthChecks.UI.Client; | ||||
| global using Microsoft.AspNetCore.Authentication.JwtBearer; | ||||
| global using Microsoft.AspNetCore.Authentication; | ||||
| global using Microsoft.AspNetCore.Authorization; | ||||
| global using Microsoft.AspNetCore.Builder; | ||||
| global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | ||||
| global using Microsoft.AspNetCore.Hosting; | ||||
| global using Microsoft.AspNetCore.Http; | ||||
| global using Microsoft.AspNetCore.Mvc; | ||||
| global using Microsoft.AspNetCore; | ||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config; | ||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters; | ||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; | ||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; | ||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; | ||||
| global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator; | ||||
| global using Microsoft.Extensions.Configuration; | ||||
| global using Microsoft.Extensions.DependencyInjection; | ||||
| global using Microsoft.Extensions.Diagnostics.HealthChecks; | ||||
| global using Microsoft.Extensions.Hosting; | ||||
| global using Microsoft.Extensions.Logging; | ||||
| global using Microsoft.Extensions.Options; | ||||
| global using Microsoft.OpenApi.Models; | ||||
| global using Serilog; | ||||
| global using Swashbuckle.AspNetCore.SwaggerGen; | ||||
| global using System.Collections.Generic; | ||||
| global using System.IdentityModel.Tokens.Jwt; | ||||
| global using System.Linq; | ||||
| @ -38,4 +30,5 @@ global using System.Text.Json; | ||||
| global using System.Threading.Tasks; | ||||
| global using System.Threading; | ||||
| 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) | ||||
|         { | ||||
|             _logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); | ||||
|             _logger.LogError(e, "Error calling via gRPC: {Status}", e.Status); | ||||
|             return default; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -1,44 +0,0 @@ | ||||
| namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; | ||||
| 
 | ||||
| public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler | ||||
| { | ||||
|     private readonly IHttpContextAccessor _httpContextAccessor; | ||||
|     private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger; | ||||
| 
 | ||||
|     public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<HttpClientAuthorizationDelegatingHandler> logger) | ||||
|     { | ||||
|         _httpContextAccessor = httpContextAccessor; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||||
|     { | ||||
|         request.Version = new System.Version(2, 0); | ||||
|         request.Method = HttpMethod.Get; | ||||
| 
 | ||||
|         var authorizationHeader = _httpContextAccessor.HttpContext | ||||
|             .Request.Headers["Authorization"]; | ||||
| 
 | ||||
|         if (!string.IsNullOrEmpty(authorizationHeader)) | ||||
|         { | ||||
|             request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | ||||
|         } | ||||
| 
 | ||||
|         var token = await GetToken(); | ||||
| 
 | ||||
|         if (token != null) | ||||
|         { | ||||
|             request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | ||||
|         } | ||||
| 
 | ||||
|         return await base.SendAsync(request, cancellationToken); | ||||
|     } | ||||
| 
 | ||||
|     async Task<string> GetToken() | ||||
|     { | ||||
|         const string ACCESS_TOKEN = "access_token"; | ||||
| 
 | ||||
|         return await _httpContextAccessor.HttpContext | ||||
|             .GetTokenAsync(ACCESS_TOKEN); | ||||
|     } | ||||
| } | ||||
| @ -10,10 +10,14 @@ | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <Folder Include="wwwroot\" /> | ||||
|     <Compile Remove="wwwroot\**" /> | ||||
|     <Content Remove="wwwroot\**" /> | ||||
|     <EmbeddedResource Remove="wwwroot\**" /> | ||||
|     <None Remove="wwwroot\**" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Yarp.ReverseProxy" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.UI.Client" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.Uris" /> | ||||
|     <PackageReference Include="Google.Protobuf" /> | ||||
| @ -25,11 +29,13 @@ | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" /> | ||||
|     <PackageReference Include="Serilog.AspNetCore" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" /> | ||||
|     <PackageReference Include="Swashbuckle.AspNetCore" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <Protobuf Include="..\..\..\Services\Basket\Basket.API\Proto\basket.proto" GrpcServices="Client" /> | ||||
|     <Protobuf Include="..\..\..\Services\Catalog\Catalog.API\Proto\catalog.proto" GrpcServices="Client" /> | ||||
|  | ||||
| @ -2,7 +2,6 @@ | ||||
| 
 | ||||
| public class UpdateBasketItemsRequest | ||||
| { | ||||
| 
 | ||||
|     public string BasketId { get; set; } | ||||
| 
 | ||||
|     public ICollection<UpdateBasketItemData> Updates { get; set; } | ||||
|  | ||||
| @ -1,23 +1,24 @@ | ||||
| await BuildWebHost(args).RunAsync(); | ||||
| IWebHost BuildWebHost(string[] args) => | ||||
|     WebHost | ||||
|         .CreateDefaultBuilder(args) | ||||
|         .ConfigureAppConfiguration(cb => | ||||
|         { | ||||
|             var sources = cb.Sources; | ||||
|             sources.Insert(3, new Microsoft.Extensions.Configuration.Json.JsonConfigurationSource() | ||||
|             { | ||||
|                 Optional = true, | ||||
|                 Path = "appsettings.localhost.json", | ||||
|                 ReloadOnChange = false | ||||
|             }); | ||||
|         }) | ||||
|         .UseStartup<Startup>() | ||||
|         .UseSerilog((builderContext, config) => | ||||
|         { | ||||
|             config | ||||
|                 .MinimumLevel.Information() | ||||
|                 .Enrich.FromLogContext() | ||||
|                 .WriteTo.Console(); | ||||
|         }) | ||||
|         .Build(); | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
| 
 | ||||
| builder.AddServiceDefaults(); | ||||
| 
 | ||||
| builder.Services.AddReverseProxy(builder.Configuration); | ||||
| builder.Services.AddControllers(); | ||||
| 
 | ||||
| builder.Services.AddHealthChecks(builder.Configuration); | ||||
| 
 | ||||
| builder.Services.AddApplicationServices(); | ||||
| builder.Services.AddGrpcServices(); | ||||
| 
 | ||||
| builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls")); | ||||
| 
 | ||||
| var app = builder.Build(); | ||||
| 
 | ||||
| app.UseServiceDefaults(); | ||||
| 
 | ||||
| app.UseHttpsRedirection(); | ||||
| 
 | ||||
| app.MapControllers(); | ||||
| app.MapReverseProxy(); | ||||
| 
 | ||||
| await app.RunAsync(); | ||||
|  | ||||
| @ -5,5 +5,4 @@ public interface IBasketService | ||||
|     Task<BasketData> GetByIdAsync(string id); | ||||
| 
 | ||||
|     Task UpdateAsync(BasketData currentBasket); | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -2,10 +2,10 @@ | ||||
| 
 | ||||
| public class OrderingService : IOrderingService | ||||
| { | ||||
|     private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | ||||
|     private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | ||||
|     private readonly ILogger<OrderingService> _logger; | ||||
| 
 | ||||
|     public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | ||||
|     public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | ||||
|     { | ||||
|         _orderingGrpcClient = orderingGrpcClient; | ||||
|         _logger = logger; | ||||
| @ -48,14 +48,14 @@ public class OrderingService : IOrderingService | ||||
|         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, | ||||
|         }; | ||||
| 
 | ||||
|         basketData.Items.ForEach(i => command.Items.Add(new BasketItem | ||||
|         basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem | ||||
|         { | ||||
|             Id = i.Id, | ||||
|             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": { | ||||
|     "IncludeScopes": false, | ||||
|     "Debug": { | ||||
|       "LogLevel": { | ||||
|         "Default": "Warning" | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning", | ||||
|       "System.Net.Http": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "OpenApi": { | ||||
|     "Endpoint": { | ||||
|       "Name": "Purchase BFF V1" | ||||
|     }, | ||||
|     "Document": { | ||||
|       "Description": "Shopping Aggregator for Mobile Clients", | ||||
|       "Title": "Shopping Aggregator for Mobile Clients", | ||||
|       "Version": "v1" | ||||
|     }, | ||||
|     "Auth": { | ||||
|       "ClientId": "mobileshoppingaggswaggerui", | ||||
|       "AppName": "Mobile shopping BFF Swagger UI" | ||||
|     } | ||||
|   }, | ||||
|   "Identity": { | ||||
|     "Url": "http://localhost:5223", | ||||
|     "Audience": "mobileshoppingagg", | ||||
|     "Scopes": { | ||||
|       "webshoppingagg": "Shopping Aggregator for Mobile Clients" | ||||
|     } | ||||
|   }, | ||||
|   "ReverseProxy": { | ||||
|     "Routes": { | ||||
|       "c-short": { | ||||
|         "ClusterId": "catalog", | ||||
|         "Match": { | ||||
|           "Path": "c/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/c" } | ||||
|         ] | ||||
|       }, | ||||
|       "c-long": { | ||||
|         "ClusterId": "catalog", | ||||
|         "Match": { | ||||
|           "Path": "catalog-api/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/catalog-api" } | ||||
|         ] | ||||
|       }, | ||||
|       "b-short": { | ||||
|         "ClusterId": "basket", | ||||
|         "Match": { | ||||
|           "Path": "b/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/b" } | ||||
|         ] | ||||
|       }, | ||||
|       "b-long": { | ||||
|         "ClusterId": "basket", | ||||
|         "Match": { | ||||
|           "Path": "basket-api/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/basket-api" } | ||||
|         ] | ||||
|       }, | ||||
|       "o-short": { | ||||
|         "ClusterId": "orders", | ||||
|         "Match": { | ||||
|           "Path": "o/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/o" } | ||||
|         ] | ||||
|       }, | ||||
|       "o-long": { | ||||
|         "ClusterId": "orders", | ||||
|         "Match": { | ||||
|           "Path": "ordering-api/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/ordering-api" } | ||||
|         ] | ||||
|       }, | ||||
|       "h-long": { | ||||
|         "ClusterId": "signalr", | ||||
|         "Match": { | ||||
|           "Path": "hub/notificationhub/{**catch-all}" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "Console": { | ||||
|       "LogLevel": { | ||||
|         "Default": "Warning" | ||||
|     "Clusters": { | ||||
|       "basket": { | ||||
|         "Destinations": { | ||||
|           "destination0": { | ||||
|             "Address": "http://localhost:5221" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "catalog": { | ||||
|         "Destinations": { | ||||
|           "destination0": { | ||||
|             "Address": "http://localhost:5222" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "orders": { | ||||
|         "Destinations": { | ||||
|           "destination0": { | ||||
|             "Address": "http://localhost:5224" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "signalr": { | ||||
|         "Destinations": { | ||||
|           "destination0": { | ||||
|             "Address": "http://localhost:5225" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   }, | ||||
|   "Urls": { | ||||
|     "Basket": "http://localhost:5221", | ||||
|     "Catalog": "http://localhost:5222", | ||||
|     "Orders": "http://localhost:5224", | ||||
|     "Identity": "http://localhost:5223", | ||||
|     "Signalr": "http://localhost:5225", | ||||
|     "GrpcBasket": "http://localhost:6221", | ||||
|     "GrpcCatalog": "http://localhost:6222", | ||||
|     "GrpcOrdering": "http://localhost:6224" | ||||
|   }, | ||||
|   "CatalogUrlHC": "http://localhost:5222/hc", | ||||
|   "OrderingUrlHC": "http://localhost:5224/hc", | ||||
|   "BasketUrlHC": "http://localhost:5221/hc", | ||||
|   "IdentityUrlHC": "http://localhost:5223/hc" | ||||
| } | ||||
|  | ||||
| @ -8,16 +8,19 @@ | ||||
|     "grpcCatalog": "http://localhost:81", | ||||
|     "grpcOrdering": "http://localhost:5581" | ||||
|   }, | ||||
|   "IdentityUrlExternal": "http://localhost:5105", | ||||
|   "IdentityUrl": "http://localhost:5105", | ||||
|   "Identity": { | ||||
|     "ExternalUrl": "http://localhost:5105", | ||||
|     "Url": "http://localhost:5105", | ||||
|   }, | ||||
|   "Logging": { | ||||
|     "IncludeScopes": false, | ||||
|     "Debug": { | ||||
|       "IncludeScopes": false, | ||||
|       "LogLevel": { | ||||
|         "Default": "Debug" | ||||
|       } | ||||
|     }, | ||||
|     "Console": { | ||||
|       "IncludeScopes": false, | ||||
|       "LogLevel": { | ||||
|         "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.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" | ||||
| COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" | ||||
| COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" | ||||
| COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" | ||||
| COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" | ||||
| COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" | ||||
|  | ||||
| @ -0,0 +1,63 @@ | ||||
| internal static class Extensions | ||||
| { | ||||
|     public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy")); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddHealthChecks() | ||||
|             .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | ||||
|             .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | ||||
|             .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" }) | ||||
|             .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddApplicationServices(this IServiceCollection services) | ||||
|     { | ||||
|         // Register delegating handlers | ||||
|         services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | ||||
| 
 | ||||
|         // Register http services | ||||
|         services.AddHttpClient<IOrderApiClient, OrderApiClient>() | ||||
|             .AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddGrpcServices(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddTransient<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         services.AddScoped<IBasketService, BasketService>(); | ||||
| 
 | ||||
|         services.AddGrpcClient<Basket.BasketClient>((services, options) => | ||||
|         { | ||||
|             var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | ||||
|             options.Address = new Uri(basketApi); | ||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         services.AddScoped<ICatalogService, CatalogService>(); | ||||
| 
 | ||||
|         services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | ||||
|         { | ||||
|             var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | ||||
|             options.Address = new Uri(catalogApi); | ||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         services.AddScoped<IOrderingService, OrderingService>(); | ||||
| 
 | ||||
|         services.AddGrpcClient<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) => | ||||
|         { | ||||
|             var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | ||||
|             options.Address = new Uri(orderingApi); | ||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
| @ -1,34 +0,0 @@ | ||||
| namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters | ||||
| { | ||||
|     namespace Basket.API.Infrastructure.Filters | ||||
|     { | ||||
|         public class AuthorizeCheckOperationFilter : IOperationFilter | ||||
|         { | ||||
|             public void Apply(OpenApiOperation operation, OperationFilterContext context) | ||||
|             { | ||||
|                 // Check for authorize attribute | ||||
|                 var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() || | ||||
|                                     context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any(); | ||||
| 
 | ||||
|                 if (!hasAuthorize) return; | ||||
| 
 | ||||
|                 operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); | ||||
|                 operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); | ||||
| 
 | ||||
|                 var oAuthScheme = new OpenApiSecurityScheme | ||||
|                 { | ||||
|                     Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } | ||||
|                 }; | ||||
| 
 | ||||
|                 operation.Security = new List<OpenApiSecurityRequirement> | ||||
|             { | ||||
|                 new() | ||||
|                 { | ||||
|                     [ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" } | ||||
|                 } | ||||
|             }; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,41 +1,27 @@ | ||||
| global using CatalogApi; | ||||
| global using Grpc.Core.Interceptors; | ||||
| global using System; | ||||
| 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.Interceptors; | ||||
| global using GrpcBasket; | ||||
| global using GrpcOrdering; | ||||
| global using HealthChecks.UI.Client; | ||||
| global using Microsoft.AspNetCore.Authentication; | ||||
| global using Microsoft.AspNetCore.Authorization; | ||||
| global using Microsoft.AspNetCore.Builder; | ||||
| global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | ||||
| global using Microsoft.AspNetCore.Hosting; | ||||
| global using Microsoft.AspNetCore.Http; | ||||
| global using Microsoft.AspNetCore.Mvc; | ||||
| global using Microsoft.AspNetCore; | ||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; | ||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters; | ||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; | ||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | ||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | ||||
| global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; | ||||
| global using Microsoft.Extensions.Configuration; | ||||
| global using Microsoft.Extensions.DependencyInjection; | ||||
| global using Microsoft.Extensions.Diagnostics.HealthChecks; | ||||
| global using Microsoft.Extensions.Hosting; | ||||
| global using Microsoft.Extensions.Logging; | ||||
| global using Microsoft.Extensions.Options; | ||||
| global using Microsoft.OpenApi.Models; | ||||
| global using Serilog; | ||||
| global using Swashbuckle.AspNetCore.SwaggerGen; | ||||
| global using System.Collections.Generic; | ||||
| global using System.IdentityModel.Tokens.Jwt; | ||||
| global using System.Linq; | ||||
| global using System.Net.Http.Headers; | ||||
| global using System.Net.Http; | ||||
| global using System.Net; | ||||
| global using System.Text.Json; | ||||
| global using System.Threading.Tasks; | ||||
| global using System.Threading; | ||||
| global using System; | ||||
| global using Microsoft.IdentityModel.Tokens; | ||||
| global using Serilog.Context; | ||||
| global using Services.Common; | ||||
|  | ||||
| @ -28,7 +28,7 @@ public class GrpcExceptionInterceptor : Interceptor | ||||
|         } | ||||
|         catch (RpcException e) | ||||
|         { | ||||
|             _logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); | ||||
|             _logger.LogError(e, "Error calling via gRPC: {Status}", e.Status); | ||||
|             return default; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -1,40 +0,0 @@ | ||||
| namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; | ||||
| 
 | ||||
| public class HttpClientAuthorizationDelegatingHandler | ||||
|         : DelegatingHandler | ||||
| { | ||||
|     private readonly IHttpContextAccessor _httpContextAccessor; | ||||
| 
 | ||||
|     public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor) | ||||
|     { | ||||
|         _httpContextAccessor = httpContextAccessor; | ||||
|     } | ||||
| 
 | ||||
|     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||||
|     { | ||||
|         var authorizationHeader = _httpContextAccessor.HttpContext | ||||
|             .Request.Headers["Authorization"]; | ||||
| 
 | ||||
|         if (!string.IsNullOrWhiteSpace(authorizationHeader)) | ||||
|         { | ||||
|             request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | ||||
|         } | ||||
| 
 | ||||
|         var token = await GetTokenAsync(); | ||||
| 
 | ||||
|         if (token != null) | ||||
|         { | ||||
|             request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | ||||
|         } | ||||
| 
 | ||||
|         return await base.SendAsync(request, cancellationToken); | ||||
|     } | ||||
| 
 | ||||
|     Task<string> GetTokenAsync() | ||||
|     { | ||||
|         const string ACCESS_TOKEN = "access_token"; | ||||
| 
 | ||||
|         return _httpContextAccessor.HttpContext | ||||
|             .GetTokenAsync(ACCESS_TOKEN); | ||||
|     } | ||||
| } | ||||
| @ -1,208 +1,38 @@ | ||||
| var appName = "Web.Shopping.HttpAggregator"; | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
| 
 | ||||
| builder.AddServiceDefaults(); | ||||
| 
 | ||||
| builder.Services.AddReverseProxy(builder.Configuration); | ||||
| builder.Services.AddControllers(); | ||||
| 
 | ||||
| builder.Services.AddHealthChecks(builder.Configuration); | ||||
| builder.Services.AddCors(options => | ||||
| { | ||||
|     // TODO: Read allowed origins from configuration | ||||
|     options.AddPolicy("CorsPolicy", | ||||
|         builder => builder | ||||
|         .SetIsOriginAllowed((host) => true) | ||||
|         .AllowAnyMethod() | ||||
|         .AllowAnyHeader() | ||||
|         .AllowCredentials()); | ||||
| }); | ||||
| 
 | ||||
| builder.Services.AddApplicationServices(); | ||||
| builder.Services.AddGrpcServices(); | ||||
| 
 | ||||
| builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls")); | ||||
| 
 | ||||
| builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration)); | ||||
| builder.Services.AddHealthChecks() | ||||
|     .AddCheck("self", () => HealthCheckResult.Healthy()) | ||||
|     .AddUrlGroup(new Uri(builder.Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | ||||
|     .AddUrlGroup(new Uri(builder.Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | ||||
|     .AddUrlGroup(new Uri(builder.Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) | ||||
|     .AddUrlGroup(new Uri(builder.Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) | ||||
|     .AddUrlGroup(new Uri(builder.Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); | ||||
| builder.Services.AddCustomMvc(builder.Configuration) | ||||
|     .AddCustomAuthentication(builder.Configuration) | ||||
|     .AddApplicationServices() | ||||
|     .AddGrpcServices(); | ||||
| var app = builder.Build(); | ||||
| if (app.Environment.IsDevelopment()) | ||||
| { | ||||
|     app.UseDeveloperExceptionPage(); | ||||
| } | ||||
| else | ||||
| { | ||||
|     app.UseExceptionHandler("/Home/Error"); | ||||
| } | ||||
| var pathBase = builder.Configuration["PATH_BASE"]; | ||||
| if (!string.IsNullOrEmpty(pathBase)) | ||||
| { | ||||
|     app.UsePathBase(pathBase); | ||||
| } | ||||
| 
 | ||||
| app.UseServiceDefaults(); | ||||
| 
 | ||||
| app.UseHttpsRedirection(); | ||||
| 
 | ||||
| app.UseSwagger().UseSwaggerUI(c => | ||||
| { | ||||
|     c.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Purchase BFF V1"); | ||||
| 
 | ||||
|     c.OAuthClientId("webshoppingaggswaggerui"); | ||||
|     c.OAuthClientSecret(string.Empty); | ||||
|     c.OAuthRealm(string.Empty); | ||||
|     c.OAuthAppName("web shopping bff Swagger UI"); | ||||
| }); | ||||
| 
 | ||||
| app.UseRouting(); | ||||
| app.UseCors("CorsPolicy"); | ||||
| app.UseAuthentication(); | ||||
| app.UseAuthorization(); | ||||
| 
 | ||||
| app.MapDefaultControllerRoute(); | ||||
| app.MapControllers(); | ||||
| app.MapHealthChecks("/hc", new HealthCheckOptions() | ||||
| { | ||||
|     Predicate = _ => true, | ||||
|     ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | ||||
| }); | ||||
| app.MapHealthChecks("/liveness", new HealthCheckOptions | ||||
| { | ||||
|     Predicate = r => r.Name.Contains("self") | ||||
| }); | ||||
| app.MapReverseProxy(); | ||||
| 
 | ||||
| try | ||||
| { | ||||
|     Log.Information("Starts Web Application ({ApplicationContext})...", Program.AppName); | ||||
|     await app.RunAsync(); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| catch (Exception ex) | ||||
| { | ||||
|     Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); | ||||
|     return 1; | ||||
| } | ||||
| finally | ||||
| { | ||||
|     Log.CloseAndFlush(); | ||||
| } | ||||
| 
 | ||||
| Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) | ||||
| { | ||||
|     var seqServerUrl = configuration["Serilog:SeqServerUrl"]; | ||||
|     var logstashUrl = configuration["Serilog:LogstashgUrl"]; | ||||
|     return new LoggerConfiguration() | ||||
|         .MinimumLevel.Verbose() | ||||
|         .Enrich.WithProperty("ApplicationContext", Program.AppName) | ||||
|         .Enrich.FromLogContext() | ||||
|         .WriteTo.Console() | ||||
|         .ReadFrom.Configuration(configuration) | ||||
|         .CreateLogger(); | ||||
| } | ||||
| public partial class Program | ||||
| { | ||||
| 
 | ||||
|     public static string Namespace = typeof(Program).Assembly.GetName().Name; | ||||
|     public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); | ||||
| } | ||||
| 
 | ||||
| public static class ServiceCollectionExtensions | ||||
| { | ||||
|     public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | ||||
| 
 | ||||
|         var identityUrl = configuration.GetValue<string>("urls:identity"); | ||||
|         services.AddAuthentication("Bearer") | ||||
|         .AddJwtBearer(options => | ||||
|         { | ||||
|             options.Authority = identityUrl; | ||||
|             options.RequireHttpsMetadata = false; | ||||
|             options.Audience = "webshoppingagg"; | ||||
|             options.TokenValidationParameters = new TokenValidationParameters | ||||
|             { | ||||
|                 ValidateAudience = false | ||||
|             }; | ||||
|         }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
|     public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddOptions(); | ||||
|         services.Configure<UrlsConfig>(configuration.GetSection("urls")); | ||||
| 
 | ||||
|         services.AddControllers() | ||||
|                 .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | ||||
| 
 | ||||
|         services.AddSwaggerGen(options => | ||||
|         { | ||||
|             options.SwaggerDoc("v1", new OpenApiInfo | ||||
|             { | ||||
|                 Title = "Shopping Aggregator for Web Clients", | ||||
|                 Version = "v1", | ||||
|                 Description = "Shopping Aggregator for Web Clients" | ||||
|             }); | ||||
|             options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | ||||
|             { | ||||
|                 Type = SecuritySchemeType.OAuth2, | ||||
|                 Flows = new OpenApiOAuthFlows() | ||||
|                 { | ||||
|                     Implicit = new OpenApiOAuthFlow() | ||||
|                     { | ||||
|                         AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), | ||||
|                         TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"), | ||||
|                         Scopes = new Dictionary<string, string>() | ||||
|                         { | ||||
|                             { "webshoppingagg", "Shopping Aggregator for Web Clients" } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             options.OperationFilter<AuthorizeCheckOperationFilter>(); | ||||
|         }); | ||||
|         services.AddCors(options => | ||||
|         { | ||||
|             options.AddPolicy("CorsPolicy", | ||||
|                 builder => builder | ||||
|                 .SetIsOriginAllowed((host) => true) | ||||
|                 .AllowAnyMethod() | ||||
|                 .AllowAnyHeader() | ||||
|                 .AllowCredentials()); | ||||
|         }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
|     public static IServiceCollection AddApplicationServices(this IServiceCollection services) | ||||
|     { | ||||
|         //register delegating handlers | ||||
|         services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | ||||
|         services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | ||||
| 
 | ||||
|         //register http services | ||||
| 
 | ||||
|         services.AddHttpClient<IOrderApiClient, OrderApiClient>() | ||||
|             .AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddGrpcServices(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddTransient<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         services.AddScoped<IBasketService, BasketService>(); | ||||
| 
 | ||||
|         services.AddGrpcClient<Basket.BasketClient>((services, options) => | ||||
|         { | ||||
|             var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | ||||
|             options.Address = new Uri(basketApi); | ||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         services.AddScoped<ICatalogService, CatalogService>(); | ||||
| 
 | ||||
|         services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | ||||
|         { | ||||
|             var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | ||||
|             options.Address = new Uri(catalogApi); | ||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         services.AddScoped<IOrderingService, OrderingService>(); | ||||
| 
 | ||||
|         services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) => | ||||
|         { | ||||
|             var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | ||||
|             options.Address = new Uri(orderingApi); | ||||
|         }).AddInterceptor<GrpcExceptionInterceptor>(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
| await app.RunAsync(); | ||||
|  | ||||
| @ -1,29 +1,12 @@ | ||||
| { | ||||
|   "iisSettings": { | ||||
|     "windowsAuthentication": false, | ||||
|     "anonymousAuthentication": true, | ||||
|     "iisExpress": { | ||||
|       "applicationUrl": "http://localhost:57425/", | ||||
|       "sslPort": 0 | ||||
|     } | ||||
|   }, | ||||
|   "profiles": { | ||||
|     "IIS Express": { | ||||
|       "commandName": "IISExpress", | ||||
|     "Web.Shopping.HttpAggregator": { | ||||
|       "commandName": "Project", | ||||
|       "launchBrowser": true, | ||||
|       "launchUrl": "api/values", | ||||
|       "applicationUrl": "http://localhost:5229/", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|     }, | ||||
|     "PurchaseForMvc": { | ||||
|       "commandName": "Project", | ||||
|       "launchBrowser": true, | ||||
|       "launchUrl": "api/values", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       }, | ||||
|       "applicationUrl": "http://localhost:61632/" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -2,10 +2,10 @@ | ||||
| 
 | ||||
| public class OrderingService : IOrderingService | ||||
| { | ||||
|     private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | ||||
|     private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; | ||||
|     private readonly ILogger<OrderingService> _logger; | ||||
| 
 | ||||
|     public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | ||||
|     public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) | ||||
|     { | ||||
|         _orderingGrpcClient = orderingGrpcClient; | ||||
|         _logger = logger; | ||||
| @ -48,14 +48,14 @@ public class OrderingService : IOrderingService | ||||
|         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, | ||||
|         }; | ||||
| 
 | ||||
|         basketData.Items.ForEach(i => command.Items.Add(new BasketItem | ||||
|         basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem | ||||
|         { | ||||
|             Id = i.Id, | ||||
|             OldUnitPrice = (double)i.OldUnitPrice, | ||||
|  | ||||
| @ -5,30 +5,18 @@ | ||||
|     <AssemblyName>Web.Shopping.HttpAggregator</AssemblyName> | ||||
|     <RootNamespace>Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator</RootNamespace> | ||||
|     <DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | ||||
|     <GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> | ||||
|     <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>     | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <Folder Include="wwwroot\" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.UI.Client" /> | ||||
|     <PackageReference Include="Yarp.ReverseProxy" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.Uris" /> | ||||
|     <PackageReference Include="Google.Protobuf" /> | ||||
|     <PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" /> | ||||
|     <PackageReference Include="Grpc.Core" /> | ||||
|     <PackageReference Include="Grpc.Net.Client" /> | ||||
|     <PackageReference Include="Grpc.Net.ClientFactory" /> | ||||
|     <PackageReference Include="Grpc.Tools" PrivateAssets="All" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" /> | ||||
|     <PackageReference Include="Serilog.AspNetCore" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" /> | ||||
|     <PackageReference Include="Swashbuckle.AspNetCore" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|  | ||||
| @ -1,15 +1,8 @@ | ||||
| { | ||||
|   "Logging": { | ||||
|     "IncludeScopes": false, | ||||
|     "Debug": { | ||||
|       "LogLevel": { | ||||
|         "Default": "Debug" | ||||
|       } | ||||
|     }, | ||||
|     "Console": { | ||||
|       "LogLevel": { | ||||
|         "Default": "Debug" | ||||
|       } | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,15 +1,138 @@ | ||||
| { | ||||
|   "Logging": { | ||||
|     "IncludeScopes": false, | ||||
|     "Debug": { | ||||
|       "LogLevel": { | ||||
|         "Default": "Warning" | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning", | ||||
|       "System.Net.Http": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "OpenApi": { | ||||
|     "Endpoint": { | ||||
|       "Name": "Purchase BFF V1" | ||||
|     }, | ||||
|     "Document": { | ||||
|       "Description": "Shopping Aggregator for Web Clients", | ||||
|       "Title": "Shopping Aggregator for Web Clients", | ||||
|       "Version": "v1" | ||||
|     }, | ||||
|     "Auth": { | ||||
|       "ClientId": "webshoppingaggswaggerui", | ||||
|       "AppName": "Web Shopping BFF Swagger UI" | ||||
|     } | ||||
|   }, | ||||
|   "Identity": { | ||||
|     "Url": "http://localhost:5223", | ||||
|     "Audience": "webshoppingagg", | ||||
|     "Scopes": { | ||||
|       "webshoppingagg": "Shopping Aggregator for Web Clients" | ||||
|     } | ||||
|   }, | ||||
|   "ReverseProxy": { | ||||
|     "Routes": { | ||||
|       "c-short": { | ||||
|         "ClusterId": "catalog", | ||||
|         "Match": { | ||||
|           "Path": "c/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/c" } | ||||
|         ] | ||||
|       }, | ||||
|       "c-long": { | ||||
|         "ClusterId": "catalog", | ||||
|         "Match": { | ||||
|           "Path": "catalog-api/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/catalog-api" } | ||||
|         ] | ||||
|       }, | ||||
|       "b-short": { | ||||
|         "ClusterId": "basket", | ||||
|         "Match": { | ||||
|           "Path": "b/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/b" } | ||||
|         ] | ||||
|       }, | ||||
|       "b-long": { | ||||
|         "ClusterId": "basket", | ||||
|         "Match": { | ||||
|           "Path": "basket-api/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/basket-api" } | ||||
|         ] | ||||
|       }, | ||||
|       "o-short": { | ||||
|         "ClusterId": "orders", | ||||
|         "Match": { | ||||
|           "Path": "o/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/o" } | ||||
|         ] | ||||
|       }, | ||||
|       "o-long": { | ||||
|         "ClusterId": "orders", | ||||
|         "Match": { | ||||
|           "Path": "ordering-api/{**catch-all}" | ||||
|         }, | ||||
|         "Transforms": [ | ||||
|           { "PathRemovePrefix": "/ordering-api" } | ||||
|         ] | ||||
|       }, | ||||
|       "h-long": { | ||||
|         "ClusterId": "signalr", | ||||
|         "Match": { | ||||
|           "Path": "hub/notificationhub/{**catch-all}" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "Console": { | ||||
|       "LogLevel": { | ||||
|         "Default": "Warning" | ||||
|     "Clusters": { | ||||
|       "basket": { | ||||
|         "Destinations": { | ||||
|           "destination0": { | ||||
|             "Address": "http://localhost:5221" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "catalog": { | ||||
|         "Destinations": { | ||||
|           "destination0": { | ||||
|             "Address": "http://localhost:5222" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "orders": { | ||||
|         "Destinations": { | ||||
|           "destination0": { | ||||
|             "Address": "http://localhost:5224" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "signalr": { | ||||
|         "Destinations": { | ||||
|           "destination0": { | ||||
|             "Address": "http://localhost:5225" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   }, | ||||
|   "Urls": { | ||||
|     "Basket": "http://localhost:5221", | ||||
|     "Catalog": "http://localhost:5222", | ||||
|     "Orders": "http://localhost:5224", | ||||
|     "Identity": "http://localhost:5223", | ||||
|     "Signalr": "http://localhost:5225", | ||||
|     "GrpcBasket": "http://localhost:6221", | ||||
|     "GrpcCatalog": "http://localhost:6222", | ||||
|     "GrpcOrdering": "http://localhost:6224" | ||||
|   }, | ||||
|   "CatalogUrlHC": "http://localhost:5222/hc", | ||||
|   "OrderingUrlHC": "http://localhost:5224/hc", | ||||
|   "BasketUrlHC": "http://localhost:5221/hc", | ||||
|   "IdentityUrlHC": "http://localhost:5223/hc" | ||||
| } | ||||
|  | ||||
| @ -1,11 +0,0 @@ | ||||
| { | ||||
|   "urls": { | ||||
|     "basket": "http://localhost:55103", | ||||
|     "catalog": "http://localhost:55101", | ||||
|     "orders": "http://localhost:55102", | ||||
|     "identity": "http://localhost:55105", | ||||
|     "grpcBasket": "http://localhost:5580", | ||||
|     "grpcCatalog": "http://localhost:81", | ||||
|     "grpcOrdering": "http://localhost:5581" | ||||
|   } | ||||
| } | ||||
| @ -12,9 +12,10 @@ namespace EventBus.Tests | ||||
|             Handled = false; | ||||
|         } | ||||
| 
 | ||||
|         public async Task Handle(TestIntegrationEvent @event) | ||||
|         public Task Handle(TestIntegrationEvent @event) | ||||
|         { | ||||
|             Handled = true; | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -12,9 +12,10 @@ namespace EventBus.Tests | ||||
|             Handled = false; | ||||
|         } | ||||
| 
 | ||||
|         public async Task Handle(TestIntegrationEvent @event) | ||||
|         public Task Handle(TestIntegrationEvent @event) | ||||
|         { | ||||
|             Handled = true; | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,12 +8,6 @@ public interface IEventBus | ||||
|         where T : IntegrationEvent | ||||
|         where TH : IIntegrationEventHandler<T>; | ||||
| 
 | ||||
|     void SubscribeDynamic<TH>(string eventName) | ||||
|         where TH : IDynamicIntegrationEventHandler; | ||||
| 
 | ||||
|     void UnsubscribeDynamic<TH>(string eventName) | ||||
|         where TH : IDynamicIntegrationEventHandler; | ||||
| 
 | ||||
|     void Unsubscribe<T, TH>() | ||||
|         where TH : IIntegrationEventHandler<T> | ||||
|         where T : IntegrationEvent; | ||||
|  | ||||
| @ -59,7 +59,7 @@ public class DefaultRabbitMQPersistentConnection | ||||
|                 .Or<BrokerUnreachableException>() | ||||
|                 .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => | ||||
|                 { | ||||
|                     _logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s ({ExceptionMessage})", $"{time.TotalSeconds:n1}", ex.Message); | ||||
|                     _logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s", $"{time.TotalSeconds:n1}"); | ||||
|                 } | ||||
|             ); | ||||
| 
 | ||||
| @ -81,7 +81,7 @@ public class DefaultRabbitMQPersistentConnection | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); | ||||
|                 _logger.LogCritical("Fatal error: RabbitMQ connections could not be created and opened"); | ||||
| 
 | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
| @ -4,7 +4,6 @@ using Microsoft.Extensions.DependencyInjection; | ||||
| public class EventBusRabbitMQ : IEventBus, IDisposable | ||||
| { | ||||
|     const string BROKER_NAME = "eshop_event_bus"; | ||||
|     const string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; | ||||
| 
 | ||||
|     private readonly IRabbitMQPersistentConnection _persistentConnection; | ||||
|     private readonly ILogger<EventBusRabbitMQ> _logger; | ||||
| @ -58,7 +57,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable | ||||
|             .Or<SocketException>() | ||||
|             .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => | ||||
|             { | ||||
|                 _logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s ({ExceptionMessage})", @event.Id, $"{time.TotalSeconds:n1}", ex.Message); | ||||
|                 _logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s", @event.Id, $"{time.TotalSeconds:n1}"); | ||||
|             }); | ||||
| 
 | ||||
|         var eventName = @event.GetType().Name; | ||||
| @ -194,7 +193,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             _logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", message); | ||||
|             _logger.LogWarning(ex, "Error Processing message \"{Message}\"", message); | ||||
|         } | ||||
| 
 | ||||
|         // Even on exception we take the message off the queue. | ||||
|  | ||||
| @ -6,7 +6,6 @@ | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Autofac" /> | ||||
|     <PackageReference Include="Microsoft.CSharp" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging" />     | ||||
|     <PackageReference Include="Polly" /> | ||||
|  | ||||
| @ -7,7 +7,6 @@ global using RabbitMQ.Client.Exceptions; | ||||
| global using System; | ||||
| global using System.IO; | ||||
| global using System.Net.Sockets; | ||||
| global using Autofac; | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | ||||
| 
 | ||||
| @ -12,7 +12,6 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable | ||||
|     private readonly string _subscriptionName; | ||||
|     private readonly ServiceBusSender _sender; | ||||
|     private readonly ServiceBusProcessor _processor; | ||||
|     private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; | ||||
|     private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; | ||||
| 
 | ||||
|     public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, | ||||
| @ -141,7 +140,7 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable | ||||
|         var ex = args.Exception; | ||||
|         var context = args.ErrorSource; | ||||
| 
 | ||||
|         _logger.LogError(ex, "ERROR handling message: {ExceptionMessage} - Context: {@ExceptionContext}", ex.Message, context); | ||||
|         _logger.LogError(ex, "Error handling message - Context: {@ExceptionContext}", context); | ||||
| 
 | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
| @ -198,4 +197,4 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable | ||||
|         _subsManager.Clear(); | ||||
|         await _processor.CloseAsync(); | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -6,7 +6,6 @@ | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Autofac" /> | ||||
|     <PackageReference Include="Azure.Messaging.ServiceBus" /> | ||||
|     <PackageReference Include="Microsoft.CSharp" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging" /> | ||||
|  | ||||
| @ -2,7 +2,6 @@ | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | ||||
| global using System.Threading.Tasks; | ||||
| global using System; | ||||
| global using Autofac; | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | ||||
| global using Microsoft.Extensions.Logging; | ||||
| global using System.Text; | ||||
|  | ||||
| @ -1,30 +1,30 @@ | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using System; | ||||
| using System.Data.SqlClient; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Polly; | ||||
| using System; | ||||
| using System.Data.SqlClient; | ||||
| 
 | ||||
| namespace Microsoft.AspNetCore.Hosting | ||||
| { | ||||
|     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"); | ||||
|             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(); | ||||
|             var services = scope.ServiceProvider; | ||||
|             var logger = services.GetRequiredService<ILogger<TContext>>(); | ||||
|             var context = services.GetService<TContext>(); | ||||
|             using var scope = services.CreateScope(); | ||||
|             var scopeServices = scope.ServiceProvider; | ||||
|             var logger = scopeServices.GetRequiredService<ILogger<TContext>>(); | ||||
|             var context = scopeServices.GetService<TContext>(); | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
| @ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Hosting | ||||
| 
 | ||||
|                 if (underK8s) | ||||
|                 { | ||||
|                     InvokeSeeder(seeder, context, services); | ||||
|                     InvokeSeeder(seeder, context, scopeServices); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
| @ -43,14 +43,14 @@ namespace Microsoft.AspNetCore.Hosting | ||||
|                             sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), | ||||
|                             onRetry: (exception, timeSpan, retry, ctx) => | ||||
|                             { | ||||
|                                 logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries); | ||||
|                                 logger.LogWarning(exception, "[{prefix}] Error migrating database (attempt {retry} of {retries})", nameof(TContext), retry, retries); | ||||
|                             }); | ||||
| 
 | ||||
|                     //if the sql server container is not created on run docker compose this | ||||
|                     //migration can't fail for network related exception. The retry options for DbContext only  | ||||
|                     //apply to transient exceptions | ||||
|                     // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) | ||||
|                     retry.Execute(() => InvokeSeeder(seeder, context, services)); | ||||
|                     retry.Execute(() => InvokeSeeder(seeder, context, scopeServices)); | ||||
|                 } | ||||
| 
 | ||||
|                 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) | ||||
|  | ||||
| @ -3,7 +3,6 @@ | ||||
|     <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> | ||||
|     <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled> | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageVersion Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" /> | ||||
|     <PackageVersion Include="AspNetCore.HealthChecks.AzureStorage" Version="6.1.2" /> | ||||
| @ -14,8 +13,6 @@ | ||||
|     <PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5" /> | ||||
|     <PackageVersion Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="6.0.5" /> | ||||
|     <PackageVersion Include="AspNetCore.HealthChecks.Uris" Version="6.0.3" /> | ||||
|     <PackageVersion Include="Autofac" Version="6.5.0" /> | ||||
|     <PackageVersion Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" /> | ||||
|     <PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" /> | ||||
|     <PackageVersion Include="Azure.Identity" Version="1.8.2" /> | ||||
|     <PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.12.0" /> | ||||
| @ -29,6 +26,7 @@ | ||||
|     <PackageVersion Include="FluentValidation.AspNetCore" Version="11.2.2" /> | ||||
|     <PackageVersion Include="Google.Protobuf" Version="3.22.0" /> | ||||
|     <PackageVersion Include="Grpc.AspNetCore.Server" Version="2.51.0" /> | ||||
|     <PackageVersion Include="Grpc.AspNetCore" Version="2.51.0" /> | ||||
|     <PackageVersion Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.51.0" /> | ||||
|     <PackageVersion Include="Grpc.Core" Version="2.46.6" /> | ||||
|     <PackageVersion Include="Grpc.Net.Client" Version="2.51.0" /> | ||||
| @ -51,6 +49,7 @@ | ||||
|     <PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.3" /> | ||||
|     <PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.3" /> | ||||
|     <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" /> | ||||
|     <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" /> | ||||
|     <PackageVersion Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="7.0.3" /> | ||||
|     <PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.3" /> | ||||
|     <PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="7.0.3" /> | ||||
| @ -82,12 +81,6 @@ | ||||
|     <PackageVersion Include="Newtonsoft.Json" Version="13.0.2" /> | ||||
|     <PackageVersion Include="Polly" Version="7.2.3" /> | ||||
|     <PackageVersion Include="RabbitMQ.Client" Version="6.4.0" /> | ||||
|     <PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" /> | ||||
|     <PackageVersion Include="Serilog.Enrichers.Environment" Version="2.2.1-dev-00787" /> | ||||
|     <PackageVersion Include="Serilog.Settings.Configuration" Version="3.5.0-dev-00359" /> | ||||
|     <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.1-dev-00896" /> | ||||
|     <PackageVersion Include="Serilog.Sinks.Http" Version="8.0.0" /> | ||||
|     <PackageVersion Include="Serilog.Sinks.Seq" Version="5.2.3-dev-00260" /> | ||||
|     <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" /> | ||||
|     <PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" /> | ||||
|     <PackageVersion Include="System.Data.SqlClient" Version="4.8.5" /> | ||||
| @ -95,5 +88,6 @@ | ||||
|     <PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" /> | ||||
|     <PackageVersion Include="xunit" Version="2.4.2" /> | ||||
|     <PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" /> | ||||
|     <PackageVersion Include="Yarp.ReverseProxy" Version="2.0.0" /> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
| </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"> | ||||
| 	<PropertyGroup> | ||||
| 		<TargetFramework>net7.0</TargetFramework> | ||||
| 		<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> | ||||
| 		<DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | ||||
| 		<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> | ||||
| 		<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled> | ||||
| 	</PropertyGroup> | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net7.0</TargetFramework> | ||||
|     <DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | ||||
|     <UserSecretsId>2964ec8e-0d48-4541-b305-94cab537f867</UserSecretsId> | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
| 	<ItemGroup> | ||||
| 		<Content Update="web.config"> | ||||
| 			<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | ||||
| 		</Content> | ||||
| 	</ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Grpc.AspNetCore" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.Redis" /> | ||||
|     <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" /> | ||||
|   </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.UI.Client" /> | ||||
| 		<PackageReference Include="Autofac.Extensions.DependencyInjection" /> | ||||
| 		<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" /> | ||||
| 		<PackageReference Include="Azure.Identity" /> | ||||
| 		<PackageReference Include="Google.Protobuf" /> | ||||
| 		<PackageReference Include="Grpc.AspNetCore.Server" /> | ||||
| 		<PackageReference Include="Grpc.Tools" PrivateAssets="All" /> | ||||
| 		<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" /> | ||||
| 		<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" /> | ||||
| 		<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" /> | ||||
| 		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" /> | ||||
| 		<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" /> | ||||
| 		<PackageReference Include="Microsoft.AspNetCore.HealthChecks" /> | ||||
| 		<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" /> | ||||
| 		<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" /> | ||||
| 		<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" /> | ||||
| 		<PackageReference Include="Serilog.AspNetCore" /> | ||||
| 		<PackageReference Include="Serilog.Enrichers.Environment" /> | ||||
| 		<PackageReference Include="Serilog.Settings.Configuration" /> | ||||
| 		<PackageReference Include="Serilog.Sinks.Console" /> | ||||
| 		<PackageReference Include="Serilog.Sinks.Http" /> | ||||
| 		<PackageReference Include="Serilog.Sinks.Seq" /> | ||||
| 		<PackageReference Include="Swashbuckle.AspNetCore" /> | ||||
| 		<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" /> | ||||
| 	</ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <Protobuf Include="Proto\basket.proto" GrpcServices="Server" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
| 	<ItemGroup> | ||||
| 		<Protobuf Include="Proto\basket.proto" GrpcServices="Server" Generator="MSBuild:Compile" /> | ||||
| 		<Content Include="@(Protobuf)" /> | ||||
| 		<None Remove="@(Protobuf)" /> | ||||
| 	</ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\..\Services.Common\Services.Common.csproj" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
| 	<ItemGroup> | ||||
| 		<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" /> | ||||
| 		<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" /> | ||||
| 		<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" /> | ||||
| 	</ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <InternalsVisibleTo Include="Basket.FunctionalTests" /> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
|  | ||||
| @ -56,7 +56,7 @@ public class BasketController : ControllerBase | ||||
|             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, | ||||
|             basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName, | ||||
| @ -71,7 +71,7 @@ public class BasketController : ControllerBase | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             _logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName}", eventMessage.Id, Program.AppName); | ||||
|             _logger.LogError(ex, "Error Publishing integration event: {IntegrationEventId}", eventMessage.Id); | ||||
| 
 | ||||
|             throw; | ||||
|         } | ||||
|  | ||||
| @ -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.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" | ||||
| COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" | ||||
| COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" | ||||
| COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" | ||||
| COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" | ||||
| COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" | ||||
|  | ||||
							
								
								
									
										20
									
								
								src/Services/Basket/Basket.API/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/Services/Basket/Basket.API/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| public static class Extensions | ||||
| { | ||||
|     public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddHealthChecks() | ||||
|             .AddRedis(_ => configuration.GetRequiredConnectionString("redis"), "redis", tags: new[] { "ready", "liveness" }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         return services.AddSingleton(sp => | ||||
|         { | ||||
|             var redisConfig = ConfigurationOptions.Parse(configuration.GetRequiredConnectionString("redis"), true); | ||||
| 
 | ||||
|             return ConnectionMultiplexer.Connect(redisConfig); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @ -1,59 +1,29 @@ | ||||
| global using Autofac.Extensions.DependencyInjection; | ||||
| global using Autofac; | ||||
| global using Azure.Core; | ||||
| global using Azure.Identity; | ||||
| global using Basket.API.Infrastructure.ActionResults; | ||||
| global using Basket.API.Infrastructure.Exceptions; | ||||
| global using Basket.API.Infrastructure.Filters; | ||||
| global using Basket.API.Infrastructure.Middlewares; | ||||
| global using 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; | ||||
| global using System.Collections.Generic; | ||||
| global using System.ComponentModel.DataAnnotations; | ||||
| global using System.IdentityModel.Tokens.Jwt; | ||||
| global using System.IO; | ||||
| global using System.Linq; | ||||
| global using System.Net; | ||||
| global using System.Security.Claims; | ||||
| global using System.Text.Json; | ||||
| global using System.Threading.Tasks; | ||||
| global using System; | ||||
| 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) | ||||
|     { | ||||
|         using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) | ||||
|         using (_logger.BeginScope(new List<KeyValuePair<string, object>> { new ("IntegrationEventContext", @event.Id) })) | ||||
|         { | ||||
|             _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); | ||||
|             _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); | ||||
| 
 | ||||
|             await _repository.DeleteBasketAsync(@event.UserId.ToString()); | ||||
|         } | ||||
|  | ||||
| @ -15,9 +15,9 @@ public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandl | ||||
| 
 | ||||
|     public async Task Handle(ProductPriceChangedIntegrationEvent @event) | ||||
|     { | ||||
|         using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) | ||||
|         using (_logger.BeginScope(new List<KeyValuePair<string, object>> { new ("IntegrationEventContext", @event.Id) })) | ||||
|         { | ||||
|             _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); | ||||
|             _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); | ||||
| 
 | ||||
|             var userIds = _repository.GetUsers(); | ||||
| 
 | ||||
| @ -36,7 +36,7 @@ public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandl | ||||
| 
 | ||||
|         if (itemsToUpdate != null) | ||||
|         { | ||||
|             _logger.LogInformation("----- ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate); | ||||
|             _logger.LogInformation("ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate); | ||||
| 
 | ||||
|             foreach (var item in itemsToUpdate) | ||||
|             { | ||||
|  | ||||
| @ -1,333 +1,30 @@ | ||||
| using Autofac.Core; | ||||
| using Microsoft.Azure.Amqp.Framing; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
| 
 | ||||
| var appName = "Basket.API"; | ||||
| 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.AddServiceDefaults(); | ||||
| 
 | ||||
| builder.Services.AddGrpc(options => | ||||
| { | ||||
|     options.EnableDetailedErrors = true; | ||||
| }); | ||||
| builder.Services.AddApplicationInsightsTelemetry(builder.Configuration); | ||||
| builder.Services.AddApplicationInsightsKubernetesEnricher(); | ||||
| builder.Services.AddControllers(options => | ||||
| { | ||||
|     options.Filters.Add(typeof(HttpGlobalExceptionFilter)); | ||||
|     options.Filters.Add(typeof(ValidateModelStateFilter)); | ||||
| builder.Services.AddGrpc(); | ||||
| builder.Services.AddControllers(); | ||||
| builder.Services.AddProblemDetails(); | ||||
| 
 | ||||
| }) // Added for functional tests | ||||
|             .AddApplicationPart(typeof(BasketController).Assembly) | ||||
|             .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" | ||||
|     }); | ||||
| builder.Services.AddHealthChecks(builder.Configuration); | ||||
| builder.Services.AddRedis(builder.Configuration); | ||||
| 
 | ||||
|     options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | ||||
|     { | ||||
|         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" } } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| builder.Services.AddTransient<ProductPriceChangedIntegrationEventHandler>(); | ||||
| builder.Services.AddTransient<OrderStartedIntegrationEventHandler>(); | ||||
| 
 | ||||
|     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<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(); | ||||
| 
 | ||||
| if (app.Environment.IsDevelopment()) | ||||
| { | ||||
|     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.UseServiceDefaults(); | ||||
| 
 | ||||
| app.MapGrpcService<BasketService>(); | ||||
| app.MapDefaultControllerRoute(); | ||||
| 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(); | ||||
| eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>(); | ||||
| eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>(); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| catch (Exception ex) | ||||
| { | ||||
|     Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); | ||||
|     return 1; | ||||
| } | ||||
| finally | ||||
| { | ||||
|     Log.CloseAndFlush(); | ||||
| } | ||||
| 
 | ||||
| Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) | ||||
| { | ||||
|     var seqServerUrl = configuration["Serilog:SeqServerUrl"]; | ||||
|     var logstashUrl = configuration["Serilog:LogstashgUrl"]; | ||||
|     return new LoggerConfiguration() | ||||
|         .MinimumLevel.Verbose() | ||||
|         .Enrich.WithProperty("ApplicationContext", Program.AppName) | ||||
|         .Enrich.FromLogContext() | ||||
|         .WriteTo.Console() | ||||
|         .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) | ||||
|         .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl, null) | ||||
|         .ReadFrom.Configuration(configuration) | ||||
|         .CreateLogger(); | ||||
| } | ||||
| 
 | ||||
| (int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) | ||||
| { | ||||
|     var grpcPort = config.GetValue("GRPC_PORT", 5001); | ||||
|     var port = config.GetValue("PORT", 80); | ||||
|     return (port, grpcPort); | ||||
| } | ||||
| void ConfigureEventBus(IApplicationBuilder app) | ||||
| { | ||||
|     var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); | ||||
| 
 | ||||
|     eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>(); | ||||
|     eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>(); | ||||
| } | ||||
| public partial class Program | ||||
| { | ||||
| 
 | ||||
|     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 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; | ||||
|     } | ||||
| } | ||||
| await app.RunAsync(); | ||||
|  | ||||
| @ -1,26 +1,11 @@ | ||||
| { | ||||
|   "iisSettings": { | ||||
|     "windowsAuthentication": false, | ||||
|     "anonymousAuthentication": true, | ||||
|     "iisExpress": { | ||||
|       "applicationUrl": "http://localhost:58017/", | ||||
|       "sslPort": 0 | ||||
|     } | ||||
|   }, | ||||
|   "profiles": { | ||||
|     "IIS Express": { | ||||
|       "commandName": "IISExpress", | ||||
|       "launchBrowser": true, | ||||
|       "launchUrl": "swagger", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|     }, | ||||
|     "Microsoft.eShopOnContainers.Services.Basket.API": { | ||||
|     "Basket.API": { | ||||
|       "commandName": "Project", | ||||
|       "launchBrowser": true, | ||||
|       "launchUrl": "http://localhost:55103/", | ||||
|       "applicationUrl": "http://localhost:5221", | ||||
|       "environmentVariables": { | ||||
|         "Identity__Url": "http://localhost:5223", | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @ -0,0 +1,17 @@ | ||||
| { | ||||
|   "dependencies": { | ||||
|     "secrets1": { | ||||
|       "type": "secrets" | ||||
|     }, | ||||
|     "rabbitmq1": { | ||||
|       "type": "rabbitmq", | ||||
|       "connectionId": "eventbus", | ||||
|       "dynamicId": null | ||||
|     }, | ||||
|     "redis1": { | ||||
|       "type": "redis", | ||||
|       "connectionId": "ConnectionStrings:Redis", | ||||
|       "dynamicId": null | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,26 @@ | ||||
| { | ||||
|   "dependencies": { | ||||
|     "secrets1": { | ||||
|       "type": "secrets.user" | ||||
|     }, | ||||
|     "rabbitmq1": { | ||||
|       "containerPorts": "5672:5672,15672:15672", | ||||
|       "secretStore": "LocalSecretsFile", | ||||
|       "containerName": "rabbitmq", | ||||
|       "containerImage": "rabbitmq:3-management-alpine", | ||||
|       "type": "rabbitmq.container", | ||||
|       "connectionId": "eventbus", | ||||
|       "dynamicId": null | ||||
|     }, | ||||
|     "redis1": { | ||||
|       "serviceConnectorResourceId": "", | ||||
|       "containerPorts": "6379:6379", | ||||
|       "secretStore": "LocalSecretsFile", | ||||
|       "containerName": "basket-redis", | ||||
|       "containerImage": "redis:alpine", | ||||
|       "type": "redis.container", | ||||
|       "connectionId": "ConnectionStrings:Redis", | ||||
|       "dynamicId": null | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; | ||||
| namespace Basket.API.Repositories; | ||||
| 
 | ||||
| public class RedisBasketRepository : IBasketRepository | ||||
| { | ||||
| @ -6,9 +6,9 @@ public class RedisBasketRepository : IBasketRepository | ||||
|     private readonly ConnectionMultiplexer _redis; | ||||
|     private readonly IDatabase _database; | ||||
| 
 | ||||
|     public RedisBasketRepository(ILoggerFactory loggerFactory, ConnectionMultiplexer redis) | ||||
|     public RedisBasketRepository(ILogger<RedisBasketRepository> logger, ConnectionMultiplexer redis) | ||||
|     { | ||||
|         _logger = loggerFactory.CreateLogger<RedisBasketRepository>(); | ||||
|         _logger = logger; | ||||
|         _redis = redis; | ||||
|         _database = redis.GetDatabase(); | ||||
|     } | ||||
| @ -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", | ||||
|   "AzureServiceBusEnabled": false, | ||||
|   "EventBusConnection": "localhost" | ||||
|  | ||||
| @ -1,30 +1,48 @@ | ||||
| { | ||||
|   "Serilog": { | ||||
|     "SeqServerUrl": null, | ||||
|     "LogstashgUrl": null, | ||||
|     "MinimumLevel": { | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Override": { | ||||
|         "Microsoft": "Warning", | ||||
|         "Microsoft.eShopOnContainers": "Information", | ||||
|         "System": "Warning" | ||||
|       } | ||||
|       "Microsoft.AspNetCore": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "Kestrel": { | ||||
|     "EndpointDefaults": { | ||||
|       "Protocols": "Http2" | ||||
|     "Endpoints": { | ||||
|       "Http": { | ||||
|         "Url": "http://localhost:5221" | ||||
|       }, | ||||
|       "gRPC": { | ||||
|         "Url": "http://localhost:6221", | ||||
|         "Protocols": "Http2" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "SubscriptionClientName": "Basket", | ||||
|   "ApplicationInsights": { | ||||
|     "InstrumentationKey": "" | ||||
|   "OpenApi": { | ||||
|     "Endpoint": { | ||||
|       "Name": "Basket.API V1" | ||||
|     }, | ||||
|     "Document": { | ||||
|       "Description": "The Basket Service HTTP API", | ||||
|       "Title": "eShopOnContainers - Basket HTTP API", | ||||
|       "Version": "v1" | ||||
|     }, | ||||
|     "Auth": { | ||||
|       "ClientId": "basketswaggerui", | ||||
|       "AppName": "Basket Swagger UI" | ||||
|     } | ||||
|   }, | ||||
|   "EventBusRetryCount": 5, | ||||
|   "UseVault": false, | ||||
|   "Vault": { | ||||
|     "Name": "eshop", | ||||
|     "ClientId": "your-client-id", | ||||
|     "ClientSecret": "your-client-secret" | ||||
|   "ConnectionStrings": { | ||||
|     "Redis": "localhost", | ||||
|     "EventBus": "localhost" | ||||
|   }, | ||||
|   "Identity": { | ||||
|     "Audience": "basket", | ||||
|     "Url": "http://localhost:5223", | ||||
|     "Scopes": { | ||||
|       "basket": "Basket API" | ||||
|     } | ||||
|   }, | ||||
|   "EventBus": { | ||||
|     "SubscriptionClientName": "Basket", | ||||
|     "RetryCount": 5 | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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("unique_name", IDENTITY_ID)); | ||||
|         identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); | ||||
|         identity.AddClaim(new Claim("scope", "basket")); | ||||
| 
 | ||||
|         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 | ||||
| { | ||||
| @ -6,18 +9,8 @@ public class BasketScenarioBase | ||||
| 
 | ||||
|     public TestServer CreateServer() | ||||
|     { | ||||
|         var path = Assembly.GetAssembly(typeof(BasketScenarioBase)) | ||||
|             .Location; | ||||
| 
 | ||||
|         var hostBuilder = new WebHostBuilder() | ||||
|             .UseContentRoot(Path.GetDirectoryName(path)) | ||||
|             .ConfigureAppConfiguration(cb => | ||||
|             { | ||||
|                 cb.AddJsonFile("appsettings.json", optional: false) | ||||
|                 .AddEnvironmentVariables(); | ||||
|             }); | ||||
| 
 | ||||
|         return new TestServer(hostBuilder); | ||||
|         var factory = new BasketApplication(); | ||||
|         return factory.Server; | ||||
|     } | ||||
| 
 | ||||
|     public static class Get | ||||
| @ -26,6 +19,11 @@ public class BasketScenarioBase | ||||
|         { | ||||
|             return $"{ApiUrlBase}/{id}"; | ||||
|         } | ||||
| 
 | ||||
|         public static string GetBasketByCustomer(string customerId) | ||||
|         { | ||||
|             return $"{ApiUrlBase}/{customerId}"; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class Post | ||||
| @ -33,4 +31,37 @@ public class BasketScenarioBase | ||||
|         public static string Basket = $"{ApiUrlBase}/"; | ||||
|         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> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <None Remove="appsettings.json" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <Content Include="appsettings.json"> | ||||
|     <Content Include="appsettings.Basket.json"> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </Content> | ||||
|   </ItemGroup> | ||||
|  | ||||
| @ -1,16 +1,15 @@ | ||||
| namespace Basket.FunctionalTests; | ||||
| 
 | ||||
| public class BasketScenarios | ||||
|     : BasketScenarioBase | ||||
| public class BasketScenarios :  | ||||
|     BasketScenarioBase | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task Post_basket_and_response_ok_status_code() | ||||
|     { | ||||
|         using var server = CreateServer(); | ||||
|         var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); | ||||
|         var response = await server.CreateClient() | ||||
|             .PostAsync(Post.Basket, content); | ||||
| 
 | ||||
|         var uri = "/api/v1/basket/"; | ||||
|         var response = await server.CreateClient().PostAsync(uri, content); | ||||
|         response.EnsureSuccessStatusCode(); | ||||
|     } | ||||
| 
 | ||||
| @ -20,7 +19,6 @@ public class BasketScenarios | ||||
|         using var server = CreateServer(); | ||||
|         var response = await server.CreateClient() | ||||
|             .GetAsync(Get.GetBasket(1)); | ||||
| 
 | ||||
|         response.EnsureSuccessStatusCode(); | ||||
|     } | ||||
| 
 | ||||
| @ -33,9 +31,12 @@ public class BasketScenarios | ||||
|         await server.CreateClient() | ||||
|             .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); | ||||
| 
 | ||||
|         response.EnsureSuccessStatusCode(); | ||||
|  | ||||
| @ -1,23 +1,19 @@ | ||||
| global using Basket.FunctionalTests.Base; | ||||
| global using System; | ||||
| global using System.Collections.Generic; | ||||
| global using System.IO; | ||||
| global using System.Net.Http; | ||||
| global using System.Security.Claims; | ||||
| global using System.Text; | ||||
| global using System.Text.Json; | ||||
| global using System.Threading.Tasks; | ||||
| global using Basket.FunctionalTests.Base; | ||||
| global using Microsoft.AspNetCore.Builder; | ||||
| global using Microsoft.AspNetCore.Hosting; | ||||
| global using Microsoft.AspNetCore.Http; | ||||
| global using Microsoft.AspNetCore.Routing; | ||||
| global using Microsoft.AspNetCore.TestHost; | ||||
| global using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; | ||||
| global using Microsoft.eShopOnContainers.Services.Basket.API.Model; | ||||
| global using Microsoft.eShopOnContainers.Services.Basket.API; | ||||
| global using Microsoft.Extensions.Configuration; | ||||
| global using Microsoft.Extensions.DependencyInjection; | ||||
| global using Microsoft.Extensions.Logging; | ||||
| global using StackExchange.Redis; | ||||
| global using System.Collections.Generic; | ||||
| global using System.IO; | ||||
| global using System.Net.Http; | ||||
| global using System.Reflection; | ||||
| global using System.Security.Claims; | ||||
| global using System.Text.Json; | ||||
| global using System.Text; | ||||
| global using System.Threading.Tasks; | ||||
| global using System; | ||||
| global using Xunit; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
|  | ||||
| using Basket.API.Repositories; | ||||
| 
 | ||||
| namespace Basket.FunctionalTests | ||||
| { | ||||
| @ -9,8 +9,8 @@ namespace Basket.FunctionalTests | ||||
|         [Fact] | ||||
|         public async Task UpdateBasket_return_and_add_basket() | ||||
|         { | ||||
|             using var server = CreateServer(); | ||||
|             var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>(); | ||||
|             var server = CreateServer(); | ||||
|             var redis = server.Services.GetRequiredService<ConnectionMultiplexer>(); | ||||
| 
 | ||||
|             var redisBasketRepository = BuildBasketRepository(redis); | ||||
| 
 | ||||
| @ -22,16 +22,13 @@ namespace Basket.FunctionalTests | ||||
| 
 | ||||
|             Assert.NotNull(basket); | ||||
|             Assert.Single(basket.Items); | ||||
| 
 | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Delete_Basket_return_null() | ||||
|         { | ||||
| 
 | ||||
|             using var server = CreateServer(); | ||||
|             var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>(); | ||||
|             var server = CreateServer(); | ||||
|             var redis = server.Services.GetRequiredService<ConnectionMultiplexer>(); | ||||
| 
 | ||||
|             var redisBasketRepository = BuildBasketRepository(redis); | ||||
| 
 | ||||
| @ -52,7 +49,7 @@ namespace Basket.FunctionalTests | ||||
|         RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux) | ||||
|         { | ||||
|             var loggerFactory = new LoggerFactory(); | ||||
|             return new RedisBasketRepository(loggerFactory, connMux); | ||||
|             return new RedisBasketRepository(loggerFactory.CreateLogger<RedisBasketRepository>(), connMux); | ||||
|         } | ||||
| 
 | ||||
|         List<BasketItem> BuildBasketItems() | ||||
|  | ||||
| @ -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 | ||||
|         var fakeCatalogItem = GetFakeCatalogItem(); | ||||
| 
 | ||||
|         _basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny<ApplicationUser>(), It.IsAny<Int32>())) | ||||
|         _basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny<ApplicationUser>(), It.IsAny<int>())) | ||||
|             .Returns(Task.FromResult(1)); | ||||
| 
 | ||||
|         //Act | ||||
|  | ||||
							
								
								
									
										42
									
								
								src/Services/Catalog/Catalog.API/Apis/PicApi.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/Services/Catalog/Catalog.API/Apis/PicApi.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| using Microsoft.AspNetCore.Routing; | ||||
| 
 | ||||
| namespace Catalog.API.Apis; | ||||
| 
 | ||||
| public static class PicApi | ||||
| { | ||||
|     public static IEndpointConventionBuilder MapPicApi(this IEndpointRouteBuilder routes) | ||||
|     { | ||||
|         return routes.MapGet("api/v1/catalog/items/{catalogItemId:int}/pic", | ||||
|             async (int catalogItemId, CatalogContext db, IWebHostEnvironment environment) => | ||||
|         { | ||||
|             var item = await db.CatalogItems.FindAsync(catalogItemId); | ||||
| 
 | ||||
|             if (item is null) | ||||
|             { | ||||
|                 return Results.NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var path = Path.Combine(environment.ContentRootPath, "Pics", item.PictureFileName); | ||||
| 
 | ||||
|             string imageFileExtension = Path.GetExtension(item.PictureFileName); | ||||
|             string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); | ||||
| 
 | ||||
|             return Results.File(path, mimetype); | ||||
|         }) | ||||
|         .WithTags("Pic") | ||||
|         .Produces(404); | ||||
| 
 | ||||
|         static string GetImageMimeTypeFromImageFileExtension(string extension) => extension switch | ||||
|         { | ||||
|             ".png" => "image/png", | ||||
|             ".gif" => "image/gif", | ||||
|             ".jpg" or ".jpeg" => "image/jpeg", | ||||
|             ".bmp" => "image/bmp", | ||||
|             ".tiff" => "image/tiff", | ||||
|             ".wmf" => "image/wmf", | ||||
|             ".jp2" => "image/jp2", | ||||
|             ".svg" => "image/svg+xml", | ||||
|             _ => "application/octet-stream", | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @ -2,22 +2,15 @@ | ||||
| 
 | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net7.0</TargetFramework> | ||||
|     <DebugType>portable</DebugType> | ||||
|     <PreserveCompilationContext>true</PreserveCompilationContext> | ||||
|     <AssemblyName>Catalog.API</AssemblyName> | ||||
|     <PackageId>Catalog.API</PackageId> | ||||
|     <UserSecretsId>aspnet-Catalog.API-20161122013618</UserSecretsId> | ||||
|     <DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | ||||
|     <GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> | ||||
|     <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled> | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <Content Update="appsettings.json"> | ||||
|       <CopyToOutputDirectory>Always</CopyToOutputDirectory> | ||||
|     </Content> | ||||
|     <Content Update="wwwroot;"> | ||||
|       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </Content> | ||||
|     <Content Include="Pics\**\*;"> | ||||
|       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | ||||
| @ -25,49 +18,23 @@ | ||||
|     <Content Include="Setup\**\*;"> | ||||
|       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | ||||
|     </Content> | ||||
|     <Content Remove="Setup\Catalogitems - Copy.zip" /> | ||||
|     <None Remove="Setup\Catalogitems - Copy.zip" /> | ||||
|     <Compile Include="IntegrationEvents\EventHandling\AnyFutureIntegrationEventHandler.cs.txt" /> | ||||
|     <Content Update="web.config;"> | ||||
|       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | ||||
|     </Content> | ||||
|     <None Include="IntegrationEvents\EventHandling\AnyFutureIntegrationEventHandler.cs.txt" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <Protobuf Include="Proto\catalog.proto" GrpcServices="Server" Generator="MSBuild:Compile" /> | ||||
|     <Content Include="@(Protobuf)" /> | ||||
|     <None Remove="@(Protobuf)" /> | ||||
|     <Protobuf Include="Proto\catalog.proto" GrpcServices="Server" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <InternalsVisibleTo Include="Catalog.FunctionalTests" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="System.IdentityModel.Tokens.Jwt" /> | ||||
|     <PackageReference Include="Azure.Identity" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.AzureStorage" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.SqlServer" /> | ||||
|     <PackageReference Include="AspNetCore.HealthChecks.UI.Client" /> | ||||
|     <PackageReference Include="Autofac.Extensions.DependencyInjection" /> | ||||
|     <PackageReference Include="Google.Protobuf" /> | ||||
|     <PackageReference Include="Grpc.AspNetCore.Server" /> | ||||
|     <PackageReference Include="Grpc.Tools" PrivateAssets="All" /> | ||||
|     <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" /> | ||||
|     <PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" /> | ||||
|     <PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.HealthChecks" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyModel" /> | ||||
|     <PackageReference Include="Grpc.AspNetCore" /> | ||||
|     <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" /> | ||||
|     <PackageReference Include="Serilog.AspNetCore" /> | ||||
|     <PackageReference Include="Serilog.Enrichers.Environment" /> | ||||
|     <PackageReference Include="Serilog.Settings.Configuration" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Http" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Seq" /> | ||||
|     <PackageReference Include="Swashbuckle.AspNetCore" /> | ||||
|     <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" /> | ||||
|     <PackageReference Include="System.Data.SqlClient" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Tools"> | ||||
| @ -77,19 +44,7 @@ | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" /> | ||||
|     <ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" /> | ||||
|     <ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" /> | ||||
|     <ProjectReference Include="..\..\..\BuildingBlocks\EventBus\IntegrationEventLogEF\IntegrationEventLogEF.csproj" /> | ||||
|     <ProjectReference Include="..\..\Services.Common\Services.Common.csproj" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <None Update="Pics\*"> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </None> | ||||
|     <None Update="Setup\*"> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </None> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
| </Project> | ||||
|  | ||||
| @ -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.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" | ||||
| COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" | ||||
| COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" | ||||
| COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" | ||||
| COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" | ||||
| COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" | ||||
|  | ||||
							
								
								
									
										92
									
								
								src/Services/Catalog/Catalog.API/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/Services/Catalog/Catalog.API/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| 
 | ||||
| public static class Extensions | ||||
| { | ||||
|     public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         var hcBuilder = services.AddHealthChecks(); | ||||
| 
 | ||||
|         hcBuilder | ||||
|             .AddSqlServer(_ => configuration.GetRequiredConnectionString("CatalogDB"), | ||||
|                 name: "CatalogDB-check", | ||||
|                 tags: new string[] { "ready" }); | ||||
| 
 | ||||
|         var accountName = configuration["AzureStorageAccountName"]; | ||||
|         var accountKey = configuration["AzureStorageAccountKey"]; | ||||
| 
 | ||||
|         if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey)) | ||||
|         { | ||||
|             hcBuilder | ||||
|                 .AddAzureBlobStorage( | ||||
|                     $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", | ||||
|                     name: "catalog-storage-check", | ||||
|                     tags: new string[] { "ready" }); | ||||
|         } | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddDbContexts(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         static void ConfigureSqlOptions(SqlServerDbContextOptionsBuilder sqlOptions) | ||||
|         { | ||||
|             sqlOptions.MigrationsAssembly(typeof(Program).Assembly.FullName); | ||||
| 
 | ||||
|             // Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency  | ||||
| 
 | ||||
|             sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); | ||||
|         }; | ||||
| 
 | ||||
|         services.AddDbContext<CatalogContext>(options => | ||||
|         { | ||||
|             var connectionString = configuration.GetRequiredConnectionString("CatalogDB"); | ||||
| 
 | ||||
|             options.UseSqlServer(connectionString, ConfigureSqlOptions); | ||||
|         }); | ||||
| 
 | ||||
|         services.AddDbContext<IntegrationEventLogContext>(options => | ||||
|         { | ||||
|             var connectionString = configuration.GetRequiredConnectionString("CatalogDB"); | ||||
| 
 | ||||
|             options.UseSqlServer(connectionString, ConfigureSqlOptions); | ||||
|         }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddApplicationOptions(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.Configure<CatalogSettings>(configuration); | ||||
| 
 | ||||
|         // TODO: Move to the new problem details middleware | ||||
|         services.Configure<ApiBehaviorOptions>(options => | ||||
|         { | ||||
|             options.InvalidModelStateResponseFactory = context => | ||||
|             { | ||||
|                 var problemDetails = new ValidationProblemDetails(context.ModelState) | ||||
|                 { | ||||
|                     Instance = context.HttpContext.Request.Path, | ||||
|                     Status = StatusCodes.Status400BadRequest, | ||||
|                     Detail = "Please refer to the errors property for additional details." | ||||
|                 }; | ||||
| 
 | ||||
|                 return new BadRequestObjectResult(problemDetails) | ||||
|                 { | ||||
|                     ContentTypes = { "application/problem+json", "application/problem+xml" } | ||||
|                 }; | ||||
|             }; | ||||
|         }); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static IServiceCollection AddIntegrationServices(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>( | ||||
|             sp => (DbConnection c) => new IntegrationEventLogService(c)); | ||||
| 
 | ||||
|         services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
| @ -13,7 +13,7 @@ public static class LinqSelectExtensions | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 returnedValue = new SelectTryResult<TSource, TResult>(element, default(TResult), ex); | ||||
|                 returnedValue = new SelectTryResult<TSource, TResult>(element, default, ex); | ||||
|             } | ||||
|             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 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; | ||||
| global using System.Collections.Generic; | ||||
| global using System.Data.Common; | ||||
| global using System.Data.SqlClient; | ||||
| global using System.Globalization; | ||||
| global using System.IO.Compression; | ||||
| global using System.IO; | ||||
| global using System.IO.Compression; | ||||
| global using System.Linq; | ||||
| global using System.Net; | ||||
| global using System.Text.RegularExpressions; | ||||
| global using System.Threading.Tasks; | ||||
| global using System; | ||||
| global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters; | ||||
| global using HealthChecks.UI.Client; | ||||
| global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; | ||||
| global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | ||||
| global using Grpc.Core; | ||||
| global using Microsoft.AspNetCore.Builder; | ||||
| global using Microsoft.AspNetCore.Hosting; | ||||
| global using Microsoft.AspNetCore.Http; | ||||
| global using Microsoft.AspNetCore.Mvc; | ||||
| global using Microsoft.EntityFrameworkCore; | ||||
| 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.Extensions.Diagnostics.HealthChecks; | ||||
| global using Microsoft.OpenApi.Models; | ||||
| global using RabbitMQ.Client; | ||||
| global using System.Reflection; | ||||
| global using Microsoft.Extensions.FileProviders; | ||||
| 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.Extensions.Configuration; | ||||
| 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) | ||||
|         { | ||||
|             logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | ||||
|             logger.LogError(ex, "Error reading CSV headers"); | ||||
|             return GetPreconfiguredCatalogBrands(); | ||||
|         } | ||||
| 
 | ||||
|         return File.ReadAllLines(csvFileCatalogBrands) | ||||
|                                     .Skip(1) // skip header row | ||||
|                                     .SelectTry(x => CreateCatalogBrand(x)) | ||||
|                                     .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) | ||||
|                                     .SelectTry(CreateCatalogBrand) | ||||
|                                     .OnCaughtException(ex => { logger.LogError(ex, "Error creating brand while seeding database"); return null; }) | ||||
|                                     .Where(x => x != null); | ||||
|     } | ||||
| 
 | ||||
| @ -75,9 +75,9 @@ public class CatalogContextSeed | ||||
|     { | ||||
|         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 | ||||
| @ -115,14 +115,14 @@ public class CatalogContextSeed | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | ||||
|             logger.LogError(ex, "Error reading CSV headers"); | ||||
|             return GetPreconfiguredCatalogTypes(); | ||||
|         } | ||||
| 
 | ||||
|         return File.ReadAllLines(csvFileCatalogTypes) | ||||
|                                     .Skip(1) // skip header row | ||||
|                                     .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); | ||||
|     } | ||||
| 
 | ||||
| @ -130,7 +130,7 @@ public class CatalogContextSeed | ||||
|     { | ||||
|         type = type.Trim('"').Trim(); | ||||
| 
 | ||||
|         if (String.IsNullOrEmpty(type)) | ||||
|         if (string.IsNullOrEmpty(type)) | ||||
|         { | ||||
|             throw new Exception("catalog Type Name is empty"); | ||||
|         } | ||||
| @ -170,7 +170,7 @@ public class CatalogContextSeed | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); | ||||
|             logger.LogError(ex, "Error reading CSV headers"); | ||||
|             return GetPreconfiguredItems(); | ||||
|         } | ||||
| 
 | ||||
| @ -181,11 +181,11 @@ public class CatalogContextSeed | ||||
|                     .Skip(1) // skip header row | ||||
|                     .Select(row => Regex.Split(row, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) | ||||
|                     .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); | ||||
|     } | ||||
| 
 | ||||
|     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()) | ||||
|         { | ||||
| @ -205,7 +205,7 @@ public class CatalogContextSeed | ||||
|         } | ||||
| 
 | ||||
|         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"); | ||||
|         } | ||||
| @ -224,7 +224,7 @@ public class CatalogContextSeed | ||||
|         if (availableStockIndex != -1) | ||||
|         { | ||||
|             string availableStockString = column[availableStockIndex].Trim('"').Trim(); | ||||
|             if (!String.IsNullOrEmpty(availableStockString)) | ||||
|             if (!string.IsNullOrEmpty(availableStockString)) | ||||
|             { | ||||
|                 if (int.TryParse(availableStockString, out int availableStock)) | ||||
|                 { | ||||
| @ -241,7 +241,7 @@ public class CatalogContextSeed | ||||
|         if (restockThresholdIndex != -1) | ||||
|         { | ||||
|             string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); | ||||
|             if (!String.IsNullOrEmpty(restockThresholdString)) | ||||
|             if (!string.IsNullOrEmpty(restockThresholdString)) | ||||
|             { | ||||
|                 if (int.TryParse(restockThresholdString, out int restockThreshold)) | ||||
|                 { | ||||
| @ -258,7 +258,7 @@ public class CatalogContextSeed | ||||
|         if (maxStockThresholdIndex != -1) | ||||
|         { | ||||
|             string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); | ||||
|             if (!String.IsNullOrEmpty(maxStockThresholdString)) | ||||
|             if (!string.IsNullOrEmpty(maxStockThresholdString)) | ||||
|             { | ||||
|                 if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) | ||||
|                 { | ||||
| @ -275,7 +275,7 @@ public class CatalogContextSeed | ||||
|         if (onReorderIndex != -1) | ||||
|         { | ||||
|             string onReorderString = column[onReorderIndex].Trim('"').Trim(); | ||||
|             if (!String.IsNullOrEmpty(onReorderString)) | ||||
|             if (!string.IsNullOrEmpty(onReorderString)) | ||||
|             { | ||||
|                 if (bool.TryParse(onReorderString, out bool onReorder)) | ||||
|                 { | ||||
| @ -361,7 +361,7 @@ public class CatalogContextSeed | ||||
|                 sleepDurationProvider: retry => TimeSpan.FromSeconds(5), | ||||
|                 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 | ||||
|         { | ||||
|             _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); | ||||
|             _eventBus.Publish(evt); | ||||
| @ -34,14 +34,14 @@ public class CatalogIntegrationEventService : ICatalogIntegrationEventService, I | ||||
|         } | ||||
|         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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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(): | ||||
|         //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) | ||||
|     { | ||||
|         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>(); | ||||
| 
 | ||||
|  | ||||
| @ -16,9 +16,9 @@ public class OrderStatusChangedToPaidIntegrationEventHandler : | ||||
| 
 | ||||
|     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 | ||||
|             foreach (var orderStockItem in @event.OrderStockItems) | ||||
|  | ||||
| @ -1,388 +1,43 @@ | ||||
| var builder = WebApplication.CreateBuilder(new WebApplicationOptions | ||||
| { | ||||
|     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; | ||||
|     }); | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
| 
 | ||||
| }); | ||||
| builder.Services.AddAppInsight(builder.Configuration); | ||||
| builder.Services.AddGrpc().Services | ||||
|     .AddCustomMVC(builder.Configuration) | ||||
|     .AddCustomDbContext(builder.Configuration) | ||||
|     .AddCustomOptions(builder.Configuration) | ||||
|     .AddCustomHealthCheck(builder.Configuration) | ||||
|     .AddIntegrationServices(builder.Configuration) | ||||
|     .AddEventBus(builder.Configuration) | ||||
|     .AddSwagger(builder.Configuration); | ||||
| builder.AddServiceDefaults(); | ||||
| 
 | ||||
| builder.Services.AddGrpc(); | ||||
| builder.Services.AddControllers(); | ||||
| 
 | ||||
| // Application specific services | ||||
| builder.Services.AddHealthChecks(builder.Configuration); | ||||
| builder.Services.AddDbContexts(builder.Configuration); | ||||
| builder.Services.AddApplicationOptions(builder.Configuration); | ||||
| builder.Services.AddIntegrationServices(); | ||||
| 
 | ||||
| builder.Services.AddTransient<OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | ||||
| builder.Services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>(); | ||||
| 
 | ||||
| var app = builder.Build(); | ||||
| 
 | ||||
| if (app.Environment.IsDevelopment()) | ||||
| { | ||||
|     app.UseDeveloperExceptionPage(); | ||||
| } | ||||
| else | ||||
| { | ||||
|     app.UseExceptionHandler("/Home/Error"); | ||||
| } | ||||
| app.UseServiceDefaults(); | ||||
| 
 | ||||
| var pathBase = app.Configuration["PATH_BASE"]; | ||||
| 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.MapPicApi(); | ||||
| 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.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 env = app.Services.GetService<IWebHostEnvironment>(); | ||||
|     var settings = app.Services.GetService<IOptions<CatalogSettings>>(); | ||||
|     var logger = app.Services.GetService<ILogger<CatalogContextSeed>>(); | ||||
|     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>(); | ||||
|     await integEventContext.Database.MigrateAsync(); | ||||
|     app.Logger.LogInformation("Starting web host ({ApplicationName})...", AppName); | ||||
|     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; | ||||
|     } | ||||
| } | ||||
| await app.RunAsync(); | ||||
|  | ||||
| @ -1,29 +1,9 @@ | ||||
| { | ||||
|   "iisSettings": { | ||||
|     "windowsAuthentication": false, | ||||
|     "anonymousAuthentication": true, | ||||
|     "iisExpress": { | ||||
|       "applicationUrl": "http://localhost:57424/", | ||||
|       "sslPort": 0 | ||||
|     } | ||||
|   }, | ||||
|   "profiles": { | ||||
|     "IIS Express": { | ||||
|       "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": { | ||||
|     "Catalog.API": { | ||||
|       "commandName": "Project", | ||||
|       "launchBrowser": true, | ||||
|       "launchUrl": "http://localhost:55101/", | ||||
|       "applicationUrl": "http://localhost:5222/", | ||||
|       "environmentVariables": { | ||||
|         "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