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.