Use Generics to Create an Audit Trail

Use Generics to Create an Audit Trail

he 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.

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 angle bracket (< >) symbols in C#:

You define the generic type using the Of keyword in Visual Basic .NET and the angle bracket (< >) 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   {      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 a = new Values();   a.OldValue = "";   a.NewValue = "Generics Test";   Values b = new Values();   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   {      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 intList = new List();   List strList = new List();

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 intList = new List();   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 strList = new List();   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   {      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 oInvoiceAudit =          new List();      AuditFactory oAuditFactory =          new AuditFactory();      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.

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.

See also  Comparing different methods of testing your Infrastructure-as-Code

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist