Рекурсивный список FluentValidation вызывает переполнение стека

#c# #fluentvalidation

#c# #fluentvalidation

Вопрос:

Я использую FluentValidation.AspNetCore 8.2.2 и имеют объектную модель, которая содержит список дочерних элементов того же типа. Я хотел бы использовать fluent validation для проверки объекта. При попытке установить средство проверки для дочернего объекта я либо сталкиваюсь с исключением переполнения стека, и / или коллекция изменилась (типичная проблема с циклом foreach).

Чтобы протестировать и найти разрешение, я настроил простой проект библиотеки классов .net core с модульным тестированием.

Базовая модель

 using FluentValidation;

public class BaseModelItem
    {
        public int ItemId { get; set; }

        public string Name { get; set; }

        private List<BaseModelItem> ChildItems { get; set; }
    }

 public class BaseModelItemValidator : AbstractValidator<BaseModelItem>
    {
        public BaseModelItemValidator()
        {
            RuleFor(i => i.ItemId).GreaterThanOrEqualTo(0).WithMessage("Item id may not be negative.");
            RuleFor(i => i.Name).NotNull().NotEmpty().WithMessage("Item name cannot be empty.");
            RuleFor(i => i.ChildItems).ForEach(i => i.SetValidator(new BaseModelItemValidator()));
        }

    }
  

модульный тест

  public class Tests
    {
       [Test]
        public void Test_Name_Cannot_Null()
        {
            var item = new BaseModelItem
            {
                ItemId = 2,
                Name = null,
                ChildItems = new List<BaseModelItem>()
            };
            var validator = new BaseModelItemValidator();
            validator.ShouldHaveValidationErrorFor(t => t.Name, item);
            Assert.Pass();
        }
}
  

Этот тест вызовет исключение переполнения стека.
Я пробовал использовать резервные поля, инициализировать или даже изменять массив.
Я могу успешно отменить исключение потока стекирования с помощью пользовательского средства проверки.

  public class BaseModelItemValidator : AbstractValidator<BaseModelItem>
    {
        public BaseModelItemValidator()
        {
            RuleFor(i => i.ItemId).GreaterThanOrEqualTo(0).WithMessage("Item id may not be negative.");
            RuleFor(i => i.Name).NotNull().NotEmpty().WithMessage("Item name cannot be empty.");
            RuleFor(i => i.ChildItems).Must(BeValidChildItemList);
        }
        private bool BeValidChildItemList(List<BaseModelItem> list)
        {
            if (list.Count > 0)
            {
                RuleFor(i => i.ChildItems).ForEach(i => i.SetValidator(new BaseModelItemValidator()));

            }
            return true;
        }
    }
  

Позволяет ему проверять объекты без дочерних элементов.
Однако, если вы запускаете тест с заполненными дочерними объектами, я получаю сообщение об ошибке «Коллекция была изменена; операция перечисления может не выполняться».
Трассировка стека

 StackTrace:
   at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
   at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
   at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.MoveNext()
   at System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()
   at FluentValidation.AbstractValidator`1.Validate(ValidationContext`1 context) in ****FluentValidationsrcFluentValidationAbstractValidator.cs:line 115
   at FluentValidation.DefaultValidatorExtensions.Validate[T](IValidator`1 validator, T instance, IValidatorSelector selector, String ruleSet) in ******FluentValidationsrcFluentValidationDefaultValidatorExtensions.cs:line 876
   at FluentValidation.TestHelper.ValidationTestExtension.TestValidate[T,TValue](IValidator`1 validator, Expression`1 expression, T instanceToValidate, TValue value, String ruleSet, Boolean setProperty) in ******FluentValidationsrcFluentValidationTestHelperValidatorTestExtensions.cs:line 101
   at FluentValidation.TestHelper.ValidationTestExtension.ShouldHaveValidationErrorFor[T,TValue](IValidator`1 validator, Expression`1 expression, T objectToTest, String ruleSet) in *******FluentValidationsrcFluentValidationTestHelperValidatorTestExtensions.cs:line 40
   at Tests.Tests.Test_Name_Cannot_Null_Nested() in FluentValidationChildernFluentValidationChildern.TestsUnitTest1.cs:line 55

  

Я не могу найти приемлемое решение.

Ответ №1:

Хотя я не смог заставить Fluent работать с использованием метода setValidator, у меня есть обходной путь, который работает и может быть улучшен.

в дочернем списке, который я установил, используйте метод ‘Must’, а затем реализуйте ручную функцию для перебора дочерних элементов и создания объекта validator вручную и проверки результата.

  public class BaseModelItemValidator : AbstractValidator<BaseModelItem>
    {
        public BaseModelItemValidator()
        {
            RuleFor(i => i.ItemId).GreaterThanOrEqualTo(0).WithMessage("Item id may not be negative.");
            RuleFor(i => i.Name).NotNull().NotEmpty().WithMessage("Item name cannot be empty.");
            RuleFor(i => i.ChildItems).Must(BeValidChildItemList);
        }
        private bool BeValidChildItemList(List<BaseModelItem> list)
        {
            if (list == null || list.Count == 0) return true;
            foreach (var child in list)
            {
                var validator = new BaseModelItemValidator();
                var validatorResults = validator.Validate(child);
                if (!validatorResults.IsValid)
                {
                    return false;
                }
            }
            return true;
        }
    }
  

Ответ №2:

У меня было аналогичное требование, когда мне приходилось проверять объект, у которого был список объектов того же типа, что и дочерние. Что я сделал, так это переопределил метод Validate моего валидатора и добавил логику что-то вроде

 public override ValidationResult Validate(ValidationContext<BaseModelItem> context)
{
    var result = base.Validate(context);
    var obj = context.InstanceToValidate;

    if (obj.ChildItems != null amp;amp; obj.ChildItems.Count > 0)
    {
        foreach (var item in obj.ChildItems)
        {
            var childResult = base.Validate(item);
            foreach (var error in childResult.Errors)
            {
                result.Errors.Add(error);
            }
        }
    }

    return resu<
}
  

Я бы хотел как-то использовать setValidator, но не смог найти способ сделать это. Но в любом случае, это переопределение возвращает объект ValidationResult, который я ожидаю

Ответ №3:

Казалось бы, решение состоит в том, чтобы использовать один и тот же экземпляр средства проверки, подобный этому:

 public class BaseModelItemValidator : AbstractValidator<BaseModelItem>
{
    public BaseModelItemValidator()
    {
        RuleFor(i => i.ItemId).GreaterThanOrEqualTo(0).WithMessage("Item id may not be negative.");
        RuleFor(i => i.Name).NotNull().NotEmpty().WithMessage("Item name cannot be empty.");
        RuleFor(i => i.ChildItems).ForEach(i => i.SetValidator(this));
    }

}
  

Я могу подтвердить, что это работает для меня. Заслуга в этой проблеме