Generic Object Configuration

A total solution

In a previous article (DNDJ Volume 3, issue 4), I discussed an application that had to load, process, and transmit data received from multiple customers. The system had to perform a specific set of tasks or steps for each customer. Because of the diverse needs of each customer, however, the actual implementation of each step could vary widely, often on a per-customer basis. In addition, new customers would be added long after the application was completed, and the unique requirements of these new customers would also have to be met.

The system accomplished these goals by allowing each step to dynamically load an appropriate object from disk. A database kept track of which object would perform a particular step for a particular customer. Because these objects were loaded dynamically, new objects could be developed as needed, allowing new customers to be serviced without having to alter the existing objects or the application. By having each external object implement a specific interface, and by utilizing the .NET framework's ability to load objects from external assemblies, the application became a flexible framework for customer-specific processing.

While this approach did solve the problem, it also introduced a new one: How does one write a configuration program for an application that works in this manner? The standard approach to writing a configuration program is to "hard code" a certain amount of knowledge about each object that can be configured. This was not only distasteful to me, but it didn't answer my main question: How can the configuration program accommodate objects that will be created long after the configuration program itself is written? Steve Henke (a fellow co-worker and the ultimate author of the configuration program) and I got together to hammer this out.

During a brainstorming session, we determined that the best approach was to somehow encode each object with "metadata" that could be read by the configuration program. This "metadata" would describe the unique configuration needs of each object. We then remembered that the .NET framework provided support for class and method attributes, and that custom attributes could be created. This turned out to be the answer. We created one custom attribute that each configurable object would use to describe its configuration requirements. We created another custom attribute for each property in that object, so that the properties could be displayed on screen in an appropriate manner. The configuration program used some of the classes in the Reflection namespace to read this data. This approach allowed each object to provide enough information about itself that a generic configuration program could be written.

The remainder of this article describes the .NET technologies used by this configuration system. It shows how to create custom attributes for your own classes and how to read the information that these attributes supply. It also explains some of the challenges that we faced, and how implementing a custom base class for our configurable objects allowed these challenges to be overcome. Finally, it demonstrates how the Reflection namespace classes can be used to access object properties and invoke object methods in a completely generic manner.

The .NET framework supplies nearly two hundred built-in attributes. These attributes control how certain classes, methods, and properties are used. For example, in Listing 1, we see the standard "HelloWorld" Web service method that is created by a Web service project in Visual Studio. The attribute is the "<WebMethod()>" text found directly in front of the "HelloWorld" function definition. This attribute informs the .NET framework that the method should be made available to remote clients as a Web service method. The attribute accepts certain parameters that give additional control to the developer. In this example, I've added parameters to specify that responses should be cached for 30 seconds, and that remote clients should access the method with the name "SayHello." In Listing 2 we have an Employee class with two public fields. The entire class is marked "serializable," which means that it can be reduced to an XML or binary-encoded string representation. When the class is serialized, the "LastName" field will be included, but the "FirstName" field will not. This is because the "FirstName" field is modified with the "<NonSerialized()>" attribute.

As you can see, by using the .NET framework's built-in attributes, the developer can exert a good deal of control over how particular classes, properties, and methods are processed. Many of these built-in attributes can be applied not only to framework classes, but to those created by the developer as well. In addition, when existing attributes are not sufficient, custom attributes can be created to fill the gap.

Custom Attributes
Custom attributes are simply user-defined types that inherit from System.Attribute. A single custom class represents a particular custom attribute in a one-to-one relationship. Attribute parameters are represented via fields and/or properties of this custom class. In our configuration scenario, each configurable object needed an attribute that the configuration program could use to determine how to work with that object. Listing 3 shows the definition of such an attribute. The "DataClassAttribute" is defined as an ordinary class that inherits from System.Attribute. The developer can control where the custom attribute can be used by attaching an "<AttributeUsage()>" attribute to the class definition. In this case, the attribute can be used for classes only. Any attempt to place this attribute on a method or property would fail. Public fields or properties can be created to store the parameter information provided in the custom attribute.

Listing 4 shows how the Employee object could use the DataClassAttribute to provide information to a configuration program. (Note that in VB.NET, the word "Attribute" is used in an attribute definition, but is omitted when the attribute is used. The "DataClassAttribute" is specified correctly as "DataClass" in this example.) Using this information, a program could determine that the Employee class is configurable (IsConfigurable:=True), that it should be displayed on screen with a particular label (Label:="Employee"), and that the names of the Employee object's methods are "Delete," "Insert," "Update," and "Get" (DeleteMethod, GetMethod, etc.). The program could do this generically, without having to have specific information hard-coded into it.

