Как я могу использовать IValidatableObject?
Я это понимаю IValidatableObject
используется для проверки объекта таким образом, что давайте сравнивать свойства друг с другом.
Я все еще хотел бы иметь атрибуты для проверки отдельных свойств, но в некоторых случаях я хочу игнорировать сбои некоторых свойств.
Я пытаюсь использовать это неправильно в случае ниже? Если нет, то как мне это реализовать?
public class ValidateMe : IValidatableObject
{
[Required]
public bool Enable { get; set; }
[Range(1, 5)]
public int Prop1 { get; set; }
[Range(1, 5)]
public int Prop2 { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!this.Enable)
{
/* Return valid result here.
* I don't care if Prop1 and Prop2 are out of range
* if the whole object is not "enabled"
*/
}
else
{
/* Check if Prop1 and Prop2 meet their range requirements here
* and return accordingly.
*/
}
}
}
8 ответов
Прежде всего, спасибо @paper1337 за то, что он указал мне на нужные ресурсы... Я не зарегистрирован, поэтому я не могу голосовать за него, пожалуйста, сделайте это, если кто-нибудь еще прочитает это.
Вот как выполнить то, что я пытался сделать.
Допустимый класс:
public class ValidateMe : IValidatableObject
{
[Required]
public bool Enable { get; set; }
[Range(1, 5)]
public int Prop1 { get; set; }
[Range(1, 5)]
public int Prop2 { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (this.Enable)
{
Validator.TryValidateProperty(this.Prop1,
new ValidationContext(this, null, null) { MemberName = "Prop1" },
results);
Validator.TryValidateProperty(this.Prop2,
new ValidationContext(this, null, null) { MemberName = "Prop2" },
results);
// some other random test
if (this.Prop1 > this.Prop2)
{
results.Add(new ValidationResult("Prop1 must be larger than Prop2"));
}
}
return results;
}
}
С помощью Validator.TryValidateProperty()
добавит в коллекцию результатов, если будут неудачные проверки. Если не удалось выполнить проверку, ничего не будет добавлено в коллекцию результатов, что свидетельствует об успехе.
Выполнение проверки:
public void DoValidation()
{
var toValidate = new ValidateMe()
{
Enable = true,
Prop1 = 1,
Prop2 = 2
};
bool validateAllProperties = false;
var results = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(
toValidate,
new ValidationContext(toValidate, null, null),
results,
validateAllProperties);
}
Важно установить validateAllProperties
ложно, чтобы этот метод работал. когда validateAllProperties
ложь только свойства с [Required]
атрибут проверен. Это позволяет IValidatableObject.Validate()
Метод обработки условных проверок.
Цитата из сообщения Джеффа Хэндли в блоге об объектах и свойствах проверки с помощью Validator:
При проверке объекта в Validator.ValidateObject применяется следующий процесс:
- Проверьте атрибуты уровня свойства
- Если какие-либо валидаторы недействительны, прервите валидацию, возвращая ошибки
- Проверьте атрибуты уровня объекта
- Если какие-либо валидаторы недействительны, прервите валидацию, возвращая ошибки
- Если на платформе рабочего стола и объект реализует IValidatableObject, то вызовите его метод Validate и верните любые ошибки
Это указывает на то, что то, что вы пытаетесь сделать, не будет работать "из коробки", потому что проверка будет прервана на шаге № 2. Вы можете попытаться создать атрибуты, которые наследуются от встроенных, и, в частности, проверить наличие включенного свойства (через интерфейс) перед выполнением их обычной проверки. В качестве альтернативы, вы можете поместить всю логику для проверки сущности в Validate
метод.
Просто добавлю пару пунктов:
Поскольку Validate()
возврат сигнатуры метода IEnumerable<>
, тот yield return
может использоваться для ленивого генерирования результатов - это полезно, если некоторые из проверочных проверок являются интенсивными операциями ввода-вывода или ЦП.
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (this.Enable)
{
// ...
if (this.Prop1 > this.Prop2)
{
yield return new ValidationResult("Prop1 must be larger than Prop2");
}
Кроме того, если вы используете MVC ModelState
можно преобразовать ошибки результата проверки в ModelState
введите следующие записи (это может быть полезно, если вы выполняете проверку в пользовательском связывателе модели):
var resultsGroupedByMembers = validationResults
.SelectMany(vr => vr.MemberNames
.Select(mn => new { MemberName = mn ?? "",
Error = vr.ErrorMessage }))
.GroupBy(x => x.MemberName);
foreach (var member in resultsGroupedByMembers)
{
ModelState.AddModelError(
member.Key,
string.Join(". ", member.Select(m => m.Error)));
}
Я реализовал абстрактный класс общего использования для проверки
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace App.Abstractions
{
[Serializable]
abstract public class AEntity
{
public int Id { get; set; }
public IEnumerable<ValidationResult> Validate()
{
var vResults = new List<ValidationResult>();
var vc = new ValidationContext(
instance: this,
serviceProvider: null,
items: null);
var isValid = Validator.TryValidateObject(
instance: vc.ObjectInstance,
validationContext: vc,
validationResults: vResults,
validateAllProperties: true);
/*
if (true)
{
yield return new ValidationResult("Custom Validation","A Property Name string (optional)");
}
*/
if (!isValid)
{
foreach (var validationResult in vResults)
{
yield return validationResult;
}
}
yield break;
}
}
}
Проблема с принятым ответом состоит в том, что теперь от вызывающей стороны зависит правильность проверки объекта. Я бы либо удалил RangeAttribute и выполнил валидацию диапазона внутри метода Validate, либо я бы создал собственный атрибут подкласса RangeAttribute, который принимает имя обязательного свойства в качестве аргумента в конструкторе.
Например:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
class RangeIfTrueAttribute : RangeAttribute
{
private readonly string _NameOfBoolProp;
public RangeIfTrueAttribute(string nameOfBoolProp, int min, int max) : base(min, max)
{
_NameOfBoolProp = nameOfBoolProp;
}
public RangeIfTrueAttribute(string nameOfBoolProp, double min, double max) : base(min, max)
{
_NameOfBoolProp = nameOfBoolProp;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(_NameOfBoolProp);
if (property == null)
return new ValidationResult($"{_NameOfBoolProp} not found");
var boolVal = property.GetValue(validationContext.ObjectInstance, null);
if (boolVal == null || boolVal.GetType() != typeof(bool))
return new ValidationResult($"{_NameOfBoolProp} not boolean");
if ((bool)boolVal)
{
return base.IsValid(value, validationContext);
}
return null;
}
}
Реализуйте логику проверки, используя IValidatableObject или проверку на уровне свойства (атрибуты), а не используйте класс System.ComponentModel.DataAnnotations.Validator, подобный этому.
var validationContext = new ValidationContext(model,, null, null);
var validations = new Collection<ValidationResult>();
Validator.TryValidaObject(model, validationContext, validations, true)
любые ошибки должны присутствовать в коллекции валидаций (свойство ErrorMessage не должно быть пустым).
Что мне не нравится в iValidate, так это то, что он запускается только ПОСЛЕ всех остальных проверок.
Кроме того, по крайней мере на нашем сайте, он будет запускаться снова при попытке сохранения. Я бы посоветовал вам просто создать функцию и поместить в нее весь код проверки. В качестве альтернативы для веб-сайтов вы можете иметь свою "специальную" проверку в контроллере после создания модели. Пример:
public ActionResult Update([DataSourceRequest] DataSourceRequest request, [Bind(Exclude = "Terminal")] Driver driver)
{
if (db.Drivers.Where(m => m.IDNumber == driver.IDNumber && m.ID != driver.ID).Any())
{
ModelState.AddModelError("Update", string.Format("ID # '{0}' is already in use", driver.IDNumber));
}
if (db.Drivers.Where(d => d.CarrierID == driver.CarrierID
&& d.FirstName.Equals(driver.FirstName, StringComparison.CurrentCultureIgnoreCase)
&& d.LastName.Equals(driver.LastName, StringComparison.CurrentCultureIgnoreCase)
&& (driver.ID == 0 || d.ID != driver.ID)).Any())
{
ModelState.AddModelError("Update", "Driver already exists for this carrier");
}
if (ModelState.IsValid)
{
try
{
Мне понравился ответ Cocogza за исключением того, что вызов base.IsValid привел к исключению переполнения стека, так как он будет снова и снова вводить метод IsValid. Поэтому я изменил его, чтобы он соответствовал определенному типу проверки, в моем случае это был адрес электронной почты.
[AttributeUsage(AttributeTargets.Property)]
class ValidEmailAddressIfTrueAttribute : ValidationAttribute
{
private readonly string _nameOfBoolProp;
public ValidEmailAddressIfTrueAttribute(string nameOfBoolProp)
{
_nameOfBoolProp = nameOfBoolProp;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (validationContext == null)
{
return null;
}
var property = validationContext.ObjectType.GetProperty(_nameOfBoolProp);
if (property == null)
{
return new ValidationResult($"{_nameOfBoolProp} not found");
}
var boolVal = property.GetValue(validationContext.ObjectInstance, null);
if (boolVal == null || boolVal.GetType() != typeof(bool))
{
return new ValidationResult($"{_nameOfBoolProp} not boolean");
}
if ((bool)boolVal)
{
var attribute = new EmailAddressAttribute {ErrorMessage = $"{value} is not a valid e-mail address."};
return attribute.GetValidationResult(value, validationContext);
}
return null;
}
}
Это работает намного лучше! Он не падает и выдает хорошее сообщение об ошибке. Надеюсь, это поможет кому-то!