Designing Software Generics and Reflection in C# ASP .NET

modified

Introduction

As software becomes more complex, the ability to design a modular and re-usable architecture into your software becomes increasingly important. Using the programming concept of generics with interfaces, included in C# ASP .NET 2.0, software developers can abstract algorithms and classes into individual class libraries. The libraries can be accessed, in a generic fashion, from the main software. The key to this process relies on using interface design patterns and .NET reflection.

Generics and Reflection in C# ASP .NET

What’s a Generic?

.NET generics can cover several related topics. The common usage of generics is specifying templated object names instead of concrete ones, which lets you pass multiple types to a function rather than only the concrete form. However, in the example listed below, we will be focusing on using generics and reflection with a class library interface architecture. This is becoming a popular method of programming enterprise software, as it provides a great deal of re-usable modules and even allows for spreading assemblies across multiple servers, which means more speed.

A .generic software design is a way to refer to a class or library by using an interface name that the class inherits from, rather than using the class name itself. By decoupling the main code from the details of the class, and using its interface instead, we can deal with a set of different classes in the same way. To really show the power of C# ASP .NET generics and reflection, an example is needed. We’ll use the famous Hello World example, but within a .NET generics/reflection framework.

Classic Hello World Example

In the classic version of a Hello World application, we simply print text to the console. If you wanted to display different versions of the text, you would just write multiple sentences to the console or create individual functions to handle them. Since printing the text “Hello World” is really just an example of running an algorithm, let’s create 3 classes to handle printing the text for us. Each class needs to do it in a slightly different way. This is similar in nature to how you may need different classes to handle a certain task.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void main()
{
   HelloWorldOne MyHelloWorldOne = new HelloWorldOne();
   MyHelloWorldOne.SayHelloWorldOne();

   HelloWorldTwo MyHelloWorldTwo = new HelloWorldTwo();
   MyHelloWorldTwo.SayIt();
}

public class HelloWorldOne()
{
   public void SayHelloWorldOne()
   {
      Console.WriteLine("Hello World!");
   }
}

public class HelloWorldTwo()
{
   public void SayIt()
   {
      Console.WriteLine("Hello World Again!");
   }
}

The example above shows a straight-forward approach to including two classes to perform a similar task. The classes are provided inline, within the same .cs file. We instantiate each class and call the proper function to display the text to the console. Note that each class has its own name for the function to print the text. While this certainly works, it can be imporved upon with generics and reflection.

Doing the Hello World with Generics and Reflection

By using C# ASP .NET generics and reflection, we can completely abstract the individual Hello World classes into separate class libraries (DLL files). We can then use reflection to find any dlls in the same folder as ours, pull out their Hello World functions, and call them to write the text out. This is a powerful enterprise-level change, because it not only creates re-usable libraries, but it also allows us to offload the libraries to different servers, especially in the case of designing the libraries as COM+ modules.

Taking the above example, our main program using a generics/reflection architecture would look like the following:

1
2
3
4
5
6
7
void main()
{
    foreach (IGenericHello aHelloWorldImpl in GenericHelloWorlds.MyLoader.GetHelloWorlds())
    {
        aHelloWorldImpl.SayHello();
    }
}

The above example looks very different from the classic version. This is because the actual class libraries that contain the functions for printing the text are contained within individual class libraries. An important note is that those class libraries all inherit from the same generic interface. Because of this, we can use .NET reflection to load the DLLs, extract the interface, and call the member function SayHello(). Each library will perform its task in its own way. You can add, remove, and update libraries as you need, simply by swapping in and out, a DLL into the folder. Let’s examine the details of putting a generic Hello World implementation together.

Preparing your .NET Visual Studio Project

Since we will actually be creating multiple class libraries, as well as a main program, we will need to make multiple .NET projects within a parent solution. You can start this by creating a new Console project in Visual Studio called “GenericsDLLTest”. Once the project is created, create another new project by clicking File->New->Project. Select to make a Class Library. Under the “Solution” option, choose “Add to Solution”, so the project is added to the same solution, rather than a different folder. Name this new project “GenericHelloWorld”. The child project that you just created will house the GenericHelloWorld loader module. This is the module that finds all related dlls and extracts their desired classes.

