Использование атрибутов для нормализации и валидации бизнес-сущностей - Создание класса атрибутов нормализации данных
ОГЛАВЛЕНИЕ
/// <summary>А теперь определим несколько атрибутов, которые мы будем практически использовать. Атрибут, выполняющий обрезание определенных символов с начала и/или конца строки. Обрезаемый символ по умолчанию - пробел.
/// Базовый класс-атрибут для создания атрибутов нормализации
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public abstract class NormalizationBaseAttribute : Attribute
{
int m_normalizationOrder = 0;
/// <summary>
/// Порядковый номер при нормализации
/// </summary>
public virtual int NormalizationOrder
{
get {return m_normalizationOrder;}
}
public NormalizationBaseAttribute(int normalizationOrder)
{
m_normalizationOrder = normalizationOrder;
}
/// <summary>
/// Нормализовать указанное значение
/// </summary>
/// <param name="value">Значение для нормализации</param>
/// <returns>Нормализованный объект</returns>
public virtual object Normalize(object value)
{
return "#NormalizationBase - Normalize method not overrided!";
}
}
/// <summary>Ещё один атрибут - сведения нескольких пробелов к одному.
/// Атрибут нормализации - отсечение символов строки слева и/или справа
/// </summary>
public class NormalizationStringTrimAttribute : NormalizationBaseAttribute
{
#region members
char m_trimChar = ' ';
/// <summary>
/// Отсекаемый символ
/// </summary>
public virtual char TrimChar
{
get {return m_trimChar;}
set {m_trimChar = value;}
}
bool m_trimLeft = true;
/// <summary>
/// Отсекать слева (с начала строки)
/// </summary>
public virtual bool TrimLeft
{
get {return m_trimLeft;}
set {m_trimLeft = value;}
}
bool m_trimRight = true;
/// <summary>
/// Отсекать справа (с конца строки)
/// </summary>
public virtual bool TrimRight
{
get {return m_trimRight;}
set {m_trimRight = value;}
}
#endregion
public NormalizationStringTrimAttribute(int normalizationOrder) : base(normalizationOrder)
{}
public override object Normalize(object value)
{
if (m_trimChar == ' ')
{
if (m_trimLeft & m_trimRight)
return value.ToString().Trim();
else if (m_trimLeft)
return value.ToString().TrimStart();
else if (m_trimRight)
return value.ToString().TrimEnd();
else
return value;
}
else
{
if (m_trimLeft & m_trimRight)
return value.ToString().Trim(m_trimChar);
else if (m_trimLeft)
return value.ToString().TrimStart(m_trimChar);
else if (m_trimRight)
return value.ToString().TrimEnd(m_trimChar);
else
return value;
}
}
}
/// <summary>
/// Атрибут нормализации строки - сведение нескольких пробелов подряд к одному \
/// (максимально 24->1)Итак у нас есть 2 атрибута и с ними уже можно что-то сделать. Например написать такой код
/// </summary>
public class NormalizationStringWhiteSpaceReduceAttribute : NormalizationBaseAttribute
{
public NormalizationStringWhiteSpaceReduceAttribute(int normalizationOrder):
base(normalizationOrder)
{}
public override object Normalize(object value)
{
return value.ToString().Replace(" ", " ").Replace(" ", " ").Replace(" ", " ");
}
}
[DisplayName("ФИО")]то есть мы предполагаем что написанная нами функция получит все атрибуты нормализации для данного свойства и последовательно перебирая их обработает свойство. Однако возникает вопрос в какой последовательности они будут возвращены отражением (а это может быть очень важно)? В той ли в которой были объявлены в коде? К сожалению, ответ - 'Нет'. Приходится извращаться и добавлять параметр, который определял порядок(последовательность) применения атрибутов. Для этого и предназначено виртуальное свойство NormalizationOrder. То есть наш сценарий действий таков:
[NormalizationStringTrim()]
[NormalizationStringWhiteSpaceReduce()]
public string Name
{
get { return m_Name;}
set { m_Name = value;}
}
1 получаем для свойства массив атрибутов основанных на NormalizationBaseAttribute с помощью метода Attribute.GetCustomAttributes.
2 Сортируем его в требуемом порядке.
3 Последовательно применяем к объекту нормализацию.
Для второго пункта нам понадобится класс NormalizationComparer, предназначенный для сравнения двух атрибутов, наследующихся от NormalizationBaseAttribute. Сравнение производится по свойству NormalizationOrder.
/// <summary>основная функция нормализации
/// Класс-сравнитель для NormalizationBaseAttribute
/// (сравнивает по NormalizationOrder)
/// </summary>
public class NormalizationComparer : IComparer
{
#region IComparer Members
public int Compare(object x, object y)
{
return ((NormalizationBaseAttribute)x).NormalizationOrder.CompareTo(
((NormalizationBaseAttribute)y).NormalizationOrder );
}
#endregion
}
/// <summary>и небольшой тестовый код
/// Нормализует поля объекта согласно установленным правилам
/// </summary>
public void Normalize()
{
// Получить атрибуты уровня свойств.
// Получить все свойства данного класса и поместить их в массив
PropertyInfo[] pInfo = this.GetType().GetProperties();
// атрибуты всех свойств класса
for (int j=0; j<pInfo.Length; j++)
{
Attribute[] atts = Attribute.GetCustomAttributes(pInfo[j], typeof(NormalizationBaseAttribute));
Array.Sort(atts, new NormalizationComparer());
for (int k=0; k<atts.Length; k++)
{
NormalizationBaseAttribute att = (NormalizationBaseAttribute)atts[k];
if (att != null)
pInfo[j].SetValue(this, att.Normalize(pInfo[j].GetValue(this, null)), null);
}
}
}
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
Person p = new Person(-1);
p.ShowNames();
Console.ReadLine();
p.Name = " Иванов Иван Иванович ";
Console.WriteLine("'{0}'", p.Name);
<b>p.Normalize();</b>
Console.WriteLine("'{0}'", p.Name);
Console.ReadLine();
...
Как вы видите, оба атрибута отработали и привели наше свойство к требуемому виду.
Отлично! Но нормализацией следует пользоваться с осторожностью, ведь пользователь уверен, что ввел одно значение, а перед записью в БД произошла нормализация, и значение могло измениться... Поэтому возможно 3 варианта:
- проверять введенные пользователем данные и если они не соответствуют правильным предупреждать пользователя об этом и не позволять продолжать, пока они не будут исправлены;
- проверять введенные пользователем данные и если они не соответствуют правильным нормализовывать и извещать пользователя об этом;
- нормализовывать данные, а пользователю ничего не сообщать :)
Я предпочитаю второй вариант, если ошибок нет данные сохраняются и актуальные значения отображаются пользователю.