diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index 9c1c0a3b8..563431322 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -24,258 +24,278 @@ using WebMVC.Services; namespace Microsoft.eShopOnContainers.WebMVC { - 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 IoC container. - public void ConfigureServices(IServiceCollection services) - { - services.AddAppInsight(Configuration) - .AddHealthChecks(Configuration) - .AddCustomMvc(Configuration) - .AddHttpClientServices(Configuration) - //.AddHttpClientLogging(Configuration) //Opt-in HttpClientLogging config - .AddCustomAuthentication(Configuration); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) - { - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - - loggerFactory.AddAzureWebAppDiagnostics(); - loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - } - - var pathBase = Configuration["PATH_BASE"]; - if (!string.IsNullOrEmpty(pathBase)) - { - loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); - app.UsePathBase(pathBase); - } + 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 IoC container. + public void ConfigureServices(IServiceCollection services) + { + services.Configure(opts => + { + opts.MinimumSameSitePolicy = SameSiteMode.None; + opts.CheckConsentNeeded = context => true; + }); + services + .AddAppInsight(Configuration) + .AddHealthChecks(Configuration) + .AddCustomMvc(Configuration) + .AddHttpClientServices(Configuration) + //.AddHttpClientLogging(Configuration) //Opt-in HttpClientLogging config + .AddCustomAuthentication(Configuration); + services.AddHttpsRedirection(options => + { + options.HttpsPort = 4100; + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + + loggerFactory.AddAzureWebAppDiagnostics(); + loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseCookiePolicy(); + + string pathBase = Configuration["PATH_BASE"]; + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); + app.UsePathBase(pathBase); + } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); + app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - app.UseSession(); - app.UseStaticFiles(); - - if (Configuration.GetValue("UseLoadTest")) - { - app.UseMiddleware(); - } - - app.UseAuthentication(); - - var log = loggerFactory.CreateLogger("identity"); - - WebContextSeed.Seed(app, env, loggerFactory); - - app.UseMvc(routes => - { - routes.MapRoute( - name: "default", - template: "{controller=Catalog}/{action=Index}/{id?}"); - - routes.MapRoute( - name: "defaultError", - template: "{controller=Error}/{action=Error}"); - }); - } - } - - static class ServiceCollectionExtensions - { - - public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) - { - services.AddApplicationInsightsTelemetry(configuration); - var orchestratorType = configuration.GetValue("OrchestratorType"); - - if (orchestratorType?.ToUpper() == "K8S") - { - // Enable K8s telemetry initializer - services.EnableKubernetes(); - } - - if (orchestratorType?.ToUpper() == "SF") - { - // Enable SF telemetry initializer - services.AddSingleton((serviceProvider) => - new FabricTelemetryInitializer()); - } - - return services; - } - - public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - services.AddHealthChecks(checks => - { - var minutes = 1; - if (int.TryParse(configuration["HealthCheck:Timeout"], out var minutesParsed)) - { - minutes = minutesParsed; - } - - checks.AddUrlCheck(configuration["CatalogUrlHC"], TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(configuration["OrderingUrlHC"], TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(configuration["BasketUrlHC"], TimeSpan.Zero); //No cache for this HealthCheck, better just for demos - checks.AddUrlCheck(configuration["IdentityUrlHC"], TimeSpan.FromMinutes(minutes)); - checks.AddUrlCheck(configuration["MarketingUrlHC"], TimeSpan.FromMinutes(minutes)); - }); - - return services; - } - - public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) - { - services.AddOptions(); - services.Configure(configuration); - - services.AddMvc(); - - services.AddSession(); - - if (configuration.GetValue("IsClusterEnv") == bool.TrueString) - { - services.AddDataProtection(opts => - { - opts.ApplicationDiscriminator = "eshop.webmvc"; - }) - .PersistKeysToRedis(ConnectionMultiplexer.Connect(configuration["DPConnectionString"]), "DataProtection-Keys"); - } - return services; - } - - // Adds all Http client services (like Service-Agents) using resilient Http requests based on HttpClient factory and Polly's policies - public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration) - { - services.AddSingleton(); - - //register delegating handlers - services.AddTransient(); - services.AddTransient(); - - //set 5 min as the lifetime for each HttpMessageHandler int the pool - services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(TimeSpan.FromMinutes(5)); - - //add http client services - services.AddHttpClient() - .SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Sample. Default lifetime is 2 minutes - .AddHttpMessageHandler() - .AddPolicyHandler(GetRetryPolicy()) - .AddPolicyHandler(GetCircuitBreakerPolicy()); - - services.AddHttpClient() - .AddPolicyHandler(GetRetryPolicy()) - .AddPolicyHandler(GetCircuitBreakerPolicy()); - - services.AddHttpClient() - .AddHttpMessageHandler() - .AddHttpMessageHandler() - .AddPolicyHandler(GetRetryPolicy()) - .AddPolicyHandler(GetCircuitBreakerPolicy()); - - services.AddHttpClient() - .AddHttpMessageHandler() - .AddPolicyHandler(GetRetryPolicy()) - .AddPolicyHandler(GetCircuitBreakerPolicy()); - - services.AddHttpClient() - .AddHttpMessageHandler() - .AddPolicyHandler(GetRetryPolicy()) - .AddPolicyHandler(GetCircuitBreakerPolicy()); - - //add custom application services - services.AddTransient, IdentityParser>(); - - return services; - } - - public static IServiceCollection AddHttpClientLogging(this IServiceCollection services, IConfiguration configuration) - { - services.AddLogging(b => - { - b.AddFilter((category, level) => true); // Spam the world with logs. - - // Add console logger so we can see all the logging produced by the client by default. - b.AddConsole(c => c.IncludeScopes = true); - - // Add console logger - b.AddDebug(); - }); - - return services; - } - - public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) - { - var useLoadTest = configuration.GetValue("UseLoadTest"); - var identityUrl = configuration.GetValue("IdentityUrl"); - var callBackUrl = configuration.GetValue("CallBackUrl"); - - // Add Authentication services - - services.AddAuthentication(options => - { - options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; - }) - .AddCookie() - .AddOpenIdConnect(options => - { - options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.Authority = identityUrl.ToString(); - options.SignedOutRedirectUri = callBackUrl.ToString(); - options.ClientId = useLoadTest ? "mvctest" : "mvc"; - options.ClientSecret = "secret"; - options.ResponseType = useLoadTest ? "code id_token token" : "code id_token"; - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true; - options.RequireHttpsMetadata = false; - options.Scope.Add("openid"); - options.Scope.Add("profile"); - options.Scope.Add("orders"); - options.Scope.Add("basket"); - options.Scope.Add("marketing"); - options.Scope.Add("locations"); - options.Scope.Add("webshoppingagg"); - options.Scope.Add("orders.signalrhub"); - }); - - return services; - } - - static IAsyncPolicy GetRetryPolicy() - { - return HttpPolicyExtensions - .HandleTransientHttpError() - .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound) - .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); - - } - static IAsyncPolicy GetCircuitBreakerPolicy() - { - return HttpPolicyExtensions - .HandleTransientHttpError() - .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)); - } - } + app.UseSession(); + app.UseStaticFiles(); + + if (Configuration.GetValue("UseLoadTest")) + { + app.UseMiddleware(); + } + + app.UseAuthentication(); + + ILogger log = loggerFactory.CreateLogger("identity"); + + WebContextSeed.Seed(app, env, loggerFactory); + + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller=Catalog}/{action=Index}/{id?}"); + + routes.MapRoute( + name: "defaultError", + template: "{controller=Error}/{action=Error}"); + }); + } + } + + static class ServiceCollectionExtensions + { + + public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) + { + services.AddApplicationInsightsTelemetry(configuration); + string orchestratorType = configuration.GetValue("OrchestratorType"); + + if (orchestratorType?.ToUpper() == "K8S") + { + // Enable K8s telemetry initializer + services.EnableKubernetes(); + } + + if (orchestratorType?.ToUpper() == "SF") + { + // Enable SF telemetry initializer + services.AddSingleton((serviceProvider) => + new FabricTelemetryInitializer()); + } + + return services; + } + + public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + { + services.AddHealthChecks(checks => + { + int minutes = 1; + if (int.TryParse(configuration["HealthCheck:Timeout"], out int minutesParsed)) + { + minutes = minutesParsed; + } + + checks.AddUrlCheck(configuration["CatalogUrlHC"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(configuration["OrderingUrlHC"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(configuration["BasketUrlHC"], TimeSpan.Zero); //No cache for this HealthCheck, better just for demos + checks.AddUrlCheck(configuration["IdentityUrlHC"], TimeSpan.FromMinutes(minutes)); + checks.AddUrlCheck(configuration["MarketingUrlHC"], TimeSpan.FromMinutes(minutes)); + }); + + return services; + } + + public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions(); + services.Configure(configuration); + + services + .AddMvc(opts => + { + opts.SslPort = 4100; + opts.RequireHttpsPermanent = true; + }) + .SetCompatibilityVersion(AspNetCore.Mvc.CompatibilityVersion.Version_2_1); + + services.AddSession(); + + if (configuration.GetValue("IsClusterEnv") == bool.TrueString) + { + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webmvc"; + }) + .PersistKeysToRedis(ConnectionMultiplexer.Connect(configuration["DPConnectionString"]), "DataProtection-Keys"); + } + return services; + } + + // Adds all Http client services (like Service-Agents) using resilient Http requests based on HttpClient factory and Polly's policies + public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(); + + //register delegating handlers + services.AddTransient(); + services.AddTransient(); + + //set 5 min as the lifetime for each HttpMessageHandler int the pool + services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(TimeSpan.FromMinutes(5)); + + //add http client services + services.AddHttpClient() + .SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Sample. Default lifetime is 2 minutes + .AddHttpMessageHandler() + .AddPolicyHandler(GetRetryPolicy()) + .AddPolicyHandler(GetCircuitBreakerPolicy()); + + services.AddHttpClient() + .AddPolicyHandler(GetRetryPolicy()) + .AddPolicyHandler(GetCircuitBreakerPolicy()); + + services.AddHttpClient() + .AddHttpMessageHandler() + .AddHttpMessageHandler() + .AddPolicyHandler(GetRetryPolicy()) + .AddPolicyHandler(GetCircuitBreakerPolicy()); + + services.AddHttpClient() + .AddHttpMessageHandler() + .AddPolicyHandler(GetRetryPolicy()) + .AddPolicyHandler(GetCircuitBreakerPolicy()); + + services.AddHttpClient() + .AddHttpMessageHandler() + .AddPolicyHandler(GetRetryPolicy()) + .AddPolicyHandler(GetCircuitBreakerPolicy()); + + //add custom application services + services.AddTransient, IdentityParser>(); + + return services; + } + + public static IServiceCollection AddHttpClientLogging(this IServiceCollection services, IConfiguration configuration) + { + services.AddLogging(b => + { + b.AddFilter((category, level) => true); // Spam the world with logs. + + // Add console logger so we can see all the logging produced by the client by default. + b.AddConsole(c => c.IncludeScopes = true); + + // Add console logger + b.AddDebug(); + }); + + return services; + } + + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + bool useLoadTest = configuration.GetValue("UseLoadTest"); + string identityUrl = configuration.GetValue("IdentityUrl"); + string callBackUrl = configuration.GetValue("CallBackUrl"); + + // Add Authentication services + + services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(options => + { + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.Authority = identityUrl.ToString(); + options.SignedOutRedirectUri = callBackUrl.ToString(); + options.ClientId = useLoadTest ? "mvctest" : "mvc"; + options.ClientSecret = "secret"; + options.ResponseType = useLoadTest ? "code id_token token" : "code id_token"; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.RequireHttpsMetadata = false; + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("orders"); + options.Scope.Add("basket"); + options.Scope.Add("marketing"); + options.Scope.Add("locations"); + options.Scope.Add("webshoppingagg"); + options.Scope.Add("orders.signalrhub"); + }); + + return services; + } + + static IAsyncPolicy GetRetryPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound) + .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); + + } + static IAsyncPolicy GetCircuitBreakerPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)); + } + } }