Add FluentValidation
This commit is contained in:
@@ -8,8 +8,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<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" Version="12.5.0" />
|
||||||
|
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -17,4 +19,8 @@
|
|||||||
<ProjectReference Include="..\Imprink.Domain\Imprink.Domain.csproj" />
|
<ProjectReference Include="..\Imprink.Domain\Imprink.Domain.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Validation\Users\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Imprink.Application.Products.Dtos;
|
using Imprink.Application.Products.Dtos;
|
||||||
using Imprink.Domain.Common.Models;
|
using Imprink.Domain.Models;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Imprink.Application.Products;
|
namespace Imprink.Application.Products;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Imprink.Domain.Common.Models;
|
using Imprink.Domain.Models;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Imprink.Application.Users;
|
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
|
public class Auth0User
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Imprink.Domain.Common.Models;
|
namespace Imprink.Domain.Models;
|
||||||
|
|
||||||
public class PagedResult<T>
|
public class PagedResult<T>
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Imprink.Domain.Common.Models;
|
namespace Imprink.Domain.Models;
|
||||||
|
|
||||||
public class ProductFilterParameters
|
public class ProductFilterParameters
|
||||||
{
|
{
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using Imprink.Domain.Common.Models;
|
|
||||||
using Imprink.Domain.Entities.Product;
|
using Imprink.Domain.Entities.Product;
|
||||||
|
using Imprink.Domain.Models;
|
||||||
|
|
||||||
namespace Imprink.Domain.Repositories;
|
namespace Imprink.Domain.Repositories;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Imprink.Domain.Common.Models;
|
|
||||||
using Imprink.Domain.Entities.Users;
|
using Imprink.Domain.Entities.Users;
|
||||||
|
using Imprink.Domain.Models;
|
||||||
|
|
||||||
namespace Imprink.Domain.Repositories;
|
namespace Imprink.Domain.Repositories;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Imprink.Domain.Common.Models;
|
|
||||||
using Imprink.Domain.Entities.Product;
|
using Imprink.Domain.Entities.Product;
|
||||||
|
using Imprink.Domain.Models;
|
||||||
using Imprink.Domain.Repositories;
|
using Imprink.Domain.Repositories;
|
||||||
using Imprink.Infrastructure.Database;
|
using Imprink.Infrastructure.Database;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Imprink.Domain.Common.Models;
|
|
||||||
using Imprink.Domain.Entities.Users;
|
using Imprink.Domain.Entities.Users;
|
||||||
|
using Imprink.Domain.Models;
|
||||||
using Imprink.Domain.Repositories;
|
using Imprink.Domain.Repositories;
|
||||||
using Imprink.Infrastructure.Database;
|
using Imprink.Infrastructure.Database;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Imprink.Application.Products;
|
using Imprink.Application.Products;
|
||||||
using Imprink.Application.Products.Dtos;
|
using Imprink.Application.Products.Dtos;
|
||||||
using Imprink.Domain.Common.Models;
|
using Imprink.Domain.Models;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Imprink.Application.Users;
|
using Imprink.Application.Users;
|
||||||
using Imprink.Domain.Common.Models;
|
using Imprink.Domain.Models;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FluentValidation" Version="12.0.0" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using FluentValidation;
|
||||||
using Imprink.Application;
|
using Imprink.Application;
|
||||||
using Imprink.Application.Products.Create;
|
using Imprink.Application.Products.Create;
|
||||||
|
using Imprink.Application.Validation.Models;
|
||||||
using Imprink.Domain.Repositories;
|
using Imprink.Domain.Repositories;
|
||||||
using Imprink.Infrastructure;
|
using Imprink.Infrastructure;
|
||||||
using Imprink.Infrastructure.Database;
|
using Imprink.Infrastructure.Database;
|
||||||
using Imprink.Infrastructure.Repositories;
|
using Imprink.Infrastructure.Repositories;
|
||||||
|
using Imprink.WebApi.Filters;
|
||||||
using Imprink.WebApi.Middleware;
|
using Imprink.WebApi.Middleware;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -37,11 +40,14 @@ public static class Startup
|
|||||||
cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly);
|
cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddValidatorsFromAssembly(typeof(Auth0UserValidator).Assembly);
|
||||||
|
|
||||||
services.AddAuthentication(options =>
|
services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
}).AddJwtBearer(options =>
|
})
|
||||||
|
.AddJwtBearer(options =>
|
||||||
{
|
{
|
||||||
options.Authority = builder.Configuration["Auth0:Authority"];
|
options.Authority = builder.Configuration["Auth0:Authority"];
|
||||||
options.Audience = builder.Configuration["Auth0:Audience"];
|
options.Audience = builder.Configuration["Auth0:Audience"];
|
||||||
@@ -78,7 +84,11 @@ public static class Startup
|
|||||||
|
|
||||||
services.AddAuthorization();
|
services.AddAuthorization();
|
||||||
|
|
||||||
services.AddControllers();
|
services.AddControllers(options =>
|
||||||
|
{
|
||||||
|
options.Filters.Add<ValidationActionFilter>();
|
||||||
|
});
|
||||||
|
|
||||||
services.AddSwaggerGen();
|
services.AddSwaggerGen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user