@ -24,258 +24,278 @@ using WebMVC.Services;
namespace Microsoft.eShopOnContainers.WebMVC
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 < CookiePolicyOptions > ( 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 = 4 1 0 0 ;
} ) ;
}
// 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
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
app . Map ( "/liveness" , lapp = > lapp . Run ( async ctx = > ctx . Response . StatusCode = 2 0 0 ) ) ;
app . Map ( "/liveness" , lapp = > lapp . Run ( async ctx = > ctx . Response . StatusCode = 2 0 0 ) ) ;
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
app . UseSession ( ) ;
app . UseStaticFiles ( ) ;
if ( Configuration . GetValue < bool > ( "UseLoadTest" ) )
{
app . UseMiddleware < ByPassAuthMiddleware > ( ) ;
}
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 < string > ( "OrchestratorType" ) ;
if ( orchestratorType ? . ToUpper ( ) = = "K8S" )
{
// Enable K8s telemetry initializer
services . EnableKubernetes ( ) ;
}
if ( orchestratorType ? . ToUpper ( ) = = "SF" )
{
// Enable SF telemetry initializer
services . AddSingleton < ITelemetryInitializer > ( ( 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 < AppSettings > ( configuration ) ;
services . AddMvc ( ) ;
services . AddSession ( ) ;
if ( configuration . GetValue < string > ( "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 < IHttpContextAccessor , HttpContextAccessor > ( ) ;
//register delegating handlers
services . AddTransient < HttpClientAuthorizationDelegatingHandler > ( ) ;
services . AddTransient < HttpClientRequestIdDelegatingHandler > ( ) ;
//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 < IBasketService , BasketService > ( )
. SetHandlerLifetime ( TimeSpan . FromMinutes ( 5 ) ) //Sample. Default lifetime is 2 minutes
. AddHttpMessageHandler < HttpClientAuthorizationDelegatingHandler > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
services . AddHttpClient < ICatalogService , CatalogService > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
services . AddHttpClient < IOrderingService , OrderingService > ( )
. AddHttpMessageHandler < HttpClientAuthorizationDelegatingHandler > ( )
. AddHttpMessageHandler < HttpClientRequestIdDelegatingHandler > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
services . AddHttpClient < ICampaignService , CampaignService > ( )
. AddHttpMessageHandler < HttpClientAuthorizationDelegatingHandler > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
services . AddHttpClient < ILocationService , LocationService > ( )
. AddHttpMessageHandler < HttpClientAuthorizationDelegatingHandler > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
//add custom application services
services . AddTransient < IIdentityParser < ApplicationUser > , 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 < bool > ( "UseLoadTest" ) ;
var identityUrl = configuration . GetValue < string > ( "IdentityUrl" ) ;
var callBackUrl = configuration . GetValue < string > ( "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 < HttpResponseMessage > GetRetryPolicy ( )
{
return HttpPolicyExtensions
. HandleTransientHttpError ( )
. OrResult ( msg = > msg . StatusCode = = System . Net . HttpStatusCode . NotFound )
. WaitAndRetryAsync ( 6 , retryAttempt = > TimeSpan . FromSeconds ( Math . Pow ( 2 , retryAttempt ) ) ) ;
}
static IAsyncPolicy < HttpResponseMessage > GetCircuitBreakerPolicy ( )
{
return HttpPolicyExtensions
. HandleTransientHttpError ( )
. CircuitBreakerAsync ( 5 , TimeSpan . FromSeconds ( 3 0 ) ) ;
}
}
app . UseSession ( ) ;
app . UseStaticFiles ( ) ;
if ( Configuration . GetValue < bool > ( "UseLoadTest" ) )
{
app . UseMiddleware < ByPassAuthMiddleware > ( ) ;
}
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 < string > ( "OrchestratorType" ) ;
if ( orchestratorType ? . ToUpper ( ) = = "K8S" )
{
// Enable K8s telemetry initializer
services . EnableKubernetes ( ) ;
}
if ( orchestratorType ? . ToUpper ( ) = = "SF" )
{
// Enable SF telemetry initializer
services . AddSingleton < ITelemetryInitializer > ( ( 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 < AppSettings > ( configuration ) ;
services
. AddMvc ( opts = >
{
opts . SslPort = 4 1 0 0 ;
opts . RequireHttpsPermanent = true ;
} )
. SetCompatibilityVersion ( AspNetCore . Mvc . CompatibilityVersion . Version_2_1 ) ;
services . AddSession ( ) ;
if ( configuration . GetValue < string > ( "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 < IHttpContextAccessor , HttpContextAccessor > ( ) ;
//register delegating handlers
services . AddTransient < HttpClientAuthorizationDelegatingHandler > ( ) ;
services . AddTransient < HttpClientRequestIdDelegatingHandler > ( ) ;
//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 < IBasketService , BasketService > ( )
. SetHandlerLifetime ( TimeSpan . FromMinutes ( 5 ) ) //Sample. Default lifetime is 2 minutes
. AddHttpMessageHandler < HttpClientAuthorizationDelegatingHandler > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
services . AddHttpClient < ICatalogService , CatalogService > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
services . AddHttpClient < IOrderingService , OrderingService > ( )
. AddHttpMessageHandler < HttpClientAuthorizationDelegatingHandler > ( )
. AddHttpMessageHandler < HttpClientRequestIdDelegatingHandler > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
services . AddHttpClient < ICampaignService , CampaignService > ( )
. AddHttpMessageHandler < HttpClientAuthorizationDelegatingHandler > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
services . AddHttpClient < ILocationService , LocationService > ( )
. AddHttpMessageHandler < HttpClientAuthorizationDelegatingHandler > ( )
. AddPolicyHandler ( GetRetryPolicy ( ) )
. AddPolicyHandler ( GetCircuitBreakerPolicy ( ) ) ;
//add custom application services
services . AddTransient < IIdentityParser < ApplicationUser > , 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 < bool > ( "UseLoadTest" ) ;
string identityUrl = configuration . GetValue < string > ( "IdentityUrl" ) ;
string callBackUrl = configuration . GetValue < string > ( "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 < HttpResponseMessage > GetRetryPolicy ( )
{
return HttpPolicyExtensions
. HandleTransientHttpError ( )
. OrResult ( msg = > msg . StatusCode = = System . Net . HttpStatusCode . NotFound )
. WaitAndRetryAsync ( 6 , retryAttempt = > TimeSpan . FromSeconds ( Math . Pow ( 2 , retryAttempt ) ) ) ;
}
static IAsyncPolicy < HttpResponseMessage > GetCircuitBreakerPolicy ( )
{
return HttpPolicyExtensions
. HandleTransientHttpError ( )
. CircuitBreakerAsync ( 5 , TimeSpan . FromSeconds ( 3 0 ) ) ;
}
}
}
}