Например, пусть в нашем приложении есть сущность «Контактная информация», состоящая из атрибутов «Город», «Улица», «Дом», «Квартира», «Номер телефона». Тогда для нее могли бы действовать такие правила:

  1. Значения атрибутов «Город», «Улица» и «Дом» не должны быть пустыми.
  2. Значение атрибута «Дом» начинается с цифр, после которых может идти буква (например, «8», «52б»).
  3. «Квартира» — число в диапазоне от 1 до 1000.
  4. «Номер телефона» состоит из цифр.

Обычно проверка таких правил реализуется сразу на нескольких уровнях системы.

  1. На уровне GUI с помощью проверок ввода или элементов управления — валидаторов.
  2. В коде приложения — на прикладном уровне.
  3. В схеме базы данных — с помощью ограничений (constraints).

Проверки на разных уровнях системы одинаково важны, однако в данной статье речь пойдет только о проверке на прикладном уровне. На этом уровне сущности предметной области представляются классами, а атрибуты сущностей — свойствами. Здесь возможны два подхода к выполнению проверок:

  • превентивный, когда в методе — мутаторе свойства осуществляется проверка на допустимость присваиваемого значения и в случае нарушения правил генерируется исключение;
  • проверка значений всех или нескольких свойств в отдельном методе.