Our configuration program also needed to be able to extract information about each configurable object's properties. The ConfigurationAttribute in Listing 5 was created for this. This attribute, which can be used only on properties, provided information about how the property should be displayed on screen and how it should be validated. An example of its actual use is shown in Listing 6.

Custom Object Base Class
Using two custom attributes, each object could be "decorated" with enough data to allow the configuration program to remain generic. But how was the configuration application supposed to read this data? The Reflection namespace provided the answer. Using reflection, the custom attributes could be read and their data used to configure the objects.

To simplify things, we decided to derive each configurable object from a common base class that would encapsulate all of the Reflection code. In Listing 7, you can see examples of some of the methods of this base class. First, a new class called ConfigurationInfo was created to hold the actual property attribute information. This class was derived from our ConfigurationAttribute class, but included fields to specify the object's property name and type. Next, the GetPropertyConfigura-tionData method was used to return a list (actually a HybridDictionary) of these ConfigurationInfo objects. It did this by iterating through each explicitly declared public property and processing only those properties that possessed a ConfigurationAttribute. For those properties, a ConfigurationInfo object was instantiated and populated with data about the property. This included the custom attribute data. The GetClassConfigurationData method performed a similar task for the configurable object itself, returning a DataClassAttribute to the caller.

Once the base class existed, we found many other uses for it. Code for common tasks such as validation and exception handling was moved to the base class. This served to further simplify the configurable objects themselves.

Configuration Program
With our custom attributes created, almost all of the pieces were in place. Custom objects could provide specific data about themselves to a configuration program in a generic way. All that was needed was a configuration program that could examine a list of assemblies, obtain information about any configurable objects contained in those assemblies, and dynamically create an appropriate user interface. The list of configurable assemblies was stored in a database to help ensure that only authorized assemblies were processed. Given this list, the code in Listing 8 could return a list of DataClassAttributes that represent all of the configurable objects contained in one of those assemblies. From there, each object could be queried for its configurable property and method information.

The last remaining problem was how to examine and/or invoke an object's properties and methods in a generic manner. The Reflection namespace once again provided a ready-made solution. A PropertyInfo object was used to represent each property of the configurable object. The PropertyInfo object's GetValue and SetValue methods were used to read and write property values. Similarly, a MethodInfo object was used to represent each method of the configurable object, and the MethodInfo's Invoke method used to execute the appropriate methods.

During the design of our customer classes, we had already decided that each class would contain shared (static, or non-instance) methods for managing the creation, modification, and deletion of instances of the class. This turned out to be fortuitous, because the configuration program could use these shared classes to create, modify, and delete object instances using methods of the MethodInfo class. No further code had to be created to manage the objects.

The configuration program could now obtain a list of configurable objects, query those objects for information about any properties that needed to be configured, and invoke the appropriate add/edit/delete methods as needed. All of this could be done without having to hard-code any object-specific information into the configuration program.

Total Solution
This article, together with the previous one, shows our total solution for the use and configuration of customer-specific objects. These objects can be developed at any time, either during the application development cycle, or long thereafter. The application can dynamically load these objects as needed and manipulate their properties and methods via an appropriate object interface. The configuration program can be used to modify the settings for these objects without having to know anything about the objects themselves. All code is written generically, allowing objects to be added or removed from the system without affecting any existing code. This is why, after all, I call them "hot-swappable" objects.

More Stories By Jerry Dixon

Jerry Dixon is a senior developer and architect for ACH Food Companies in Memphis, Tennessee. Over the past 16 years he has led development projects for a number of enterprise, mid-level, and small business organizations. While he has fulfilled multiple roles as an infrastructure designer, database administrator, and software developer, he specializes in XML, SQL and ASP.NET. He is a co-leader and frequent presenter at the Memphis .NET User Group. Jerry holds the following Microsoft certifications: MCSD (VB 6.0 and .NET), MCDBA (SQL 2000), MCSA (Windows 2000 and 2003), MCSE (Windows 2000), MCAD (.NET), MCT. He resides in Olive Branch, MS with his wife and son.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.