diff --git a/src/Imprink.Application/Imprink.Application.csproj b/src/Imprink.Application/Imprink.Application.csproj
index 7d1c2ec..b274257 100644
--- a/src/Imprink.Application/Imprink.Application.csproj
+++ b/src/Imprink.Application/Imprink.Application.csproj
@@ -8,8 +8,10 @@
-
+
+
+
@@ -17,4 +19,8 @@
+
+
+
+
diff --git a/src/Imprink.Application/Products/GetProductsHandler.cs b/src/Imprink.Application/Products/GetProductsHandler.cs
index 48b5cb0..afd64cc 100644
--- a/src/Imprink.Application/Products/GetProductsHandler.cs
+++ b/src/Imprink.Application/Products/GetProductsHandler.cs
@@ -1,5 +1,5 @@
using Imprink.Application.Products.Dtos;
-using Imprink.Domain.Common.Models;
+using Imprink.Domain.Models;
using MediatR;
namespace Imprink.Application.Products;
diff --git a/src/Imprink.Application/Users/SyncUserHandler.cs b/src/Imprink.Application/Users/SyncUserHandler.cs
index 2a73cae..aadbf43 100644
--- a/src/Imprink.Application/Users/SyncUserHandler.cs
+++ b/src/Imprink.Application/Users/SyncUserHandler.cs
@@ -1,4 +1,4 @@
-using Imprink.Domain.Common.Models;
+using Imprink.Domain.Models;
using MediatR;
namespace Imprink.Application.Users;
diff --git a/src/Imprink.Application/Validation/Models/Auth0UserValidator.cs b/src/Imprink.Application/Validation/Models/Auth0UserValidator.cs
new file mode 100644
index 0000000..4df6070
--- /dev/null
+++ b/src/Imprink.Application/Validation/Models/Auth0UserValidator.cs
@@ -0,0 +1,23 @@
+using FluentValidation;
+using Imprink.Domain.Models;
+
+namespace Imprink.Application.Validation.Models;
+
+public class Auth0UserValidator : AbstractValidator
+{
+ public Auth0UserValidator()
+ {
+ RuleFor(x => x.Sub)
+ .NotEmpty();
+
+ RuleFor(x => x.Name)
+ .NotEmpty();
+
+ RuleFor(x => x.Nickname)
+ .NotEmpty();
+
+ RuleFor(x => x.Email)
+ .NotEmpty()
+ .EmailAddress();
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Validation/Models/ProductFilterParametersValidator.cs b/src/Imprink.Application/Validation/Models/ProductFilterParametersValidator.cs
new file mode 100644
index 0000000..08131c9
--- /dev/null
+++ b/src/Imprink.Application/Validation/Models/ProductFilterParametersValidator.cs
@@ -0,0 +1,45 @@
+using FluentValidation;
+using Imprink.Domain.Models;
+
+namespace Imprink.Application.Validation.Models;
+
+public class ProductFilterParametersValidator : AbstractValidator
+{
+ 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"];
+}
\ No newline at end of file
diff --git a/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs b/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs
new file mode 100644
index 0000000..b888657
--- /dev/null
+++ b/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs
@@ -0,0 +1,15 @@
+using FluentValidation;
+using Imprink.Application.Products;
+using Imprink.Application.Validation.Models;
+
+namespace Imprink.Application.Validation.Products;
+
+public class GetProductsQueryValidator : AbstractValidator
+{
+ public GetProductsQueryValidator()
+ {
+ RuleFor(x => x.FilterParameters)
+ .NotNull()
+ .SetValidator(new ProductFilterParametersValidator());
+ }
+}
\ No newline at end of file
diff --git a/src/Imprink.Domain/Common/Models/Auth0User.cs b/src/Imprink.Domain/Models/Auth0User.cs
similarity index 86%
rename from src/Imprink.Domain/Common/Models/Auth0User.cs
rename to src/Imprink.Domain/Models/Auth0User.cs
index 687980e..54f8ca6 100644
--- a/src/Imprink.Domain/Common/Models/Auth0User.cs
+++ b/src/Imprink.Domain/Models/Auth0User.cs
@@ -1,4 +1,4 @@
-namespace Imprink.Domain.Common.Models;
+namespace Imprink.Domain.Models;
public class Auth0User
{
diff --git a/src/Imprink.Domain/Common/Models/PagedResult.cs b/src/Imprink.Domain/Models/PagedResult.cs
similarity index 90%
rename from src/Imprink.Domain/Common/Models/PagedResult.cs
rename to src/Imprink.Domain/Models/PagedResult.cs
index cde7b5a..214eb90 100644
--- a/src/Imprink.Domain/Common/Models/PagedResult.cs
+++ b/src/Imprink.Domain/Models/PagedResult.cs
@@ -1,4 +1,4 @@
-namespace Imprink.Domain.Common.Models;
+namespace Imprink.Domain.Models;
public class PagedResult
{
diff --git a/src/Imprink.Domain/Common/Models/ProductFilterParameters.cs b/src/Imprink.Domain/Models/ProductFilterParameters.cs
similarity index 92%
rename from src/Imprink.Domain/Common/Models/ProductFilterParameters.cs
rename to src/Imprink.Domain/Models/ProductFilterParameters.cs
index 4631894..e932405 100644
--- a/src/Imprink.Domain/Common/Models/ProductFilterParameters.cs
+++ b/src/Imprink.Domain/Models/ProductFilterParameters.cs
@@ -1,4 +1,4 @@
-namespace Imprink.Domain.Common.Models;
+namespace Imprink.Domain.Models;
public class ProductFilterParameters
{
diff --git a/src/Imprink.Domain/Repositories/Products/IProductRepository.cs b/src/Imprink.Domain/Repositories/Products/IProductRepository.cs
index 38eaee9..f77d96d 100644
--- a/src/Imprink.Domain/Repositories/Products/IProductRepository.cs
+++ b/src/Imprink.Domain/Repositories/Products/IProductRepository.cs
@@ -1,5 +1,5 @@
-using Imprink.Domain.Common.Models;
using Imprink.Domain.Entities.Product;
+using Imprink.Domain.Models;
namespace Imprink.Domain.Repositories;
diff --git a/src/Imprink.Domain/Repositories/Users/IUserRepository.cs b/src/Imprink.Domain/Repositories/Users/IUserRepository.cs
index e5a0231..5809d58 100644
--- a/src/Imprink.Domain/Repositories/Users/IUserRepository.cs
+++ b/src/Imprink.Domain/Repositories/Users/IUserRepository.cs
@@ -1,5 +1,5 @@
-using Imprink.Domain.Common.Models;
using Imprink.Domain.Entities.Users;
+using Imprink.Domain.Models;
namespace Imprink.Domain.Repositories;
diff --git a/src/Imprink.Infrastructure/Repositories/Products/ProductRepository.cs b/src/Imprink.Infrastructure/Repositories/Products/ProductRepository.cs
index f0bf4c7..72fbc4a 100644
--- a/src/Imprink.Infrastructure/Repositories/Products/ProductRepository.cs
+++ b/src/Imprink.Infrastructure/Repositories/Products/ProductRepository.cs
@@ -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;
diff --git a/src/Imprink.Infrastructure/Repositories/Users/UserRepository.cs b/src/Imprink.Infrastructure/Repositories/Users/UserRepository.cs
index abf12d9..5ba761b 100644
--- a/src/Imprink.Infrastructure/Repositories/Users/UserRepository.cs
+++ b/src/Imprink.Infrastructure/Repositories/Users/UserRepository.cs
@@ -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;
diff --git a/src/Imprink.WebApi/Controllers/Products/ProductsController.cs b/src/Imprink.WebApi/Controllers/Products/ProductsController.cs
index da950d7..63eb0f6 100644
--- a/src/Imprink.WebApi/Controllers/Products/ProductsController.cs
+++ b/src/Imprink.WebApi/Controllers/Products/ProductsController.cs
@@ -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;
diff --git a/src/Imprink.WebApi/Controllers/Users/UserController.cs b/src/Imprink.WebApi/Controllers/Users/UserController.cs
index f41e26e..75fba37 100644
--- a/src/Imprink.WebApi/Controllers/Users/UserController.cs
+++ b/src/Imprink.WebApi/Controllers/Users/UserController.cs
@@ -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;
diff --git a/src/Imprink.WebApi/Filters/ValidationActionFilter.cs b/src/Imprink.WebApi/Filters/ValidationActionFilter.cs
new file mode 100644
index 0000000..ee678b9
--- /dev/null
+++ b/src/Imprink.WebApi/Filters/ValidationActionFilter.cs
@@ -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();
+
+ var validatorList = validators as IValidator[] ?? validators.ToArray();
+ if (validatorList.Length == 0) continue;
+ var validationContext = new ValidationContext