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 | 255 +++++++++++------- src/Web/WebMVC/WebMVC.csproj | 1 + 6 files changed, 289 insertions(+), 140 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,104 +38,11 @@ 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.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); - - 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)); - }); - - // Add application services. - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient, IdentityParser>(); - - if (Configuration.GetValue("UseResilientHttp") == bool.TrueString) - { - services.AddSingleton(sp => - { - var logger = sp.GetRequiredService>(); - var httpContextAccessor = sp.GetRequiredService(); - - var retryCount = 6; - if (!string.IsNullOrEmpty(Configuration["HttpClientRetryCount"])) - { - retryCount = int.Parse(Configuration["HttpClientRetryCount"]); - } - - var exceptionsAllowedBeforeBreaking = 5; - if (!string.IsNullOrEmpty(Configuration["HttpClientExceptionsAllowedBeforeBreaking"])) - { - exceptionsAllowedBeforeBreaking = int.Parse(Configuration["HttpClientExceptionsAllowedBeforeBreaking"]); - } - - return new ResilientHttpClientFactory(logger, httpContextAccessor, exceptionsAllowedBeforeBreaking, retryCount); - }); - services.AddSingleton(sp => sp.GetService().CreateResilientHttpClient()); - } - else - { - services.AddSingleton(); - } - 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"); - }); + services.AddAppInsight(Configuration) + .AddHealthChecks(Configuration) + .AddCustomMvc(Configuration) + .AddCustomApplicationServices(Configuration) + .AddCustomAuthentication(Configuration); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -140,8 +50,6 @@ namespace Microsoft.eShopOnContainers.WebMVC { JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); loggerFactory.AddAzureWebAppDiagnostics(); loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); @@ -173,7 +81,7 @@ namespace Microsoft.eShopOnContainers.WebMVC { app.UseMiddleware(); } - + app.UseAuthentication(); var log = loggerFactory.CreateLogger("identity"); @@ -191,23 +99,162 @@ namespace Microsoft.eShopOnContainers.WebMVC template: "{controller=Error}/{action=Error}"); }); } + } - private void RegisterAppInsights(IServiceCollection services) + static class ServiceCollectionExtensions + { + + public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) { - services.AddApplicationInsightsTelemetry(Configuration); - var orchestratorType = Configuration.GetValue("OrchestratorType"); + 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.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, IdentityParser>(); + + if (configuration.GetValue("UseResilientHttp") == bool.TrueString) + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var httpContextAccessor = sp.GetRequiredService(); + + var retryCount = 6; + if (!string.IsNullOrEmpty(configuration["HttpClientRetryCount"])) + { + retryCount = int.Parse(configuration["HttpClientRetryCount"]); + } + + var exceptionsAllowedBeforeBreaking = 5; + if (!string.IsNullOrEmpty(configuration["HttpClientExceptionsAllowedBeforeBreaking"])) + { + exceptionsAllowedBeforeBreaking = int.Parse(configuration["HttpClientExceptionsAllowedBeforeBreaking"]); + } + + return new ResilientHttpClientFactory(logger, httpContextAccessor, exceptionsAllowedBeforeBreaking, retryCount); + }); + services.AddSingleton(sp => sp.GetService().CreateResilientHttpClient()); + } + else + { + services.AddSingleton(); + } + + 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; + } + } + } 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 @@ +