Enum Pattern

by Gong Liu July 29, 2009 20:49

This article is also published on The Code Project. 

Introduction 

A programmer often has to deal with a fixed number of cases in his program. For example, if your program is about emergency management, you may consider following cases:

  1. Fire,
  2. Injured,
  3. Burglary,
  4. Blue Screen of Death,
  5. Run out of Beer

and your program may look like:

    int emergency = 3;
    switch (emergency)
    {
        case 1:
        case 2:
        case 3:
            Console.WriteLine("Call 911");
            break;
        case 4:
            Console.WriteLine("Sue Microsoft");
            break;
        case 5:
            Console.WriteLine("Brew your own");
            break;
    }

Here, each emergency case is assigned a number. The problem with it is that the program becomes hard to read if the reader doesn't know what each number stands for. This is where enum comes to the rescue:

    enum Emergency
    {
        Fire = 1,
        Injured,
        Burglary,
        Blue_Screen_of_Death,
        Run_out_of_Beer
    }

    Emergency emergency = Emergency.Injured;
    switch (emergency)
    {
        case Emergency.Fire:
        case Emergency.Injured:
        case Emergency.Burglary:
            Console.WriteLine("Call 911");
            break;
        case Emergency.Blue_Screen_of_Death:
            Console.WriteLine("Sue Microsoft");
            break;
        case Emergency.Run_out_of_Beer:
            Console.WriteLine("Brew your own");
            break;
    }

So enum is just a way of naming numbered cases. It makes your program more readable. In .Net it's very easy to get the names of numbered cases as strings:

    Console.WriteLine(emergency.ToString());
    //output: Injured
    Console.WriteLine(string.Join(",", Enum.GetNames(typeof(Emergency))));
    //output: Fire,Injured,Burglary,Blue_Screen_of_Death,Run_out_of_Beer 

However, in principle, you should reject the temptation of using enum names in the presentation layer, as the names are meant for the convenience of programmers, not for the users of your program. "Blue_Screen_of_Death" or even "BSoD" is perfectly fine in your program, but when presented to the users, it's just not that user friendly.

What if you do want to associate a user-friendly string to each enumerated case? Muaddubby in his article A Perfect C# String Enumerator was trying to do just that. He claims his string enumerator has all the right behaviors of a real enum. But he fails to address the fact that his enumerator is not a value type constant so it can't be used in a switch statement, nor can it do flag bit operations. So it is not that perfect after all. A better approach, as described in String Enumerations in C# by Matt Simner and Enum With String Values In C# by Stefan Sedich, is to leverage existing enum. They associate a string to an enumerated case by way of a custom attribute, and use reflection to gain access to the string via an extension method.

Enum Pattern 

From previous efforts of devising a string enumerator, it's evident that we need a general design pattern of associating a string, or any object for that matter, to an enumerated case. In fact, Matt Simner and Stefan Sedich in their articles have demonstrated a special case of the pattern. We just need to make it a little bit more generic so it can handle any object, not just a string. Here is what is involved:

Step 1, we need to define a base class that represents the attribute of an enumerated case.

    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
    public class EnumAttr : Attribute
    {
        public EnumAttr()
        {
        }    
    } 

Since the attribute can be any object, we don't want it to have any particular behaviors by implementing methods or properties, except that it must apply to an enum field no more than once.

Step 2, we define the following extension method to gain access to the attribute object associated with an enum field:

    public static class EnumExtension
    {
        public static EnumAttr GetAttr(this Enum value)
        {
            Type type = value.GetType();
            FieldInfo fieldInfo = type.GetField(value.ToString());
            var atts = (EnumAttr[])fieldInfo.GetCustomAttributes(typeof(EnumAttr), false);
            return atts.Length > 0 ? atts[0] : null;
        }
    }

Step 3, define your own attribute class derived from EnumAttr.

Step 4, define your enum and add an instance of your attribute class in Step 3 to each enum field. Your enum can be regular or flagged.

Step 5, use your enum as usual. Whenever you need to access the attribute data of an enum field, just call the GetAttr() extension method.

Examples

