diff --git a/src/Services/Basket/Basket.API/Basket.API.csproj b/src/Services/Basket/Basket.API/Basket.API.csproj index 77138f6a9..89f2e52de 100644 --- a/src/Services/Basket/Basket.API/Basket.API.csproj +++ b/src/Services/Basket/Basket.API/Basket.API.csproj @@ -9,6 +9,10 @@ preview + + + + PreserveNewest diff --git a/src/Services/Basket/Basket.API/Program.cs b/src/Services/Basket/Basket.API/Program.cs index ce09dfce1..085f6f9b0 100644 --- a/src/Services/Basket/Basket.API/Program.cs +++ b/src/Services/Basket/Basket.API/Program.cs @@ -1,14 +1,22 @@ var configuration = GetConfiguration(); - Log.Logger = CreateSerilogLogger(configuration); try { Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); - var host = BuildWebHost(configuration, args); + + var builder = WebApplication.CreateBuilder(args); + builder.Configuration.AddConfiguration(configuration); + + ConfigureServices(builder); + BuildWebHost(builder); + + var app = builder.Build(); + ConfigureRequestPipeline(app); Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); - host.Run(); + + app.Run(); return 0; } @@ -22,9 +30,78 @@ finally Log.CloseAndFlush(); } -IWebHost BuildWebHost(IConfiguration configuration, string[] args) => - WebHost.CreateDefaultBuilder(args) - .CaptureStartupErrors(false) +/// +/// / Method to help configure services +/// +void ConfigureServices(WebApplicationBuilder builder) +{ + builder.Services.AddGrpc(options => + { + options.EnableDetailedErrors = true; + }); + + builder.Services.AddApplicationInsightsTelemetry(configuration); + builder.Services.AddApplicationInsightsKubernetesEnricher(); + + builder.Services.AddControllers(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + options.Filters.Add(typeof(ValidateModelStateFilter)); + + }).AddApplicationPart(typeof(BasketController).Assembly) +.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); + + RegisterOpenAPIConfig(builder); + RegisterAuthService(builder); + RegisterHeathCheckConfigs(builder); + RegisterRedisDataStore(builder); + RegisterEventBusConnection(builder); + RegisterEventBus(builder); + + builder.Services.Configure(configuration); + builder.Services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .SetIsOriginAllowed((host) => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddOptions(); + + builder.Services.AddAutofac(container => + { + container.Populate(builder.Services); + }); +} + +/// +/// Method to configure app request pipeline. +/// +/// +void ConfigureRequestPipeline(WebApplication app) +{ + UsePathBase(app); + UseOpenAPI(app); + + app.UseRouting(); + app.UseCors("CorsPolicy"); + + UseConfiguredAuth(app); + + app.UseStaticFiles(); + UseMappedEndpoints(app); + UseEventBus(app); +} + +void BuildWebHost(WebApplicationBuilder builder) +{ + builder.WebHost.CaptureStartupErrors(false) .ConfigureKestrel(options => { var ports = GetDefinedPorts(configuration); @@ -45,10 +122,274 @@ IWebHost BuildWebHost(IConfiguration configuration, string[] args) => options.ConfigPath = "/Failing"; options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" }); }) - .UseStartup() .UseContentRoot(Directory.GetCurrentDirectory()) - .UseSerilog() - .Build(); + .UseSerilog(); +} + +void UseEventBus(IApplicationBuilder app) +{ + var eventBus = app.ApplicationServices.GetRequiredService(); + + eventBus.Subscribe(); + eventBus.Subscribe(); +} + +void UseMappedEndpoints(IApplicationBuilder app) +{ + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapDefaultControllerRoute(); + endpoints.MapControllers(); + //endpoints.MapGet("/_proto/", async ctx => + //{ + // ctx.Response.ContentType = "text/plain"; + // using var fs = new FileStream(Path.Combine(env.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); + // } + // } + //}); + + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + +} + +void UsePathBase(IApplicationBuilder app) +{ + + var pathBase = configuration["PATH_BASE"]; + if (!string.IsNullOrEmpty(pathBase)) + { + app.UsePathBase(pathBase); + } +} + +void UseOpenAPI(IApplicationBuilder app) +{ + var pathBase = configuration["PATH_BASE"]; + + 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"); + }); +} + +void UseConfiguredAuth(IApplicationBuilder app) +{ + app.UseAuthentication(); + app.UseAuthorization(); +} + +void RegisterOpenAPIConfig(WebApplicationBuilder builder) +{ + builder.Services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "eShopOnContainers - Basket HTTP API", + Version = "v1", + Description = "The Basket Service HTTP API" + }); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), + TokenUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/token"), + Scopes = new Dictionary() + { + { "basket", "Basket API" } + } + } + } + }); + + options.OperationFilter(); + }); +} + +void RegisterAuthService(WebApplicationBuilder builder) +{ + // prevent from mapping "sub" claim to nameidentifier. + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + + var identityUrl = configuration.GetValue("IdentityUrl"); + + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + + }).AddJwtBearer(options => + { + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = "basket"; + }); + +} + +void RegisterHeathCheckConfigs(WebApplicationBuilder builder) +{ + var hcBuilder = builder.Services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + hcBuilder + .AddRedis( + configuration["ConnectionString"], + name: "redis-check", + tags: new string[] { "redis" }); + + if (configuration.GetValue("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" }); + } +} + +void RegisterRedisDataStore(WebApplicationBuilder builder) +{ + //By connecting here we are making sure that our service + //cannot start until redis is ready. This might slow down startup, + //but given that there is a delay on resolving the ip address + //and then creating the connection it seems reasonable to move + //that cost to startup instead of having the first request pay the + //penalty. + builder.Services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true); + + configuration.ResolveDns = true; + + return ConnectionMultiplexer.Connect(configuration); + }); + +} + +void RegisterEventBusConnection(WebApplicationBuilder builder) +{ + if (configuration.GetValue("AzureServiceBusEnabled")) + { + builder.Services.AddSingleton(sp => + { + var serviceBusConnectionString = configuration["EventBusConnection"]; + var serviceBusConnection = new ServiceBusConnectionStringBuilder(serviceBusConnectionString); + + var subscriptionClientName = configuration["SubscriptionClientName"]; + return new DefaultServiceBusPersisterConnection(serviceBusConnection, subscriptionClientName); + }); + } + else + { + builder.Services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + 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); + }); + } +} + +void RegisterEventBus(WebApplicationBuilder builder) +{ + if (configuration.GetValue("AzureServiceBusEnabled")) + { + builder.Services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubcriptionsManager, iLifetimeScope); + }); + } + else + { + builder.Services.AddSingleton(sp => + { + var subscriptionClientName = configuration["SubscriptionClientName"]; + var rabbitMQPersistentConnection = sp.GetRequiredService(); + //var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + //return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + // TO DO + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, null, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + }); + } + + builder.Services.AddSingleton(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); +} Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) { @@ -94,8 +435,7 @@ IConfiguration GetConfiguration() } public class Program -{ - - public static string Namespace = typeof(Startup).Namespace; - public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); +{ + public static string Namespace = typeof(Program).Namespace; + public static string AppName = "Basket.API"; } diff --git a/src/Services/Basket/Basket.API/Properties/launchSettings.json b/src/Services/Basket/Basket.API/Properties/launchSettings.json index 60a56b153..e38293e25 100644 --- a/src/Services/Basket/Basket.API/Properties/launchSettings.json +++ b/src/Services/Basket/Basket.API/Properties/launchSettings.json @@ -19,7 +19,7 @@ "Microsoft.eShopOnContainers.Services.Basket.API": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "http://localhost:55103/", + "launchUrl": "http://localhost:5103/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Services/Basket/Basket.API/appsettings.json b/src/Services/Basket/Basket.API/appsettings.json index a5b723116..c2fc32fe5 100644 --- a/src/Services/Basket/Basket.API/appsettings.json +++ b/src/Services/Basket/Basket.API/appsettings.json @@ -26,5 +26,10 @@ "Name": "eshop", "ClientId": "your-clien-id", "ClientSecret": "your-client-secret" - } + }, + "IdentityUrlExternal": "http://localhost:5105", + "IdentityUrl": "http://localhost:5105", + "ConnectionString": "127.0.0.1", + "AzureServiceBusEnabled": false, + "EventBusConnection": "localhost" } \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Properties/launchSettings.json b/src/Tests/Services/Application.FunctionalTests/Properties/launchSettings.json new file mode 100644 index 000000000..33504c948 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "WSL": { + "commandName": "WSL2", + "distributionName": "" + } + } +} \ No newline at end of file