In this chapter we will cover:
Implementing the validation logic using the Repository pattern
Creating a custom validation attribute by extending the validation data annotation
Using XML to generate a localized validation message
Extending the validation attribute for localization
Creating custom attributes
Processing custom attributes via reflection
Using asynchronous file I/O for directory-to-directory copy
Accessing JSON using dynamic programming
This chapter will cover recipes related to core concepts in .NET, which will include the following:
Metadata-driven programming: The first six recipes will cover how to use attributes as metadata for specific purposes such as validation and localization.
Reflection: The Processing custom attributes via reflection recipe will tell you how to use reflection to create metadata processors such as applications or libraries that can understand custom attributes and provide the output based on them.
Asynchronous file I/O: This is a new feature for file input/output introduced in .NET 4.5. The Using asynchronous file I/O for directory-to-directory copy recipe will cover this feature.
Dynamic programming: .NET 4.0 introduced the concept of dynamic programming, in which blocks of code marked as dynamic will be executed directly, bypassing the compilation phase. We will look at this in the last recipe, Accessing JSON using dynamic programming.
The Repository pattern abstracts out data-based validation logic. It is a common misconception that to implement the Repository pattern you require a relational database such as MS SQL Server as the backend. Any collection can be treated as a backend for a Repository pattern. The only point to keep in mind is that the business logic or validation logic must treat it as a database for saving, retrieving, and validating its data. In this recipe, we will see how to use a generic collection as backend and abstract out the validation logic for the same.
The validation logic makes use of an entity that represents the data related to the user and a class that acts as the repository for the data allowing certain operations. In this case, the operation will include checking whether the user ID chosen by the user is unique or not.
The following steps will help check the uniqueness of a user ID that is chosen by the user:
Launch Visual Studio .NET 2012. Create a new project of Class Library project type. Name it
CookBook.Recipes.Core.CustomValidation
.Add a folder to the project and set the folder name to
DataModel
.Add a new class and name it
User.cs
.Open the
User
class and create the following public properties:Property name
Data type
UserName
String
DateOfBirth
DateTime
Password
String
Use the automatic property functionality of .NET to create the properties. The final code of the
User
class will be as follows:namespace CookBook.Recipes.Core.CustomValidation { /// <summary> /// Contains details of the user being registered /// </summary> public class User { public string UserName { get; set; } public DateTime DateOfBirth { get; set; } public string Password { get; set; } } }
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
Next, let us create the repository. Add a new folder and name it
Repository
.Add an interface to the
Repository
folder and name itIRepository.cs
.The interface will be similar to the following code snippet:
public interface IRepository { }
Open the
IRepository
interface and add the following methods:Name
Description
Parameter(s)
Return Type
AddUser
Adds a new user
User object
Void
IsUsernameUnique
Determines whether the username is already taken or not
string
Boolean
After adding the methods,
IRepository
will look like the following code:namespace CookBook.Recipes.Core.CustomValidation { public interface IRepository { void AddUser(User user); bool IsUsernameUnique(string userName); } }
Next, let us implement
IRepository
. Create a new class in theRepository
folder and name itMockRepository
.Make the
MockRepository
class implementIRepository
. The code will be as follows:namespace CookBook.Recipes.Core.Data.Repository { public class MockRepository:IRepository { #region IRepository Members /// <summary> /// Adds a new user to the collection /// </summary> /// <param name="user"></param> public void AddUser(User user) { } /// <summary> /// Checks whether a user with the username already ///exists /// </summary> /// <param name="userName"></param> /// <returns></returns> public bool IsUsernameUnique(string userName) { } #endregion } }
Now, add a private variable of type
List<Users>
in theMockRepository
class. Name it_users
. It will hold the registered users. It is astatic
variable so that it can hold usernames across multiple instantiations.Add a constructor to the class. Then initialize the
_users
list and add two users to the list:public class MockRepository:IRepository { #region Variables Private static List<User> _users; #endregion public MockRepository() { _users = new List<User>(); _users.Add(new User() { UserName = "wayne27", DateOfBirth = new DateTime(1950, 9, 27), Password = "knight" }); _users.Add(new User() { UserName = "wayne47", DateOfBirth = new DateTime(1955, 9, 27), Password = "justice" }); } #region IRepository Members /// <summary> /// Adds a new user to the collection /// </summary> /// <param name="user"></param> public void AddUser(User user) { } /// <summary> /// Checks whether a user with the username already exists /// </summary> /// <param name="userName"></param> /// <returns></returns> public bool IsUsernameUnique(string userName) { } #endregion }
Now let us add the code to check whether the username is unique. Add the following statements to the
IsUsernameUnique
method:bool exists = _users.Exists(u=>u.UserName==userName); return !exists;
The method turns out to be as follows:
public bool IsUsernameUnique(string userName) { bool exists = _users.Exists(u=>u.UserName==userName); return !exists; }
Modify the
AddUser
method so that it looks as follows:public void AddUser(User user) { _users.Add(user); }
The core of the validation logic lies in the IsUsernameUnique
method of the MockRespository
class. The reason to place the logic in a different class rather than in the attribute itself was to decouple the attribute from the logic to be validated. It is also an attempt to make it future-proof. In other words, tomorrow, if we want to test the username against a list generated from an XML file, we don't have to modify the attribute. We will just change how IsUsernameUnique
works and it will be reflected in the attribute. Also, creating a Plain Old CLR Object
(POCO) to hold values entered by the user stops the validation logic code from directly accessing the source of input, that is, the Windows application.
Coming back to the IsUsernameUnique
method, it makes use of the predicate feature provided by .NET. Predicate allows us to loop over a collection and find a particular item. Predicate can be a static function, a delegate, or a lambda. In our case it is a lambda.
bool exists = _users.Exists(u=>u.UserName==userName);
In the previous statement, .NET loops over _users
and passes the current item to u
. We then make use of the item held by u
to check whether its username is equal to the username entered by the user. The Exists
method returns true if the username is already present. However, we want to know whether the username is unique. So we flip the value returned by Exists
in the return statement, as follows:
return !exists;
.NET provides
data annotations as a part of the System.ComponentModel.DataAnnotation
namespace. Data annotations are a set of attributes that provides out of the box validation, among other things. However, sometimes none of the in-built validations will suit your specific requirements. In such a scenario, you will have to create your own validation attribute. This recipe will tell you how to do that by extending the validation attribute. The attribute developed will check whether the supplied username is unique or not. We will make use of the validation logic implemented in the previous recipe to create a custom validation attribute named UniqueUserValidator
.
The following steps will help you create a custom validation attribute to meet your specific requirements:
Launch Visual Studio 2012. Open the
CustomValidation
solution.Add a reference to
System.ComponentModel.DataAnnotations
.Add a new class to the project and name it
UniqueUserValidator
.Add the following using statements:
using System.ComponentModel.DataAnnotations; using CookBook.Recipes.Core.CustomValidation.MessageRepository; using CookBook.Recipes.Core.Data.Repository;
Derive it from
ValidationAttribute
, which is a part of theSystem.ComponentModel.DataAnnotations
namespace. In code, it would be as follows:namespace CookBook.Recipes.Core.CustomValidation { public class UniqueUserValidator:ValidationAttribute { } }
Add a property of type
IRepository
to the class and name itRepository
.Add a constructor and initialize
Repository
to an instance ofMockRepository
. Once the code is added, the class will be as follows:namespace CookBook.Recipes.Core.CustomValidation { public class UniqueUserValidator:ValidationAttribute { public IRepository Repository {get;set;} public UniqueUserValidator() { this.Repository = new MockRepository(); } } }
Override the
IsValid
method ofValidationAttribute
. Convert theobject
argument tostring
.Then call the
IsUsernameUnique
method ofIRepository
, pass the string value as a parameter, and return the result. After the modification, the code will be as follows:namespace CookBook.Recipes.Core.CustomValidation { public class UniqueUserValidator:ValidationAttribute { public IRepository Repository {get;set;} public UniqueUserValidator() { this.Repository = new MockRepository(); } public override bool IsValid(object value) { string valueToTest = Convert.ToString(value); return this.Repository.IsUsernameUnique(valueToTest); } } }
We have completed the implementation of our custom validation attribute. Now let's test it out.
Add a new Windows Forms Application project to the solution and name it
CustomValidationApp
.Add a reference to the
System.ComponentModel.DataAnnotations
andCustomValidation
projects.Rename
Form1.cs
toRegister.cs
.Open
Register.cs
in the design mode. Add controls for username, date of birth, and password and also add two buttons to the form. The form should look like the following screenshot:Name the input control and button as given in the following table:
Control
Name
Textbox
txtUsername
Button
btnOK
Since we are validating the
User Name
field, our main concern is with the textbox for the username and the OK button. I have left out the names of other controls for brevity.Switch to the code view mode. In the constructor, add event handlers for the Click event of
btnOK
as shown in the following code:public Register() { InitializeComponent(); this.btnOK.Click += new EventHandler(btnOK_Click); } void btnOK_Click(object sender, EventArgs e) { }
Open the
User
class of theCookBook.Recipes.Core.CustomValidation
project. Annotate theUserName
property withUniqueUserValidator
. After modification, theUser
class will be as follows:namespace CookBook.Recipes.Core.CustomValidation { /// <summary> /// Contains details of the user being registered /// </summary> public class User { [UniqueUserValidator(ErrorMessage="User name already exists")] public string UserName { get; set; } public DateTime DateOfBirth { get; set; } public string Password { get; set; } } }
Go back to
Register.cs
in the code view mode.Add the following
using
statements:using System.ComponentModel; using System.ComponentModel.DataAnnotations; using CookBook.Recipes.Core.CustomValidation; using CookBook.Recipes.Core.Data.Repository;
Add the following code to the event handler of
btnOK
://create a new user User user = new User() { UserName = txtUsername.Text, DateOfBirth=dtpDob.Value }; //create a validation context for the user instance ValidationContext context = new ValidationContext(user, null, null); //holds the validation errors IList<ValidationResult> errors = new List<ValidationResult>(); if (!Validator.TryValidateObject(user, context, errors, true)) { foreach (ValidationResult result in errors) MessageBox.Show(result.ErrorMessage); } else { IRepository repository = new MockRepository(); repository.AddUser(user); MessageBox.Show("New user added"); }
Hit F5 to run the application. In the textbox add a username, say,
dreamwatcher
. Click on OK. You will get a message box stating User has been added successfully.Enter the same username again and hit the OK button. This time you will get a message saying User name already exists. This means our attribute is working as desired.
Go to File | Save Solution As…, enter
CustomValidation
for Name, and click on OK.
To understand how UniqueUserValidator
works, we have to understand how it is implemented and how it is used/called. Let's start with how it is implemented. It extends ValidationAttribute
. The ValidationAttribute
class is the base class for all the validation-related attributes provided by data annotations. So the declaration is as follows:
public class UniqueUserValidator:ValidationAttribute
This allowed us to make use of the public and protected methods/attribute arguments of ValidationAttribute
as if it is a part of our attribute. Next, we have a property of type IRepository
, which gets initialized to an instance of MockRepository
. We have used the interface-based approach so that the attribute will only need a minor change if we decide to test the username against a database table or list generated from a file. In such a scenario, we will just change the following statement:
this.Repository = new MockRepository();
The previous statement will be changed to something such as the following:
this.Repository = new DBRepository();
Next, we overrode the IsValid
method. This is the method that gets called when we use UniqueUserValidator
. The parameter of the IsValid
method is an object. So we have to typecast it to string and call the IsUniqueUsername
method of the Repository
property. That is what the following statements accomplish:
string valueToTest = Convert.ToString(value); return this.Repository.IsUsernameUnique(valueToTest);
Now let us see how we used the validator. We did it by decorating the UserName
property of the User
class:
[UniqueUserValidator(ErrorMessage="User name already exists")] public string UserName {get; set;}
As I already mentioned, deriving from ValidatorAttribute
helps us in using its properties as well. That's why we can use ErrorMessage
even if we have not implemented it.
Next, we have to tell .NET to use the attribute to validate the username that has been set. That is done by the following statements in the OK button's Click handler in the Register
class:
ValidationContext context = new ValidationContext(user, null, null); //holds the validation errors IList<ValidationResult> errors = new List<ValidationResult>(); if (!Validator.TryValidateObject(user, context, errors, true))
First, we instantiate an object of ValidationContext
. Its main purpose is to set up the context in which validation will be performed. In our case the context is the User
object. Next, we call the TryValidateObject
method of the Validator
class with the User
object, the ValidationContext
object, and a list to hold the errors. We also tell the method that we need to validate all properties of the User
object by setting the last argument to true. That's how we invoke the validation routine provided by .NET.
The Implementing Model and Repository pattern recipe discussed in Chapter 7, WPF Recipes
In the last recipe you saw that we can pass error messages to be displayed to the validation attribute. However, by default, the attributes accept only a message in the English language. To display a localized custom message, it needs to be fetched from an external source such as an XML file or database. In this recipe, we will see how to use an XML file to act as a backend for localized messages.
The following steps will help you generate a localized validation message using XML:
Open
CustomValidation.sln
in Visual Studio .NET 2012.Add an XML file to the
CookBook.Recipes.Core.CustomValidation
project. Name itMessages.xml
. In the Properties window, set Build Action to Embedded Resource.Add the following to the
Messages.xml
file:<?xml version="1.0" encoding="utf-8" ?> <messages> <en> <message key="not_unique_user">User name is not unique</message> </en> <fr> <message key="not_unique_user">Nom d'utilisateur n'est pas unique</message> </fr> </messages>
Add a folder to the
CookBook.Recipes.Core.CustomValidation
project. Name itMessageRepository
.Add an interface to the
MessageRepository
folder and name itIMessageRepository
.Add a method to the interface and name it
GetMessages
. It will haveIDictionary<string,string>
as a return type and will accept astring
value as parameter. The interface will look like the following code:namespace CookBook.Recipes.Core.CustomValidation.MessageRepository { public interface IMessageRepository { IDictionary<string, string> GetMessages(string locale); } }
Add a class to the
MessageRespository
folder. Name itXmlMessageRepository
.Add the following
using
statements:using System.Xml;
Implement the
IMessageRepository
interface. The class will look like the following code once we implement the interface:namespace CookBook.Recipes.Core.CustomValidation.MessageRepository { public class XmlMessageRepository:IMessageRepository { #region IMessageRepository Members public IDictionary<string, string> GetMessages(string locale) { return null; } #endregion } }
Modify
GetMessages
so that it looks like the following code:public IDictionary<string, string> GetMessages(string locale) { XmlDocument xDoc = new XmlDocument(); xDoc.Load(Assembly.GetExecutingAssembly().GetManifestResourceStream("CustomValidation.Messages.xml")); XmlNodeList resources = xDoc.SelectNodes("messages/"+locale+"/message"); SortedDictionary<string, string> dictionary = new SortedDictionary<string, string>(); foreach (XmlNode node in resources) { dictionary.Add(node.Attributes["key"].Value, node.InnerText); } return dictionary; }
Next let us see how to modify UniqueUserValidator
so that it can localize the error message.
The Messages.xml
file and the GetMessages
method of XmlMessageRespository
form the core of the logic to generate a locale-specific message. Message.xml
contains the key to the message within the locale
tag. We have created the locale
tag using the two-letter ISO name of a locale. So, for English it is <en></en>
and for French it is <fr></fr>
.
Each locale
tag contains a message
tag. The key
attribute of the tag will have the key that will tell us which message
tag contains the error message. So our code will be as follows:
<message key="not_unique_user">User name is not unique</message>
not_unique_user
is the key to the User is not unique
error message. In the GetMessages
method, we first load the XML file. Since the file has been set as an embedded resource, we read it as a resource. To do so, we first got the executing assembly, that is, CustomValidation
. Then we called GetManifestResourceAsStream
and passed the qualified name of the resource, which in this case is CustomValidation.Messages.xml
. That is what we achieved in the following statement:
xDoc.Load(Assembly.GetExecutingAssembly().GetManifestResourceStream("CustomValidation.Messages.xml"));
Then we constructed an XPath to the message
tag using the locale passed as the parameter. Using the XPath query/expression we got the following message nodes:
XmlNodeList resources = xDoc.SelectNodes("messages/"+locale+"/message");
After getting the node list, we looped over it to construct a dictionary. The value of the key
attribute of the node became the key of the dictionary. And the value of the node became the corresponding value in the dictionary, as is evident from the following code:
SortedDictionary<string, string> dictionary = new SortedDictionary<string, string>(); foreach (XmlNode node in resources) { dictionary.Add(node.Attributes["key"].Value, node.InnerText); }
The dictionary was then returned by the method. Next, let's understand how this dictionary is used by UniqueUs
rValidator
.
If you do not want to hardcode the message or embed it as a resource, the only approach left is to extend the attribute so that messages can be decided at runtime based on the locale used. Extending an attribute for this purpose can also help if you decide to fetch the message from the database or an external translation service.
In this recipe, we will modify the UniqueUserValidator
code so that it can generate locale-based custom messages. The custom messages will be fetched from an XML file using the logic developed in the previous recipe.
The following steps will guide you as you generate locale-based custom messages:
Open
CustomValidation.sln
in Visual Studio .NET 2012.Open
UniqueUserValidator
and add the followingusing
statements:using CookBook.Recipes.Core.CustomValidation.MessageRepository; using CookBook.Recipes.Core.Data.Repository;
Then add a property of type
IMessageRepository
and instantiate it in the constructor:public IRepository Repository {get;set;} public IMessageRepository MessageRepo {get;set;} public UniqueUserValidator() { this.Repository = new MockRepository(); this.MessageRepo = new XmlMessageRepository(); }
Override the
FormatErrorMessage
method ofValidationAttribute
. In the overridden method, get the current locale and call theGetMessage
method ofIMessageRepository
with the locale value. Then, return the value corresponding to theErrorMessage
property. In code, it will be as follows:public override string FormatErrorMessage(string name) { string locale = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName; return this.MessageRepo.GetMessages(locale)[this.ErrorMessage]; }
We have completed the modifications to
UniqueUserValidator
. Now let's see how to use it. Along with using the modifiedUniqueUserValidator
code, we will also test whether it responds to the change in locale correctly.Open the
User
class, which is in theDataModel
folder of theCookBook.Recipes.Core.CustomValidation
project.Change the
ErrorMessage
parameter of theUserName
property to the following:[UniqueUserValidator(ErrorMessage = "not_unique_user")] public string UserName { get; set; }
Next, open the
Register
class ofCustomValidationApp
in the design mode. Add a label and a combobox. Name the comboboxcmbLocale
. After adding the controls, the Register form will look as follows:Next, open the
Register
class in the view code mode. Add the following code in the constructor:cmbLocale.Items.Add("en-IN"); cmbLocale.Items.Add("fr-FR"); cmbLocale.SelectedIndex = 0;
Add an event handler for the
SelectedIndexChanged
event ofcmbLocale
as follows:public Register() { InitializeComponent(); cmbLocale.Items.Add("en-IN"); cmbLocale.Items.Add("fr-FR"); cmbLocale.SelectedIndex = 0; cmbLocale.SelectedIndexChanged += new EventHandler(cmbLocale_SelectedIndexChanged); this.btnCancel.Click += new EventHandler(btnCancel_Click); this.btnOK.Click += new EventHandler(btnOK_Click); } void cmbLocale_SelectedIndexChanged(object sender, EventArgs e) { }
Add the following code to the event handler for the
SelectedIndexChanged
event ofcmbLocale
:Thread.CurrentThread.CurrentCulture = new CultureInfo(cmbLocale.SelectedItem.ToString());
Press F5 to run the application. Enter
wayne27
as username. Click on OK. You will get a message saying User name is not unique.Select fr-FR from the locale combobox. Click on the OK button. You will get a message saying Nom d'utilisateur n'est pas unique, which is the French version of the message we have used in
Messages.xml
.
The main change we did to the attribute is overriding the FormatErrorMessage
method of ValidationAttribute
. The validation framework / data annotation library calls the FormatErrorMessage
method when it needs to output a message corresponding to a property. In short, by overriding it, we can provide a customized message.
To do so, we first need to find the two-letter ISO name of the current locale. Using the CurrentCulture
property of CurrentThread
, which is a static property of the Thread
class, we can find the locale name. The following code did that and provided us with the two-letter ISO name of the current locale:
string locale = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;
Next, we passed the locale to the GetMessages
method of IMessageRepository
. From the returned dictionary, we found the message we wanted using the ErrorMessage
property/named parameter and returned it. The value in ErrorMessage
acted as the key:
return this.MessageRepo.GetMessages(locale)[this.ErrorMessage];
CustomValidationApp
performs two roles. First of all, it makes use of the UserName
property of the User
class decorated with UniqueUserValidator
to pass the key of the message we want, as shown:
[UniqueUserValidator(ErrorMessage = "not_unique_user")] public string UserName { get; set; }
The value we passed to ErrorMessage
acted as the key in FormatErrorMessage
. The other role the application performs is to provide us with a test platform for testing locales, in this case, English and French. This was done by the following code in the SelectedIndexChanged
event handler of cmbLocale
in the Register
class:
Thread.CurrentThread.CurrentCulture = new CultureInfo(cmbLocale.SelectedItem.ToString());
What we did in the preceding code is to set the current culture of the application to the culture selected in the locale combobox. When we set the CurrentCulture
property of CurrentThred
of the Thread
class to a particular culture, the culture of the application is changed until the application is closed. Use this only for testing purposes.
In the previous recipes we saw how to extend existing attributes to suit our needs. However, there are situations where you don't have an existing attribute to extend. In such cases, you will have to create your own attribute. In this recipe we will look at creating custom attributes. Our attribute will help you to keep track of bugs fixed within a class. It can be used to tag the class itself or methods within the class.
A custom attribute is a class extending from System.Attribute
. However, its behavior is quite different from a class. And to make it to work as an attribute, extra steps such as creating another class that can process the attribute are required.
Launch Visual Studio .NET 2012. Create a project of type Class Library and name it
CookBook.Recipes.Core.DefectTracker
.Delete
Class1.class
from the project.Add a folder to the project and name it
Attributes
.Next, add a class to the folder and name it
DefectTrackerAttribute
.Derive the
DefectTrackerAttribute
class fromAttribute
.Add the following properties as shown in the following table:
Name
Type
DefectID
Int
ResolvedBy
String
ResolvedOn
String
Comments
String
Once the properties are added, our class will look as follows:
namespace CookBook.Recipes.Core.DefectTracker.Attributes { public class DefectTrackerAttribute:Attribute { #region Public Properties public int DefectID {get;set;} public string ResolvedBy {get;set;} public string ResolvedOn {get;set;} public string Comments {get;set;} #endregion } }
Now, let's specify all the places where, within a class, we can use this attribute by decorating/tagging the class with the
AttributeUsage
attribute:[AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)] public class DefectTrackerAttribute:Attribute { #region Public Properties public int DefectID {get;set;} public string ResolvedBy {get;set;} public string ResolvedOn {get;set;} public string Comments {get;set;} #endregion } }
Next, add a constructor so that we can pass the values via the constructor. The
Comments
parameter will be optional. In code, the constructor will be as follows:public DefectTrackerAttribute(int defectID, string resolvedBy, string resolvedOn, string Comments = "") { this.DefectID = defectID; this.ResolvedBy = resolvedBy; this.ResolvedOn = resolvedOn; this.Comments = Comments; }
That completes the steps in creating
DefectTrackerAttribute
. Let's see how to use it.Add a project of type Class Library and name it
DefectTrackerTest
.Delete
Class1.cs
.Add a reference to the
CookBook.Recipes.Core.DefectTracker
project.Next, add a class to
DefectTrackerTest
and name itCurrencyConverter
.Add the following import:
using CookBook.Recipes.Core.DefectTracker.Attributes;
Add a
private
variable of typedouble
:private double _value;
Add a parameterized constructor that will look as follows:
[DefectTrackerAttribute(1042,"AP", "2012/02/11","Changed float param to double")] public CurrencyConverter(double value) { _value = value; }
Add
DefectTrackerAttribute
to the constructor.Add a method that accepts a
double
argument and returns adouble
value.Tag the method with
DefectTrackerAttribute
. The method will look as follows:[DefectTrackerAttribute(DefectID = 1042, ResolvedBy = "AP", ResolvedOn = "2012/02/11")] public double ToRupee() { return _value * 50; }
As I mentioned earlier, a custom attribute is really a class that is inherited from System.Attribute
. Our DefectTrackerAttribute
class is no different. So, we inherit it from Attribute
:
public class DefectTrackerAttribute:Attribute { }
Now, we have to pass information to the attribute. This can be done in two ways:
Through a constructor
As a named parameter of the constructor
Using a constructor to pass the parameter is similar to what we do for classes. However, for the constructor parameters to be used as named parameters, we will need properties. So we added properties and the constructor:
public class DefectTrackerAttribute:Attribute { #region Public Properties public int DefectID {get;set;} public string ResolvedBy {get;set;} public string ResolvedOn {get;set;} public string Comments {get;set;} #endregion public DefectTrackerAttribute() { } public DefectTrackerAttribute(int defectID, string resolvedBy, string resolvedOn, string Comments = "") { this.DefectID = defectID; this.ResolvedBy = resolvedBy; this.ResolvedOn = resolvedOn; this.Comments = Comments; } }
The members of the class to which we can apply the attribute is the most important aspect of that attribute. To specify this, we can use the AttributeUsage
attribute. Since we want to apply our attribute to classes, constructors, methods, fields, and properties, we specify it as follows:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)] public class DefectTrackerAttribute:Attribute { #region Public Properties public int DefectID {get;set;} public string ResolvedBy {get;set;} public string ResolvedOn {get;set;} public string Comments {get;set;} #endregion public DefectTrackerAttribute(int defectID, string resolvedBy, string resolvedOn, string Comments = "") { this.DefectID = defectID; this.ResolvedBy = resolvedBy; this.ResolvedOn = resolvedOn; this.Comments = Comments; } }
The AllowMultiple
argument specifies whether the attribute can be used more than once on a class or its members. As the same member may be modified multiple times for different defects, we will be using our attribute multiple times on that member. Hence, we passed AllowMultiple
as true. With that we come to the end of this recipe.
In DefectTrackerTest
, we have just one class, CurrencyConverter
. We tagged its constructor and ToRupee
with DefectTrackerAttribute
:
public CurrencyConverter(double value) { _value = value; } [DefectTrackerAttribute(DefectID = 1042, ResolvedBy = "AP", ResolvedOn = "2012/02/11")]public double ToRupee() { return _value * 50; }
In the next recipe we will see how to create a processor for our attribute and how to use them both.
In the previous recipe, we developed a custom attribute named DefectTrackerAttribute
. However, the attribute, by itself, does not do anything. Unless there is an application or library that looks at the class and members tagged/decorated by the attribute, it is just a piece of code that does nothing. So, in this recipe, we will see how to process the class tagged by DefectTrackerAttribute
using reflection.
Launch Visual Studio .NET 2012.
Open
CustomAttribute.sln
.Open the
CookBook.Recipes.Core.DefectTracker
project in Add a folder to the project. Name itProcessor
.Add a class to the folder and name it
DefectTrackerProcessor
.Add the following imports:
using System.Reflection; using CookBook.Recipes.Core.DefectTracker.Attributes;
Add the following methods to it:
Name
Parameters
Return type
GetDetails
String assmblyPath
String className
String
GetMemberDetails
String memberName
IEnumerable<DefectTrackerAttribute> attributes
String
To the
GetMemberDetails
method, add the following code:StringBuilder sb = new StringBuilder(); sb.Append("\n"); if (!sb.ToString().Contains(memberName)) { sb.Append(memberName); } foreach (var attribute in attributes) { sb.Append("ID-"); sb.Append(attribute.DefectID); sb.Append("\t"); sb.Append("Resolved By-"); sb.Append(attribute.ResolvedBy); sb.Append("\t"); sb.Append("Resolved On-"); sb.Append(attribute.ResolvedOn); } return sb.ToString();
To the
GetDetails
method, add the following code:StringBuilder details = new StringBuilder(); Assembly assembly = Assembly.LoadFrom(assemblyPath); Type type = assembly.GetType(className, true, true); //check whether the constructors have the custom attribute ConstructorInfo[] constructorInfo = type.GetConstructors(); foreach (var item in constructorInfo) { IEnumerable<DefectTrackerAttribute> attributes = item.GetCustomAttributes<DefectTrackerAttribute>(); details.Append(GetMemberDetails(item.Name, attributes)); } //check whether the methods have custom attribute MethodInfo[] methodInfo = type.GetMethods(); foreach (var item in methodInfo) { IEnumerable<DefectTrackerAttribute> attributes = item.GetCustomAttributes<DefectTrackerAttribute>(); if (attributes.Count() > 0) { details.Append(GetMemberDetails(item.Name, attributes)); } } return details.ToString();
That completes the first step. Next, let us look at how to use the processor.
Add a project of type Windows Forms Application and name it
DefectTrackerApp
.Add a reference to the
CookBook.Recipes.Core.DefectTracker
project.Rename the
Form
class toTrackDefect
.Switch to the design mode. Design the form so that it looks like the following screenshot:
Name the textboxes and buttons as follows:
Control
Description
Name
Textbox
For the assembly path, that is, the path of the
.dll
or.exe
file to be loadedtxtAssembly
Button
To display the file open dialog
btnOpen
Textbox
To enter the fully qualified class name to be loaded from the assembly
txtClassName
Button
To call
DefectTrackerProcessor
btnLoad
Textbox
To display the details of the fixed defects in the class
txtDetails
Double-click on
btnOpen
to generate the Click event handler. In the event handler, add the following code:if( diagOpen.ShowDialog() == System.Windows.Forms.DialogResult.OK) txtPath.Text = diagOpen.FileName;
Switch to the design mode. Double-click on
btnOpen
to generate the Click event handler. In the event handler, add the following code:if (!String.IsNullOrEmpty(txtPath.Text) && !String.IsNullOrEmpty(txtClassName.Text)) { txtDetails.Text = new DefectTrackerProcessor().GetDetails(txtPath.Text, txtClassName.Text); }
Add the following import:
using CookBook.Recipes.Core.DefectTracker.Attributes;
Set
DefectTrackerApp
as a startup project.Press F5 and run the application.
Click on the Open button. In the file dialog, navigate to the
bin
folder of theDefectTrackerTest
project. SelectDefectTracker.dll
. Observe the full path being displayed in the textbox next to the Open button.In the textbox next to the Load button, enter
DefectTrackerTest.CurrencyConverter
. Click on Load.The multiline textbox next to the Details label will be filled with details of the constructor and the methods that make use of the attribute.
That completes the steps for creating a processor for the custom attribute and using it. Next, let us dive more into the code to understand what is happening.
The core of the DefectTrackerProcessor
class is the GetDetails
method that uses reflection to find out whether the class whose name has been sent as the parameter contains DefectTrackerAttribute
. The first step is to load the assembly containing the class using the LoadFrom
static method of the Assembly
class. The next step is to retrieve the class from the assembly and set it to the Type
class variable. These two steps are achieved in the following statements:
Assembly assembly = Assembly.LoadFrom(assemblyPath); Type type = assembly.GetType(className, true, true);
The Type
class is the root of the functionality provided by .NET for reflection. It is the entry point to gain details regarding the members of a class or structure. In the preceding code, the GetType
method looks into the assembly and returns the Type
instance containing details of the class whose name has been passed via the className
variable. The second parameter of GetType
is set to true
so that, if the class is not found, an exception is thrown and we can know that something is wrong. We want to find the class regardless of whether the class name is passed in upper- or lowercase. Hence, the last parameter is set to true
to tell GetType
to ignore the case of the class name.
Now that we have the Type
instance containing the details of the class, we can check whether the members of the class are decorated with DefectTrackerAttribute
or not. The first class member that we check is the constructor. The following statement provides the details of the constructors within a class:
//check whether the constructors have the custom attribute ConstructorInfo[] constructorInfo = type.GetConstructors();
The
GetConstructors
method of Type
returns an array of ConstructorInfo
. Each instance of ConstructorInfo
in the array contains details such as the name and attributes decorating that constructor, for each constructor of the class that Type
represents. Next, we iterate through the array and get the list for DefectTrackerAttribute
for the current constructor as shown in the following statements:
foreach (var item in constructorInfo) { IEnumerable<DefectTrackerAttribute> attributes = item.GetCustomAttributes<DefectTrackerAttribute>(); details.Append(GetMemberDetails(item.Name, attributes)); }
Then, we passed the list to the GetMemberDetails
method along with the name. In the GetMemberDetails
method, we iterate over the list to get the defect details using the properties of DefectTrackerAttribute
as shown:
foreach (var attribute in attributes) { sb.Append("ID-"); sb.Append(attribute.DefectID); sb.Append("\t"); sb.Append("Resolved By-"); sb.Append(attribute.ResolvedBy); sb.Append("\t"); sb.Append("Resolved On-"); sb.Append(attribute.ResolvedOn); }
Similar to the constructor, we can get details of methods tagged with DefectTrackerAttribute
using the GetMethodInfo
method of the Type
class. That is what we have done in the following statements:
//check whether the methods have custom attribute MethodInfo[] methodInfo = type.GetMethods(); foreach (var item in methodInfo) { IEnumerable<DefectTrackerAttribute> attributes = item.GetCustomAttributes<DefectTrackerAttribute>() if (attributes.Count() > 0) { details.Append(GetMemberDetails(item.Name, attributes)); } }
The MethodInfo
class contains the details of a method of the class represented by Type
. The array returned by GetMethods()
contains details of all the methods within the class. We iterated over the array and determined whether the method is tagged with the attribute or not. If it is tagged, that is, the attribute list contains one or more elements, we fetched the details, just like we did for the constructor(s). Once we got the details of the constructor and the methods, we returned the details as a string
value.
In the TrackDefect
class of DefectTrackerApp
, we called the instantiated DefectTrackerProcessor
and called the
GetDetails
method with the assembly path and the class name that was entered via the UI. This is done in the Click event handler of the Load
button.
private void btnLoad_Click(object sender, EventArgs e) { if (!String.IsNullOrEmpty(txtPath.Text) && !String.IsNullOrEmpty(txtClassName.Text)) { txtDetails.Text = new DefectTrackerProcessor().GetDetails(txtPath.Text, txtClassName.Text); } }
We saw how Type
helps us to get details of constructors and methods of a class. Similarly, we can get details of the properties of a class using the GetProperties
method. It returns an array of PropertyInfo
. Each PropertyInfo
holds the details of a specific property of the class. If you want details of only a specific property, call the GetProperty()
method and pass the property name as argument.
Asynchronous file I/O has been a feature of .NET from Version 1.1 onwards. However, the loops that the developer had to run to get it working were many. In Version 4.5, .NET introduced a new API that would make using asynchronous file operation easy. At the core of the API, we have two operators—async
and await
. This recipe will focus on using these operators to implement an asynchronous directory-to-directory copy utility.
The following steps will help you perform directory-to-directory copy using asynchronous file I/O:
Launch Visual Studio .NET 2012. Create a project of type Class Library and name it
CookBook.Recipes.Core.AsyncFileIO
.Rename
Class1.cs
toUtils.cs
.Open
Utils.cs
. Make the class public and static as shown:public static class Utils { }
Add a
public static
method to the class and name itCopyDirectoryAsync
. It will take two parameters – a string containing the source directory and another string containing the target directory. It will return aTask
value of typeint
. The signature will be as follows:public static Task<int> CopyDirectoryAsync(string sourceDir, string targetDir) { }
Change the method signature to add the
async
keyword to it:public static async Task<int> CopyDirectoryAsync(string sourceDir, string targetDir) { }
Add a variable of type
int
to the method. Assign the count of the files in the target directory:int count = Directory.EnumerateFiles(targetDir).Count();
Next, add the following code:
foreach (string filename in Directory.EnumerateFiles(sourceDir)) { using (FileStream sourceStream = File.Open(filename, FileMode.Open)) { using (FileStream DestinationStream = File.Create(targetDir + filename.Substring(filename.LastIndexOf('\\')))) { await sourceStream.CopyToAsync(DestinationStream); } } }
Add the
return
statement as shown:return (Directory.EnumerateFiles(targetDir).Count() - count);
Next, let us look at how to use the
Utility
class. To use theUtility
class, we will create a Windows Forms Application project.Add a project of type Windows Forms Application and name it
AsyncFileIO
.Add a reference to
CookBook.Recipes.Core.AsyncFileIO
.Rename
Form1.cs
toFileUtility.cs
.Open
FileUtility.cs
in the design mode. Design the form so that it looks like the following screenshot:Name the controls as detailed in the following table:
Control
Description
Name
Textbox
To hold the path of the source directory
txtSource
Button
To display the directory chooser for choosing the source directory
btnSource
Textbox
To hold the path of the target directory
txtTarget
Button
To display the directory chooser for choosing the target directory
btnTarget
Button
To start copying from the source to the target directory
btnCopy
Folder Browser Dialog
To display the folder chooser
diagFolder
Double-click on
btnSource
to add the Click event handler for it. Add the following code to the event handler:if (diagFolder.ShowDialog() == System.Windows.Forms.DialogResult.OK) { txtSource.Text = diagFolder.SelectedPath; }
Switch to the design mode. Add the Click event handler for
btnTarget
by double-clicking on it. In the event handler, add the following code:if (diagFolder.ShowDialog() == System.Windows.Forms.DialogResult.OK) { txtTarget.Text = diagFolder.SelectedPath; }
Similarly, add the Click event handler for
btnCopy
. Then, add the following code to the handler:if (!String.IsNullOrEmpty(txtSource.Text) && !String.IsNullOrEmpty(txtTarget.Text)) { Utils.CopyDirectoryAsync(txtSource.Text, txtTarget.Text); }
Press F5 and run the application. Click on the Source button to choose the directory that you want to copy. Choose the directory to which you want to copy by clicking on the Target button. Then click on the Copy button to start copying.
That completes the steps to create directory-to-directory copy functionality, which does the copying asynchronously.
The whole logic of asynchronous copy is implemented within one method: the CopyDirectoryAsync
method of the Utils
class. As you have already seen, both the class and the method are public as well as static. The reason for making the method static is that we have implemented it as a utility method. Utility methods are always implemented as public and static methods. In .NET itself, all the methods of the Math
class are static methods, and the class itself is static.
Now let us look at how the logic works. If you observe the signature of the method, there are two things that make it different from other methods (or synchronous methods).
public static async Task<int> CopyDirectoryAsync(string sourceDir, string targetDir)
First is the async
keyword. It means that somewhere in the method an asynchronous task is going to be executed. Next is the return type. If you want to return any value from a method that has async
in its signature, you will have to do it using the Task
object. In our case, we wanted to return the number of files copied. So we have used Task<int>
as our return type.
As we wanted to return the number of files copied, we will have to know the current number of files in the target directory. With the following statement we can achieve this:
int count = Directory.EnumerateFiles(targetDir).Count();
In the preceding statement, we have used the EnumerateFiles
method of the Directory
class to get a list of all the filenames within the target directory and then got the number of elements in that list. Next, we have to get the files we want to copy. For that, we iterate over the filenames returned by Directory.EnumerateFiles
. To EnumerateFiles
, we passed the path of the source directory as shown:
foreach (string filename in Directory.EnumerateFiles(sourceDir)) { }
Then, we open each file for reading as a stream:
foreach (string filename in Directory.EnumerateFiles(sourceDir)) { using (FileStream sourceStream = File.Open(filename, FileMode.Open)) { } }
Once we have opened the file to be copied, we have to open another stream to the location where the file will be copied. To do that we created a file of the same name and then connected a new stream to it, as shown in the following highlighted code:
foreach (string filename in Directory.EnumerateFiles(sourceDir)) { using (FileStream sourceStream = File.Open(filename, FileMode.Open)) { using (FileStream DestinationStream = File.Create(targetDir + filename.Substring(filename.LastIndexOf('\\')))) { } } }
Now comes the most important part of our code, the statement that makes asynchronous copy work. Once we open a stream to a file in the destination directory, we can transfer the contents. To do so, we used the CopyToAsync
method of the FileStream
class. However, what makes the content transfer statement important is the await
keyword before it, as in:
await sourceStream.CopyToAsync(DestinationStream);
The await
keyword in the preceding statement tells the framework that the execution of this method is suspended until the
CopyToAsync
method is done. Apart from that, the await
keyword also tells the framework to return the control of the execution to the code that called this method, that is, CopyDirectoryAsync
. In other words, until the current file is copied, the application can resume its normal operation and would not appear to the user as if the application is frozen.
In our case, the FileUtils
class calls the
CopyDirectoryAsync
method when Copy is clicked. When the execution reaches the await
statement, the control is returned back to the FileUtils
class until the current file, as per the loop, is copied. Till the file is copied, the user can make use of any feature of the application. Once the file is copied, the file in the source directory is opened and the process continues. If the size of the files are huge, say 500 MB, you will be able to see the effect of the asynchronous transfer.
In Version 3.5, .NET introduced the var
keyword. With var
, developers got the choice of not declaring the type of the variable. It became the task of the compiler to infer the type of the variable based on the value assigned. .NET 4.0 took this concept a step ahead by introducing the keyword dynamic
.
When a variable is declared dynamic
, its type is inferred only during execution. The compiler does not check for the type and type safety of a dynamic
variable. This helps a lot when dealing with data whose type is either unknown or too complex to be bound to a compiled object. In this recipe, you will see how dynamic
can access parsed JSON data without creating classes for the JSON elements. One thing to keep in mind is that in this recipe the implementation of logic and the application that uses the implementation are one and the same. In other words, the main application itself contains the logic.
The following steps will help you access JSON using dynamic programming:
In Visual Studio .NET 2012, create a new project of type Windows Forms Application. Name it
DynamicJsonParsing
.Rename
Form1.cs
toAccessJson.cs
.Open
AccessJson
in the design mode. Design the form so that it looks like the following screenshot:Name the controls as detailed the following table:
Control
Description
Name
Label
To display the name of the element whose value will be shown
lblValueFor
Label
To display the value of the element
lblValue
Label
To display the name of the complex element whose value will be shown
lblComplexValueFor
Label
To display the value of the complex element
lblComplexValue
Textbox
To display the JSON string being parsed and accessed
txtJson
To parse, access, and display the values of JSON data.
btnParse
Add a reference to
System.Web.Extensions
.Switch to the code view mode and add the following import:
using System.Web.Script.Serialization;
Next, add the following
private
method to the class. It will return the JSON data.private string GetJsonString() { return @"{ 'order':{ 'name':'testOrder', 'value':'1000', 'products':[ {'name': 'testProduct', 'expiry': '12 months' }] }, 'delivery':'at home' }"; }
In the constructor, add the following statement after the call of the
InitializeComponents
method:txtJson.Text = GetJsonString();
Switch to the design mode. Double-click on the
btnParse
button to add a Click event handler.In the event handler, add the following statements:
var serializer = new JavaScriptSerializer(); var dictionary = serializer.Deserialize<Dictionary<string, dynamic>>(txtJson.Text); lblValueOf.Text = "Value of delivery"; lblValue.Text = dictionary["delivery"]; lblComplexValueOf.Text = "name of product of order"; lblComplexValue.Text = dictionary["order"]["products"][0]["name"];
Press F5 and run the application. Click on the
Parse
button. The values will be displayed as shown:
The core work of accessing JSON happens in the event handler for the Parse
button. We had assigned a string containing JSON. The data within the JSON string is about a particular order. The product contained in the order and the type of delivery is as shown:
{ 'order':{ 'name':'testOrder', 'value':'1000', 'products':[ {'name': 'testProduct', 'expiry': '12 months' }] }, 'delivery':'at home' }
In the JSON above, delivery is a normal data element. However, the name of a product is a complex data element since name
is a part of the products
array (note the square bracket), which itself is part of order element. For data of this type, if we go for the traditional approach to access the values, we will have to either create multiple classes or perform complex string manipulations. That is where having dynamic
helps.
In the event handler for the Parse
button, we first parsed the JSON using JavaScriptSerializer
as shown:
var serializer = new JavaScriptSerializer(); var dictionary = serializer.Deserialize<Dictionary<string, dynamic>>(txtJson.Text);
The JSON data is deserialized or parsed into a dictionary having a string
value as key and a dynamic
object as value. If we look at the dictionary as a table of Key
and Value
, it will look something like the following:
Key |
Value |
---|---|
| |
{ 'name':'testOrder', 'value':'1000', 'products':[ {'name': 'testProduct', 'expiry': '12 months' }] }
|
From the preceding table it is clear that the value for delivery is at home
. So the following statement is nothing special, just a simple way of getting the value via the key:
lblValue.Text = dictionary["delivery"];
However, if we have to find the name of the product, simply using the key won't work. If we pass the order as the key, we will get a complete piece of complex JSON data. However, this data is of type dynamic
. So, if we write statements like the ones written as follows, the compiler won't complain, and will leave it to the runtime check to assign the values:
dynamic order = dictionary["order"]; dynamic products = order["products"]; dynamic product = products[0]; dynamic name = product["name"];
The first statement assigns the value of the order to the order
variable. The order
variable will now contain an array of products. The first element of the product
array is assigned to the product
variable. Then, from the product
variable, the name key is used to access the product name. On combining all four steps, we get:
lblComplexValue.Text = dictionary["order"]["products"][0]["name"];
So, by using dynamic programming, we were able to access the parsed JSON data without having to create the class hierarchies and without having to use string manipulation.