WebApi + ClientApp, GraphQL, Reflection
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
using Events.WebAPI.Contract.Command;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Event;
|
||||
|
||||
public class AddEventValidator : AbstractValidator<AddCommand<EventDTO, int>>
|
||||
{
|
||||
public AddEventValidator()
|
||||
{
|
||||
RuleFor(a => a.Dto.Name).NotEmpty().MaximumLength(150);
|
||||
RuleFor(a => a.Dto.EventDate).NotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using MobilityOne.Common.Commands;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Event;
|
||||
|
||||
public class DeleteEventValidator : AbstractValidator<DeleteCommand<EventDTO, int>>
|
||||
{
|
||||
public DeleteEventValidator(IMediator mediator)
|
||||
{
|
||||
RuleFor(a => a.Id).NoChildRecords<DeleteCommand<EventDTO, int>, RegistrationDTO, int>(nameof(RegistrationDTO.EventId), mediator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Events.WebAPI.Contract.Command;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Event;
|
||||
|
||||
public class UpdateEventValidator : AbstractValidator<UpdateCommand<EventDTO>>
|
||||
{
|
||||
public UpdateEventValidator()
|
||||
{
|
||||
RuleFor(a => a.Dto.Name).NotEmpty().MaximumLength(150);
|
||||
RuleFor(a => a.Dto.EventDate).NotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using Events.WebAPI.Contract.Queries.Generic;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation;
|
||||
|
||||
public static class ForeignKeyValueValidatorExtension
|
||||
{
|
||||
public static IRuleBuilderOptions<TCommand, TPK> ForeignKeyExists<TCommand, TDto, TPK>(
|
||||
this IRuleBuilder<TCommand, TPK> ruleBuilder,
|
||||
IMediator mediator,
|
||||
IValidationMessageProvider validationMessageProvider,
|
||||
ValidationMessage? validationMessage = null)
|
||||
where TDto : IHasIdAsPK<TPK>
|
||||
where TPK : IEquatable<TPK>
|
||||
{
|
||||
ValidationMessage message = validationMessage ?? validationMessageProvider.ForeignKeyNotFound("{PropertyName}");
|
||||
|
||||
return ruleBuilder.MustAsync(new ForeignKeyValueValidator<TCommand, TDto, TPK>(mediator).Validate)
|
||||
.WithMessage(message.Message)
|
||||
.WithErrorCode(message.Code);
|
||||
}
|
||||
|
||||
private class ForeignKeyValueValidator<TCommand, TDto, TPK> where TDto : IHasIdAsPK<TPK> where TPK : IEquatable<TPK>
|
||||
{
|
||||
private readonly IMediator mediator;
|
||||
|
||||
public ForeignKeyValueValidator(IMediator mediator)
|
||||
{
|
||||
this.mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<bool> Validate(TCommand command, TPK value, ValidationContext<TCommand> validationContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new DoesItemExistsQuery<TDto, TPK>(value);
|
||||
try
|
||||
{
|
||||
bool itemExists = await mediator.Send(query, cancellationToken);
|
||||
return itemExists;
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
validationContext.AddFailure(exc.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Events.WebAPI.Contract.Validation;
|
||||
|
||||
public interface IValidationMessageProvider
|
||||
{
|
||||
ValidationMessage UniqueSportName(string sportName);
|
||||
ValidationMessage UniquePersonDocumentAndCountry();
|
||||
ValidationMessage PersonEmailOrContactPhoneRequired();
|
||||
ValidationMessage UniqueRegistration();
|
||||
ValidationMessage EventNotFound();
|
||||
ValidationMessage PersonNotFound();
|
||||
ValidationMessage SportNotFound();
|
||||
ValidationMessage ForeignKeyNotFound(string propertyName);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Events.WebAPI.Contract.Queries.Generic;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation;
|
||||
|
||||
public static class NoChildRecordsValidatorExtension
|
||||
{
|
||||
public static IRuleBuilderOptions<TCommand, TPK> NoChildRecords<TCommand, TDto, TPK>(this IRuleBuilder<TCommand, TPK> ruleBuilder, string columnName, IMediator mediator)
|
||||
{
|
||||
return ruleBuilder.MustAsync(new NoChildRecordsValidator<TDto, TPK>(columnName, mediator).Validate)
|
||||
.WithMessage("Cannot delete entity {PropertyValue} because there are child records in table related to " + typeof(TDto).Name.ToString());
|
||||
}
|
||||
|
||||
private class NoChildRecordsValidator<TDto, TPK>
|
||||
{
|
||||
private readonly string columnName;
|
||||
private readonly IMediator mediator;
|
||||
|
||||
public NoChildRecordsValidator(string columnName, IMediator mediator)
|
||||
{
|
||||
this.columnName = columnName;
|
||||
this.mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<bool> Validate(TPK value, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new GetCountQuery<TDto>()
|
||||
{
|
||||
Filters = $"{columnName}=={value}"
|
||||
};
|
||||
int count = await mediator.Send(query, cancellationToken);
|
||||
return count == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Events.WebAPI.Contract.Command;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using Events.WebAPI.Contract.Validation;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Person;
|
||||
|
||||
public class AddPersonValidator : AbstractValidator<AddCommand<PersonDTO, int>>
|
||||
{
|
||||
public AddPersonValidator(IMediator mediator, IValidationMessageProvider validationMessageProvider)
|
||||
{
|
||||
var uniqueIndexValidator = new UniqueIndexValidator<PersonDTO, int>(
|
||||
mediator,
|
||||
(_, _) => validationMessageProvider.UniquePersonDocumentAndCountry(),
|
||||
t => t.DocumentNumber,
|
||||
t => t.CountryCode);
|
||||
|
||||
RuleFor(a => a.Dto.FirstName).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.LastName).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.FirstNameTranscription).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.LastNameTranscription).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.AddressLine).NotEmpty().MaximumLength(200);
|
||||
RuleFor(a => a.Dto.PostalCode).NotEmpty().MaximumLength(20);
|
||||
RuleFor(a => a.Dto.City).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.AddressCountry).NotEmpty().MaximumLength(100);
|
||||
When(a => !string.IsNullOrWhiteSpace(a.Dto.Email), () =>
|
||||
{
|
||||
RuleFor(a => a.Dto.Email).MaximumLength(255).EmailAddress();
|
||||
});
|
||||
RuleFor(a => a.Dto.ContactPhone).MaximumLength(50);
|
||||
RuleFor(a => a.Dto.BirthDate).NotEmpty();
|
||||
RuleFor(a => a.Dto.DocumentNumber).NotEmpty().MaximumLength(50);
|
||||
RuleFor(a => a.Dto.CountryCode).NotEmpty().MaximumLength(3);
|
||||
ValidationMessage emailOrContactPhoneRequired = validationMessageProvider.PersonEmailOrContactPhoneRequired();
|
||||
RuleFor(a => a.Dto)
|
||||
.Must(dto => !string.IsNullOrWhiteSpace(dto.Email) || !string.IsNullOrWhiteSpace(dto.ContactPhone))
|
||||
.WithMessage(emailOrContactPhoneRequired.Message)
|
||||
.WithErrorCode(emailOrContactPhoneRequired.Code);
|
||||
|
||||
RuleFor(a => a.Dto).CustomAsync(uniqueIndexValidator.Validate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using MobilityOne.Common.Commands;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Person;
|
||||
|
||||
public class DeletePersonValidator : AbstractValidator<DeleteCommand<PersonDTO, int>>
|
||||
{
|
||||
public DeletePersonValidator(IMediator mediator)
|
||||
{
|
||||
RuleFor(a => a.Id).NoChildRecords<DeleteCommand<PersonDTO, int>, RegistrationDTO, int>(nameof(RegistrationDTO.PersonId), mediator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Events.WebAPI.Contract.Command;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using Events.WebAPI.Contract.Validation;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Person;
|
||||
|
||||
public class UpdatePersonValidator : AbstractValidator<UpdateCommand<PersonDTO>>
|
||||
{
|
||||
public UpdatePersonValidator(IMediator mediator, IValidationMessageProvider validationMessageProvider)
|
||||
{
|
||||
var uniqueIndexValidator = new UniqueIndexValidator<PersonDTO, int>(
|
||||
mediator,
|
||||
(_, _) => validationMessageProvider.UniquePersonDocumentAndCountry(),
|
||||
t => t.DocumentNumber,
|
||||
t => t.CountryCode);
|
||||
|
||||
RuleFor(a => a.Dto.FirstName).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.LastName).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.FirstNameTranscription).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.LastNameTranscription).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.AddressLine).NotEmpty().MaximumLength(200);
|
||||
RuleFor(a => a.Dto.PostalCode).NotEmpty().MaximumLength(20);
|
||||
RuleFor(a => a.Dto.City).NotEmpty().MaximumLength(100);
|
||||
RuleFor(a => a.Dto.AddressCountry).NotEmpty().MaximumLength(100);
|
||||
When(a => !string.IsNullOrWhiteSpace(a.Dto.Email), () =>
|
||||
{
|
||||
RuleFor(a => a.Dto.Email).MaximumLength(255).EmailAddress();
|
||||
});
|
||||
RuleFor(a => a.Dto.ContactPhone).MaximumLength(50);
|
||||
RuleFor(a => a.Dto.BirthDate).NotEmpty();
|
||||
RuleFor(a => a.Dto.DocumentNumber).NotEmpty().MaximumLength(50);
|
||||
RuleFor(a => a.Dto.CountryCode).NotEmpty().MaximumLength(3);
|
||||
ValidationMessage emailOrContactPhoneRequired = validationMessageProvider.PersonEmailOrContactPhoneRequired();
|
||||
RuleFor(a => a.Dto)
|
||||
.Must(dto => !string.IsNullOrWhiteSpace(dto.Email) || !string.IsNullOrWhiteSpace(dto.ContactPhone))
|
||||
.WithMessage(emailOrContactPhoneRequired.Message)
|
||||
.WithErrorCode(emailOrContactPhoneRequired.Code);
|
||||
|
||||
RuleFor(a => a.Dto).CustomAsync(uniqueIndexValidator.ValidateExisting);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Events.WebAPI.Contract.Command;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Registration;
|
||||
|
||||
public class AddRegistrationValidator : AbstractValidator<AddCommand<RegistrationDTO, int>>
|
||||
{
|
||||
public AddRegistrationValidator(IMediator mediator, IValidationMessageProvider validationMessageProvider)
|
||||
{
|
||||
var uniqueValidator = new UniqueIndexValidator<RegistrationDTO, int>(
|
||||
mediator,
|
||||
(_, _) => validationMessageProvider.UniqueRegistration(),
|
||||
t => t.EventId,
|
||||
t => t.PersonId,
|
||||
t => t.SportId);
|
||||
|
||||
RuleFor(a => a.Dto.EventId).GreaterThan(0).ForeignKeyExists<AddCommand<RegistrationDTO, int>, EventDTO, int>(mediator, validationMessageProvider, validationMessageProvider.EventNotFound());
|
||||
RuleFor(a => a.Dto.PersonId).GreaterThan(0).ForeignKeyExists<AddCommand<RegistrationDTO, int>, PersonDTO, int>(mediator, validationMessageProvider, validationMessageProvider.PersonNotFound());
|
||||
RuleFor(a => a.Dto.SportId).GreaterThan(0).ForeignKeyExists<AddCommand<RegistrationDTO, int>, SportDTO, int>(mediator, validationMessageProvider, validationMessageProvider.SportNotFound());
|
||||
RuleFor(a => a.Dto).CustomAsync(uniqueValidator.Validate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using MobilityOne.Common.Commands;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Registration;
|
||||
|
||||
public class DeleteRegistrationValidator : AbstractValidator<DeleteCommand<RegistrationDTO, int>>
|
||||
{
|
||||
public DeleteRegistrationValidator(IMediator mediator)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Events.WebAPI.Contract.Command;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Registration;
|
||||
|
||||
public class UpdateRegistrationValidator : AbstractValidator<UpdateCommand<RegistrationDTO>>
|
||||
{
|
||||
public UpdateRegistrationValidator(IMediator mediator, IValidationMessageProvider validationMessageProvider)
|
||||
{
|
||||
var uniqueValidator = new UniqueIndexValidator<RegistrationDTO, int>(
|
||||
mediator,
|
||||
(_, _) => validationMessageProvider.UniqueRegistration(),
|
||||
t => t.EventId,
|
||||
t => t.PersonId,
|
||||
t => t.SportId);
|
||||
|
||||
RuleFor(a => a.Dto.EventId).GreaterThan(0).ForeignKeyExists<UpdateCommand<RegistrationDTO>, EventDTO, int>(mediator, validationMessageProvider, validationMessageProvider.EventNotFound());
|
||||
RuleFor(a => a.Dto.PersonId).GreaterThan(0).ForeignKeyExists<UpdateCommand<RegistrationDTO>, PersonDTO, int>(mediator, validationMessageProvider, validationMessageProvider.PersonNotFound());
|
||||
RuleFor(a => a.Dto.SportId).GreaterThan(0).ForeignKeyExists<UpdateCommand<RegistrationDTO>, SportDTO, int>(mediator, validationMessageProvider, validationMessageProvider.SportNotFound());
|
||||
RuleFor(a => a.Dto).CustomAsync(uniqueValidator.ValidateExisting);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Events.WebAPI.Contract.Command;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Sport;
|
||||
|
||||
public class AddSportValidator : AbstractValidator<AddCommand<SportDTO, int>>
|
||||
{
|
||||
public AddSportValidator(IMediator mediator, IValidationMessageProvider validationMessageProvider)
|
||||
{
|
||||
var uniqueIndexValidator = new UniqueIndexValidator<SportDTO, int>(
|
||||
mediator,
|
||||
(_, values) => validationMessageProvider.UniqueSportName(values[0]),
|
||||
t => t.Name);
|
||||
|
||||
RuleFor(a => a.Dto.Name).NotEmpty().MaximumLength(100).DependentRules(() =>
|
||||
RuleFor(a => a.Dto).CustomAsync(uniqueIndexValidator.Validate));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using MobilityOne.Common.Commands;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Sport;
|
||||
|
||||
public class DeleteSportValidator : AbstractValidator<DeleteCommand<SportDTO, int>>
|
||||
{
|
||||
public DeleteSportValidator(IMediator mediator)
|
||||
{
|
||||
RuleFor(a => a.Id).NoChildRecords<DeleteCommand<SportDTO, int>, RegistrationDTO, int>(nameof(RegistrationDTO.SportId), mediator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Events.WebAPI.Contract.Command;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation.Sport;
|
||||
|
||||
public class UpdateSportValidator : AbstractValidator<UpdateCommand<SportDTO>>
|
||||
{
|
||||
public UpdateSportValidator(IMediator mediator, IValidationMessageProvider validationMessageProvider)
|
||||
{
|
||||
var uniqueIndexValidator = new UniqueIndexValidator<SportDTO, int>(
|
||||
mediator,
|
||||
(_, values) => validationMessageProvider.UniqueSportName(values[0]),
|
||||
t => t.Name);
|
||||
|
||||
RuleFor(a => a.Dto.Name).NotEmpty().MaximumLength(100).DependentRules(() =>
|
||||
RuleFor(a => a.Dto).CustomAsync(uniqueIndexValidator.ValidateExisting));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using System.Linq.Expressions;
|
||||
using Events.WebAPI.Contract.Command;
|
||||
using Events.WebAPI.Contract.DTOs;
|
||||
using Events.WebAPI.Contract.Queries.Generic;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using MediatR;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation;
|
||||
|
||||
public class UniqueIndexValidator<TDto, TPK> where TDto : IHasIdAsPK<TPK>
|
||||
where TPK : IEquatable<TPK>
|
||||
{
|
||||
private readonly IMediator mediator;
|
||||
private readonly Expression<Func<TDto, object?>>[] selectors;
|
||||
private readonly Func<IReadOnlyList<string>, IReadOnlyList<string>, ValidationMessage>? errorMessageFactory;
|
||||
|
||||
public UniqueIndexValidator(
|
||||
IMediator mediator,
|
||||
Func<IReadOnlyList<string>, IReadOnlyList<string>, ValidationMessage>? errorMessageFactory = null,
|
||||
params Expression<Func<TDto, object?>>[] selectors)
|
||||
{
|
||||
this.mediator = mediator;
|
||||
this.errorMessageFactory = errorMessageFactory;
|
||||
this.selectors = selectors;
|
||||
}
|
||||
|
||||
public async Task Validate(string value, ValidationContext<AddCommand<TDto, TPK>> context, CancellationToken cancellationToken)
|
||||
{
|
||||
string columnName = GetColumnName(context);
|
||||
|
||||
var query = new GetCountQuery<TDto>
|
||||
{
|
||||
Filters = $"{columnName}==*{EscapeFilterValue(value)}"
|
||||
};
|
||||
|
||||
int count = await mediator.Send(query, cancellationToken);
|
||||
if (count > 0)
|
||||
{
|
||||
ValidationMessage validationMessage = BuildErrorMessage([columnName], [value]);
|
||||
context.AddFailure(new ValidationFailure(columnName, validationMessage.Message) { ErrorCode = validationMessage.Code });
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Validate(TDto dto, ValidationContext<AddCommand<TDto, TPK>> context, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new GetCountQuery<TDto>();
|
||||
|
||||
List<string> columnNames = [];
|
||||
List<string> values = [];
|
||||
List<string> filters = [];
|
||||
foreach (var selector in selectors)
|
||||
{
|
||||
string columnName = GetColumnName(selector);
|
||||
columnNames.Add(columnName);
|
||||
object? rawValue = selector.Compile().Invoke(dto);
|
||||
string value = FormatValue(rawValue);
|
||||
values.Add(value);
|
||||
filters.Add(BuildEqualsFilter(columnName, rawValue));
|
||||
}
|
||||
|
||||
query.Filters = string.Join(",", filters);
|
||||
|
||||
int count = await mediator.Send(query, cancellationToken);
|
||||
if (count > 0)
|
||||
{
|
||||
ValidationMessage validationMessage = BuildErrorMessage(columnNames, values);
|
||||
context.AddFailure(new ValidationFailure(context.PropertyPath, validationMessage.Message) { ErrorCode = validationMessage.Code });
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ValidateExisting(string value, ValidationContext<UpdateCommand<TDto>> context, CancellationToken cancellationToken)
|
||||
{
|
||||
string columnName = GetColumnName(context);
|
||||
|
||||
UpdateCommand<TDto> validatingObject = context.InstanceToValidate;
|
||||
var query = new GetItemsQuery<TDto>
|
||||
{
|
||||
Filters = $"{columnName}==*{EscapeFilterValue(value)}",
|
||||
Page = 1,
|
||||
PageSize = 2
|
||||
};
|
||||
|
||||
List<TDto> items = await mediator.Send(query, cancellationToken);
|
||||
if (items.Count > 0)
|
||||
{
|
||||
bool valueBelongsToValidatingItem = items.Any(item => item.Id.Equals(validatingObject.Dto.Id));
|
||||
if (!valueBelongsToValidatingItem)
|
||||
{
|
||||
ValidationMessage validationMessage = BuildErrorMessage([columnName], [value]);
|
||||
context.AddFailure(new ValidationFailure(columnName, validationMessage.Message) { ErrorCode = validationMessage.Code });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ValidateExisting(TDto dto, ValidationContext<UpdateCommand<TDto>> context, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new GetItemsQuery<TDto>();
|
||||
|
||||
List<string> columnNames = [];
|
||||
List<string> values = [];
|
||||
List<string> filters = [];
|
||||
foreach (var selector in selectors)
|
||||
{
|
||||
string columnName = GetColumnName(selector);
|
||||
columnNames.Add(columnName);
|
||||
object? rawValue = selector.Compile().Invoke(dto);
|
||||
string value = FormatValue(rawValue);
|
||||
values.Add(value);
|
||||
filters.Add(BuildEqualsFilter(columnName, rawValue));
|
||||
}
|
||||
|
||||
query.Filters = string.Join(",", filters);
|
||||
query.Page = 1;
|
||||
query.PageSize = 2;
|
||||
|
||||
List<TDto> items = await mediator.Send(query, cancellationToken);
|
||||
if (items.Count > 0)
|
||||
{
|
||||
bool valueBelongsToValidatingItem = items.Any(item => item.Id.Equals(dto.Id));
|
||||
if (!valueBelongsToValidatingItem)
|
||||
{
|
||||
ValidationMessage validationMessage = BuildErrorMessage(columnNames, values);
|
||||
context.AddFailure(new ValidationFailure(context.PropertyPath, validationMessage.Message) { ErrorCode = validationMessage.Code });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ValidationMessage BuildErrorMessage(IReadOnlyList<string> columnNames, IReadOnlyList<string> values)
|
||||
{
|
||||
if (errorMessageFactory != null)
|
||||
return errorMessageFactory(columnNames, values);
|
||||
|
||||
return columnNames.Count == 1
|
||||
? new ValidationMessage(ValidationErrorCodes.UniqueConstraintViolation, $"{columnNames[0]} must be unique. Value {values[0]} has been already used!")
|
||||
: new ValidationMessage(ValidationErrorCodes.UniqueConstraintViolation, $"n-tuple ({string.Join(", ", columnNames)}) = ({string.Join(", ", values)}) must be unique.");
|
||||
}
|
||||
|
||||
private string GetColumnName(Expression<Func<TDto, object?>> expression)
|
||||
{
|
||||
Expression body = expression.Body;
|
||||
if (body is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert)
|
||||
body = unaryExpression.Operand;
|
||||
|
||||
if (body is MemberExpression memberExpression)
|
||||
return memberExpression.Member.Name;
|
||||
|
||||
if (body is MethodCallExpression methodCallExpression && methodCallExpression.Object is MemberExpression objectMemberExpression)
|
||||
return objectMemberExpression.Member.Name;
|
||||
|
||||
throw new Exception($"Invalid nodetype ({body.NodeType}) in expression");
|
||||
}
|
||||
|
||||
private string GetColumnName<T>(ValidationContext<T> context)
|
||||
{
|
||||
if (selectors.Length != 1)
|
||||
throw new Exception($"Unique index contains several columns, and must not be called on a single property {context.PropertyPath}");
|
||||
|
||||
Expression body = selectors[0].Body;
|
||||
if (body is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert)
|
||||
body = unaryExpression.Operand;
|
||||
|
||||
if (body is not MemberExpression memberExpression)
|
||||
throw new Exception($"Invalid nodetype ({body.NodeType}) in expression");
|
||||
|
||||
string columnName = memberExpression.Member.Name;
|
||||
if (columnName != context.PropertyPath.Replace(nameof(UpdateCommand<TDto>.Dto) + ".", ""))
|
||||
throw new Exception($"Unique index is defined on {columnName} but called on {context.PropertyPath}");
|
||||
|
||||
return columnName;
|
||||
}
|
||||
|
||||
private static string EscapeFilterValue(string value)
|
||||
{
|
||||
string escaped = value
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace(",", "\\,")
|
||||
.Replace("|", "\\|");
|
||||
|
||||
return string.Equals(escaped, "null", StringComparison.Ordinal)
|
||||
? "\\null"
|
||||
: escaped;
|
||||
}
|
||||
|
||||
private static string BuildEqualsFilter(string columnName, object? value)
|
||||
{
|
||||
string formattedValue = FormatValue(value);
|
||||
return value is string
|
||||
? $"{columnName}==*{EscapeFilterValue(formattedValue)}"
|
||||
: $"{columnName}=={formattedValue}";
|
||||
}
|
||||
|
||||
private static string FormatValue(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => "null",
|
||||
string stringValue => stringValue,
|
||||
DateOnly dateOnlyValue => dateOnlyValue.ToString("yyyy-MM-dd"),
|
||||
DateTime dateTimeValue => dateTimeValue.ToString("O"),
|
||||
bool boolValue => boolValue ? "true" : "false",
|
||||
IFormattable formattable => formattable.ToString(null, System.Globalization.CultureInfo.InvariantCulture),
|
||||
_ => value.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace Events.WebAPI.Contract.Validation;
|
||||
|
||||
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IBaseRequest
|
||||
{
|
||||
private readonly IEnumerable<IValidator<TRequest>> validators;
|
||||
|
||||
public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
|
||||
{
|
||||
this.validators = validators;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
if (validators.Any())
|
||||
{
|
||||
var context = new ValidationContext<TRequest>(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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Events.WebAPI.Contract.Validation;
|
||||
|
||||
public static class ValidationErrorCodes
|
||||
{
|
||||
public const string UniqueConstraintViolation = "unique_constraint_violation";
|
||||
public const string SportNameNotUnique = "sport_name_not_unique";
|
||||
public const string PersonDocumentCountryNotUnique = "person_document_country_not_unique";
|
||||
public const string PersonEmailOrContactPhoneRequired = "person_email_or_contact_phone_required";
|
||||
public const string RegistrationNotUnique = "registration_not_unique";
|
||||
public const string ForeignKeyNotFound = "foreign_key_not_found";
|
||||
public const string EventNotFound = "event_not_found";
|
||||
public const string PersonNotFound = "person_not_found";
|
||||
public const string SportNotFound = "sport_not_found";
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Events.WebAPI.Contract.Validation;
|
||||
|
||||
public sealed record ValidationMessage(string Code, string Message);
|
||||
Reference in New Issue
Block a user