Skip to content

Commit

Permalink
Add a way to do dynamic validation with fluentvalidations
Browse files Browse the repository at this point in the history
  • Loading branch information
Barsonax committed May 9, 2024
1 parent 8e3e414 commit e0a9e6c
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Linq.Expressions;
using System.Reflection;

namespace CleanAspCore.Extensions.FluentValidation;

public static class FluentValidationExtensions
{
public static void ValidateNullableReferences<TModel>(this AbstractValidator<TModel> validator)
{
IEnumerable<PropertyInfo> properties = GetNonNullableProperties<TModel>(new NullabilityInfoContext());

validator.ApplyRuleToProperties(new GenericNotNullOrEmptyRule(), properties);
}

private static IEnumerable<PropertyInfo> GetNonNullableProperties<TModel>(NullabilityInfoContext nullabilityInfoContext) =>
typeof(TModel)
.GetProperties()
.Where(x => nullabilityInfoContext.Create(x).WriteState == NullabilityState.NotNull);

private static void ApplyRuleInternal<TModel, TProperty>(AbstractValidator<TModel> validator, IGenericRule rule, PropertyInfo propertyInfo)
{
var builder = CreateRuleBuilder<TModel, TProperty>(validator, propertyInfo);
rule.ApplyRule(builder);
}

private static IRuleBuilderInitial<TModel, TProperty> CreateRuleBuilder<TModel, TProperty>(AbstractValidator<TModel> validator, PropertyInfo propertyInfo)
{
ArgumentNullException.ThrowIfNull(validator);
ArgumentNullException.ThrowIfNull(propertyInfo);

ParameterExpression entityParam = Expression.Parameter(typeof(TModel), "x");
Expression columnExpr = Expression.Property(entityParam, propertyInfo);

return validator.RuleFor(Expression.Lambda<Func<TModel, TProperty>>(columnExpr, entityParam));
}

public static void ApplyRuleToProperties<TModel>(this AbstractValidator<TModel> validator, IGenericRule rule, IEnumerable<PropertyInfo> properties)
{
foreach (PropertyInfo property in properties)
{
validator.ApplyRuleToProperty(rule, property);
}
}

public static void ApplyRuleToProperty<TModel>(this AbstractValidator<TModel> validator, IGenericRule rule, PropertyInfo property)
{
MethodInfo methodInfo = typeof(FluentValidationExtensions).GetMethod(nameof(ApplyRuleInternal), BindingFlags.Static | BindingFlags.NonPublic)!;
Type[] argumentTypes = [typeof(TModel), property.PropertyType];
MethodInfo genericMethod = methodInfo.MakeGenericMethod(argumentTypes);
genericMethod.Invoke(null, [validator, rule, property]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CleanAspCore.Extensions.FluentValidation;

public class GenericNotNullOrEmptyRule : IGenericRule
{
public void ApplyRule<T, TProperty>(IRuleBuilderInitial<T, TProperty> builder) => builder.NotNull().NotEmpty();
}
6 changes: 6 additions & 0 deletions CleanAspCore/Extensions/FluentValidation/IGenericRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CleanAspCore.Extensions.FluentValidation;

public interface IGenericRule
{
public void ApplyRule<T, TProperty>(IRuleBuilderInitial<T, TProperty> builder);
}
4 changes: 2 additions & 2 deletions CleanAspCore/Features/Departments/Endpoints/AddDepartments.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CleanAspCore.Data;
using CleanAspCore.Data.Models;
using CleanAspCore.Extensions.FluentValidation;
using Microsoft.AspNetCore.Http.HttpResults;

namespace CleanAspCore.Features.Departments.Endpoints;
Expand Down Expand Up @@ -33,8 +34,7 @@ private sealed class CreateDepartmentRequestValidator : AbstractValidator<Create
{
public CreateDepartmentRequestValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.City).NotEmpty();
this.ValidateNullableReferences();
}
}
}
7 changes: 4 additions & 3 deletions CleanAspCore/Features/Employees/Endpoints/AddEmployee.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CleanAspCore.Data;
using CleanAspCore.Data.Models;
using CleanAspCore.Extensions.FluentValidation;
using Microsoft.AspNetCore.Http.HttpResults;

namespace CleanAspCore.Features.Employees.Endpoints;
Expand Down Expand Up @@ -42,9 +43,9 @@ private sealed class CreateEmployeeRequestValidator : AbstractValidator<CreateEm
{
public CreateEmployeeRequestValidator()
{
RuleFor(x => x.FirstName).NotEmpty();
RuleFor(x => x.LastName).NotEmpty();
RuleFor(x => x.Email).EmailAddress().NotNull();
this.ValidateNullableReferences();

RuleFor(x => x.Email).EmailAddress();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using CleanAspCore.Data;
using CleanAspCore.Data.Extensions;
using CleanAspCore.Data.Models;
using CleanAspCore.Extensions.FluentValidation;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using NotFound = Microsoft.AspNetCore.Http.HttpResults.NotFound;
Expand Down Expand Up @@ -46,6 +47,8 @@ public class UpdateEmployeeRequestValidator : AbstractValidator<UpdateEmployeeRe
{
public UpdateEmployeeRequestValidator()
{
this.ValidateNullableReferences();

RuleFor(x => x.Email).EmailAddress();
}
}
3 changes: 2 additions & 1 deletion CleanAspCore/Features/Jobs/Endpoints/AddJobs.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CleanAspCore.Data;
using CleanAspCore.Data.Models;
using CleanAspCore.Extensions.FluentValidation;
using Microsoft.AspNetCore.Http.HttpResults;

namespace CleanAspCore.Features.Jobs.Endpoints;
Expand Down Expand Up @@ -31,7 +32,7 @@ private sealed class CreateJobRequestValidator : AbstractValidator<CreateJobRequ
{
public CreateJobRequestValidator()
{
RuleFor(x => x.Name).NotEmpty();
this.ValidateNullableReferences();
}
}
}

0 comments on commit e0a9e6c

Please sign in to comment.