Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# Replacing switch(enum) flow control with Reflection

0.00/5 (No votes)
18 Aug 2017 1  
Hard coded enumerations are great, until you need to use them for program flow control that is.

Introduction

As a C# developer, I love enumerations.  They are simple to use and type safe.  They represent a known property of the object being represented.  The place where enum begins to loose its usefullness is when they get used for program flow control inside a switch statement.  It is tedious and error prone to write out each enumeration and link it to what functions need to be called. 

While I am not advocating for the elimation of using enumerations for program flow control, there are times when a program's enumerations can become unruly, either through bad practices or through organizational demands that force a program to grow way beyond its orginal scope.  While not truely necessary, this technique can replace a large switching statement with something that consumes less screen space.

We've all seen (and perhaps done) this:

switch (myEnum)
{
    case myEnum.CaseA:
        // handle case Alpha
    case myEnum.CaseB:
        // handle case Beta
    // and so on...ad infinitum
}

The real problem is when a new enum gets added to switches that branch on large enumerations.  The side effects can be numerous and painful to track down if multiple switch(enum) statements are buried deep in the code when an infrequently used enumeration is accidently left out.  I labored to devise an approach to eliminate or minimize the unexpected effects from using enum types for flow control.

Background

I have been expanding my knowledge in and around networking protocols along with how to establish communications between processes running on separate computers over the network.  Like a lot of other programmers new to network communications, I decided to start with a simple Chat application in a client-server configuration.  The necessity for the code in this article was born out the need to make enumerations used in flow control easier to manage. 


I was using the enum types to define the message type of a translated message that would be handled by other functions. Each message type needed its own method to process it so the first program used a switch(MessageType){case...} statement to handle a handfull of message types. As the number of message types grew, so did the necessity to update the switch. With each new type added, so did the opportunities for error.


The next step in simplifying this was to create a Dictionary<MessageType, Action<<byte[]>> to map each message type to its handler. I looked at this and knew that there had to be a better way to map functions so that I wouldn't have to update them for each new function or message type. This is what resulted.

Using the code

The simplest, least error-prone way I could devise to map a enum type with many members to their various handler methods was using System.Reflection.  With Reflection, I was able to use a custom Attribute to adorn the methods that were responsible for handling each enum type's branch of the code, completely eliminating the need for a large switch...case statement.  The use of Reflection is resource intensive.  It is advised that if this technique is used, it should be used sparingly.

For this example I will use a very simple program that moves a point around and reports back to the Console the point's new position and what direction it moved.

I will start off with defining the enumerations and my Attribute that will adorn the functions so they get mapped to the right enum.

// just the basic cardinal directions for now
public enum Direction { GoUp, GoLeft, GoRight, GoDown }

[AttributeUsage(AttributeTargets.Method)]
public class MappedMethodAttribute : Attribute
{
    public Direction Direction { get; set; }
    public MappedMethodAttribute(Direction direction)
    {
        Direction = direction;
    }
}

Here we defined our enumeration and a custom attribute that will let the compiler see a flag on certain methods, even at runtime. We can take advantage of this.

Next, I defined a class that will contain the functions to map, a way to envoke them, and using Reflection to create the method map when the class is instantiated.

class MappedMethodExample
{
    private Dictionary<Direction, Func<Point, string>> _mappedMethods;

    public MappedMethodExample()
    {
        _mappedMethods = new Dictionary<Direction, Func<Point, string>>();

        MapMethods();
    }