The other day I was looking for something on Craig's List and wondered around to the personal classified ad section. To my surprise (well, not that surprised) I found that there were a lot more categories than just man-seeking-woman and woman-seeking-man. Here is a partial list:

    public class SexInterestAttr : EnumAttr
    {
        public string Desc { get; set; }
    }

    public enum SexInterest
    {
        [SexInterestAttr(Desc = "woman seeking man")]
        w4m,
        [SexInterestAttr(Desc = "man seeking man")]
        m4m,
        [SexInterestAttr(Desc = "man seeking woman")]
        m4w,
        [SexInterestAttr(Desc = "woman seeking woman")]
        w4w,
        [SexInterestAttr(Desc = "couple seeking couple")]
        mw4mw,
        [SexInterestAttr(Desc = "couple seeking woman")]
        mw4w,
        [SexInterestAttr(Desc = "couple seeking man")]
        mw4m,
        [SexInterestAttr(Desc = "woman seeking couple")]
        w4mw,
        [SexInterestAttr(Desc = "man seeking couple")]
        m4mw,
        [SexInterestAttr(Desc = "woman seeking lesbian couple")]
        w4ww,
        [SexInterestAttr(Desc = "man seeking gay couple")]
        m4mm,
        [SexInterestAttr(Desc = "gay couple seeking man")]
        mm4m,
        [SexInterestAttr(Desc = "lesbian couple seeking woman")]
        ww4w,
        [SexInterestAttr(Desc = "lesbian couple seeking man")]
        ww4m,
        [SexInterestAttr(Desc = "gay couple seeking woman")]
        mm4w,
        [SexInterestAttr(Desc = "man seeking lesbian couple")]
        m4ww,
        [SexInterestAttr(Desc = "woman seeking gay couple")]
        w4mm,
    }

Here, our attribute class, SexInterestAttr, contains only one property Desc, a user-friendly string to describe the sex interest. Notice that in our SexInterest enum we can use the object initializer syntax to create instance of SexInterestAttr. The following code snip shows how to use the enum and its attribute:

    var interests = (SexInterest[])Enum.GetValues(typeof(SexInterest));
    Console.WriteLine(string.Join(System.Environment.NewLine,
        interests.Select(i => string.Format("{0} - {1}", i.ToString(),
            ((SexInterestAttr)i.GetAttr()).Desc)).ToArray()));
Output:
    w4m - woman seeking man
    m4m - man seeking man
    m4w - man seeking woman
    w4w - woman seeking woman
    mw4mw - couple seeking couple
    mw4w - couple seeking woman
    mw4m - couple seeking man
    w4mw - woman seeking couple
    m4mw - man seeking couple
    w4ww - woman seeking lesbian couple
    m4mm - man seeking gay couple
    mm4m - gay couple seeking man
    ww4w - lesbian couple seeking woman
    ww4m - lesbian couple seeking man
    mm4w - gay couple seeking woman
    m4ww - man seeking lesbian couple
    w4mm - woman seeking gay couple

Adding attribute to enum doesn't alter its value type. We can still use it as usual such as in a switch statement:

    var interest = SexInterest.w4m;
    switch (interest)
    {
        case SexInterest.m4m:
            Console.WriteLine("you are a gay");
            break;
        case SexInterest.w4w:
            Console.WriteLine("you are a lesbian");
            break;
        case SexInterest.m4w:
        case SexInterest.w4m:
            Console.WriteLine("you are a hetero");
            break;
        default:
            Console.WriteLine("whatever. it's a free country");
            break;
    }
    //ourput: you are a hetero

As a side point, one of the advantages of using extension method is that it seemingly becomes part of our enum type and actually shows up in the IntelliSense:   

 

The above example is quite simple. We only associate a string to each enumerated case. Now let's take a look at a more complex example. Assume we have an enumeration of US presidents and each president can be described with the following PresidentAttr class:

    public class PresidentAttr : EnumAttr
    {
        public PresidentAttr(string name, string party, int yearTookOffice,
            int yearBorn, int yearDied)
        {
            Name = name;
            Party = party;
            YearTookOffice = yearTookOffice;
            YearBorn = yearBorn;
            YearDied = yearDied;
        }
        public string Name { get; set; }
        public string Party { get; set; }
        public int YearTookOffice { get; set; }
        public int YearBorn { get; set; }
        public int YearDied { get; set; }
        public bool IsAlive { get { return (YearDied <= 0); } }
        public int AgeTookOffice { get { return YearTookOffice - YearBorn; } }
    } 

