Click here to Skip to main content
15,867,834 members
Articles / Programming Languages / C#

Automating the Routine Tasks using Extentions, Attributes and Reflection in .NET

Rate me:
Please Sign up or sign in to vote.
4.82/5 (14 votes)
10 Mar 2016CPOL3 min read 25.5K   246   23   12
.NET provides many techniques to make your code cleaner, easy to read and maintain. Extensions and Attributes are two examples helping developers to not only maintain and extend the applications simpler than before but also makes repetitive tasks less annoying for developers.

Image 1

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

C#
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

C#
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

C#
[AttributeUsage(AttributeTargets.Property,AllowMultiple =true)]
public class RangeValidatorAttribute : Attribute
{
    public int MaxValue { get; set; } // Maximum acceptable value
    public int MinValue { get; set; } // Minimum acceptable value

    public RangeValidatorAttribute(int MinValue, int MaxValue)
    {
        this.MaxValue = MaxValue;
        this.MinValue = MinValue;
    }
}

IsOddNumberAttribute.cs

C#
[AttributeUsage(AttributeTargets.Property,AllowMultiple =true)]
public class IsOddNumberAttribute : Attribute
{
}

ToUpperAttribute.cs

C#
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

C#
[AttributeUsage(AttributeTargets.Property)]
   public class AutoPupulateAttribute : Attribute
   {
       public string[] PropertyNames { get; set; }
       public AutoPupulateAttribute(params string[] names)
       {
           if (names != null)
           {
               PropertyNames = names;
           }
       }
   }

ValidationException.cs

C#
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.

C#
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></t></t></t></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.

C#
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>
This article was originally posted at https://github.com/persianboss/CustomAttributes

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
csharpbd25-Apr-16 18:25
professionalcsharpbd25-Apr-16 18:25 
Thanks for Share!!!!!
GeneralMy vote of 4 Pin
Donsw29-Dec-15 16:20
Donsw29-Dec-15 16:20 
GeneralRe: My vote of 4 Pin
Masoud Zehtabi Oskuie31-Dec-15 14:38
professionalMasoud Zehtabi Oskuie31-Dec-15 14:38 
QuestionValidation side Pin
tlford6528-Dec-15 11:23
professionaltlford6528-Dec-15 11:23 
AnswerRe: Validation side Pin
Masoud Zehtabi Oskuie29-Dec-15 1:16
professionalMasoud Zehtabi Oskuie29-Dec-15 1:16 
QuestionAdd System.Reflection Pin
tlford6528-Dec-15 11:17
professionaltlford6528-Dec-15 11:17 
AnswerRe: Add System.Reflection Pin
Masoud Zehtabi Oskuie31-Dec-15 14:37
professionalMasoud Zehtabi Oskuie31-Dec-15 14:37 
QuestionGood job! Pin
MK-Gii28-Dec-15 4:47
MK-Gii28-Dec-15 4:47 
AnswerRe: Good job! Pin
Masoud Zehtabi Oskuie28-Dec-15 7:16
professionalMasoud Zehtabi Oskuie28-Dec-15 7:16 
QuestionLogic AND operator Pin
MartinNohrRimage27-Dec-15 5:05
MartinNohrRimage27-Dec-15 5:05 
AnswerRe: Logic AND operator Pin
Masoud Zehtabi Oskuie27-Dec-15 8:32
professionalMasoud Zehtabi Oskuie27-Dec-15 8:32 
GeneralRe: Logic AND operator Pin
MartinNohrRimage28-Dec-15 14:02
MartinNohrRimage28-Dec-15 14:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.