    private void MapMethods()
    {
        foreach (MethodInfo mInfo in typeof(MappedMethodExample).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance))
        {
            object[] attributes = mInfo.GetCustomAttributes(true);

            foreach (object attribute in attributes)
            {
                var mappedMethodAttr = attribute as MappedMethodAttribute;

                if (mappedMethodAttr != null)
                {
                    Direction dir = mappedMethodAttr.Direction;
                    var methodToMap = (Func<Point, string>)Delegate.CreateDelegate(typeof(Func<Point, string>), this, mInfo);

                    _mappedMethods.Add(dir, methodToMap);
                }
            }
        }
    }

    [MappedMethod(Direction.GoDown)]
    private string HandleMove_Down(Point newPosition)
    {
        return String.Format("You moved down.  Now located at: {0}", newPosition.ToString());
    }
    [MappedMethod(Direction.GoLeft)]
    private string HandleMove_Left(Point newPosition)
    {
        return String.Format("You moved left.  Now located at: {0}", newPosition.ToString());
    }
    [MappedMethod(Direction.GoRight)]
    private string HandleMove_Right(Point newPosition)
    {
        return String.Format("You moved right.  Now located at: {0}", newPosition.ToString());
    }
    [MappedMethod(Direction.GoUp)]
    private string HandleMove_Up(Point newPosition)
    {
        return String.Format("You moved up.  Now located at: {0}", newPosition.ToString());
    }

    public string HandleMove(Direction direction, Point position)
    {
        try
        {
            return _mappedMethods[direction].Invoke(position);
        }
        catch (KeyNotFoundException)
        {
            throw;
        }
    }
}

A little detail on what's going on here.

At first glance, there's quite a bit of things going on here. The purpose of this class is to provide a place to put methods that will be mapped to specific enumerations while keeping how each path is handled safely tucked away in private methods. The class constists of a generic Dictionary that contains an enum key and a Func that the key is mapped to. The real magic starts with the Attribute that adorns the method handlers. As you can see, each method has been been related to a specific enumeration by its Attribute.

The next bit of magic happens in the MapMethods method.

First, we use Reflection to get only methods adorned with our custom attribute.

MethodInfo mInfo in typeof(MappedMethodExample).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)

We use the binding flags NonPublic and Instance to tell the runtime that we only want to examine private methods for only this instance of the class.

Next, it is simply using the foreach loop to iterate through each method.  In each method, we examine its custom attributes and see if one of them has our MappedMethodAttribute.  If it does, the attribute tells us what direction to map it to and then adds its corresponding method to the map.

This next bit of code gave me some trouble

var methodToMap = (Func<Point, string>)Delegate.CreateDelegate(typeof(Func<Point, string>), this, mInfo);

What this does is to create a Delegate (read Function Pointer) to the method adorned with our custom attribute.

The thing that gave me trouble is that you must include this so that Delegate.CreateDelegate knows to point to the function in the mapper class.

Rounding it Out

Here's the simple console program I wrote to test.

class Program
{
    static void Main(string[] args)
    {
        var methodHandler = new MappedMethodExample();
        Point myLocation = new Point(0, 0);

        myLocation.X += 1;

        Console.WriteLine(methodHandler.HandleMove(Direction.GoUp, myLocation));

        myLocation.Y += 1;

        Console.WriteLine(methodHandler.HandleMove(Direction.GoRight, myLocation));

        myLocation.X -= 1;

        Console.WriteLine(methodHandler.HandleMove(Direction.GoDown, myLocation));

        myLocation.Y -= 1;

        Console.WriteLine(methodHandler.HandleMove(Direction.GoDown, myLocation));
    }
}

Points of Interest

The thing I like about this approach is that you can write a program that uses enumerations for flow control, with all the type safety they bring, and limit all of the control methods to high up in your call stack.  Adding a new enumeration, which always seems to happen, can be realatively painless by having a single point of entry to all the code that would handle the new enumeration.  The addition of the enumeration to the method map happens automatically.

In the context of this example, say I wanted to add in new enumerations for other cardinal points.  Writing up handlers for new enumerations is as simple as:

[MappedMethod(Direction.NW)]
private string HandleMove_NorthWest(Point newPosition)
{
    return String.Format("You moved north-west.  Now located at: {0}", newPosition.ToString());
}

Another little caveat to this is that you could easily extend your class to contain collections of Func delegates that all get called.

It's even possible to write a new Attribute to mark class with methods that also need to handle enumeration specific code paths and use reflection to map those classes and methods automatically at start-up.

History

Original Write-Up: 8/16/2017
Revision: 8/18/2017 - Cleaned up some language and added more detail

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here