Browse Source

Merge branch 'polly-integration' into dev

pull/126/head
Ramón Tomás 8 years ago
parent
commit
15d0e98dd1
12 changed files with 233 additions and 156 deletions
  1. +15
    -8
      src/Web/WebMVC/Controllers/OrderController.cs
  2. +16
    -19
      src/Web/WebMVC/Services/BasketService.cs
  3. +10
    -22
      src/Web/WebMVC/Services/CatalogService.cs
  4. +11
    -14
      src/Web/WebMVC/Services/OrderingService.cs
  5. +45
    -0
      src/Web/WebMVC/Services/Utilities/HttpApiClient.cs
  6. +99
    -0
      src/Web/WebMVC/Services/Utilities/HttpApiClientWrapper.cs
  7. +16
    -0
      src/Web/WebMVC/Services/Utilities/IHttpClient.cs
  8. +0
    -90
      src/Web/WebMVC/Services/Utilities/RetryWithExponentialBackoff.cs
  9. +12
    -3
      src/Web/WebMVC/Startup.cs
  10. +7
    -0
      src/Web/WebMVC/Views/Order/Create.cshtml
  11. +1
    -0
      src/Web/WebMVC/WebMVC.csproj
  12. +1
    -0
      src/Web/WebMVC/appsettings.json

+ 15
- 8
src/Web/WebMVC/Controllers/OrderController.cs View File

@ -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<IActionResult> 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);
}


+ 16
- 19
src/Web/WebMVC/Services/BasketService.cs View File

@ -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<AppSettings> _settings;
private HttpClient _apiClient;
private IHttpClient _apiClient;
private readonly string _remoteServiceBaseUrl;
private IHttpContextAccessor _httpContextAccesor;
public BasketService(IOptionsSnapshot<AppSettings> settings, IHttpContextAccessor httpContextAccesor)
public BasketService(IOptionsSnapshot<AppSettings> settings, IHttpContextAccessor httpContextAccesor, IHttpClient httpClient)
{
_settings = settings;
_remoteServiceBaseUrl = _settings.Value.BasketUrl;
_httpContextAccesor = httpContextAccesor;
_apiClient = httpClient;
}
public async Task<Basket> 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);


+ 10
- 22
src/Web/WebMVC/Services/CatalogService.cs View File

@ -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<AppSettings> _settings;
private HttpClient _apiClient;
private IHttpClient _apiClient;
private readonly string _remoteServiceBaseUrl;
public CatalogService(IOptionsSnapshot<AppSettings> settings, ILoggerFactory loggerFactory) {
public CatalogService(IOptionsSnapshot<AppSettings> 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<Catalog> 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<Catalog>(dataString);
return response;
@ -62,7 +52,6 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
public async Task<IEnumerable<SelectListItem>> 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<IEnumerable<SelectListItem>> GetTypes()
{
_apiClient = new HttpClient();
var url = $"{_remoteServiceBaseUrl}catalogTypes";
var dataString = await _apiClient.GetStringAsync(url);


+ 11
- 14
src/Web/WebMVC/Services/OrderingService.cs View File

@ -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<AppSettings> _settings;
private readonly IHttpContextAccessor _httpContextAccesor;
public OrderingService(IOptionsSnapshot<AppSettings> settings, IHttpContextAccessor httpContextAccesor)
public OrderingService(IOptionsSnapshot<AppSettings> settings, IHttpContextAccessor httpContextAccesor, IHttpClient httpClient)
{
_remoteServiceBaseUrl = $"{settings.Value.OrderingUrl}/api/v1/orders";
_settings = settings;
_httpContextAccesor = httpContextAccesor;
_apiClient = httpClient;
}
async public Task<Order> 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");


+ 45
- 0
src/Web/WebMVC/Services/Utilities/HttpApiClient.cs View File

@ -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<string> GetStringAsync(string uri)
{
return await HttpInvoker(async () =>
await _client.GetStringAsync(uri));
}
public async Task<HttpResponseMessage> PostAsync<T>(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<HttpResponseMessage> DeleteAsync(string uri)
{
return await HttpInvoker(async () =>
await _client.DeleteAsync(uri));
}
private async Task<T> HttpInvoker<T>(Func<Task<T>> action)
{
return await action();
}
}
}

+ 99
- 0
src/Web/WebMVC/Services/Utilities/HttpApiClientWrapper.cs View File

@ -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<HttpRequestException>()
.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<HttpRequestException>()
.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<string> GetStringAsync(string uri)
{
return await HttpInvoker(async () =>
await _client.GetStringAsync(uri));
}
public async Task<HttpResponseMessage> PostAsync<T>(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<HttpResponseMessage> DeleteAsync(string uri)
{
return await HttpInvoker(async () =>
await _client.DeleteAsync(uri));
}
private async Task<T> HttpInvoker<T>(Func<Task<T>> action)
{
// Executes the action applying all
// the policies defined in the wrapper
return await _policyWrapper
.ExecuteAsync(async () => await action());
}
}
}

+ 16
- 0
src/Web/WebMVC/Services/Utilities/IHttpClient.cs View File

@ -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<string> GetStringAsync(string uri);
Task<HttpResponseMessage> PostAsync<T>(string uri, T item);
Task<HttpResponseMessage> DeleteAsync(string uri);
}
}

+ 0
- 90
src/Web/WebMVC/Services/Utilities/RetryWithExponentialBackoff.cs View File

@ -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
{
/// <summary>
/// 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
/// });
/// </summary>
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<Task> 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;
}
}
}
/// <summary>
/// Usage:
/// ExponentialBackoff backoff = new ExponentialBackoff(3, 10, 100);
/// retry:
/// try {
/// // ...
/// }
/// catch (Exception ex) {
/// await backoff.Delay(cancellationToken);
/// goto retry;
/// }
/// </summary>
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);
}
}
}

+ 12
- 3
src/Web/WebMVC/Startup.cs View File

@ -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<AppSettings>(Configuration);
// Add application services.
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<ICatalogService, CatalogService>();
services.AddTransient<IOrderingService, OrderingService>();
services.AddTransient<IBasketService, BasketService>();
services.AddTransient<IIdentityParser<ApplicationUser>, IdentityParser>();
if(Configuration.GetValue<string>("ActivateCircuitBreaker") == bool.TrueString)
{
services.AddSingleton<IHttpClient, HttpApiClientWrapper>();
}
else
{
services.AddSingleton<IHttpClient, HttpApiClient>();
}
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.


+ 7
- 0
src/Web/WebMVC/Views/Order/Create.cshtml View File

@ -11,6 +11,13 @@
<div class="container">
<form method="post" asp-controller="Order" asp-action="Create">
<section class="esh-orders_new-section">
<div class="row">
@foreach (var error in ViewData.ModelState.Values.SelectMany(err => err.Errors)) {
<div class="alert alert-warning" role="alert">
&nbsp;@error.ErrorMessage
</div>
}
</div>
<h4 class="esh-orders_new-title">Shipping address</h4>
<div class="row">
<div class="col-md-6">


+ 1
- 0
src/Web/WebMVC/WebMVC.csproj View File

@ -32,6 +32,7 @@
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="Polly" Version="5.0.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.0" />


+ 1
- 0
src/Web/WebMVC/appsettings.json View File

@ -4,6 +4,7 @@
"BasketUrl": "http://localhost:5103",
"IdentityUrl": "http://localhost:5105",
"CallBackUrl": "http://localhost:5100/",
"ActivateCircuitBreaker": "True",
"Logging": {
"IncludeScopes": false,
"LogLevel": {


Loading…
Cancel
Save