Building an audit trail into your application provides a mechanism for tracking who updated what when, and the new generics feature in Whidbey helps you build that trail.

The Whidbey release of .NET will include a new Common Language Runtime (CLR) feature called generics. Generics allow you to use a variable to represent a desired data type, and thereby create very generic code (hence the name) that works with any data type.

You define the data type for the generic variable at run time and the CLR substitutes that data type for the variable everywhere in the code that it is used; basically providing you with strongly typed generic code.

Understanding Generics

Generics provide for multiple levels of abstraction, which can make them somewhat difficult to get your head around. So before I show you how to use generics in a more complex "real-world" application, it's best to start with a simple example using structures.

You define the generic type using the Of keyword in Visual Basic .NET and the < > symbols in C#.

Structures provide a straightforward mechanism for defining a set of related properties and methods. For example, a point consists of an x-value and a y-value, represented by two properties in a point structure.

You could use a structure to define a value with properties for the old value and the new value. If you wanted your value structure to support multiple data types, such as string, integers, and so on, you need to define a different structure for each data type.

In Visual Basic .NET:

' Without generics
Public Structure StringValues
    Public sOldValue As String
    Public sNewValue As String
End Structure
Public Structure IntValues
    Public iOldValue As Int32
    Public iNewValue As Int32
End Structure


In C#:

// Without generics
public struct StringValues
{
   public string sOldValue;
   public string sNewValue;
}
public struct IntValues
{
   public int iOldValue;
   public int iNewValue;
}

This can be unwieldy, especially if you have many different data types that you need to support.

With generics, you can create one set of code and use a variable to represent the desired type. You define the generic type using the Of keyword in Visual Basic .NET and the < > symbols in C#:

In Visual Basic .NET "Whidbey":

' With generics
Public Structure Values(Of T)
   Public OldValue As T
   Public NewValue As T
End Structure


In C# .NET "Whidbey":

// With generics
public struct Values<T>
{
public T OldValue;
public T NewValue;
}

One structure then supports any type of data. You define the desired data type when you create the structure:

In Visual Basic .NET "Whidbey":

Dim a As New Values(Of String)
a.OldValue = ""
a.NewValue = "Generics Test"

Dim b As New Values(Of Int32)
b.OldValue = 0
b.NewValue = 10

In C# .NET "Whidbey":

Values<string> a = new Values<string>();
a.OldValue = "";
a.NewValue = "Generics Test";

Values<int> b = new Values<int>();
b.OldValue = 0;
b.NewValue = 10;

The first three lines in both the Visual Basic .NET and C# examples define a structure of type String and the last three lines define a structure of type Integer. The CLR replaces each reference to the generic variable T with the defined data type ? providing a strongly typed structure.

Strongly typed implies that it enforces the data type at compile time. So if you attempt to set the OldValue or NewValue to anything but a String in the first example or an Integer in the second example, you will get a compile error.

Using strong typing improves the quality of your application because it minimizes the possibility of data type errors. And, since it does not have to perform boxing or data type casting, generic code performs better.

Note that in Visual Basic .NET, assigning a value of the wrong type only generates a compile-time error if you use Option Strict On in your project. Otherwise the error occurs at run time.

Building a Generic List

When your application needs to retain a set of data or objects, you can use one of the many collection-type classes provided in the .NET Framework such as Stack, Queue, and Dictionary. These classes allow you to store any type of object in the collection. In some cases this may be a good thing, such as when you want to keep a list of the last set of changed objects. In many cases, however, you want to ensure that a collection only contains objects of a particular type.

Generics provide a way to build general code that is specific at run time.

In Visual Studio .NET 2003 you can only create a strongly-typed collection class by building a collection class for each type. For example, if you want a strongly-typed collection for strings and a strongly-typed collection for integers, you would create two collection classes, one for strings and one for integers. This makes it laborious to create strongly-typed collections for many different types.

With Whidbey and generics, you can create a single collection class and define its type at run time, just like in the prior structure.

In Visual Basic .NET "Whidbey":

Public Class List(Of ItemType)
  Private elements() As ItemType
End Class

In C# "Whidbey":

public class List<T>
{
   private T[] elements;
}


The Visual Basic .NET example defines a List class with an elements array that will only hold items of a specific type, generically represented in the example as ItemType.

Likewise, the C# example defines a List class with an elements array that will only hold items of a specific type, generically represented in the example as T.

In both examples, you can use any variable name to represent the generic variables shown as ItemType and T.

When you use the List class, you define the desired data type for the list.

In Visual Basic .NET "Whidbey":

Dim intList as New List(Of Integer)
Dim strList as New List(Of String)

In C# .NET "Whidbey":

List<int> intList = new List<int>();
List<string> strList = new List<string>();

The first line of each example creates a list of integers and the second line creates a list of strings.

You can then add code to the List class to manage the list. Every List class needs a count of the number of elements, a way to add elements, and a way to get and set element contents.