Первый подход более привлекателен — он позволяет вовсе не допускать нарушения правил, так как проверка выполняется в момент присвоения значения свойству и в отличие от второго подхода происходит обязательно (отдельный метод проверки можно забыть вызвать). Однако его применение не всегда возможно — он не годится, если объект какое-то время должен находиться в рассогласованном состоянии или заполнение объекта данными и их проверка выполняются в разных частях кода. В такой ситуации не обойтись без специального метода проверки. Для нашего примера он мог бы выглядеть следующим образом (здесь и далее примеры кода приведены на языке C# 3.0):

public class ContactInfo
{
public string City { get; set; }
public string Street { get; set; }
public string House { get; set; }
public int? Apartment { get; set; }
public string Phone { get; set; }
public bool Validate()
{
if (string.IsNullOrEmpty(City))
return false;
if (string.IsNullOrEmpty(Street))
return false;
if (string.IsNullOrEmpty(House))
return false;
// Метод-расширение Match проверяет
// строку
// на соответствие регулярному
// выражению
if (!House.Match(@ ”[1-9]d*[а-я]
{0,1}”)) return false;
if (Apartment.HasValue &&
Apartment < 1 &&
Apartment > 1000)
return false;
if (!string.IsNullOrEmpty(Phone) &&
!Phone.Match(@”[1-9]d{0,2}-d{2}-
d{2}”))
return false;
return true;
}
}

Как правило, метод проверки большей частью состоит из проверок значений отдельных свойств. В основном это проверки на пустые значения, попадание числа в определенный диапазон, проверка длины/формата строки и т.п. Их описание в коде имеет следующие недостатки:

  • код проверки значений свойств отделен от определения свойств;
  • одинаковый код проверок повторяется вновь и вновь (меняются лишь граничные значения, форматы);
  • проверка выполняется в императивном стиле, что затрудняет понимание кода.

Было бы удобно осуществлять проверки путем задания ограничений на значения свойств в декларативном стиле непосредственно в определении свойства. На платформе .NET для этого годятся пользовательские атрибуты (custom attributes).

Реализация

Сами по себе атрибуты к проверке не способны, нужен код, использующий атрибуты для ее выполнения. Таким образом, наша библиотека будет состоять из двух частей:

  1. Атрибуты-ограничения для проверки значений свойств. Они будут задавать вид ограничения, его параметры и проводить проверку значения.
  2. Метод, реализующий алгоритм проверки. Он будет извлекать из метаданных информацию об атрибутах-ограничениях, получать значения свойств и делегировать проверку атрибутам.

Атрибуты

Для начала введем базовый класс для всех атрибутов-ограничений:

using System;
[AttributeUsage(AttributeTargets.Property)]
public abstract class BaseValidationAttribute:
Attribute
{
public abstract bool IsValid(object value);
}

Метод IsValid будет перегружаться производными классами для выполнения проверки значения свойства на соответствие заданному ограничению.

Не будем приводить реализацию всех атрибутов-ограничений, остановимся лишь на двух — проверке строки на пустоту и проверке на соответствие регулярному выражению:

public class NotEmptyAttribute:
BaseValidationAttribute
{
public override bool IsValid(object value)
{
var str = value as string;
return string.IsNullOrEmpty(str);
}
}
public class RegExAttribute:
BaseValidationAttribute
{
private readonly Regex _regexValidator;
public RegExAttribute(string pattern)
{
_regexValidator = new Regex(pattern,
RegexOptions.Compiled);
}
public override bool IsValid(object value)
{
if (value == null) return true;
var str = value.ToString();
var match = _regexValidator.Match(str);
return match.Success &&
match.Index == 0 &&
match.Length == str.Length;
}
}

Аналогично можно определить классы для проверки на null, минимального/максимального значения и т.п. Теперь наш класс ContactInfo мог бы выглядеть так:

public class ContactInfo
{
[NotEmpty]
public string City { get; set; }
[NotEmpty]
public string Street { get; set; }
[NotEmpty, RegEx(@”[1-9]d*[а-я]{0,1}”)]
public string House { get; set; }
[MinValue(1), MaxValue(1000)]
public int? Apartment { get; set; }
[RegEx(@”[1-9]d{0,2}-d{2}-d{2}”)]
public string Phone { get; set; }
}

Как видите, определение класса стало намного короче и гораздо нагляднее.

Выполнение проверки

Сами по себе атрибуты бесполезны, необходим код, который будет эти атрибуты использовать. Начнем со вспомогательного класса, хранящего информацию о методе для получения значения свойства и перечень атрибутов-ограничений этого свойства:

internal sealed class PropertyValidator
{
private readonly MethodInfo
_property;
private readonly BaseValidationAttribute[]
_attributes;
public PropertyValidator(PropertyInfo
property)
{
_property = property.GetGetMethod();
_attributes = (BaseValidationAttribute[])property
.GetCustomAttributes(typeof(BaseVali dationAttribute),
true);
}
public bool Validate(object obj)
{
var propValue = _property.Invoke(obj, null);
return _attributes.All(attr => attr. IsValid(propValue));
}
}

Конструктор класса PropertyValidator из описания свойства извлекает информацию о методе получения значения свойства и всех атрибутах-ограничениях, связанных со свойством, а метод IsValid проверяет значение свойства переданного объекта на соответствие заданным ограничениям.

Также нам необходим код — такой, чтобы с помощью механизма отражения (reflection) он получал для типа проверяемого объекта информацию о тех его свойствах, на которые наложены ограничения с помощью атрибутов. Поскольку отражение работает довольно медленно, эта информация будет кэшироваться:

internal static class TypeValidationExtensions
{
private static readonly ReaderWriterLockSlim
cacheLock = new ReaderWriterLockSlim();
private static readonly
Dictionary
cache = new Dictionary PropertyValidator[]>();
internal static PropertyValidator[]
GetValidators(this object obj)
{
var type = obj.GetType();
PropertyValidator[] validators;
cacheLock.EnterReadLock();
cache.TryGetValue(type, out validators);
cacheLock.ExitReadLock();
if (validators != null)
return validators;
validators = type.GetValidators();
cacheLock.EnterWriteLock();
if (!cache.ContainsKey(type))
cache.Add(type, validators);
cacheLock.ExitWriteLock();
return validators;
}
private static PropertyValidator[]
GetValidators(this Type type)
{
return type.GetProperties()
.Where(IsPropertyValidatable)
.Select(prop => new PropertyValidator(prop))
.ToArray();
}
private static bool IsPropertyValidatable(PropertyInfo prop)
{
return prop.CanRead &&
prop.GetIndexParameters().Length == 0 &&
prop.IsDefined(typeof(BaseValidat ionAttribute), true);
}
}

Так как проверка может осуществляться одновременно из нескольких потоков, перед обращением к кэшу на него накладывается блокировка.

Метод IsPropertyValidatable используется для выбора свойств, значения которых будут проверяться. Такое свойство должно иметь метод для получения значения, не являться индексатором и быть помечено атрибутом-ограничением. Метод GetValidators, принимающий параметр типа Type, использует метод IsPropertyValidatable для отбора свойств переданного в качестве параметра типа.

Метод GetValidators, принимающий параметр типа Оbject, возвращает вызывающей стороне перечень экземпляров класса PropertyValidator, который извлекается из кэша или, если в кэше его нет, создается и кэшируется.

Ну вот, самое сложное позади, весь вспомогательный код написан, и можно приступить непосредственно к валидации:

public static class Validator
{
public static bool Validate(this object obj)
{
if (obj == null)
return true;
return obj.GetValidators()
.All(validator => validator.
Validate(obj));
}
}

Теперь экземпляры класса ContactInfo можно проверять следующим образом:

var contact = new ContactInfo
{
City = ”Казань”,
Street = ”Пушкина”,
House = ”12б”
};
contact.Validate(); // true

На рисунке представлена диаграмма последовательностей, на которой видно, что происходит за кулисами функции Validator.Validate.

Заключение

Способ выполнения проверок, описанный в статье, с успехом применялся в реальных проектах и показал свою эффективность. Конечно, приведенная в статье реализация довольно примитивна и для практического использования мало годится, однако задачей статьи было продемонстрировать сам подход к решению проблемы, а примеры кода можно использовать в качестве отправной точки при написании своей библиотеки. Тем же, кто хочет реализацию «здесь и сейчас», можно порекомендовать библиотеку Business Logic Toolkit (http://www.bltoolkit.net/), распространяемую на условиях MIT License, или библиотеку Enterprise Library (http://msdn.microsoft.com/entlib) предлагаемую на условиях Microsoft Public License.

Об авторе:
Эдуард Петрухин, разработчик компании Auriga, Inc.