Our President enum is defined as:

    public enum President
    {
        [PresidentAttr("George Washington", "No Party", 1789, 1732, 1799)]
        GeorgeWashington,
        [PresidentAttr("John Adams", "Federalist", 1797, 1735, 1826)]
        JohnAdams,
        [PresidentAttr("Thomas Jefferson", "Democratic-Republican", 1801, 1743, 1826)]
        ThomasJefferson,
        ...  
      
        [PresidentAttr("Bill Clinton", "Democratic", 1993, 1946, 0)]
        BillClinton,
        [PresidentAttr("George W. Bush", "Republican", 2001, 1946, 0)]
        GeorgeWBush,
        [PresidentAttr("Barack Obama", "Democratic", 2009, 1961, 0)]
        BarackObama
    } 

Now we can have some funs with our President enum:

    var presidents = (President[])Enum.GetValues(typeof(President));
    Console.WriteLine(string.Join(System.Environment.NewLine,
        presidents.Select(p => (PresidentAttr)p.GetAttr()).
        Select(a => string.Format("{0} ({1}-{2}), took office in {3}",
            a.Name, a.YearBorn, (a.YearDied<=0)? "" : a.YearDied.ToString(),
            a.YearTookOffice)).ToArray()));

Output:
    George Washington (1732-1799), took office in 1789
    John Adams (1735-1826), took office in 1797
    Thomas Jefferson (1743-1826), took office in 1801
    ...
    Bill Clinton (1946-), took office in 1993
    George W. Bush (1946-), took office in 2001
    Barack Obama (1961-), took office in 2009

    Console.WriteLine("There are {0} Democratic presidents",
        presidents.Count(p => ((PresidentAttr)p.GetAttr()).Party == "Democratic"));
    Console.WriteLine("They are:");
    Console.WriteLine(string.Join(System.Environment.NewLine,
        Array.FindAll(presidents, p => ((PresidentAttr)p.GetAttr()).Party == "Democratic").
        Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));

Output:
    There are 16 Democratic presidents
    They are:
    Andrew Jackson
    Martin Van Buren
    James K. Polk
    Franklin Pierce
    James Buchanan
    Andrew Johnson
    Grover Cleveland
    Grover Cleveland (2nd term)
    Woodrow Wilson
    Franklin D. Roosevelt
    Harry S. Truman
    John F. Kennedy
    Lyndon B. Johnson
    Jimmy Carter
    Bill Clinton
    Barack Obama

    Console.WriteLine("Presidents still alive: {0}",
        string.Join(",", Array.FindAll(presidents, p =>
            ((PresidentAttr)p.GetAttr()).IsAlive).Select(p =>
                ((PresidentAttr)p.GetAttr()).Name).ToArray()));

Output:
    Presidents still alive:
        Jimmy Carter,George H. W. Bush,Bill Clinton,George W. Bush,Barack Obama

    int maxAge = presidents.ToList().Max(p =>((PresidentAttr)p.GetAttr()).AgeTookOffice);
    var maxPAttr = (PresidentAttr)Array.Find(presidents, p =>
        ((PresidentAttr)p.GetAttr()).AgeTookOffice == maxAge).GetAttr();
    Console.WriteLine("The oldest president was {0}, a {1}, who took office at age {2}.",
        maxPAttr.Name, maxPAttr.Party, maxPAttr.AgeTookOffice);

Output:
    The oldest president was Ronald Reagan, a Republican, who took office at age 70.