At the end of this tutorial, you should have four projects includes in the same solution, similar to the following:

Solution ‘GenericsDLLTest’

  • GenericHelloWorld
  • GenericsDLLTest
  • HelloWorldBetter
  • HelloWorldPlain

It Always Starts With an Interface

We’ll start off easy, with a basic interface. As simple as the interface looks, it is the key to generics. We define a basic IGenericHelloWorld interface which contains one function to print text to the console. All class libraries that we create will inherit from this interface and define their own version of the SayHello() function. You can create this interface as a new class in the GenericHelloWorld project and name the file IGenericHelloWorld.cs.

1
2
3
4
public interface IGenericHelloWorld
{
    void SayHello(string strText);
}

It’s important to note that we create this interface in the child project created in the last step. This interface will reside in a separate class library because we will need to add it as a reference to each HelloWorld library that we create (and the main program). In general, everything we make will need to know about the IGenericHelloWorld, and will thus require a reference to its dll.

Loading DLLs Just Got Easy

We now create a class to handle loading the DLLs and returning a list of their generic interfaces. Our main program will call the loader class to retrieve a list of all HelloWorld libraries. As DLLs are added and removed from the target folder, this class will return a different list, and thus, perform different algorithms of your choosing. For simplicity, there is only one static function called GetHelloWorldImplementations() that we will call. This class will also reside in the GenericHelloWorld project with a file named GenericHelloWorldLoader.cs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System.Runtime.InteropServices;
using System.Runtime.Remoting;
using System.Reflection;
using System.IO;

public class GenericHelloWorldLoader
{
    public static List<IGenericHelloWorld> GetHelloWorldImplementations()
    {
        List<IGenericHelloWorld> m_ResultList = new List<IGenericHelloWorld>();

        try
        {
            // Get the directory our exe is running from.
            string strPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
            strPath = strPath.Substring(0, strPath.LastIndexOf('\\'));

            // All DLLs will be expected to be found in a helloworlds folder.
            strPath += @"\helloworlds";

            // Search for all DLLs in the folder and try to extract their interface.
            foreach (string strFile in Directory.GetFiles(strPath, "*.dll"))
            {
                // Open the class library.
                Assembly anAssembly = Assembly.LoadFrom(strFile);

                // Try to extract our IGenericHelloWorld interface, if it exists.
                foreach (Type aType in anAssembly.GetTypes())
                {
                    if (aType.GetInterface(typeof(IGenericHelloWorld).FullName) != null)
                    {
                        IGenericHelloWorld aGenericHelloWorldImplementation = (IGenericHelloWorld)Activator.CreateInstance(aType);
                        if (aGenericHelloWorldImplementation != null)
                        {
                            // We found an interface, let's add it to the list.
                            m_ResultList.Add(aGenericHelloWorldImplementation);
                        }
                    }
                }
            }
        }
        catch (Exception excep)
        {
            throw new Exception("Error, please verify a \"helloworlds\" folder exists in the same folder as the executable.", excep);
        }

        return m_ResultList;
    }
}

As you can see in the above example, we look for a folder called “helloworlds”, located in the same directory as our main executable. Within that folder, we search for all DLLs, attempt to open each one, and then attempt to extract our generic interface IGenericHelloWorld. If the DLL implements the interface, we add it to the list of items to return and continue. In the end, our main program retrieves a list of interfaces and can call the SayHello() function on each one to activate each class. This is the power of .NET generics and reflection at work.

Defining the Main Program

Now that we have created the generic DLL loader, we should create our main program that utilizes the loader. This will go in the project GenericsDLLTest. The main program is quite simple, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using GenericHelloWorld;

class Program
{
    static void Main(string[] args)
    {
        foreach (IGenericHelloWorld aHelloWorldImpl in GenericHelloWorld.GenericHelloWorldLoader.GetHelloWorldImplementations())
        {
            aHelloWorldImpl.SayHello("Mr. Smith");
        }

        Console.ReadKey();
    }
}

