You've seen how to use .NET classes to create instances of objects and invoke methods on those instances dynamically. Now you need to provide a layer of abstraction so that the necessary information is available at run time instead of compile time.
|In order to unload a dynamically loaded assembly, you must load the assembly into a separate application domain. Use the static method AppDomain.CreateDomain. Then, use the returned AppDomain's Load method to load an assembly into the domain. When you want to unload the domain, use the static method AppDomain.Unload. This will allow you to unload the validation assemblies in that app domain from the currently running program and replace them with updated versions.|
We'll use XML as the data markup language for building the data validators as well as for passing parameters to the validation routine. This allows your application to have a common data representation when communicating between modules. Internally the modules may perform transformations on the data, but the caller never knows about that transformation.
We'll use Interfaces to implement our validation. The syntax for interface declarations is similar to that for class declarations. An interface is like a class in which every member is abstract; it can only contain property and method declarations without function bodies. An interface may not contain field declarations, initializer declarations, or nested class declarations. An interface can implement additional interfaces. A class may extend only one base class, but a class may implement many interfaces. Such implementation of multiple interfaces by a class allows for a form of multiple inheritance that is simpler than in other object-oriented languages, for example, in C++.
Using an interface to provide validation methods provides an easy way to determine if a particular class object is to be validated.
IValidator v = foo as IValidator;
if (null != v)
In this example foo
is an instance of a class of Type Foo. Foo implements the IValidator interface, so "v" is not null. You can then call a method on the interface using the instance of the class. If foo
did not implement the IValidator interface, the "v" would be null, and you could not call the interface method.
Note the use of the operator as
in the above example. The as
operator acts like a cast except that it yields null on conversion failure instead of raising an exception. More formally, an expression of the form:
expression as type
is equivalent to...
expression is type ? (type)expression : (type)null
...except that expression is evaluated only once.
Using the is keyword means that it attempts to convert the object to the specified Type. If it can convert the object to a Type without throwing an exception, it returns true
. However, you would still have to perform a cast to the Type again, so that requires two casts on the object whereas the as
operator only requires one cast, and is more efficient.
is more efficient then using the is
operator. You use the is
operator to check whether the run time type of an object is compatible with a given type. Use the is
operator in an expression of the form: expression is type
Now that you know how to use the .NET classes to dynamically load Types and invoke methods at run time, let's define the data validation rules and store that information. I'll refer to the data validation rules as metadata; that is, data that describes other data.
For Brierley & Partners' projects, we store metadata in a series of tables stored in a Microsoft SQL Server database. Using a series of metadata classes, we load the information from the database and make it available to any program that utilizes these classes. The sample code presented in the next section uses a simplified XML file to hold the validation information that applications will load at run time.
Deciding where to store the metadata is a secondary consideration. What is important is storing the information you need to do the actual validation. At a minimum you need to know the assembly name, the class Type, and the method name you want to invoke. You'll also need to know what parameters to pass to the validator and which class members you will be validating. Listing 1
demonstrates an XML document containing the data validation information.
This sample validation file defines the validation that will occur for a class named "Class2." There is one validator defined that uses a class Type StringValidation contained in an assembly named StringValidation. The validator method to call is named "Validate." It has four parameters defined: Min
, and Value
specifies the minimum length of the string, Max
the maximum length of the string, Action
specifies what action (if any) to perform on the string (more about this later), and Value
, which is the value being passed to the validator. Each parameter has a type, which indicates that the parameter is a constructor parameter, an Invocation
parameter (used in the Validate
method), or both. The "Value" node of each parameter is used by the validator to perform initialization in the case of constructor parameters, and as the source data for the Validate
You'll notice that the "Value" node for the Value
parameter is the name of a class member. The sample builds a hash table containing a class member name as a key and the name of its property get/set as the value. It also uses a single class to perform all validation on the classes. If you know the name of a property you can use reflection to obtain a PropertyInfo object on a class based on the property name. The PropertyInfo class has GetValue
methods that you can invoke to retrieve/set the actual data contained in the class member.
Type t = obj.GetType();
PropertyInfo info = t.GetProperty("Foo");
String s = info.GetValue(obj, null).ToString();
Using validation metadata allows you to add, modify, and remove validation from class members without having to recompile any objects. You can also modify the validator and the metadata as needed without having to rebuild the main application.