Add FluentValidation
This commit is contained in:
@@ -8,8 +8,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="FluentValidation" Version="12.0.0-preview1" />
|
||||
<PackageReference Include="FluentValidation" Version="12.0.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -17,4 +19,8 @@
|
||||
<ProjectReference Include="..\Imprink.Domain\Imprink.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Validation\Users\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Imprink.Application.Products.Dtos;
|
||||
using Imprink.Domain.Common.Models;
|
||||
using Imprink.Domain.Models;
|
||||
using MediatR;
|
||||
|
||||
namespace Imprink.Application.Products;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Imprink.Domain.Common.Models;
|
||||
using Imprink.Domain.Models;
|
||||
using MediatR;
|
||||
|
||||
namespace Imprink.Application.Users;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Domain.Models;
|
||||
|
||||
namespace Imprink.Application.Validation.Models;
|
||||
|
||||
public class Auth0UserValidator : AbstractValidator<Auth0User>
|
||||
{
|
||||
public Auth0UserValidator()
|
||||
{
|
||||
RuleFor(x => x.Sub)
|
||||
.NotEmpty();
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty();
|
||||
|
||||
RuleFor(x => x.Nickname)
|
||||
.NotEmpty();
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty()
|
||||
.EmailAddress();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Domain.Models;
|
||||
|
||||
namespace Imprink.Application.Validation.Models;
|
||||
|
||||
public class ProductFilterParametersValidator : AbstractValidator<ProductFilterParameters>
|
||||
{
|
||||
public ProductFilterParametersValidator()
|
||||
{
|
||||
RuleFor(x => x.PageNumber)
|
||||
.GreaterThan(0).WithMessage("PageNumber must be greater than 0.");
|
||||
|
||||
RuleFor(x => x.PageSize)
|
||||
.InclusiveBetween(1, 100).WithMessage("PageSize must be between 1 and 100.");
|
||||
|
||||
RuleFor(x => x.SearchTerm)
|
||||
.Length(1, 100).WithMessage("Length must be between 1 and 100.")
|
||||
.When(x => !string.IsNullOrWhiteSpace(x.SearchTerm));
|
||||
|
||||
RuleFor(x => x.MinPrice)
|
||||
.GreaterThanOrEqualTo(0).When(x => x.MinPrice.HasValue)
|
||||
.WithMessage("MinPrice cannot be negative.");
|
||||
|
||||
RuleFor(x => x.MaxPrice)
|
||||
.GreaterThanOrEqualTo(0).When(x => x.MaxPrice.HasValue)
|
||||
.WithMessage("MaxPrice cannot be negative.");
|
||||
|
||||
RuleFor(x => x)
|
||||
.Must(x => !x.MinPrice.HasValue || !x.MaxPrice.HasValue || x.MinPrice <= x.MaxPrice)
|
||||
.WithMessage("MinPrice cannot be greater than MaxPrice.");
|
||||
|
||||
RuleFor(x => x.SortBy)
|
||||
.NotEmpty().WithMessage("SortBy is required.")
|
||||
.Must(value => AllowedSortColumns.Contains(value, StringComparer.OrdinalIgnoreCase))
|
||||
.WithMessage("SortBy must be one of: Name, Price, CreatedDate.");
|
||||
|
||||
RuleFor(x => x.SortDirection)
|
||||
.NotEmpty().WithMessage("SortDirection is required.")
|
||||
.Must(value => value.Equals("ASC", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("DESC", StringComparison.OrdinalIgnoreCase))
|
||||
.WithMessage("SortDirection must be 'ASC' or 'DESC'.");
|
||||
}
|
||||
|
||||
private static readonly string[] AllowedSortColumns = ["Name", "Price", "CreatedDate"];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Products;
|
||||
using Imprink.Application.Validation.Models;
|
||||
|
||||
namespace Imprink.Application.Validation.Products;
|
||||
|
||||
public class GetProductsQueryValidator : AbstractValidator<GetProductsQuery>
|
||||
{
|
||||
public GetProductsQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.FilterParameters)
|
||||
.NotNull()
|
||||
.SetValidator(new ProductFilterParametersValidator());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Imprink.Domain.Common.Models;
|
||||
namespace Imprink.Domain.Models;
|
||||
|
||||
public class Auth0User
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Imprink.Domain.Common.Models;
|
||||
namespace Imprink.Domain.Models;
|
||||
|
||||
public class PagedResult<T>
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Imprink.Domain.Common.Models;
|
||||
namespace Imprink.Domain.Models;
|
||||
|
||||
public class ProductFilterParameters
|
||||
{
|
||||
@@ -1,5 +1,5 @@
|
||||
using Imprink.Domain.Common.Models;
|
||||
using Imprink.Domain.Entities.Product;
|
||||
using Imprink.Domain.Models;
|
||||
|
||||
namespace Imprink.Domain.Repositories;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Imprink.Domain.Common.Models;
|
||||
using Imprink.Domain.Entities.Users;
|
||||
using Imprink.Domain.Models;
|
||||
|
||||
namespace Imprink.Domain.Repositories;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Imprink.Domain.Common.Models;
|
||||
using Imprink.Domain.Entities.Product;
|
||||
using Imprink.Domain.Models;
|
||||
using Imprink.Domain.Repositories;
|
||||
using Imprink.Infrastructure.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Imprink.Domain.Common.Models;
|
||||
using Imprink.Domain.Entities.Users;
|
||||
using Imprink.Domain.Models;
|
||||
using Imprink.Domain.Repositories;
|
||||
using Imprink.Infrastructure.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Imprink.Application.Products;
|
||||
using Imprink.Application.Products.Dtos;
|
||||
using Imprink.Domain.Common.Models;
|
||||
using Imprink.Domain.Models;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Imprink.Application.Users;
|
||||
using Imprink.Domain.Common.Models;
|
||||
using Imprink.Domain.Models;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
41
src/Imprink.WebApi/Filters/ValidationActionFilter.cs
Normal file
41
src/Imprink.WebApi/Filters/ValidationActionFilter.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace Imprink.WebApi.Filters;
|
||||
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
public class ValidationActionFilter(IServiceProvider serviceProvider) : IAsyncActionFilter
|
||||
{
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
foreach (var parameter in context.ActionDescriptor.Parameters)
|
||||
{
|
||||
if (!context.ActionArguments.TryGetValue(parameter.Name, out var argument) || argument == null) continue;
|
||||
var argumentType = argument.GetType();
|
||||
var validatorType = typeof(IValidator<>).MakeGenericType(argumentType);
|
||||
|
||||
var validators = serviceProvider.GetServices(validatorType).Cast<IValidator>();
|
||||
|
||||
var validatorList = validators as IValidator[] ?? validators.ToArray();
|
||||
if (validatorList.Length == 0) continue;
|
||||
var validationContext = new ValidationContext<object>(argument);
|
||||
var validationTasks = validatorList.Select(v => v.ValidateAsync(validationContext));
|
||||
var validationResults = await Task.WhenAll(validationTasks);
|
||||
|
||||
var failures = validationResults
|
||||
.SelectMany(r => r.Errors)
|
||||
.Where(f => f != null)
|
||||
.ToList();
|
||||
|
||||
if (failures.Count <= 0) continue;
|
||||
var errors = failures.Select(e => new {
|
||||
e.PropertyName, e.ErrorMessage
|
||||
});
|
||||
|
||||
context.Result = new BadRequestObjectResult(new { Errors = errors });
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="12.0.0" />
|
||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using System.Security.Claims;
|
||||
using FluentValidation;
|
||||
using Imprink.Application;
|
||||
using Imprink.Application.Products.Create;
|
||||
using Imprink.Application.Validation.Models;
|
||||
using Imprink.Domain.Repositories;
|
||||
using Imprink.Infrastructure;
|
||||
using Imprink.Infrastructure.Database;
|
||||
using Imprink.Infrastructure.Repositories;
|
||||
using Imprink.WebApi.Filters;
|
||||
using Imprink.WebApi.Middleware;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -37,11 +40,14 @@ public static class Startup
|
||||
cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly);
|
||||
});
|
||||
|
||||
services.AddValidatorsFromAssembly(typeof(Auth0UserValidator).Assembly);
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer(options =>
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = builder.Configuration["Auth0:Authority"];
|
||||
options.Audience = builder.Configuration["Auth0:Audience"];
|
||||
@@ -78,7 +84,11 @@ public static class Startup
|
||||
|
||||
services.AddAuthorization();
|
||||
|
||||
services.AddControllers();
|
||||
services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add<ValidationActionFilter>();
|
||||
});
|
||||
|
||||
services.AddSwaggerGen();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user