Introduction
Most of the developers are facing some similar scenarios and sub tasks routinely assigned to them as parts of their main tasks. For sure, a large number of solutions are available to handle those annoying tasks but any of them has their own cons and pros. The main goal of this article is to show one of the good ways of handling them using attributes and extensions. along with Reflection. This article does NOT apply complicated design patterns and its goal is to simply show how to take advantage of .NET in the basic scenarios. The author assumes that the reader of this article is an intermediate to senior developer who knows OOP (object oriented programming) and also advanced techniques of application development in .NET.
Background
Validating and formatting objects are two basic parts of any application. Usually, in a project, they must be addressed simultaneously. Developers usually use some frameworks and tools to handle those but in some cases, developers are not allowed to use 3rd party tools and framework, so what do you want to do? Fortunately, you can handle most of your simple scenarios using the usual tips and tricks such as attributes and extensions.
Using the Code
NOTE: This solution has been developed in Visual Studio 2015 Community Edition targeting .NET 4.6. However, you can take the project and change the target framework down to .NET 2 with minor changes including New C# 6 features which are not available in earlier versions. Alternatively, you can take the files and projects to the earlier version of Visual Studios.
Let's talk about the simple scenarios in a project. This assumes that you have the following class and you want to meet the following requirements:
- The
age
property must be an odd number and between 1 and 100
- The first letter of the name must be formatted (First Letter Uppercase)
- The full name cannot be assigned outside of the class and it must be populated by concatenating the
Name
and FamilyName
Person.cs
public class Person
{
public int Age { get; set; }
public string FamilyName { get; set; }
public string Name { get; set; }
public string FullName { get; private set; }
}
One of the ways to meet the requirement is to decorate the properties using custom attributes. But how? Let's take a look at the following code. Isn't readable?
Person.cs
public class Person
{
[RangeValidator(1, 100)]
[IsOddNumber]
public int Age { get; set; }
[ToUpper(ToUpperTypes.FirstAndAfterSpace)]
public string FamilyName { get; set; }
[ToUpper(ToUpperTypes.FirstLetter)]
public string Name { get; set; }
[AutoPupulate("Name", "FamilyName")]
public string FullName { get; private set; }
}
Although the above code is very easy to read and understand, it does not do anything, as we require to implement the attributes decorating the properties. Let's see the implementation of these attributes. The following codes are the actual implementation of these attributes. These attributes are basically classes derived from attributes class. They may have constructors and properties.
RangeValidatorAttribute.cs
[AttributeUsage(AttributeTargets.Property,AllowMultiple =true)]
public class RangeValidatorAttribute : Attribute
{
public int MaxValue { get; set; } public int MinValue { get; set; }
public RangeValidatorAttribute(int MinValue, int MaxValue)
{
this.MaxValue = MaxValue;
this.MinValue = MinValue;
}
}
IsOddNumberAttribute.cs
[AttributeUsage(AttributeTargets.Property,AllowMultiple =true)]
public class IsOddNumberAttribute : Attribute
{
}
ToUpperAttribute.cs
public enum ToUpperTypes
{
FirstLetter,
FirstAndAfterSpace,
AllLetters
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class ToUpperAttribute : Attribute
{
public ToUpperTypes Type { get; set; }
public ToUpperAttribute(ToUpperTypes Type = ToUpperTypes.AllLetters)
{
this.Type = Type;
}
}
AutoPupulateAttribute.cs
[AttributeUsage(AttributeTargets.Property)]
public class AutoPupulateAttribute : Attribute
{
public string[] PropertyNames { get; set; }
public AutoPupulateAttribute(params string[] names)
{
if (names != null)
{
PropertyNames = names;
}
}
}
ValidationException.cs
public class ValidationException : Exception
{
public enum Types
{
RangeException,
OddException
}
public ValidationException(Types ExceptionType, string Message) : base(Message) { }
public ValidationException
(Types ExceptionType, string Message, Exception InnerException) :
base(Message, InnerException) { }
}
As you see, none of these attributes does anything special. They are just decorators marking your properties. But the question is "How can we automate our tasks?". The question requires us to take advantage of Reflection. As we run the application, we can access to the objects including attributes. For example, we can have a series of extensions parsing the object (using reflections) and do the predefined operation to meet the previous requirement.
The following code basically contains a series of extensions to class but beware that they are generic extensions which will globally affect the whole classes in your project when they are referenced in your code.
Please note that if the properties (in this case) were not marked by attributes, no error will be raised. Attributes are optional.
using System;
using System.Linq;
using System.Text;
namespace FormatingAttributes
{
public static class ClassExtnetions
{
public static T AutoPopulate<t>(this T input) where T : class
{
var propertyInfos = input.GetType().GetProperties();
foreach (var propertyInfo in propertyInfos)
{
var customAttributes = propertyInfo.GetCustomAttributes(true);
foreach (var customAttribute in customAttributes)
{
if (!(customAttribute is AutoPupulateAttribute)) continue;
var propNamesInfo = customAttribute.GetType().GetProperty("PropertyNames");
var propNames = (string[])propNamesInfo.GetValue(customAttribute);
var result = new StringBuilder();
foreach (var propValue in propNames.Select
(propName => input.GetType().GetProperty(propName)).Select
(propinfo => propinfo.GetValue(input)))
{
result.Append(propValue);
result.Append(" ");
}
propertyInfo.SetValue(input, result.ToString());
}
}
return input;
}
public static string ConvertToString<t>(this T input) where T : class
{
var output = new StringBuilder();
var className = input.GetType().ToString();
var properties = input.GetType().GetProperties();
output.Append($"Class Name : {className} {Environment.NewLine}");
foreach (var property in properties)
{
output.Append($"{property.Name} :
{property.GetValue(input)} {Environment.NewLine}");
}
return output.ToString();
}
public static T Format<t>(this T input) where T : class
{
var propertyInfos = input.GetType().GetProperties();
foreach (var propertyInfo in propertyInfos)
{
var customAttributes = propertyInfo.GetCustomAttributes(true);
foreach (var customAttribute in customAttributes)
{
if (!(customAttribute is ToUpperAttribute)) continue;
var value = propertyInfo.GetValue(input).ToString();
var customAttributeType = customAttribute.GetType().GetProperty("Type");
var type = (ToUpperTypes)customAttributeType.GetValue(customAttribute);
ToUpperAttribute(ref value, type);
propertyInfo.SetValue(input, value);
}
}
return input;
}
private static void ToUpperAttribute(ref string value, ToUpperTypes type)
{
switch (type)
{
case ToUpperTypes.FirstLetter:
if (string.IsNullOrEmpty(value))
{
return;
}
value = string.Concat(char.ToUpper(value.ToCharArray()[0]).ToString(),
value.Substring(1));
break;
case ToUpperTypes.FirstAndAfterSpace:
if (string.IsNullOrEmpty(value))
{
return;
}
var result = new StringBuilder();
var splittedValues = value.Split(' ');
foreach (var splittedValue in splittedValues)
{
result.Append(string.Concat(char.ToUpper
(splittedValue.ToCharArray()[0]).ToString(),
splittedValue.Substring(1)));
result.Append(' ');
}
value = result.ToString();
break;
case ToUpperTypes.AllLetters:
value = value.ToUpper();
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public static bool Validate<t>(this T input)
{
var propertyInfos = input.GetType().GetProperties();
return propertyInfos.Select
(propertyInfo => Validate(input, propertyInfo.Name)).FirstOrDefault();
}
public static bool Validate<t>(this T input, string PropertyName)
{
var propertyInfo = input.GetType().GetProperty(PropertyName);
var customAttributes = propertyInfo.GetCustomAttributes(true);
var isValid = true;
foreach (var customAttribute in customAttributes)
{
if (customAttribute is RangeValidatorAttribute)
{
var value = (int) propertyInfo.GetValue(input);
var minValueProperty = customAttribute.GetType().GetProperty("MinValue");
var maxValueProperty = customAttribute.GetType().GetProperty("MaxValue");
var minValue = (int) minValueProperty.GetValue(customAttribute);
var maxValue = (int) maxValueProperty.GetValue(customAttribute);
isValid &= RangeValidator(value, minValue, maxValue, PropertyName);
}
else if (customAttribute is IsOddNumberAttribute)
{
var value = (int) propertyInfo.GetValue(input);
if (value%2 == 0)
{
throw new ValidationException(ValidationException.Types.OddException,
$"The value of Property {PropertyName} is {value} which is not Odd!!!");
}
}
}
return isValid;
}
private static bool RangeValidator(int value, int min, int max, string PropertyName)
{
var isValid = value <= max & value >= min;
if (!isValid)
{
throw new ValidationException(ValidationException.Types.RangeException,
$"The value of property {PropertyName} is not between {min} and {max}");
}
return true;
}
}
}</t>
As you see, we have format, Validate and Auto Populate functions in our extension class. So, in your code, you can easily create a new instance of person
class and call those functions as this class is targeting all of the classes when it's referenced in your code.
The following is a simple console application which creates an instance of person
class and runs the functions.
internal class Program
{
private static void Main(string[] args)
{
var persons = new List<person>();
persons.Add(
new Person()
{
Name = "john",
FamilyName = "smith dC",
Age = 22
}
);
persons.Add(
new Person()
{
Name = "Judy",
FamilyName = "Abbout Story",
Age = 65
}
);
foreach (var person in persons)
{
try
{
var isValid = person.Format().AutoPopulate().Validate();
Console.WriteLine(person.ToString());
}
catch (Exception ex)
{
Console.WriteLine(person.ToString());
Console.WriteLine();
if (ex is ValidationException)
{
Console.WriteLine(ex.Message);
Console.WriteLine();
}
}
}
Console.ReadLine();
}
}</person>