Consider this hypothetical: You need to track different ways it is permissible to contact a customer.

Or perhaps there are multiple attributes you wish to attach to an entity. Further, perhaps you want to define new attributes that can vary from entity to entity. From a database perspective, this can present a thorny problem. Fortunately, indexers in C# provide an elegant solution to the problem. In this article, I will show you how to use indexers to expand an organization’s data and at the same time, have minimal impact on an organization’s database structure. I will also show you how to incorporate this technique with NHibernate and how to verify your results with the MBUnit and TestDriven.net unit testing frameworks.

For purposes of demonstration, let’s assume you have been given the following requirements:

  • You need to track ways to contact customers.
  • Customers can opt to not be contacted.
  • New ways to contact customers can be defined.
  • Database maintenance needs to be minimized.

Step 1 - Solving the Data Problem

Let’s think a moment about the first two requirements: the need to track ways to contact a customer and the fact that customers can choose to not be contacted. What are some ways a customer can be contacted? These four options come to mind:

  • Email
  • Mail
  • Phone
  • Fax

A customer can choose to be contacted or not be contacted by any of these methods or any combination therein. The options are binary in nature. The choice to not be contacted is just another option. But, it is a special option because it does have a superseding effect on the other options. That, however, is a business rule concern, not a data concern. For now, I will set aside the business rule concern and pick that up later.

Collectively, whether a customer chooses to be contacted by one of the aforementioned options or chooses to not be contacted at all is simply an attribute of the customer that can be turned on or off. In other words, the value that represents whether a customer chooses to receive email solicitations, at any given time, is either true or false.

Let’s tackle the next two requirements that specify that new contact methods can be defined and that database maintenance and impact needs to be minimized. You don’t want to have to add new columns to your database structure each time a new contact method comes online.

Fortunately, this is a simple problem to solve. You know that you need at least one data field to persist the data. In fact, you can store all of the contact methods in one field if you use an integer field. That, along with T-SQL bitwise operators, solves the data problem. Figure 1 illustrates a sample data structure where the Customers table has one field to hold the information that specifies how a customer may be contacted. The following query fetches contacts that wish to be contacted via email. Notice the query has an additional criterion that filters any customers that don’t wish to be contacted. This is one way the do not contact business rule can be enforced.

Figure 1: Sample customer table structure and data. The Contactmethods column holds the data that specifies how a customer can be contacted.

This query uses an inline fabricated table that holds constant values assigned to each contact type. Because the inline table only has one row, there is no need to include join criteria. If a customer wishes to be contacted by email, phone, fax and regular mail, you simply add up the values. In this case, the contactmethods value would be 30 (2+4+8+16).

Select c.*
   From dbo.customers c,
   (
  Select 1 as DoNotContact,
         2 as Email,
         4 as Phone,
         8 as Fax,
         16 as Mail
 
   ) as contacttypes
where
c.contactmethods & contacttypes.email <> 0 and
c.contactmethods & contacttypes.DoNotContact = 0

From a maintenance perspective, if you need to create a new contact type for social networking sites like LinkedIn, Facebook, Twitter, etc., you simply need to define constant values. The data structures remain unchanged. Of course, some aspect of the application will change, but that is to be expected. The key is to limit your changes to areas, from a cost perspective, which are most amendable to changes. The database is usually not that area. If your data structures don’t need to change, you don’t need to modify and test many of the stored procedures that operate against your data.

If your world stopped at the database door, the work would be done. Within the database there is context. For example, you know that a value of 2 means email, 4 means phone and 6 means email and phone because those definitions are in the inline table. You could go one step further and define a table-based function to hold the data:

CREATE FUNCTION ContactTypes()
RETURNS @contacttypes TABLE
(DoNotContact int,
 Email int, Phone int,
 Fax int, Mail int)
AS
BEGIN
   Insert into @contacttypes
   Select 1 as DoNotContact,
          2 as Email,
          4 as Phone,
          8 as Fax,
          16 as Mail
   RETURN
END

With a table-based function in place, the first example reduces to:

Select c.*
 From dbo.customers c,
 dbo.ContactTypes() contacttypes
where
c.contactmethods & contacttypes.email <> 0 and
c.contactmethods & contacttypes.DoNotContact = 0

Indeed, you could opt to store the data in a traditional table instead of embedding the constants in code. That is certainly a valid approach. For metadata that rarely changes, a table-based function is a nice alternative.

The code makes it clear what the various values mean. Outside of the database, however, there is no such context, unless you ask the database for the information. That, however, requires a live connection to the database and the ability for the coding environment to take advantage of those definitions at design time. As it stands now, the development environment outside of the database lacks context. Data without context has no meaning and consequently, no value. With that, let me turn your attention to the middle tier.

