Article

Hot-Swappable Objects

Keep your data flexible with AppDomains and Assemblies

Like many of you, I have been using .NET since before it was officially released. Even now, after years of using the product, I am still impressed by its incredibly flexible nature. When using the classes in the .NET framework, there are usually half a dozen ways of performing the task at hand. While this does tend to make learning the framework a bit of a chore, it seems to pay off time and time again. I was thinking of this flexibility one day when a client's requirements for a new application surfaced.

The client needed a new application that would service many different customers. Data from each customer needed to be loaded, processed, and transmitted to its destination. Needless to say, the customers each had their own unique data formats, processing rules, and transmission requirements. In addition, new customers would be added in the future, and their requirements would not be known until after the application was already in production. I was asked to design a system that could handle not only the disparate needs of the current customers, but those of the yet-unknown customers as well. As you will see, the .NET framework's flexible nature presented a ready-made solution.

The general idea was to define a fixed number of processing tasks, or steps, that were very general in nature. For example, each customer's data would be loaded, parsed, manipulated in some way, and then sent to a specific destination. For each step, a "hot-swappable" object would be created that was capable of executing that particular step for a specific customer. The class representing the object would be defined in its own class library, outside of the main application. The object would be dynamically instantiated and invoked as needed by a controlling process. If many customers had similar needs, then a more generic object could process that step for multiple customers. In the worst case, there would be one object per customer per step. In actual practice, however, several objects would be able to service multiple customers, thus limiting the number of objects that must be created.

In this article, I show how this system was created, focusing on the .NET technologies that were key to the system's creation. Although unfamiliar to some, the concepts presented here are not new. There are several languages that have been able to implement such an architecture for years. However, the .NET framework removed much of the complexity that would be present in other languages. In fact, most of my time was spent not in creating reams of code, but in researching how the existing .NET classes could be used. Instead of creating code, I was able to leverage the .NET framework. At first, I expected that I would have to learn the use of many new .NET classes, but in the end, there were only three topics that were required: Interfaces, App Domains, and Assemblies.

Interfaces

In programming terms, an Interface is a contract between a class that provides some functionality and a class that utilizes that functionality. Such interfaces can describe common class members such as methods, properties, indexers, and events. The interface does not provide or describe the implementation of the individual members, just their public signatures. Two different objects may conform to the same interface, but may implement the related functionality in vastly different ways. (For example, stopping a car and stopping a diesel truck can both be accomplished by pressing the brake pedal, but internally, each vehicle performs a very different process.)

The usefulness of interfaces becomes apparent when you apply them to multiple objects. When you write code that uses an object, you usually tie your code directly to that object. In Listing 1 you can see two classes that both implement a Brake() method. If you create and instantiate a variable of type Car, you can easily invoke the Car object's Brake() method. However, if you then try to use this same variable to instantiate a Truck object, you will receive an error. A single variable cannot refer to two types of object. (Yes, you can create a variable of type object to do this, but that is less efficient and more prone to error. It can also be slower. I generally avoid such late-bound programming when I can.) In Listing 2 the classes have been altered to implement a common Vehicle interface. Now, a single variable of type Vehicle can refer to either the Car or Truck class. This variable can be used to invoke the Brake() method without have to refer to either of the objects directly. By having an object implement an interface, you remove the requirement that a variable must be tied to a particular object type. You also make knowledge of the actual object used unimportant. This is a core idea behind the "hot-pluggable" object architecture.

AppDomains and Assemblies

AppDomains, more properly called Application Domains, are security and process boundaries that .NET places around the various objects that are created within the same application scope. AppDomains isolate objects in one application from those in another. Each application has one default AppDomain, although more can be created. In the "hot-pluggable" object scenario, AppDomains are important because they allow management of the objects' lifetimes. If the objects are loaded into the default AppDomain, then they will reside in memory for the life of the application. If this is undesirable, a new AppDomain can be created and the objects loaded there. When the objects are no longer needed, the entire AppDomain can be unloaded via the AppDomain.Unload method.

In .NET, Assemblies are the fundamental units of application deployment, security, and scoping. They represent the smallest portion of code that can be loaded and manipulated. This usually equates to a single .EXE or .DLL file, but can contain other resources as well. The code in an assembly cannot be executed until it is loaded into an AppDomain. In the "hot-pluggable" object scenario, each object is created in its own Class Library, and therefore in its own assembly. In order for one of these objects to be used, it must be loaded from disk, placed into an AppDomain, and instantiated.

In Listing 3 the CreateObjectFrom-Assembly function performs these tasks. First, it uses the shared (static) AppDomain.CurrentDomain method to obtain a reference to the default application domain. This is done because I decided to load the objects into the default AppDomain. Had I decided to load them into a new AppDomain, then a call to AppDomain.CreateDomain would have been used. Next, the function determines the path of the controlling process's assembly by trimming unneeded portions off the fully qualified name of the executing assembly. Because this application was implemented as a Windows Service, the more common methods of determining the startup path, such as Application.StartupPath, do not work. The function then invokes the shared Assembly.LoadFrom method, passing in the fully qualified name of the DLL file that contains the desired object. This loads the assembly into memory, but does not create any of the assembly's objects. To do this, the AppDomain.CreateInstanceAndUnwrap method is called with the full name of the assembly and the name of the type (such as "MyDLL.MyObject").

Putting It All Together

By having each "hot-pluggable" object implement an interface, we have avoided having to link a variable directly to an object via its type definition. Next, we have used the .NET framework's ability to dynamically load objects and assemblies to create needed objects on the fly. The final portion of the solution is to store the definition of each customer in a database, where the appropriate object for each step is defined. For example, in Figure 1, there are four customers defined. The data file received from Customer A may be vastly different from the data file received from Customer B. When it is time for Customer A's data file to be parsed, the appropriate object (CustomerA.PlugIns.Parser) can be dynamically loaded from the appropriate assembly (CustomerA.dll). The proper method then must be invoked. For simplicity, I decided that all such methods would be called "Process," but this is not required. (See Listing 4 for a function that will find the method that implements a specific interface.) When parsing Customer B's data file, an entirely different object will be loaded. This can be extended to an unlimited number of customers after the system has been placed into production. The system simply consults the database to determine which object is needed, loads that object, and executes the required method via the proper interface. When new customers are added, existing objects can be reused, or, if needed, entirely new ones can be created. This allows the system to process customers in ways not even dreamed of when the application was created. As long as the new objects implement the proper interface, and they are entered into the database, the objects will be invoked at the proper time.

An important note to consider here is security. Care must be taken to ensure that only valid objects are loaded in this manner, as it is possible for someone to substitute a "rogue" object into your program. I prevented this by allowing objects to be loaded from one directory only - the directory containing the controlling application. (This is why Listing 3 computes the path to the executing assembly.) This directory was protected by NTFS permissions, so that only administrators and this application could access the directory.

As useful as this architecture was, it did introduce one problem: configuration. I wanted to provide a generic configuration application for this system, but how could I when new objects could be created long after the configuration program was placed into production? The answer was to be found in the Reflection namespace and the use of custom attributes, but this is a conversation for another time.

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.