From cf4b7ff47ffa239df0b59ee0741e30424ae8aea0 Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Castro Date: Wed, 16 May 2018 12:44:32 +0200 Subject: [PATCH] Added sample for use new IHttpClientFactory feature on WebMVC --- src/Web/WebMVC/Controllers/CartController.cs | 1 - ...ttpClientAuthorizationDelegatingHandler.cs | 49 ++++ .../HttpClientDefaultPolicies.cs | 60 +++++ src/Web/WebMVC/Services/BasketService.cs | 63 ++--- src/Web/WebMVC/Startup.cs | 253 +++++++++++------- src/Web/WebMVC/WebMVC.csproj | 1 + 6 files changed, 288 insertions(+), 139 deletions(-) create mode 100644 src/Web/WebMVC/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs create mode 100644 src/Web/WebMVC/Infrastructure/HttpClientDefaultPolicies.cs diff --git a/src/Web/WebMVC/Controllers/CartController.cs b/src/Web/WebMVC/Controllers/CartController.cs index 660da1d56..30ac77e8b 100644 --- a/src/Web/WebMVC/Controllers/CartController.cs +++ b/src/Web/WebMVC/Controllers/CartController.cs @@ -71,7 +71,6 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers { var user = _appUserParser.Parse(HttpContext.User); await _basketSvc.AddItemToBasket(user, productDetails.Id); - //await _basketSvc.AddItemToBasket(user, product); } return RedirectToAction("Index", "Catalog"); } diff --git a/src/Web/WebMVC/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs b/src/Web/WebMVC/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs new file mode 100644 index 000000000..255bd1649 --- /dev/null +++ b/src/Web/WebMVC/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace WebMVC.Infrastructure +{ + public class HttpClientAuthorizationDelegatingHandler + : DelegatingHandler + { + private readonly IHttpContextAccessor _httpContextAccesor; + + public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccesor) + { + _httpContextAccesor = httpContextAccesor; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var authorizationHeader = _httpContextAccesor.HttpContext + .Request.Headers["Authorization"]; + + if (!string.IsNullOrEmpty(authorizationHeader)) + { + request.Headers.Add("Authorization", new List() { authorizationHeader }); + } + + var token = await GetToken(); + + if (token != null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + + return await base.SendAsync(request, cancellationToken); + } + + async Task GetToken() + { + const string ACCESS_TOKEN = "access_token"; + + return await _httpContextAccesor.HttpContext + .GetTokenAsync(ACCESS_TOKEN); + } + } +} diff --git a/src/Web/WebMVC/Infrastructure/HttpClientDefaultPolicies.cs b/src/Web/WebMVC/Infrastructure/HttpClientDefaultPolicies.cs new file mode 100644 index 000000000..2340febd9 --- /dev/null +++ b/src/Web/WebMVC/Infrastructure/HttpClientDefaultPolicies.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Logging; +using Polly; +using System; +using System.Net.Http; + +namespace WebMVC.Infrastructure +{ + public class HttpClientDefaultPolicies + { + const int RETRY_COUNT = 6; + const int EXCEPTIONS_ALLOWED_BEFORE_CIRCUIT_BREAKER = 5; + + private readonly ILogger _logger; + + public HttpClientDefaultPolicies(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Policy GetWaitAndRetryPolicy() + { + return Policy.Handle() + .WaitAndRetryAsync( + // number of retries + RETRY_COUNT, + // exponential backofff + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + // on retry + (exception, timeSpan, retryCount, context) => + { + var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " + + $"of {context.PolicyKey} " + + $"at {context.OperationKey}, " + + $"due to: {exception}."; + _logger.LogWarning(msg); + _logger.LogDebug(msg); + }); + } + + public Policy GetCircuitBreakerPolicy() + { + return Policy.Handle() + .CircuitBreakerAsync( + // number of exceptions before breaking circuit + EXCEPTIONS_ALLOWED_BEFORE_CIRCUIT_BREAKER, + // time circuit opened before retry + TimeSpan.FromMinutes(1), + (exception, duration) => + { + // on circuit opened + _logger.LogTrace("Circuit breaker opened"); + }, + () => + { + // on circuit closed + _logger.LogTrace("Circuit breaker reset"); + }); + } + } +} diff --git a/src/Web/WebMVC/Services/BasketService.cs b/src/Web/WebMVC/Services/BasketService.cs index 54287a796..4a0719f1b 100644 --- a/src/Web/WebMVC/Services/BasketService.cs +++ b/src/Web/WebMVC/Services/BasketService.cs @@ -1,11 +1,9 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http; -using Microsoft.eShopOnContainers.WebMVC.ViewModels; +using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Microsoft.Extensions.Options; using Newtonsoft.Json; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using WebMVC.Infrastructure; using WebMVC.Models; @@ -15,29 +13,26 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public class BasketService : IBasketService { private readonly IOptionsSnapshot _settings; - private readonly IHttpClient _apiClient; + private readonly HttpClient _apiClient; private readonly string _basketByPassUrl; private readonly string _purchaseUrl; - private readonly IHttpContextAccessor _httpContextAccesor; private readonly string _bffUrl; - public BasketService(IOptionsSnapshot settings, - IHttpContextAccessor httpContextAccesor, IHttpClient httpClient) + public BasketService(HttpClient httpClient,IOptionsSnapshot settings) { + _apiClient = httpClient; _settings = settings; + _basketByPassUrl = $"{_settings.Value.PurchaseUrl}/api/v1/b/basket"; _purchaseUrl = $"{_settings.Value.PurchaseUrl}/api/v1"; - _httpContextAccesor = httpContextAccesor; - _apiClient = httpClient; } public async Task GetBasket(ApplicationUser user) { - var token = await GetUserTokenAsync(); var getBasketUri = API.Basket.GetBasket(_basketByPassUrl, user.Id); - var dataString = await _apiClient.GetStringAsync(getBasketUri, token); + var dataString = await _apiClient.GetStringAsync(getBasketUri); return string.IsNullOrEmpty(dataString) ? new Basket() { BuyerId = user.Id} : @@ -46,10 +41,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task UpdateBasket(Basket basket) { - var token = await GetUserTokenAsync(); var updateBasketUri = API.Basket.UpdateBasket(_basketByPassUrl); + var content = new StringContent(JsonConvert.SerializeObject(basket), System.Text.Encoding.UTF8, "application/json"); - var response = await _apiClient.PostAsync(updateBasketUri, basket, token); + var response = await _apiClient.PostAsync(updateBasketUri, content); response.EnsureSuccessStatusCode(); @@ -58,10 +53,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task Checkout(BasketDTO basket) { - var token = await GetUserTokenAsync(); var updateBasketUri = API.Basket.CheckoutBasket(_basketByPassUrl); + var content = new StringContent(JsonConvert.SerializeObject(basket), System.Text.Encoding.UTF8, "application/json"); - var response = await _apiClient.PostAsync(updateBasketUri, basket, token); + var response = await _apiClient.PostAsync(updateBasketUri, content); response.EnsureSuccessStatusCode(); } @@ -69,54 +64,52 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task SetQuantities(ApplicationUser user, Dictionary quantities) { - var token = await GetUserTokenAsync(); var updateBasketUri = API.Purchase.UpdateBasketItem(_purchaseUrl); - var userId = user.Id; - - var response = await _apiClient.PutAsync(updateBasketUri, new + var basketUpdate = new { - BasketId = userId, + BasketId = user.Id, Updates = quantities.Select(kvp => new { BasketItemId = kvp.Key, NewQty = kvp.Value }).ToArray() - }, token); + }; + + var content = new StringContent(JsonConvert.SerializeObject(basketUpdate), System.Text.Encoding.UTF8, "application/json"); + + var response = await _apiClient.PutAsync(updateBasketUri,content); response.EnsureSuccessStatusCode(); + var jsonResponse = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(jsonResponse); } public async Task GetOrderDraft(string basketId) { - var token = await GetUserTokenAsync(); var draftOrderUri = API.Purchase.GetOrderDraft(_purchaseUrl, basketId); - var json = await _apiClient.GetStringAsync(draftOrderUri, token); - return JsonConvert.DeserializeObject(json); + var response = await _apiClient.GetStringAsync(draftOrderUri); + + return JsonConvert.DeserializeObject(response); } public async Task AddItemToBasket(ApplicationUser user, int productId) { - var token = await GetUserTokenAsync(); var updateBasketUri = API.Purchase.AddItemToBasket(_purchaseUrl); - var userId = user.Id; - var response = await _apiClient.PostAsync(updateBasketUri, new + var newItem = new { CatalogItemId = productId, - BasketId = userId, + BasketId = user.Id, Quantity = 1 - }, token); + }; - } + var content = new StringContent(JsonConvert.SerializeObject(newItem), System.Text.Encoding.UTF8, "application/json"); - async Task GetUserTokenAsync() - { - var context = _httpContextAccesor.HttpContext; - return await context.GetTokenAsync("access_token"); + var response = await _apiClient.PostAsync(updateBasketUri, content); } } } diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index 6d26e5790..b8be7a4e5 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -14,9 +14,12 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; +using Polly; +using Polly.Registry; using StackExchange.Redis; using System; using System.IdentityModel.Tokens.Jwt; +using System.Net.Http; using WebMVC.Infrastructure; using WebMVC.Infrastructure.Middlewares; using WebMVC.Services; @@ -35,48 +38,156 @@ namespace Microsoft.eShopOnContainers.WebMVC // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - RegisterAppInsights(services); + services.AddAppInsight(Configuration) + .AddHealthChecks(Configuration) + .AddCustomMvc(Configuration) + .AddCustomApplicationServices(Configuration) + .AddCustomAuthentication(Configuration); + } - services.AddMvc(); + // 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(); - services.AddSession(); + loggerFactory.AddAzureWebAppDiagnostics(); + loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); - if (Configuration.GetValue("IsClusterEnv") == bool.TrueString) + if (env.IsDevelopment()) { - services.AddDataProtection(opts => - { - opts.ApplicationDiscriminator = "eshop.webmvc"; - }) - .PersistKeysToRedis(ConnectionMultiplexer.Connect(Configuration["DPConnectionString"]), "DataProtection-Keys"); + 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); + } + + +#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)); +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + app.UseSession(); + app.UseStaticFiles(); + + if (Configuration.GetValue("UseLoadTest")) + { + app.UseMiddleware(); } - services.Configure(Configuration); + 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)) + 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)); + 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)); }); - // Add application services. + return services; + } + + public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration 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"); + } + + services.Configure(configuration); + + return services; + } + + public static IServiceCollection AddCustomApplicationServices(this IServiceCollection services, IConfiguration configuration) + { services.AddSingleton(); + services.AddSingleton(); + var defaultPolicies = services.BuildServiceProvider().GetService(); + + var registry = services.AddPolicyRegistry(); + registry.Add("WaitAndRetry", defaultPolicies.GetWaitAndRetryPolicy()); + registry.Add("CircuitBreaker", defaultPolicies.GetCircuitBreakerPolicy()); + + services.AddHttpClient() + .AddHttpMessageHandler() + .AddPolicyHandlerFromRegistry("WaitAndRetry") + .AddPolicyHandlerFromRegistry("CircuitBreaker"); + services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient, IdentityParser>(); - if (Configuration.GetValue("UseResilientHttp") == bool.TrueString) + if (configuration.GetValue("UseResilientHttp") == bool.TrueString) { services.AddSingleton(sp => { @@ -84,15 +195,15 @@ namespace Microsoft.eShopOnContainers.WebMVC var httpContextAccessor = sp.GetRequiredService(); var retryCount = 6; - if (!string.IsNullOrEmpty(Configuration["HttpClientRetryCount"])) + if (!string.IsNullOrEmpty(configuration["HttpClientRetryCount"])) { - retryCount = int.Parse(Configuration["HttpClientRetryCount"]); + retryCount = int.Parse(configuration["HttpClientRetryCount"]); } var exceptionsAllowedBeforeBreaking = 5; - if (!string.IsNullOrEmpty(Configuration["HttpClientExceptionsAllowedBeforeBreaking"])) + if (!string.IsNullOrEmpty(configuration["HttpClientExceptionsAllowedBeforeBreaking"])) { - exceptionsAllowedBeforeBreaking = int.Parse(Configuration["HttpClientExceptionsAllowedBeforeBreaking"]); + exceptionsAllowedBeforeBreaking = int.Parse(configuration["HttpClientExceptionsAllowedBeforeBreaking"]); } return new ResilientHttpClientFactory(logger, httpContextAccessor, exceptionsAllowedBeforeBreaking, retryCount); @@ -103,18 +214,25 @@ namespace Microsoft.eShopOnContainers.WebMVC { services.AddSingleton(); } - var useLoadTest = Configuration.GetValue("UseLoadTest"); - var identityUrl = Configuration.GetValue("IdentityUrl"); - var callBackUrl = Configuration.GetValue("CallBackUrl"); - + + 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 => { + + services.AddAuthentication(options => + { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() - .AddOpenIdConnect(options => { + .AddOpenIdConnect(options => + { options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.Authority = identityUrl.ToString(); options.SignedOutRedirectUri = callBackUrl.ToString(); @@ -133,81 +251,10 @@ namespace Microsoft.eShopOnContainers.WebMVC options.Scope.Add("webshoppingagg"); options.Scope.Add("orders.signalrhub"); }); - } - - // 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.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - 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); - } - -#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)); -#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}"); - }); + return services; } - private void RegisterAppInsights(IServiceCollection services) - { - 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()); - } - } } + } diff --git a/src/Web/WebMVC/WebMVC.csproj b/src/Web/WebMVC/WebMVC.csproj index 2c102e98f..771225c06 100644 --- a/src/Web/WebMVC/WebMVC.csproj +++ b/src/Web/WebMVC/WebMVC.csproj @@ -23,6 +23,7 @@ +