In our last example we will demonstrate President enum with [Flags] attribute, or PresidentFlagged. Here is its definition:

    [Flags]
    public enum PresidentFlagged : long
    {
        [PresidentAttr("George Washington", "No Party", 1789, 1732, 1799)]
        GeorgeWashington = 0x1,
        [PresidentAttr("John Adams", "Federalist", 1797, 1735, 1826)]
        JohnAdams = 0x2,
        [PresidentAttr("Thomas Jefferson", "Democratic-Republican", 1801, 1743, 1826)]
        ThomasJefferson = 0x4,
        ...
        [PresidentAttr("Bill Clinton", "Democratic", 1993, 1946, 0)]
        BillClinton = 0x20000000000,
        [PresidentAttr("George W. Bush", "Republican", 2001, 1946, 0)]
        GeorgeWBush = 0x40000000000,
        [PresidentAttr("Barack Obama", "Democratic", 2009, 1961, 0)]
        BarackObama = 0x80000000000,
        _MostInfluential = AbrahamLincoln | FranklinDRoosevelt | GeorgeWashington
            | ThomasJefferson | AndrewJackson | TheodoreRoosevelt,
        _DiedInOffice = WilliamHenryHarrison | ZacharyTaylor | AbrahamLincoln | JamesAGarfield
            | WilliamMcKinley | WarrenGHarding | FranklinDRoosevelt | JohnFKennedy,
        _Assassinated = AbrahamLincoln | JamesAGarfield | WilliamMcKinley | JohnFKennedy,
    } 

Notice that we need to assign an integer number to each enum field in the order of 2^n in order for the flag bit operation to work. Also, we predefine three combined values, _MostInfluential, _DiedInOffice and _Assassinated. I prefer to prefix the combined values with an underscore "_" so they can be differentiated from single values and are put on top of the list of all possible values when displayed in the IntelliSense (see image below).

In the example we also need following helper method to parse a combined enum value into individual single enum values:

    private static PresidentFlagged[] ParseValues(PresidentFlagged combinedValue)
    {
        List<PresidentFlagged> values = new List<PresidentFlagged>();
        PresidentFlagged[] allvalues = (PresidentFlagged[])Enum.
            GetValues(typeof(PresidentFlagged));
        return Array.FindAll(allvalues, v =>
            ((combinedValue & v) == v) && !v.ToString().StartsWith("_"));
    } 

Notice when finding individual single values of a combined enum value we exclude the predefined combined values that are identified by the "_" prefix. Now the fun part:

    PresidentFlagged jfk = PresidentFlagged.JohnFKennedy;
    var jfkAttr = (PresidentAttr)jfk.GetAttr();
    Console.WriteLine("Was {0} died in office? {1}", jfkAttr.Name,
        ((PresidentFlagged._DiedInOffice & jfk) == jfk));
    Console.WriteLine("Was he Assassinated? {0}",
        ((PresidentFlagged._Assassinated & jfk) == jfk));

Output:
    Was John F. Kennedy died in office? True
    Was he Assassinated? True

    var Influentials = ParseValues(PresidentFlagged._MostInfluential);
    Console.WriteLine("Most influential presidents:");
    Console.WriteLine(string.Join(System.Environment.NewLine,
        Influentials.Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));

Output:
    Most influential presidents:
    George Washington
    Thomas Jefferson
    Andrew Jackson
    Abraham Lincoln
    Theodore Roosevelt
    Franklin D. Roosevelt

    var myFavorites = ParseValues(PresidentFlagged.AbrahamLincoln
        | PresidentFlagged.FranklinDRoosevelt
        | PresidentFlagged.BillClinton | PresidentFlagged.BarackObama);
    Console.WriteLine("My favorite presidents:");
    Console.WriteLine(string.Join(System.Environment.NewLine,
        myFavorites.Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));
    Console.WriteLine("Are they all Democratic? {0}",
        myFavorites.All(p => ((PresidentAttr)p.GetAttr()).Party == "Democratic"));

Output:
    My favorite presidents:
    Abraham Lincoln
    Franklin D. Roosevelt
    Bill Clinton
    Barack Obama
    Are they all Democratic? False

Conclusions

An enum pattern, in which each enumerated case is associated to an instance of a generic attribute class, is identified and implemented. It is particularly useful in situations where there is a finite number of cases and each case is characterized by a set of constants.

You can download the C# project below.

TestEnum.zip (9.74 kb)

Congee with Tofu Dishes

by Gong Liu July 19, 2009 06:12

ASP.Net Multithreading in a Web Farm Environment

by Gong Liu July 06, 2009 08:04

This article is also published on The Code Project