Data without context has no meaning and consequently, no value.

Step 2 - Employing the Indexer

With the data problem solved you can set about the task of creating a middle-tier component that will allow you to manage these contact methods with the necessary context that give the data some meaning. C# Indexers come into play here. You can read more in-depth detail about Indexers in the C# Programmer’s Guide on MSDN (http://msdn.microsoft.com/en-us/library/2549tw02.aspx). Listing 1 illustrates a simple IntegerIndexer structure. The structure has one internal field called _bitvalue. The constructor takes an int for an initial BitValue. Since the ultimate purpose of the structure is to render and accept a single integer value, the private _bitvalue internal field is exposed via the public BitValue Property along with the necessary get and set methods. The following code illustrates how to implement the structure:

// first bit = 1
// second bit = 2
// third bit = 4
// fourth bit = 8
IntegerIndexer myindexer =
  new IntegerIndexer(11); // 1+2+8
bool isfirstbitset = myindexer[0]; //true
bool issecondbitset = myindexer[1]; //true
bool isthirdbitset = myindexer[2]; //false
bool isfourthbitset = myindexer[3]; //true
int bitvalue = myindexer.BitValue; //11

As you can see, a new instance of the structure was created with 11 as the initial BitVlaue. The public bool this[int index] method gives the structure indexing capability. When you pass an integer that corresponds to a bit location, the code employs a bitwise and returns a value that specifies whether the corresponding bit location is set to on or off (true or false). In the preceeding code snippet which provided a default value of 11, that corresponded to bits 1, 2 and 4 being turned on. Because bit 3 was untouched, it was left in the off position. The rest of the code interrogates each bit position and returns either a true or false. The last line fetches the internal sum of the bit values.

You can also toggle the values. The next code snippet shows the fourth bit turned off and the third bit turned on. The last line of code interrogates the internal bit value, and as you would expect, the sum of the turned on bits is 7. This is the sum of bits 1, 2 and 3 with values of 1, 2 and 4 respectively.

//Turn off fourth bit
myindexer[3] = false;
isfourthbitset = myindexer[3]; //now false
//Turn on third bit
myindexer[2] = true;
bitvalue = myindexer.BitValue; // 7 (1+2+4)

Setting of numerical bit locations is all well and good, but where is the context? What business meaning can you ascribe to these bit values? This is where the ContactType class comes into play.

Step 3 - Giving the Middle Tier Context with the ContactType Class

Listing 2 illustrates the ContactType class. The element that provides context is the ContactMethod Enum Type. As written, the members of the ContactMethod Enum will yield the following values:

  • DoNotContact - 1
  • Email - 2
  • Phone - 4
  • Fax - 8
  • Mail - 16

The following code instantiates this class:

ContactType contacttype = new ContactType(11);

As you can see, the public bool this[int index] method was lifted from the IntegerIndexer stucture with one slight modification. Instead of accepting an int type, the new version only accepts a ContactMethod Enum member. In order to get the bitwise operators to work, the passed ContactMethod Enum member needs to be cast to an int type. At this point, you no longer need the IntegerIndex structure.

With the ContactType class in place, let me recast the previous two code examples:

ContactType contacttype = new ContactType(11);
    
bool donotcontact =
 contacttype[ContactMethod.DoNotContact]; //true
bool email =
 contacttype[ContactMethod.Email]; //true
bool phone =
 contacttype[ContactMethod.Phone]; //false
bool fax =
 contacttype[ContactMethod.Fax]; // true
bool mail =
 contacttype[ContactMethod.Mail]; //false
    
int bitvalue = contacttype.ContactMethods; // 11

This is certainly a lot more readable. With context in place, you can see this customer does not wish to be contacted in any form. However, at some point, this customer did wish to be contacted by email, phone and fax. With this approach, we can put this customer in the do not contact category without losing historical data. If this customer wishes to be contacted again, all we need to do is turn off the do not contact attribute. Like the previous example, the attributes can be toggled:

//Turn off the fax contact
contacttype[ContactMethod.Fax] = false;
isfourthbitset =
  contacttype[ContactMethod.Fax]; //now false
//Turn on the phone contact
contacttype[ContactMethod.Phone] = true;
bitvalue =
  contacttype.ContactMethods; // 7 (1+2+4)

This code works exactly like the first set of code that used the IntegerIndexer structure. This code, however, has the added benefit of context. Just as you could do with the IntegerIndexer structure, you can easily toggle the value of the individual attributes. And when you need to get the sum of the bits that are turned on so that data can be written back to the database, your code can interrogate the ContactMethods property. I’ll illustrate this process in the next section.

