Browse Source

Merge pull request #140 from dotnet/#42-Global-exception-handling

#42 global exception handling
pull/142/head
David Sanz 7 years ago
committed by GitHub
parent
commit
51c1fd658f
22 changed files with 297 additions and 47 deletions
  1. +18
    -0
      src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs
  2. +24
    -0
      src/Services/Basket/Basket.API/Infrastructure/Exceptions/BasketDomainException.cs
  3. +67
    -0
      src/Services/Basket/Basket.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs
  4. +6
    -1
      src/Services/Basket/Basket.API/Startup.cs
  5. +18
    -0
      src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs
  6. +24
    -0
      src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs
  7. +63
    -0
      src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs
  8. +8
    -9
      src/Services/Catalog/Catalog.API/Startup.cs
  9. +1
    -4
      src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs
  10. +2
    -2
      src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs
  11. +6
    -3
      src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs
  12. +2
    -7
      src/Services/Ordering/Ordering.API/Startup.cs
  13. +5
    -4
      src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/PaymentMethod.cs
  14. +5
    -4
      src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs
  15. +3
    -2
      src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs
  16. +23
    -0
      src/Services/Ordering/Ordering.Domain/Exceptions/OrderingDomainException.cs
  17. +1
    -1
      src/Services/Ordering/Ordering.Domain/SeedWork/IUnitOfWork.cs
  18. +2
    -2
      src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs
  19. +2
    -1
      src/Services/Ordering/Ordering.Infrastructure/Repositories/RequestManager.cs
  20. +2
    -0
      src/Web/WebMVC/Services/BasketService.cs
  21. +2
    -0
      src/Web/WebMVC/Services/OrderingService.cs
  22. +13
    -7
      src/Web/WebMVC/Services/Utilities/HttpApiClientWrapper.cs

+ 18
- 0
src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs View File

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Basket.API.Infrastructure.ActionResults
{
public class InternalServerErrorObjectResult : ObjectResult
{
public InternalServerErrorObjectResult(object error)
: base(error)
{
StatusCode = StatusCodes.Status500InternalServerError;
}
}
}

+ 24
- 0
src/Services/Basket/Basket.API/Infrastructure/Exceptions/BasketDomainException.cs View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Basket.API.Infrastructure.Exceptions
{
/// <summary>
/// Exception type for app exceptions
/// </summary>
public class BasketDomainException : Exception
{
public BasketDomainException()
{ }
public BasketDomainException(string message)
: base(message)
{ }
public BasketDomainException(string message, Exception innerException)
: base(message, innerException)
{ }
}
}

+ 67
- 0
src/Services/Basket/Basket.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs View File