In Visual Basic .NET "Whidbey":

Private iIndex As Integer = -1
Public Sub Add(ByVal element As ItemType)
  ' Start the array out at 10
  If iIndex = -1 Then
    ReDim elements(10)
  End If

  If (iIndex = elements.Length) Then
   ReDim Preserve elements(iIndex + 10)
  End If
  iIndex += 1
  elements(iIndex) = element
End Sub

Default Public Property _ Item(ByVal index _
As Integer) As ItemType
    Get
        Return elements(index)
    End Get
    Set(ByVal Value As ItemType)
        elements(index) = Value
    End Set
End Property

Public ReadOnly Property Count() As Integer
    Get
   ' The index starts at 0, the count at 1
    ' So adjust the index to the count
     Return iIndex + 1
    End Get
End Property


In C# .NET "Whidbey":

private int count = -1;
public void Add(T element)
{
   if (count == -1)
   {
   // Start with 10 elements
   elements = new T[10];
   }
         
// Increment by 10 more as needed
if (count == elements.Length)
   {
   elements = new T[count + 10];
   }
   elements[++count] = element;
   }

public T this[int index]
{
   get { return elements[index]; }
   set { elements[index] = value; }
}

public int Count
{
   get { return count; }
}

You can then add items and retrieve their contents in a strongly-typed manner. This means that value types, such as integers, won't be boxed and won't require casting. It also means that no one can accidentally put anything but an item of the defined type into the list.

You can use the List class to manage a list of integers:

In Visual Basic .NET "Whidbey":

Dim intList as New List(Of Integer)
intList.Add(1) ' No boxing
intList.Add(2) ' No boxing
intList.Add("Three") ' Compile-time error

Dim i As Int32 = intList(0) ' No cast required

For j As Int32 = 0 To intList.Count
    Debug.WriteLine(intList(j).ToString)
Next


In C# .NET "Whidbey":

List<int> intList = new List<int>();
intList.Add(1); // No boxing
intList.Add(2); // No boxing
//intList.Add("Three");
// Compile-time error
int i = intList[0]; // No cast required

for (int j = 0; j <=intList.Count; j++)
{
   Debug.WriteLine(intList[j]);
}

Since the List class is generic, you can also use it to manage a collection of strings:

In Visual Basic .NET "Whidbey":

Dim strList as New List(Of String)
strList.Add("This")
strList.Add("tests")
strList.Add("generics")

For j As Int32 = 0 To strList.Count
    Debug.WriteLine(strList(j)) ' No cast required
Next

In C# .NET "Whidbey":

List<string> strList = new List<string>();
strList.Add("This");
strList.Add("tests");
strList.Add("generics");

for (int j = 0; j<=strList.Count; j++)
{
Debug.WriteLine(strList[j]); // No cast required
}

Using generics you can create one set of code that works with any data type and yet is strongly-typed at run time. And, since there are no boxing or casting operations, you also get improved performance.

Leaving an Audit Trail

Applications with critical business data often require an audit trail. Take invoicing for example. In a perfect world, an application should generate invoices for a business using input data such as time sheets, job reports, or purchase orders. There should be no need for the users to update these documents. But in the real world, there are data input errors, special customer requests, last minute discounts or uplifts, and so on. So users occasionally need to update some invoices. But do you just want any user to be able to update any invoice? (Probably not.)

In addition to building code that controls which users have access to invoicing, you can build features into the application to track which users updated which fields. This provides an auditing mechanism to answer any future questions regarding changes to the invoice.

Though these examples use invoicing, you can apply the concepts similarly to any other business entity where you want to audit changes to the data.

Here I've created an Audit class to collect the set of auditing data. It tracks what field was changed, the old value and new value, the user that made the change, and the date and time that the user made the change.

In Visual Basic .NET "Whidbey":

Public Class Audit
  Private m_sPropertyName As String
  Private m_sOriginalValue As String
  Private m_sNewValue As String
  Private m_sUserName As String _
     = "Deborah"
 Private m_dtAuditDate As Date
    End Class

In C# .NET "Whidbey":

      public class Audit
      {
         string m_sPropertyName="";
         string m_sOriginalValue="";
         string m_sNewValue ="";
         string m_sUserName= "Deborah";
         DateTime m_dtAuditDate =DateTime.Now;
      }

Notice that the user name in both examples is hard-coded to my name. In a real application you would want to set the appropriate user's identification information. This may be the username that was used to log into the system or some ID entered into your application.

The constructor for this Audit class sets the properties based on the passed in parameters.

In Visual Basic .NET "Whidbey":

Public Sub New(ByVal sPropertyName As String, _
ByVal sOriginalValue As String, _
ByVal sNewValue As String)
        m_sPropertyName = sPropertyName
        m_sOriginalValue = sOriginalValue
        m_sNewValue = sNewValue
        m_dtAuditDate = Now
    End Sub

In C# .NET "Whidbey":