Step 4 - Applying the Solution

With the ContactType Class in place, how can you go about leveraging it in your applications? For purposes of this demonstration, I will take an existing data class that uses NHibernate (http://www.nhforge.net) as its persistence mechanism. For an in-depth discussion on database persistence with NHibernate, check out Chad Myers’ 2-part series in the May/June 2009 and July/August 2009 issues of CODE.

Applying the class requires two steps:

  • Add code to the data class in order to consume the ContactType class.
  • Write unit tests to see if the database updates work properly.

Depending on your specific situation, the way you go about implementing this functionality may change slightly.

Before going further, allow me a moment to discuss the details upon which the ContactType class will be implemented. There is, already, a partial Customers class that I created with the My Generation Code Generation Tool (http://www.mygenerationsoftware.com/). I used the code that it creates as-is. Because the partial directive is used in the class definition, it is simple to augment the generated code without worry of losing the changes. The following code augments the already existing Customers class:

public partial class Customers
    
 {private ContactType _contacttypes ;
    
  public virtual void InitializeContactTypes()
   {_contacttypes = new
     ContactType(this.Contactmethods);}
    
  public virtual void SaveContactTypes()
   {this.Contactmethods =
      this.ContactTypes.ContactMethods;}
    
  public virtual ContactType ContactTypes
   {get { return _contacttypes; }} }

There is not too much going on here. A private member exists to hold a reference to the ContactTypes class. In addition, there is an initialization method that instantiates the class. Notice in the initialization, the current value for contactmethods (an integer value) is passed in the constructor just as in the previous examples. The difference here is that the value passed comes from the database. There is also a separate method that takes the ContactTypes class contact data and applies it the Customer class’ Contactmethods property. This process wrapped into the SaveCustomer method:

public void SaveCustomer (Customers customer)
 {
  ITransaction tx = _session.BeginTransaction();
  customer.SaveContactTypes();
  _session.Save(customer);
  tx.Commit();
 }

With the ContactTypes class functionality wrapped into the Customers class, the next step involves writing a unit test to make sure everything works correctly.

[Test]
 CanSetContactTypeToEmailAndPhone()
 {const int email = 2;
  const int phone = 4;
  Customers c = _provider.GetCustomerById(1);
  //assert that current value is 0
  Assert.AreEqual(c.Contactmethods,0);
  
  c.ContactTypes[ContactMethod.Email] = true;
  c.ContactTypes[ContactMethod.Phone] = true;
  _provider.SaveCustomer(c);
  author = _provider.GetCustomerById(1);
  //assert that our changes are stored in the db
  Assert.AreEqual (c.Contactmethods,
    email+phone);}

Just as in the previous examples with the ContactTypes class reference and IntegerIndexer structure, you simply toggle the values of a few selected contact types. In this case, you set the email and phone types to true. Notice that there are two asserts in this test. The first assert tests to make sure you are starting with no attributes turned on, thereby starting at a known data state. Once the data has been saved, you request the customer data again. The second assert tests to make sure the email and phone contact attributes have been applied to this customer. Figure 2 illustrates the changes were successfully applied to the Customers table. The following snippet shows the output from the successful test:

Figure 2: Customer ID 1 with the new contactmethods value based on turning the email and phone contact attributes on.
------ Test started: Assembly: DataAccessTest.dll
Starting the MbUnit Test Execution
Exploring DataAccessTest, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null
MbUnit 2.4.2.355 Addin
Found 1 tests
[fixture-setup] success
[success] NHibernateDataProviderTest.
Setup.CanSetContactTypeToEmailAndPhone.TearDown
[fixture-teardown] success
[reports] generating HTML report
file:///C:\DataAccessTest.Tests.html
1 passed, 0 failed, 0 skipped, took 3.25 seconds
(MbUnit).

I used the MBUnit test framework (http://www.gallio.org/) and the TestDriven.net unit testing add-in (http://www.testdriven.net/) in this example. TestDriven.net allows you to easily consume unit testing framework services within the Visual Studio IDE. For a good overview on unit testing, check out Griffin Caprio’s Unit Testing Overview article in the November/December 2004 issue of CODE.

Conclusion

In this article, you have seen how a single integer value can store multiple data values and how indexers can make the task of retrieving that data more manageable. The addition of enumerated types add context to what would otherwise be a meaningless integer value. The class and enum type also adds a validation layer to the application. You have also seen how this technique can easily be incorporated with data persistence mechanisms like NHibernate. Data and class functionality verification is a very simple process with unit testing frameworks like MBUnit and TestDriven.net.