In one of my recent projects I have a situation where I need to generate two files on the server based on user input. Both files can be quite big (in terms of mobile devices), and take long time to generate by the server and to download by the user. The two files must be generated sequentially, as the second file depends on the first. One way to implement this would be to generate both files and then return to the user the download URLs in one request-response. The problem with this approach is that the user has to wait very long time before the server finishes the job. An obvious improvement is to return immediately after the first file is generated, and at the same time to spawn a new thread to generate the second file. This way, upon the request responded, the user can start to download the first file while the server is working on the generation of the second file. One problem with this approach is that the user will not know when the second file is ready for download. My solution is to create another page that allows the user to check the status of the second file. If it is ready, the response will include the download URL so the user can start to download it. If it is not ready, the user can either wait or do something else and check the status later. This works fine in my case, as the downloading time for the first file almost always longer than the time needed to generate the second file.

Another problem with this approach is that my ASP.Net application is running in a web farm environment in which I have a shared file store for the generated files and the newly spawned thread does not have proper permissions to write to the shared file store. The following diagram shows my simple web farm of two web servers, WS1 and WS2, and the file store. The file store is just a shared folder on one of the two web servers.

 

I use the steps below to configure the web servers so they can access to the shared folder. These steps apply to Windows 2000 (Advanced) Server OS. Yes! I'm still using Windows 2000 servers Smile

  1. Create a Windows user account, say "SharedUsr", on both WS1 and WS2. 
  2. Add <identity impersonate="true" userName="SharedUsr" password="xxxxxx"/> to web.config on both servers.
  3. Run these commands on both servers to grant "SharedUsr" access to the shared folder used by my ASP.Net application: 
    C:\WINNT\Microsoft.NET\Framework\v2.0.50727>aspnet_regiis -ga WS1\SharedUsr
    C:\WINNT\Microsoft.NET\Framework\v2.0.50727>aspnet_regiis -ga WS2\SharedUsr
  4. Make sure "SharedUsr" has write permission on the shared folder

This, however, does not work if the current thread spawns a new thread to write to the shared folder, because the new thread does not automatically impersonate "SharedUsr". I have to write code to force the impersonation. The following is the code. I have left out some details for clarity.  

......
using System.IO;
using System.Security.Principal;
using System.Threading;

public partial class GenerateFiles : System.Web.UI.Page
{
    private WindowsIdentity wid = null
    protected void Page_Load(object sender, EventArgs e)
    { 
        //Generate file1 ......

        wid = WindowsIdentity.GetCurrent(); //This is "SharedUsr" 
        try
        {
            //Use a thread from the threadpool to generate file2
            if (ThreadPool.QueueUserWorkItem(new WaitCallback(File2Callback),
                new File2Generator(file2Location)))
            {
                //Return file1 URL to user so he can start to download file1
                Response.Write(file1URL); 
            }
            else
            {
                Response.Write("Server too busy. The work item could not be queued.");
            }
        }
        catch (Exception ex)
        {
            Response.Write(ex.Message);
        }  
    }

    private void File2Callback(object o)
    {
        //Set the pooled worker thread's principal to that of wid   
        WindowsPrincipal principal = new WindowsPrincipal(wid);
        Thread.CurrentPrincipal = principal;

        //Make the the pooled worker thread to impersonate 'SharedUsr'
        WindowsImpersonationContext wic = wid.Impersonate();

        //Generate file2
        File2Generator f2gen = (File2Generator)o;
        f2gen.Generate();

        //Undo impersonation before return to threadpool
        wic.Undo();
    }
}

In the begining of Page_Load, we assume file1 has been generated. Then we use a worker thread from the threadpool to queue the generation of file2 for execution. The QueueUserWorkItem method has two parameters. The first parameter specifies the function to be called when the worker thread has its turn. The second parameter is the data to be passed to the function. In our case, it's an instantce of File2Generator class, which encompasses necessary data and functionality to generate file2. The implementation of File2Generator is not listed here because the detail is not important for making the point.

The callback function, File2Callback, will be executed on the worker thread that has the Windows identity "ASPNET", not "SharedUsr". So in it we need to change the worker thread's impersonation before calling the Generate method of File2Generator.

About

A seasoned computer professional. A tofu culture evangelist...
more >>

Tag Cloud

Calendar

<<  April 2017  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar
Copyright © 2008-2011 Gong Liu. All rights reserved. | credits | contact me
The content on this site represents my own personal opinions, and does not reflect those of my employer in any way.