public Audit(string sPropertyName,
   string sOriginalValue,
   string sNewValue)
{
   m_sPropertyName = sPropertyName;
   m_sOriginalValue = sOriginalValue;
   m_sNewValue = sNewValue;
   m_dtAuditDate = DateTime.Now;
      }

You can expose any of the values associated with the Audit object using properties. You may want to consider making the properties read-only so they can be retrieved, but not updated.

In Visual Basic.NET "Whidbey":

Public ReadOnly Property PropertyName() _
  As String
     Get
       Return m_sPropertyName
     End Get
    End Property

Public ReadOnly Property AuditDate() _
As Date
   Get
     Return m_dtAuditDate
   End Get
End Property


In C# .NET "Whidbey":

       public string PropertyName
      {
         get {return m_sPropertyName;}
      }

      public DateTime AuditDate
      {
         get { return m_dtAuditDate ;}
      }

Each business object could include code that creates Audit objects, but by using a class factory to build the Audit objects you keep that code encapsulated. The class factory generates the Audit objects as needed. In this example, the AuditFactory class builds Audit objects only if the data changed.

In Visual Basic .NET "Whidbey":

Public Class AuditFactory(Of BOType)
  Public Function Add(ByVal bo As BOType, _
   ByVal sPropertyName As String, _
   ByVal sNewValue As String) As Audit
   Dim boPropertyInfo As _
     System.Reflection.PropertyInfo = _
      bo.GetType.GetProperty(sPropertyName)
   Dim sOriginalValue As String = _
    boPropertyInfo.GetValue(bo, Nothing).ToString

   If sOriginalValue <> sNewValue Then
     ' Create an audit entry
     Dim oAudit As New Audit(sPropertyName, _
         sOriginalValue, sNewValue)
     Return oAudit
   Else
      Return Nothing
   End If

 End Function
End Class

In C# .NET "Whidbey":

public class AuditFactory<BOType>
{
   public Audit Add(BOType bo,
   string sPropertyName, string sNewValue)
         {
   System.Reflection.PropertyInfo
   boPropertyInfo =
     typeof(BOType).GetProperty(sPropertyName);
         string sOriginalValue=
         string)boPropertyInfo.GetValue(bo, null);
   if (sOriginalValue != sNewValue)
      {
   Audit oAudit = new Audit(sPropertyName,
                  sOriginalValue, sNewValue);
      return oAudit;
      }
   else
      {
   return null;
   }
}

The class factory uses generics to define the type of business object that will be audited at run time. The Add method in the class factory uses reflection to get the current value of a particular property. It then compares the current value with the new value that was passed in to this method. If the value is changed then an Audit object is created and returned.

Code in the business object keeps the list of audit records. This example uses an Invoice class, though you can use any class.

In Visual Basic .NET "Whidbey":

Public Class Invoice
Dim oInvoiceAudit As New List(Of Audit)
Dim oAuditFactory As New AuditFactory(Of Invoice)
    Dim oAudit As Audit

    Dim m_sInvoiceDescription As String = ""

    Public Property InvoiceDescription() As String
       Get
        Return m_sInvoiceDescription
       End Get
       Set(ByVal Value As String)
        oAudit = oAuditFactory.Add(Me, _
       "InvoiceDescription", Value)
    If oAudit IsNot Nothing Then
       oInvoiceAudit.Add(oAudit)
    End If
     m_sInvoiceDescription = Value
    End Set
   End Property
 End Class

In C# .NET "Whidbey":

public class Invoice
{
List<Audit> oInvoiceAudit =
  new List<Audit>();
AuditFactory<Invoice> oAuditFactory =
new AuditFactory<Invoice>();
   Audit oAudit;

string m_sInvoiceDescription = "";
public string InvoiceDescription
   {
   get { return m_sInvoiceDescription; }
   set
   {
   oAudit = oAuditFactory.Add(this,
   "InvoiceDescription", value);
   if (oAudit != null)
   {
         oInvoiceAudit.Add(oAudit);
   }
   m_sInvoiceDescription = value;
   }
   }
}

This code first creates a list of Audit objects. It uses the generic List class to manage the list. It then creates an instance of the AuditFactory, defining that it will create audit records for the Invoice class. An Audit object is declared but not created because the AuditFactory is responsible for creating the Audit objects.

The code for each property in the class then calls the AuditFactory, which compares the original property value and new property value to determine whether to create an Audit object. The property code shown in the examples provide a pattern that you can use to define any other properties of the class.

Conclusion

Generics provide a way to build general code that is specific at run time. Generics give you the best of both worlds: the efficiency of building generic code and the type safety and performance of building strongly-typed code.

The techniques shown in this article only show a small fraction of the power of generics. You can use them in interfaces and in delegates. You can specify constraints to limit the valid data types that your users can use in the generic code. You can define multiple generic types in one class. Plus, the .NET Framework provides a pre-defined set of generic collections.