@ -0,0 +1,67 @@
using Basket.API.Infrastructure.ActionResults;
using Basket.API.Infrastructure.Exceptions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace Basket.API.Infrastructure.Filters
{
public class HttpGlobalExceptionFilter : IExceptionFilter
{
private readonly IHostingEnvironment env;
private readonly ILogger<HttpGlobalExceptionFilter> logger;
public HttpGlobalExceptionFilter(IHostingEnvironment env, ILogger<HttpGlobalExceptionFilter> logger)
{
this.env = env;
this.logger = logger;
}
public void OnException(ExceptionContext context)
{
logger.LogError(new EventId(context.Exception.HResult),
context.Exception,
context.Exception.Message);
if (context.Exception.GetType() == typeof(BasketDomainException))
{
var json = new JsonErrorResponse
{
Messages = new[] { context.Exception.Message }
};
context.Result = new BadRequestObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
else
{
var json = new JsonErrorResponse
{
Messages = new[] { "An error ocurr.Try it again." }
};
if (env.IsDevelopment())
{
json.DeveloperMeesage = context.Exception;
}
context.Result = new InternalServerErrorObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
context.ExceptionHandled = true;
}
private class JsonErrorResponse
{
public string[] Messages { get; set; }
public object DeveloperMeesage { get; set; }
}
}
}

+ 6
- 1
src/Services/Basket/Basket.API/Startup.cs View File

@ -14,6 +14,7 @@ using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events;
using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
using System;
using Basket.API.Infrastructure.Filters;
namespace Microsoft.eShopOnContainers.Services.Basket.API
{
@ -35,7 +36,11 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
services.AddMvc(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
}).AddControllersAsServices();
services.Configure<BasketSettings>(Configuration);
//By connecting here we are making sure that our service


+ 18
- 0
src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs View File

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Catalog.API.Infrastructure.ActionResults
{
public class InternalServerErrorObjectResult : ObjectResult
{
public InternalServerErrorObjectResult(object error)
: base(error)
{
StatusCode = StatusCodes.Status500InternalServerError;
}
}
}

+ 24
- 0
src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Catalog.API.Infrastructure.Exceptions
{
/// <summary>
/// Exception type for app exceptions
/// </summary>
public class CatalogDomainException : Exception
{
public CatalogDomainException()
{ }
public CatalogDomainException(string message)
: base(message)
{ }
public CatalogDomainException(string message, Exception innerException)
: base(message, innerException)
{ }
}
}

+ 63
- 0
src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs View File

@ -0,0 +1,63 @@
using Catalog.API.Infrastructure.ActionResults;
using Catalog.API.Infrastructure.Exceptions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System.Net;
namespace Catalog.API.Infrastructure.Filters
{
public class HttpGlobalExceptionFilter : IExceptionFilter
{
private readonly IHostingEnvironment env;
private readonly ILogger<HttpGlobalExceptionFilter> logger;
public HttpGlobalExceptionFilter(IHostingEnvironment env, ILogger<HttpGlobalExceptionFilter> logger)
{
this.env = env;
this.logger = logger;
}
public void OnException(ExceptionContext context)
{
logger.LogError(new EventId(context.Exception.HResult),
context.Exception,
context.Exception.Message);
if (context.Exception.GetType() == typeof(CatalogDomainException))
{
var json = new JsonErrorResponse
{
Messages = new[] { context.Exception.Message }
};
context.Result = new BadRequestObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
else
{
var json = new JsonErrorResponse
{
Messages = new[] { "An error ocurr.Try it again." }
};
if (env.IsDevelopment())
{
json.DeveloperMeesage = context.Exception;
}
context.Result = new InternalServerErrorObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
context.ExceptionHandled = true;
}
private class JsonErrorResponse
{
public string[] Messages { get; set; }
public object DeveloperMeesage { get; set; }
}
}
}

+ 8
- 9
src/Services/Catalog/Catalog.API/Startup.cs View File

@ -1,5 +1,6 @@
namespace Microsoft.eShopOnContainers.Services.Catalog.API
{
using global::Catalog.API.Infrastructure.Filters;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
@ -15,7 +16,6 @@
using Microsoft.Extensions.Options;
using System;
using System.Data.Common;
using System.Data.SqlClient;
using System.Reflection;
public class Startup
@ -41,6 +41,12 @@
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
}).AddControllersAsServices();
services.AddDbContext<CatalogContext>(options =>
{
options.UseSqlServer(Configuration["ConnectionString"],
@ -86,20 +92,13 @@
var serviceProvider = services.BuildServiceProvider();
var configuration = serviceProvider.GetRequiredService<IOptionsSnapshot<Settings>>().Value;
services.AddSingleton<IEventBus>(new EventBusRabbitMQ(configuration.EventBusConnection));
services.AddMvc();
services.AddSingleton<IEventBus>(new EventBusRabbitMQ(configuration.EventBusConnection));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//Configure logs
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();


+ 1
- 4
src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs View File

@ -51,11 +51,8 @@
_orderRepository.Add(order);
var result = await _orderRepository.UnitOfWork
return await _orderRepository.UnitOfWork
.SaveEntitiesAsync();
return result > 0;
}
}
}

+ 2
- 2
src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs View File

@ -48,9 +48,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands
return CreateResultForDuplicateRequest();
}
else
{
await _requestManager.CreateRequestForCommandAsync<T>(message.Id);
{
var result = await _mediator.SendAsync(message.Command);
await _requestManager.CreateRequestForCommandAsync<T>(message.Id);
return result;
}
}


+ 6
- 3
src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs View File

@ -1,11 +1,12 @@
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Filters
{
using AspNetCore.Mvc;
using global::Ordering.Domain.Exceptions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.ActionResults;
using Microsoft.Extensions.Logging;
using System;
using System.Net;
public class HttpGlobalExceptionFilter : IExceptionFilter
{
@ -24,7 +25,7 @@
context.Exception,
context.Exception.Message);
if (context.Exception.GetType() == typeof(ArgumentException)) //TODO:Select a common exception for application like EshopException
if (context.Exception.GetType() == typeof(OrderingDomainException))
{
var json = new JsonErrorResponse
{
@ -32,6 +33,7 @@
};
context.Result = new BadRequestObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
else
{
@ -46,8 +48,9 @@
}
context.Result = new InternalServerErrorObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
context.ExceptionHandled = true;
}
private class JsonErrorResponse


+ 2
- 7
src/Services/Ordering/Ordering.API/Startup.cs View File

@ -45,7 +45,7 @@
// Add framework services.
services.AddMvc(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
}).AddControllersAsServices(); //Injecting Controllers themselves thru DI
//For further info see: http://docs.autofac.org/en/latest/integration/aspnetcore.html#controllers-as-services
@ -106,12 +106,7 @@
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("CorsPolicy");
app.UseFailingMiddleware();


+ 5
- 4
src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/PaymentMethod.cs View File

@ -1,4 +1,5 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
using Ordering.Domain.Exceptions;
using System;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate
@ -22,13 +23,13 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B
public PaymentMethod(int cardTypeId, string alias, string cardNumber, string securityNumber, string cardHolderName, DateTime expiration)
{
_cardNumber = !string.IsNullOrWhiteSpace(cardNumber) ? cardNumber : throw new ArgumentException(nameof(cardNumber));
_securityNumber = !string.IsNullOrWhiteSpace(securityNumber) ? securityNumber : throw new ArgumentException(nameof(securityNumber));
_cardHolderName = !string.IsNullOrWhiteSpace(cardHolderName) ? cardHolderName : throw new ArgumentException(nameof(cardHolderName));
_cardNumber = !string.IsNullOrWhiteSpace(cardNumber) ? cardNumber : throw new OrderingDomainException(nameof(cardNumber));
_securityNumber = !string.IsNullOrWhiteSpace(securityNumber) ? securityNumber : throw new OrderingDomainException(nameof(securityNumber));
_cardHolderName = !string.IsNullOrWhiteSpace(cardHolderName) ? cardHolderName : throw new OrderingDomainException(nameof(cardHolderName));
if (expiration < DateTime.UtcNow)
{
throw new ArgumentException(nameof(expiration));
throw new OrderingDomainException(nameof(expiration));
}
_alias = alias;


+ 5
- 4
src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs View File

@ -1,4 +1,5 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
using Ordering.Domain.Exceptions;
using System;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate
@ -24,12 +25,12 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O
{
if (units <= 0)
{
throw new ArgumentNullException("Invalid number of units");
throw new OrderingDomainException("Invalid number of units");
}
if ((unitPrice * units) < discount)
{
throw new ArgumentException("The total of order item is lower than applied discount");
throw new OrderingDomainException("The total of order item is lower than applied discount");
}
ProductId = productId;
@ -58,7 +59,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O
{
if (discount < 0)
{
throw new ArgumentException("Discount is not valid");
throw new OrderingDomainException("Discount is not valid");
}
_discount = discount;
@ -68,7 +69,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O
{
if (units < 0)
{
throw new ArgumentException("Invalid units");
throw new OrderingDomainException("Invalid units");
}
_units += units;


+ 3
- 2
src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs View File

@ -1,5 +1,6 @@
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate
{
using global::Ordering.Domain.Exceptions;
using Seedwork;
using SeedWork;
using System;
@ -34,7 +35,7 @@
if (state == null)
{
throw new ArgumentException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}");
throw new OrderingDomainException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}");
}
return state;
@ -46,7 +47,7 @@
if (state == null)
{
throw new ArgumentException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}");
throw new OrderingDomainException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}");
}
return state;


+ 23
- 0
src/Services/Ordering/Ordering.Domain/Exceptions/OrderingDomainException.cs View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ordering.Domain.Exceptions
{
/// <summary>
/// Exception type for domain exceptions
/// </summary>
public class OrderingDomainException : Exception
{
public OrderingDomainException()
{ }
public OrderingDomainException(string message)
: base(message)
{ }
public OrderingDomainException(string message, Exception innerException)
: base(message, innerException)
{ }
}
}

+ 1
- 1
src/Services/Ordering/Ordering.Domain/SeedWork/IUnitOfWork.cs View File

@ -7,6 +7,6 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork
public interface IUnitOfWork : IDisposable
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
Task<int> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken));
Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken));
}
}

