From 19217e093935d840a16651786f11a5bfc4109431 Mon Sep 17 00:00:00 2001 From: Tarun Jain Date: Mon, 27 Mar 2023 23:56:55 +0530 Subject: [PATCH] Basket.API: Commit to migrate to WebApplication Builder --- src/Services/Basket/Basket.API/Program.cs | 318 ++++++++++++++---- src/Services/Basket/Basket.API/Startup.cs | 287 ---------------- .../Base/BasketScenarioBase.cs | 2 +- .../Base/BasketTestStartup.cs | 31 -- 4 files changed, 253 insertions(+), 385 deletions(-) delete mode 100644 src/Services/Basket/Basket.API/Startup.cs delete mode 100644 src/Services/Basket/Basket.FunctionalTests/Base/BasketTestStartup.cs diff --git a/src/Services/Basket/Basket.API/Program.cs b/src/Services/Basket/Basket.API/Program.cs index e7208476d..b99dfa82d 100644 --- a/src/Services/Basket/Basket.API/Program.cs +++ b/src/Services/Basket/Basket.API/Program.cs @@ -1,57 +1,221 @@ -var configuration = GetConfiguration(); +using Autofac.Core; +using Microsoft.Azure.Amqp.Framing; +using Microsoft.Extensions.Configuration; -Log.Logger = CreateSerilogLogger(configuration); +var appName = "Basket.API"; +var builder = WebApplication.CreateBuilder(new WebApplicationOptions { + Args = args, + ApplicationName = typeof(Program).Assembly.FullName, + ContentRootPath = Directory.GetCurrentDirectory() +}); +if (builder.Configuration.GetValue("UseVault", false)) { + TokenCredential credential = new ClientSecretCredential( + builder.Configuration["Vault:TenantId"], + builder.Configuration["Vault:ClientId"], + builder.Configuration["Vault:ClientSecret"]); + builder.Configuration.AddAzureKeyVault(new Uri($"https://{builder.Configuration["Vault:Name"]}.vault.azure.net/"), credential); +} + +builder.Services.AddGrpc(options => { + 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)); + +}) // 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" + }); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() { + Implicit = new OpenApiOAuthFlow() { + AuthorizationUrl = new Uri($"{builder.Configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), + TokenUrl = new Uri($"{builder.Configuration.GetValue("IdentityUrlExternal")}/connect/token"), + Scopes = new Dictionary() + { + { "basket", "Basket API" } + } + } + } + }); + + options.OperationFilter(); +}); + +// prevent from mapping "sub" claim to nameidentifier. +JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + +var identityUrl = builder.Configuration.GetValue("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(builder.Configuration); + +builder.Services.AddSingleton(sp => { + var settings = sp.GetRequiredService>().Value; + var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true); + + return ConnectionMultiplexer.Connect(configuration); +}); + + +if (builder.Configuration.GetValue("AzureServiceBusEnabled")) { + builder.Services.AddSingleton(sp => { + var serviceBusConnectionString = builder.Configuration["EventBusConnection"]; + + return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); + }); +} +else { + builder.Services.AddSingleton(sp => { + var logger = sp.GetRequiredService>(); + + 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(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +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(); -try -{ + +app.MapGrpcService(); +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 host = BuildWebHost(configuration, args); + Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); - host.Run(); + await app.RunAsync(); return 0; } -catch (Exception ex) -{ +catch (Exception ex) { Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); return 1; } -finally -{ +finally { Log.CloseAndFlush(); } -IWebHost BuildWebHost(IConfiguration configuration, string[] args) => - WebHost.CreateDefaultBuilder(args) - .CaptureStartupErrors(false) - .ConfigureKestrel(options => - { - var ports = GetDefinedPorts(configuration); - options.Listen(IPAddress.Any, ports.httpPort, listenOptions => - { - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); - - options.Listen(IPAddress.Any, ports.grpcPort, listenOptions => - { - listenOptions.Protocols = HttpProtocols.Http2; - }); - - }) - .ConfigureAppConfiguration(x => x.AddConfiguration(configuration)) - .UseFailing(options => - { - options.ConfigPath = "/Failing"; - options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" }); - }) - .UseStartup() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseSerilog() - .Build(); - -Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) -{ +Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) { var seqServerUrl = configuration["Serilog:SeqServerUrl"]; var logstashUrl = configuration["Serilog:LogstashgUrl"]; return new LoggerConfiguration() @@ -65,37 +229,59 @@ Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) .CreateLogger(); } -IConfiguration GetConfiguration() -{ - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddEnvironmentVariables(); - - var config = builder.Build(); - - if (config.GetValue("UseVault", false)) - { - TokenCredential credential = new ClientSecretCredential( - config["Vault:TenantId"], - config["Vault:ClientId"], - config["Vault:ClientSecret"]); - builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential); - } - - return builder.Build(); -} - -(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) -{ +(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(); -public partial class Program -{ + eventBus.Subscribe(); + eventBus.Subscribe(); +} +public partial class Program { - public static string Namespace = typeof(Startup).Namespace; + 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("AzureServiceBusEnabled")) { + services.AddSingleton(sp => { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubscriptionsManager = sp.GetRequiredService(); + string subscriptionName = configuration["SubscriptionClientName"]; + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubscriptionsManager, sp, subscriptionName); + }); + } + else { + services.AddSingleton(sp => { + var subscriptionClientName = configuration["SubscriptionClientName"]; + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubscriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubscriptionsManager, subscriptionClientName, retryCount); + }); + } + + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); + return services; + } +} diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs deleted file mode 100644 index 8c2631e5c..000000000 --- a/src/Services/Basket/Basket.API/Startup.cs +++ /dev/null @@ -1,287 +0,0 @@ -namespace Microsoft.eShopOnContainers.Services.Basket.API; -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 virtual IServiceProvider ConfigureServices(IServiceCollection services) - { - services.AddGrpc(options => - { - options.EnableDetailedErrors = true; - }); - - RegisterAppInsights(services); - - services.AddControllers(options => - { - options.Filters.Add(typeof(HttpGlobalExceptionFilter)); - options.Filters.Add(typeof(ValidateModelStateFilter)); - - }) // Added for functional tests - .AddApplicationPart(typeof(BasketController).Assembly) - .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); - - 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(); - }); - - ConfigureAuthService(services); - - services.AddCustomHealthCheck(Configuration); - - services.Configure(Configuration); - - //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. - services.AddSingleton(sp => - { - var settings = sp.GetRequiredService>().Value; - var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true); - - return ConnectionMultiplexer.Connect(configuration); - }); - - - if (Configuration.GetValue("AzureServiceBusEnabled")) - { - services.AddSingleton(sp => - { - var serviceBusConnectionString = Configuration["EventBusConnection"]; - - return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); - }); - } - else - { - 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); - }); - } - - RegisterEventBus(services); - - - services.AddCors(options => - { - options.AddPolicy("CorsPolicy", - builder => builder - .SetIsOriginAllowed((host) => true) - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials()); - }); - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - - services.AddOptions(); - - var container = new ContainerBuilder(); - container.Populate(services); - - return new AutofacServiceProvider(container.Build()); - } - - // 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) - { - //loggerFactory.AddAzureWebAppDiagnostics(); - //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); - - var pathBase = 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"); - ConfigureAuth(app); - - app.UseStaticFiles(); - - 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") - }); - }); - - ConfigureEventBus(app); - } - - private void RegisterAppInsights(IServiceCollection services) - { - services.AddApplicationInsightsTelemetry(Configuration); - services.AddApplicationInsightsKubernetesEnricher(); - } - - private void ConfigureAuthService(IServiceCollection services) - { - // prevent from mapping "sub" claim to nameidentifier. - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); - - var identityUrl = Configuration.GetValue("IdentityUrl"); - - services.AddAuthentication("Bearer").AddJwtBearer(options => - { - options.Authority = identityUrl; - options.RequireHttpsMetadata = false; - options.Audience = "basket"; - options.TokenValidationParameters.ValidateAudience = false; - }); - services.AddAuthorization(options => - { - options.AddPolicy("ApiScope", policy => - { - policy.RequireAuthenticatedUser(); - policy.RequireClaim("scope", "basket"); - }); - }); - } - - protected virtual void ConfigureAuth(IApplicationBuilder app) - { - app.UseAuthentication(); - app.UseAuthorization(); - } - - private void RegisterEventBus(IServiceCollection services) - { - if (Configuration.GetValue("AzureServiceBusEnabled")) - { - services.AddSingleton(sp => - { - var serviceBusPersisterConnection = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubscriptionsManager = sp.GetRequiredService(); - string subscriptionName = Configuration["SubscriptionClientName"]; - - return new EventBusServiceBus(serviceBusPersisterConnection, logger, - eventBusSubscriptionsManager, sp, subscriptionName); - }); - } - else - { - services.AddSingleton(sp => - { - var subscriptionClientName = Configuration["SubscriptionClientName"]; - var rabbitMQPersistentConnection = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubscriptionsManager = sp.GetRequiredService(); - - var retryCount = 5; - if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"])) - { - retryCount = int.Parse(Configuration["EventBusRetryCount"]); - } - - return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubscriptionsManager, subscriptionClientName, retryCount); - }); - } - - services.AddSingleton(); - - services.AddTransient(); - services.AddTransient(); - } - - private void ConfigureEventBus(IApplicationBuilder app) - { - var eventBus = app.ApplicationServices.GetRequiredService(); - - eventBus.Subscribe(); - eventBus.Subscribe(); - } -} \ No newline at end of file diff --git a/src/Services/Basket/Basket.FunctionalTests/Base/BasketScenarioBase.cs b/src/Services/Basket/Basket.FunctionalTests/Base/BasketScenarioBase.cs index b50b58bbd..5fe08bb58 100644 --- a/src/Services/Basket/Basket.FunctionalTests/Base/BasketScenarioBase.cs +++ b/src/Services/Basket/Basket.FunctionalTests/Base/BasketScenarioBase.cs @@ -15,7 +15,7 @@ public class BasketScenarioBase { cb.AddJsonFile("appsettings.json", optional: false) .AddEnvironmentVariables(); - }).UseStartup(); + }); return new TestServer(hostBuilder); } diff --git a/src/Services/Basket/Basket.FunctionalTests/Base/BasketTestStartup.cs b/src/Services/Basket/Basket.FunctionalTests/Base/BasketTestStartup.cs deleted file mode 100644 index b19d825cd..000000000 --- a/src/Services/Basket/Basket.FunctionalTests/Base/BasketTestStartup.cs +++ /dev/null @@ -1,31 +0,0 @@ - - -namespace Basket.FunctionalTests.Base -{ - class BasketTestsStartup : Startup - { - public BasketTestsStartup(IConfiguration env) : base(env) - { - } - - public override IServiceProvider ConfigureServices(IServiceCollection services) - { - // Added to avoid the Authorize data annotation in test environment. - // Property "SuppressCheckForUnhandledSecurityMetadata" in appsettings.json - services.Configure(Configuration); - return base.ConfigureServices(services); - } - - protected override void ConfigureAuth(IApplicationBuilder app) - { - if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) - { - app.UseMiddleware(); - } - else - { - base.ConfigureAuth(app); - } - } - } -}