An important note is that back in your main program project, be sure to right-click on your project, select “Add Reference”, navigate to the bin/Debug folder for your GenericHelloWorldLoader, and select GenericHelloWorld.dll to add as a reference. You will also do this for each class library that you create. Also remember to include a “using GenericHelloWorld” at the top of your file to include the reference.

At this point, you should create a folder called “helloworlds” in the same directory as your main executable. This is where you will place a copy of each class library DLL file. The main program will search this folder to locate modules to execute. Advanced Visual Studio users can even setup post-build steps to automatically copy the compiled libraries into the helloworlds folder. Although, manually copying the files is good enough for this example.

Our First Generic Hello World Implementation

Now it’s time to begin creating the individual generic Hello World modules. Just as we did with the loader module, we’ll create a new project called “HelloWorldPlain” and add it to the current solution. Be sure to add a reference to the GenericHelloWorld.dll in this child project. In the Class1.cs file, we define the following class:

1
2
3
4
5
6
7
8
9
10
11
12
13
using GenericHelloWorld;

public class HelloWorldPlain : IGenericHelloWorld
{
    #region IGenericHelloWorld Members

    public void SayHello(string strText)
    {
        Console.WriteLine("Hello World!");
    }

    #endregion
}

Notice that this concrete implementation derives from our interface and fills in the body for the SayHello() function. Since we are creating a very simple class, we ignore the parameter strText (although the interface includes it for other classes that may want to utilize it).

Upon building the class, you should have a HelloWorldPlain.dll file in the bin/Debug folder for the child project. You can now copy that DLL into the main program’s “helloworlds” folder. You can then select the main program in visual studio and run it. The main program will locate the HelloWorldPlain.dll and execute its SayHello() function, displaying the following text to the console:

Hello World!

One is a Lonely Number

Here is where .NET modularization and generic design come in. Just as we did in the first Hello World implementation, we will create another child project, add it to the same solution, inherit from our interface, and provide a body for the SayHello() function. Don’t forget to add a reference to GenericHelloWorld for this child project.

1
2
3
4
5
6
7
8
9
10
11
12
13
using GenericHelloWorld;

public class HelloWorldBetter : IGenericHelloWorld
{
    #region IGenericHelloWorld Members

    public void SayHello(string strText)
    {
        Console.WriteLine("Hello World, " + strText);
    }

    #endregion
}

The process is the same as the last one, except this time, we utilize the strText parameter along with our usual text. After building the library, copy the HelloWorldBetter.dll into the main program’s “helloworlds” folder. Select the main program in Visual Studio and run it. You should now see two lines displayed on the console, even though we have not changed a single line in the main program’s executable:

Hello World!
Hello World, Mr. Smith

The loader locates both DLLs, extracts their interfaces, and the main program executes each one.

The Good, The Bad, and The Complicated

Now that you can see what .NET generics can do, you may want to utilize it throughout your software design. However, designing a software architecture in this manner doesn’t come without its drawbacks. You should carefully consider when and when not to use generics and reflection.

First, the good. The generics-reflection design lets you create enterprise-style architectures, re-usable modules, neatly organized libraries, and speed enhancements, particularly when creating COM+ libraries and spreading them across servers. The design also creates an organized method for multiple developers to work on a single project, by splitting the work up amongst individual libraries.

Next, the bad. Generics and reflection are complicated. It can add unneccessary complexity to your software architecture, especially when used in the wrong instances. It complicates the build process by forcing the developer to manage multiple child projects, multiple folders, and complicated post-build steps.

In short, consider the pros and cons of generic software patterns when designing your application’s architecure.

Conclusion

C# ASP .NET generic design patterns with reflection can enhance a software architecture with re-usable modules, generic interfaces, and speed enhancements. Generic modules allow swapping in and out individual DLLs from an application’s folder, instantly changing algorithms the program uses, often without interrupting the program’s execution. By using generics and reflection in your own software, you can bring enterprise-style patterns to your own projects.

About the Author

This article was written by Kory Becker, software developer and architect, skilled in a range of technologies, including web application development, machine learning, artificial intelligence, and data science.

Share