+ 2
- 2
src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs View File

@ -235,7 +235,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
.IsRequired();
}
public async Task<int> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken))
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
// Dispatch Domain Events collection.
// Choices:
@ -250,7 +250,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
// performed thought the DbContext will be commited
var result = await base.SaveChangesAsync();
return result;
return true;
}
}
}

+ 2
- 1
src/Services/Ordering/Ordering.Infrastructure/Repositories/RequestManager.cs View File

@ -1,4 +1,5 @@
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure;
using Ordering.Domain.Exceptions;
using System;
using System.Collections.Generic;
using System.Text;
@ -26,7 +27,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositor
var exists = await ExistAsync(id);
var request = exists ?
throw new Exception($"Request with {id} already exists") :
throw new OrderingDomainException($"Request with {id} already exists") :
new ClientRequest()
{
Id = id,


+ 2
- 0
src/Web/WebMVC/Services/BasketService.cs View File

@ -56,6 +56,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
var response = await _apiClient.PostAsync(basketUrl, basket);
response.EnsureSuccessStatusCode();
return basket;
}


+ 2
- 0
src/Web/WebMVC/Services/OrderingService.cs View File

@ -88,6 +88,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
throw new Exception("Error creating order, try later");
response.EnsureSuccessStatusCode();
}
public void OverrideUserInfoIntoOrder(Order original, Order destination)


+ 13
- 7
src/Web/WebMVC/Services/Utilities/HttpApiClientWrapper.cs View File

@ -1,8 +1,10 @@
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Polly;
using Polly.Wrap;
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
@ -76,13 +78,17 @@ namespace WebMVC.Services.Utilities
// a new StringContent must be created for each retry
// as it is disposed after each call
HttpInvoker(() =>
{
var response = _client.PostAsync(uri, new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"));
// raise exception if HttpResponseCode 500
// needed for circuit breaker to track fails
if (response.Result.StatusCode == HttpStatusCode.InternalServerError)
{
var response = _client.PostAsync(uri, new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"));
// raise exception if not success response
// needed for circuit breaker to track fails
response.Result.EnsureSuccessStatusCode();
return response;
});
throw new HttpRequestException();
}
return response;
});
public Task<HttpResponseMessage> DeleteAsync(string uri) =>
HttpInvoker(() => _client.DeleteAsync(uri));


Loading…
Cancel
Save