From 42fc6f2bdbe3c409e45acf2ab32b7ca99f7f8c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Jos=C3=A9=20Alves=20Pires?= Date: Tue, 2 Feb 2021 18:08:14 -0300 Subject: [PATCH] implementing filters --- .../Commom/Middlewares/ValidationBehaviour.cs | 38 ++++++++ .../ServiceCollectionConfiguration.cs | 20 ++++ .../DTOs/ExchangeRateDTO.cs | 11 +++ .../Exceptions/ValidationException.cs | 26 ++++++ .../Queries/GetCurrencyExchange.cs | 38 ++++++++ .../Queries/GetCurrencyExchangeValidator.cs | 14 +++ .../VirtualMind.Application.csproj | 11 +++ VirtualMind.Domain/Enums/Currency.cs | 8 ++ VirtualMind.Domain/VirtualMind.Domain.csproj | 11 +++ .../Controllers/WeatherForecastController.cs | 12 ++- .../Filters/ApiExceptionFilterAttribute.cs | 93 +++++++++++++++++++ .../Filters/FilterServiceConfiguration.cs | 31 +++++++ VirtualMind.WebApp/Startup.cs | 8 +- VirtualMind.WebApp/VirtualMind.WebApp.csproj | 5 + VirtualMind.sln | 7 ++ 15 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 VirtualMind.Application/Commom/Middlewares/ValidationBehaviour.cs create mode 100644 VirtualMind.Application/Configurations/ServiceCollectionConfiguration.cs create mode 100644 VirtualMind.Application/DTOs/ExchangeRateDTO.cs create mode 100644 VirtualMind.Application/Exceptions/ValidationException.cs create mode 100644 VirtualMind.Application/Queries/GetCurrencyExchange.cs create mode 100644 VirtualMind.Application/Queries/GetCurrencyExchangeValidator.cs create mode 100644 VirtualMind.Domain/Enums/Currency.cs create mode 100644 VirtualMind.Domain/VirtualMind.Domain.csproj create mode 100644 VirtualMind.WebApp/Filters/ApiExceptionFilterAttribute.cs create mode 100644 VirtualMind.WebApp/Filters/FilterServiceConfiguration.cs diff --git a/VirtualMind.Application/Commom/Middlewares/ValidationBehaviour.cs b/VirtualMind.Application/Commom/Middlewares/ValidationBehaviour.cs new file mode 100644 index 0000000..57456cd --- /dev/null +++ b/VirtualMind.Application/Commom/Middlewares/ValidationBehaviour.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentValidation; +using MediatR; +using ValidationException = VirtualMind.Application.Exceptions.ValidationException; + +namespace VirtualMind.Application.Commom.Middlewares +{ + public class ValidationBehaviour : IPipelineBehavior + where TRequest : IRequest + { + private readonly IEnumerable> _validators; + + public ValidationBehaviour(IEnumerable> validators) + { + _validators = validators; + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + if (_validators.Any()) + { + var context = new ValidationContext(request); + + var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); + + var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); + + if (failures.Count != 0) + throw new ValidationException(failures); + } + + return await next(); + } + } +} diff --git a/VirtualMind.Application/Configurations/ServiceCollectionConfiguration.cs b/VirtualMind.Application/Configurations/ServiceCollectionConfiguration.cs new file mode 100644 index 0000000..ffa82a5 --- /dev/null +++ b/VirtualMind.Application/Configurations/ServiceCollectionConfiguration.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using FluentValidation; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using VirtualMind.Application.Commom.Middlewares; + +namespace VirtualMind.Application.Configurations +{ + public static class ServiceCollectionConfiguration + { + public static IServiceCollection AddApplicationDI(this IServiceCollection services) + { + services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); + services.AddMediatR(Assembly.GetExecutingAssembly()); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); + + return services; + } + } +} diff --git a/VirtualMind.Application/DTOs/ExchangeRateDTO.cs b/VirtualMind.Application/DTOs/ExchangeRateDTO.cs new file mode 100644 index 0000000..b43250f --- /dev/null +++ b/VirtualMind.Application/DTOs/ExchangeRateDTO.cs @@ -0,0 +1,11 @@ +namespace VirtualMind.Application.DTOs +{ + public class ExchangeRateDTO + { + public string Purchase { get; set; } + + public string Sale { get; set; } + + public string LastUpdate { get; set; } + } +} diff --git a/VirtualMind.Application/Exceptions/ValidationException.cs b/VirtualMind.Application/Exceptions/ValidationException.cs new file mode 100644 index 0000000..0b53c4d --- /dev/null +++ b/VirtualMind.Application/Exceptions/ValidationException.cs @@ -0,0 +1,26 @@ +using FluentValidation.Results; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace VirtualMind.Application.Exceptions +{ + public class ValidationException : Exception + { + public ValidationException() + : base("Validation failures have occurred.") + { + Errors = new Dictionary(); + } + + public ValidationException(IEnumerable failures) + : this() + { + Errors = failures + .GroupBy(e => e.PropertyName, e => e.ErrorMessage) + .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); + } + + public IDictionary Errors { get; } + } +} diff --git a/VirtualMind.Application/Queries/GetCurrencyExchange.cs b/VirtualMind.Application/Queries/GetCurrencyExchange.cs new file mode 100644 index 0000000..2899200 --- /dev/null +++ b/VirtualMind.Application/Queries/GetCurrencyExchange.cs @@ -0,0 +1,38 @@ +using MediatR; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using VirtualMind.Application.DTOs; + +namespace VirtualMind.Application.Queries +{ + public class GetCurrencyExchange : IRequest> + { + //public AcceptableCurrencies CurrencyType { get; set; } + + public string CurrencyType { get; set; } + } + + //TODO: Put this on domain + public enum AcceptableCurrencies + { + USD, + REAL + } + + public class GetCurrencyExchangeHandler : IRequestHandler> + { + public async Task> Handle(GetCurrencyExchange request, CancellationToken cancellationToken) + { + var exchange = new ExchangeRateDTO(); + exchange.LastUpdate = "today"; + exchange.Purchase = "12.00"; + exchange.Sale = "10"; + + var exchangeList = new List(); + exchangeList.Add(exchange); + + return await Task.FromResult(exchangeList); + } + } +} diff --git a/VirtualMind.Application/Queries/GetCurrencyExchangeValidator.cs b/VirtualMind.Application/Queries/GetCurrencyExchangeValidator.cs new file mode 100644 index 0000000..e3b36e0 --- /dev/null +++ b/VirtualMind.Application/Queries/GetCurrencyExchangeValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace VirtualMind.Application.Queries +{ + public class GetCurrencyExchangeValidator : AbstractValidator + { + public GetCurrencyExchangeValidator() + { + RuleFor(input => input.CurrencyType) + .NotNull().WithMessage("[CurrencyType] can't be null!") + .NotEmpty().WithMessage("[CurrencyType] field is required!"); + } + } +} diff --git a/VirtualMind.Application/VirtualMind.Application.csproj b/VirtualMind.Application/VirtualMind.Application.csproj index cb63190..97d89c6 100644 --- a/VirtualMind.Application/VirtualMind.Application.csproj +++ b/VirtualMind.Application/VirtualMind.Application.csproj @@ -4,4 +4,15 @@ netcoreapp3.1 + + + + + + + + + + + diff --git a/VirtualMind.Domain/Enums/Currency.cs b/VirtualMind.Domain/Enums/Currency.cs new file mode 100644 index 0000000..cf2adf4 --- /dev/null +++ b/VirtualMind.Domain/Enums/Currency.cs @@ -0,0 +1,8 @@ +namespace VirtualMind.Domain.Enums +{ + public enum Currency + { + USD = 0, + REAL= 1 + } +} diff --git a/VirtualMind.Domain/VirtualMind.Domain.csproj b/VirtualMind.Domain/VirtualMind.Domain.csproj new file mode 100644 index 0000000..f5ffd1d --- /dev/null +++ b/VirtualMind.Domain/VirtualMind.Domain.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/VirtualMind.WebApp/Controllers/WeatherForecastController.cs b/VirtualMind.WebApp/Controllers/WeatherForecastController.cs index 395c3e0..c41ac37 100644 --- a/VirtualMind.WebApp/Controllers/WeatherForecastController.cs +++ b/VirtualMind.WebApp/Controllers/WeatherForecastController.cs @@ -1,9 +1,11 @@ -using Microsoft.AspNetCore.Mvc; +using MediatR; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using VirtualMind.Application.Queries; namespace VirtualMind.WebApp.Controllers { @@ -17,15 +19,19 @@ public class WeatherForecastController : ControllerBase }; private readonly ILogger _logger; + private readonly IMediator _mediator; - public WeatherForecastController(ILogger logger) + public WeatherForecastController(ILogger logger, IMediator mediator) { _logger = logger; + _mediator = mediator; } [HttpGet] - public IEnumerable Get() + public async Task> Get([FromQuery]GetCurrencyExchange getCurrencyExchange) { + var resposne = await this._mediator.Send(getCurrencyExchange); + var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { diff --git a/VirtualMind.WebApp/Filters/ApiExceptionFilterAttribute.cs b/VirtualMind.WebApp/Filters/ApiExceptionFilterAttribute.cs new file mode 100644 index 0000000..e5d04f2 --- /dev/null +++ b/VirtualMind.WebApp/Filters/ApiExceptionFilterAttribute.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Collections.Generic; +using VirtualMind.Application.Exceptions; + +namespace VirtualMind.WebApp.Filters +{ + public class ApiExceptionFilterAttribute : ExceptionFilterAttribute + { + + private readonly IDictionary> _exceptionHandlers; + + public ApiExceptionFilterAttribute() + { + // Register known exception types and handlers. + _exceptionHandlers = new Dictionary> + { + { typeof(ValidationException), HandleValidationException } + }; + } + + public override void OnException(ExceptionContext context) + { + HandleException(context); + + base.OnException(context); + } + + private void HandleException(ExceptionContext context) + { + Type type = context.Exception.GetType(); + + if (_exceptionHandlers.ContainsKey(type)) + { + _exceptionHandlers[type].Invoke(context); + return; + } + + if (!context.ModelState.IsValid) + { + HandleInvalidModelStateException(context); + return; + } + + HandleUnknownException(context); + } + + private void HandleValidationException(ExceptionContext context) + { + var exception = context.Exception as ValidationException; + + var details = new ValidationProblemDetails(exception.Errors) + { + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" + }; + + context.Result = new BadRequestObjectResult(details); + + context.ExceptionHandled = true; + } + + private void HandleInvalidModelStateException(ExceptionContext context) + { + var details = new ValidationProblemDetails(context.ModelState) + { + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" + }; + + context.Result = new BadRequestObjectResult(details); + + context.ExceptionHandled = true; + } + + private void HandleUnknownException(ExceptionContext context) + { + var details = new ProblemDetails + { + Status = StatusCodes.Status500InternalServerError, + Title = "An error occurred while processing your request.", + Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1" + }; + + context.Result = new ObjectResult(details) + { + StatusCode = StatusCodes.Status500InternalServerError + }; + + context.ExceptionHandled = true; + } + } +} diff --git a/VirtualMind.WebApp/Filters/FilterServiceConfiguration.cs b/VirtualMind.WebApp/Filters/FilterServiceConfiguration.cs new file mode 100644 index 0000000..6d89e12 --- /dev/null +++ b/VirtualMind.WebApp/Filters/FilterServiceConfiguration.cs @@ -0,0 +1,31 @@ +using FluentValidation.AspNetCore; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace VirtualMind.WebApp.Filters +{ + public static class FilterServiceConfiguration + { + public static void RegisterGlobalFilters(this IServiceCollection services) + { + EnableExceptionFilterAtribute(services); + SupressDefaultValidators(services); + } + + private static void EnableExceptionFilterAtribute(IServiceCollection services) + { + services.AddControllersWithViews(options => + options.Filters.Add()) + .AddFluentValidation(); + + } + + private static void SupressDefaultValidators(IServiceCollection services) + { + services.Configure(options => + { + options.SuppressModelStateInvalidFilter = true; + }); + } + } +} diff --git a/VirtualMind.WebApp/Startup.cs b/VirtualMind.WebApp/Startup.cs index e760935..dd150ef 100644 --- a/VirtualMind.WebApp/Startup.cs +++ b/VirtualMind.WebApp/Startup.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using VirtualMind.Application.Configurations; +using VirtualMind.WebApp.Filters; namespace VirtualMind.WebApp { @@ -18,8 +20,10 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) - { - services.AddControllersWithViews(); + { + services.AddApplicationDI(); + services.RegisterGlobalFilters(); + // In production, the Angular files will be served from this directory services.AddSpaStaticFiles(configuration => { diff --git a/VirtualMind.WebApp/VirtualMind.WebApp.csproj b/VirtualMind.WebApp/VirtualMind.WebApp.csproj index 4d9da78..42058f4 100644 --- a/VirtualMind.WebApp/VirtualMind.WebApp.csproj +++ b/VirtualMind.WebApp/VirtualMind.WebApp.csproj @@ -13,6 +13,7 @@ + @@ -23,6 +24,10 @@ + + + + diff --git a/VirtualMind.sln b/VirtualMind.sln index 9f49c74..cd18991 100644 --- a/VirtualMind.sln +++ b/VirtualMind.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4 - Infra", "4 - Infra", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualMind.Application", "VirtualMind.Application\VirtualMind.Application.csproj", "{C7E47AE3-D1C8-4395-88C8-AAE5818D5ACC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualMind.Domain", "VirtualMind.Domain\VirtualMind.Domain.csproj", "{4F80DA53-FD10-4CDF-93B5-CA99737CE7D4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +31,10 @@ Global {C7E47AE3-D1C8-4395-88C8-AAE5818D5ACC}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7E47AE3-D1C8-4395-88C8-AAE5818D5ACC}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7E47AE3-D1C8-4395-88C8-AAE5818D5ACC}.Release|Any CPU.Build.0 = Release|Any CPU + {4F80DA53-FD10-4CDF-93B5-CA99737CE7D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F80DA53-FD10-4CDF-93B5-CA99737CE7D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F80DA53-FD10-4CDF-93B5-CA99737CE7D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F80DA53-FD10-4CDF-93B5-CA99737CE7D4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -36,6 +42,7 @@ Global GlobalSection(NestedProjects) = preSolution {D72102A2-1750-4B82-B91A-1B132D86B094} = {92605427-E7F4-457F-B8E7-9A31412FD4B7} {C7E47AE3-D1C8-4395-88C8-AAE5818D5ACC} = {0AEF29E2-7E5A-4012-AEB4-184C1BC44328} + {4F80DA53-FD10-4CDF-93B5-CA99737CE7D4} = {F3E702F6-1383-4A90-AE2C-2863A1E1EDD1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BBFF54F8-17FC-477F-852E-5089020CE7BF}