diff --git a/src/Web/WebMVC/Controllers/OrderController.cs b/src/Web/WebMVC/Controllers/OrderController.cs index fc435ef6b..3d9317ae6 100644 --- a/src/Web/WebMVC/Controllers/OrderController.cs +++ b/src/Web/WebMVC/Controllers/OrderController.cs @@ -7,6 +7,7 @@ using Microsoft.eShopOnContainers.WebMVC.Services; using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Microsoft.AspNetCore.Authorization; using System.Net.Http; +using Polly.CircuitBreaker; namespace Microsoft.eShopOnContainers.WebMVC.Controllers { @@ -37,18 +38,24 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers [HttpPost] public async Task Create(Order model, string action) { - if (ModelState.IsValid) + try { - var user = _appUserParser.Parse(HttpContext.User); - await _orderSvc.CreateOrder(model); + if (ModelState.IsValid) + { + var user = _appUserParser.Parse(HttpContext.User); + await _orderSvc.CreateOrder(model); - //Empty basket for current user. - await _basketSvc.CleanBasket(user); + //Empty basket for current user. + await _basketSvc.CleanBasket(user); - //Redirect to historic list. - return RedirectToAction("Index"); + //Redirect to historic list. + return RedirectToAction("Index"); + } + } + catch(BrokenCircuitException ex) + { + ModelState.AddModelError("Error", "Not possible to create a new order, please try later on"); } - return View(model); } diff --git a/src/Web/WebMVC/Services/BasketService.cs b/src/Web/WebMVC/Services/BasketService.cs index c5b1bf0bc..bcb138b87 100644 --- a/src/Web/WebMVC/Services/BasketService.cs +++ b/src/Web/WebMVC/Services/BasketService.cs @@ -1,29 +1,29 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.eShopOnContainers.WebMVC.ViewModels; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -using System.Net.Http; +using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Microsoft.Extensions.Options; using Newtonsoft.Json; -using Microsoft.AspNetCore.Authentication; -using Microsoft.eShopOnContainers.WebMVC.Extensions; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using WebMVC.Services.Utilities; namespace Microsoft.eShopOnContainers.WebMVC.Services { public class BasketService : IBasketService { private readonly IOptionsSnapshot _settings; - private HttpClient _apiClient; + private IHttpClient _apiClient; private readonly string _remoteServiceBaseUrl; private IHttpContextAccessor _httpContextAccesor; - public BasketService(IOptionsSnapshot settings, IHttpContextAccessor httpContextAccesor) + public BasketService(IOptionsSnapshot settings, IHttpContextAccessor httpContextAccesor, IHttpClient httpClient) { _settings = settings; _remoteServiceBaseUrl = _settings.Value.BasketUrl; _httpContextAccesor = httpContextAccesor; + _apiClient = httpClient; } public async Task GetBasket(ApplicationUser user) @@ -31,8 +31,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services var context = _httpContextAccesor.HttpContext; var token = await context.Authentication.GetTokenAsync("access_token"); - _apiClient = new HttpClient(); - _apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var basketUrl = $"{_remoteServiceBaseUrl}/{user.Id.ToString()}"; var dataString = await _apiClient.GetStringAsync(basketUrl); @@ -52,13 +51,12 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services { var context = _httpContextAccesor.HttpContext; var token = await context.Authentication.GetTokenAsync("access_token"); - - _apiClient = new HttpClient(); - _apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + + _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var basketUrl = _remoteServiceBaseUrl; - StringContent content = new StringContent(JsonConvert.SerializeObject(basket), System.Text.Encoding.UTF8, "application/json"); - var response = await _apiClient.PostAsync(basketUrl, content); + + var response = await _apiClient.PostAsync(basketUrl, basket); return basket; } @@ -120,8 +118,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services var context = _httpContextAccesor.HttpContext; var token = await context.Authentication.GetTokenAsync("access_token"); - _apiClient = new HttpClient(); - _apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var basketUrl = $"{_remoteServiceBaseUrl}/{user.Id.ToString()}"; var response = await _apiClient.DeleteAsync(basketUrl); diff --git a/src/Web/WebMVC/Services/CatalogService.cs b/src/Web/WebMVC/Services/CatalogService.cs index 2c2035873..83b139c1f 100644 --- a/src/Web/WebMVC/Services/CatalogService.cs +++ b/src/Web/WebMVC/Services/CatalogService.cs @@ -1,35 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.eShopOnContainers.WebMVC.ViewModels; -using Microsoft.CodeAnalysis.Options; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.Net.Http; -using Microsoft.AspNetCore.Mvc.Rendering; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading.Tasks; +using WebMVC.Services.Utilities; namespace Microsoft.eShopOnContainers.WebMVC.Services { public class CatalogService : ICatalogService { private readonly IOptionsSnapshot _settings; - private HttpClient _apiClient; + private IHttpClient _apiClient; private readonly string _remoteServiceBaseUrl; - public CatalogService(IOptionsSnapshot settings, ILoggerFactory loggerFactory) { + public CatalogService(IOptionsSnapshot settings, ILoggerFactory loggerFactory, IHttpClient httpClient) { _settings = settings; _remoteServiceBaseUrl = $"{_settings.Value.CatalogUrl}/api/v1/catalog/"; - + _apiClient = httpClient; var log = loggerFactory.CreateLogger("catalog service"); log.LogDebug(settings.Value.CatalogUrl); } public async Task GetCatalogItems(int page,int take, int? brand, int? type) { - _apiClient = new HttpClient(); var itemsQs = $"items?pageIndex={page}&pageSize={take}"; var filterQs = ""; @@ -45,16 +41,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services var dataString = ""; // - // Using HttpClient with Retry and Exponential Backoff + // Using a HttpClient wrapper with Retry and Exponential Backoff // - var retry = new RetryWithExponentialBackoff(); - await retry.RunAsync(async () => - { - // work with HttpClient call - dataString = await _apiClient.GetStringAsync(catalogUrl); - }); + dataString = await _apiClient.GetStringAsync(catalogUrl); - //var dataString = await _apiClient.GetStringAsync(catalogUrl); var response = JsonConvert.DeserializeObject(dataString); return response; @@ -62,7 +52,6 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task> GetBrands() { - _apiClient = new HttpClient(); var url = $"{_remoteServiceBaseUrl}catalogBrands"; var dataString = await _apiClient.GetStringAsync(url); @@ -81,7 +70,6 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services public async Task> GetTypes() { - _apiClient = new HttpClient(); var url = $"{_remoteServiceBaseUrl}catalogTypes"; var dataString = await _apiClient.GetStringAsync(url); diff --git a/src/Web/WebMVC/Services/OrderingService.cs b/src/Web/WebMVC/Services/OrderingService.cs index 184592839..d53db3090 100644 --- a/src/Web/WebMVC/Services/OrderingService.cs +++ b/src/Web/WebMVC/Services/OrderingService.cs @@ -8,30 +8,30 @@ using Microsoft.Extensions.Options; using System.Net.Http; using Newtonsoft.Json; using Microsoft.AspNetCore.Authentication; +using WebMVC.Services.Utilities; namespace Microsoft.eShopOnContainers.WebMVC.Services { public class OrderingService : IOrderingService { - private HttpClient _apiClient; + private IHttpClient _apiClient; private readonly string _remoteServiceBaseUrl; private readonly IOptionsSnapshot _settings; private readonly IHttpContextAccessor _httpContextAccesor; - public OrderingService(IOptionsSnapshot settings, IHttpContextAccessor httpContextAccesor) + public OrderingService(IOptionsSnapshot settings, IHttpContextAccessor httpContextAccesor, IHttpClient httpClient) { _remoteServiceBaseUrl = $"{settings.Value.OrderingUrl}/api/v1/orders"; _settings = settings; _httpContextAccesor = httpContextAccesor; + _apiClient = httpClient; } async public Task GetOrder(ApplicationUser user, string Id) { var context = _httpContextAccesor.HttpContext; var token = await context.Authentication.GetTokenAsync("access_token"); - - _apiClient = new HttpClient(); - _apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var ordersUrl = $"{_remoteServiceBaseUrl}/{Id}"; var dataString = await _apiClient.GetStringAsync(ordersUrl); @@ -46,8 +46,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services var context = _httpContextAccesor.HttpContext; var token = await context.Authentication.GetTokenAsync("access_token"); - _apiClient = new HttpClient(); - _apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var ordersUrl = _remoteServiceBaseUrl; var dataString = await _apiClient.GetStringAsync(ordersUrl); @@ -77,17 +76,15 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services var context = _httpContextAccesor.HttpContext; var token = await context.Authentication.GetTokenAsync("access_token"); - _apiClient = new HttpClient(); - _apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); - _apiClient.DefaultRequestHeaders.Add("x-requestid", order.RequestId.ToString()); + _apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + _apiClient.Inst.DefaultRequestHeaders.Add("x-requestid", order.RequestId.ToString()); + var ordersUrl = $"{_remoteServiceBaseUrl}/new"; order.CardTypeId = 1; order.CardExpirationApiFormat(); SetFakeIdToProducts(order); - - StringContent content = new StringContent(JsonConvert.SerializeObject(order), System.Text.Encoding.UTF8, "application/json"); - - var response = await _apiClient.PostAsync(ordersUrl, content); + + var response = await _apiClient.PostAsync(ordersUrl, order); if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) throw new Exception("Error creating order, try later"); diff --git a/src/Web/WebMVC/Services/Utilities/HttpApiClient.cs b/src/Web/WebMVC/Services/Utilities/HttpApiClient.cs new file mode 100644 index 000000000..8b142f393 --- /dev/null +++ b/src/Web/WebMVC/Services/Utilities/HttpApiClient.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace WebMVC.Services.Utilities +{ + public class HttpApiClient : IHttpClient + { + private HttpClient _client; + private ILogger _logger; + public HttpClient Inst => _client; + public HttpApiClient() + { + _client = new HttpClient(); + _logger = new LoggerFactory().CreateLogger(nameof(HttpApiClientWrapper)); + } + + public async Task GetStringAsync(string uri) + { + return await HttpInvoker(async () => + await _client.GetStringAsync(uri)); + } + + public async Task PostAsync(string uri, T item) + { + var contentString = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"); + return await HttpInvoker(async () => + await _client.PostAsync(uri, contentString)); + } + + public async Task DeleteAsync(string uri) + { + return await HttpInvoker(async () => + await _client.DeleteAsync(uri)); + } + + private async Task HttpInvoker(Func> action) + { + return await action(); + } + } +} + diff --git a/src/Web/WebMVC/Services/Utilities/HttpApiClientWrapper.cs b/src/Web/WebMVC/Services/Utilities/HttpApiClientWrapper.cs new file mode 100644 index 000000000..dba0db9df --- /dev/null +++ b/src/Web/WebMVC/Services/Utilities/HttpApiClientWrapper.cs @@ -0,0 +1,99 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Polly; +using Polly.Wrap; +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace WebMVC.Services.Utilities +{ + public class HttpApiClientWrapper : IHttpClient + { + private HttpClient _client; + private PolicyWrap _policyWrapper; + private ILogger _logger; + public HttpClient Inst => _client; + public HttpApiClientWrapper() + { + _client = new HttpClient(); + _logger = new LoggerFactory().CreateLogger(nameof(HttpApiClientWrapper)); + + // Add Policies to be applied + _policyWrapper = Policy.WrapAsync( + CreateRetryPolicy(), + CreateCircuitBreakerPolicy() + ); + } + + private Policy CreateCircuitBreakerPolicy() + { + return Policy + .Handle() + .CircuitBreakerAsync( + // number of exceptions before breaking circuit + 3, + // 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"); + } + ); + } + + private Policy CreateRetryPolicy() + { + return Policy + .Handle() + .WaitAndRetryAsync( + // number of retries + 3, + // exponential backofff + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + // on retry + (exception, timeSpan, retryCount, context) => + { + _logger.LogTrace($"Retry {retryCount} " + + $"of {context.PolicyKey} " + + $"at {context.ExecutionKey}, " + + $"due to: {exception}."); + }); + } + + public async Task GetStringAsync(string uri) + { + return await HttpInvoker(async () => + await _client.GetStringAsync(uri)); + } + + public async Task PostAsync(string uri, T item) + { + // a new StringContent must be created for each retry + // as it is disposed after each call + return await HttpInvoker(async () => + await _client.PostAsync(uri, + new StringContent(JsonConvert.SerializeObject(item), + System.Text.Encoding.UTF8, "application/json"))); + } + + public async Task DeleteAsync(string uri) + { + return await HttpInvoker(async () => + await _client.DeleteAsync(uri)); + } + + private async Task HttpInvoker(Func> action) + { + // Executes the action applying all + // the policies defined in the wrapper + return await _policyWrapper + .ExecuteAsync(async () => await action()); + } + } + +} diff --git a/src/Web/WebMVC/Services/Utilities/IHttpClient.cs b/src/Web/WebMVC/Services/Utilities/IHttpClient.cs new file mode 100644 index 000000000..2a36466bc --- /dev/null +++ b/src/Web/WebMVC/Services/Utilities/IHttpClient.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace WebMVC.Services.Utilities +{ + public interface IHttpClient + { + HttpClient Inst { get; } + Task GetStringAsync(string uri); + Task PostAsync(string uri, T item); + Task DeleteAsync(string uri); + } +} diff --git a/src/Web/WebMVC/Services/Utilities/RetryWithExponentialBackoff.cs b/src/Web/WebMVC/Services/Utilities/RetryWithExponentialBackoff.cs deleted file mode 100644 index b9569c843..000000000 --- a/src/Web/WebMVC/Services/Utilities/RetryWithExponentialBackoff.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.WebMVC.Services -{ - /// - /// When working with cloud services and Docker containers, it's very important to always catch - /// TimeoutException, and retry the operation. - /// RetryWithExponentialBackoff makes it easy to implement such pattern. - /// Usage: - /// var retry = new RetryWithExponentialBackoff(); - /// await retry.RunAsync(async ()=> - /// { - /// // work with HttpClient - /// }); - /// - public sealed class RetryWithExponentialBackoff - { - private readonly int maxRetries, delayMilliseconds, maxDelayMilliseconds; - - public RetryWithExponentialBackoff(int maxRetries = 50, int delayMilliseconds = 200, int maxDelayMilliseconds = 2000) - { - this.maxRetries = maxRetries; - this.delayMilliseconds = delayMilliseconds; - this.maxDelayMilliseconds = maxDelayMilliseconds; - } - - public async Task RunAsync(Func func) - { - ExponentialBackoff backoff = new ExponentialBackoff(this.maxRetries, this.delayMilliseconds, this.maxDelayMilliseconds); - retry: - try - { - await func(); - } - catch (Exception ex) when (ex is TimeoutException || ex is System.Net.Http.HttpRequestException) - { - Debug.WriteLine("Exception raised is: " + ex.GetType().ToString() + " -- Message: " + ex.Message + " -- Inner Message: " + ex.InnerException.Message); - await backoff.Delay(); - goto retry; - } - } - } - - - /// - /// Usage: - /// ExponentialBackoff backoff = new ExponentialBackoff(3, 10, 100); - /// retry: - /// try { - /// // ... - /// } - /// catch (Exception ex) { - /// await backoff.Delay(cancellationToken); - /// goto retry; - /// } - /// - public struct ExponentialBackoff - { - private readonly int m_maxRetries, m_delayMilliseconds, m_maxDelayMilliseconds; - private int m_retries, m_pow; - - public ExponentialBackoff(int maxRetries, int delayMilliseconds, int maxDelayMilliseconds) - { - m_maxRetries = maxRetries; - m_delayMilliseconds = delayMilliseconds; - m_maxDelayMilliseconds = maxDelayMilliseconds; - m_retries = 0; - m_pow = 1; - } - - public Task Delay() - { - if (m_retries == m_maxRetries) - { - throw new TimeoutException("Max retry attempts exceeded."); - } - ++m_retries; - if (m_retries < 31) - { - m_pow = m_pow << 1; // m_pow = Pow(2, m_retries - 1) - } - int delay = Math.Min(m_delayMilliseconds * (m_pow - 1) / 2, m_maxDelayMilliseconds); - return Task.Delay(delay); - } - } -} diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index 8bd7708d3..7888ae253 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -14,6 +14,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.AspNetCore.Http; using System.Threading; using Microsoft.Extensions.Options; +using WebMVC.Services.Utilities; namespace Microsoft.eShopOnContainers.WebMVC { @@ -43,14 +44,22 @@ namespace Microsoft.eShopOnContainers.WebMVC { services.AddMvc(); services.Configure(Configuration); - + // Add application services. - services.AddSingleton(); - + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient, IdentityParser>(); + + if(Configuration.GetValue("ActivateCircuitBreaker") == bool.TrueString) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Web/WebMVC/Views/Order/Create.cshtml b/src/Web/WebMVC/Views/Order/Create.cshtml index 58108b8e9..e05d3a7b7 100644 --- a/src/Web/WebMVC/Views/Order/Create.cshtml +++ b/src/Web/WebMVC/Views/Order/Create.cshtml @@ -11,6 +11,13 @@
+
+ @foreach (var error in ViewData.ModelState.Values.SelectMany(err => err.Errors)) { + + } +

Shipping address

diff --git a/src/Web/WebMVC/WebMVC.csproj b/src/Web/WebMVC/WebMVC.csproj index eb282bca3..e03f5e2c9 100644 --- a/src/Web/WebMVC/WebMVC.csproj +++ b/src/Web/WebMVC/WebMVC.csproj @@ -32,6 +32,7 @@ All + diff --git a/src/Web/WebMVC/appsettings.json b/src/Web/WebMVC/appsettings.json index 9fe15aa25..b848c11b1 100644 --- a/src/Web/WebMVC/appsettings.json +++ b/src/Web/WebMVC/appsettings.json @@ -4,6 +4,7 @@ "BasketUrl": "http://localhost:5103", "IdentityUrl": "http://localhost:5105", "CallBackUrl": "http://localhost:5100/", + "ActivateCircuitBreaker": "True", "Logging": { "IncludeScopes": false, "LogLevel": {