Adding Property Get logging to LINQ to SQL with T4 by ThinqLinq

Adding Property Get logging to LINQ to SQL with T4

After my last INETA talk in Sarasota,  I had two separate people ask me how to enable auditing of LINQ to SQL when properties are read. Currently the classes created by the LINQ to SQL designer and SqlMetal add events to track before and after an individual property are changed through the INotifyPropertyChanged and INotifyPropertyChanging interfaces, but they don't include hooks to detect when a property is read.

One option to add read notification is to replace the default code generator with L2ST4 templates and to modify the template to include the necessary events on the property Gets. So how do we do this?

I'll leave it up to you to download, install and configure the templates to work on your dbml file using the instructions on the L2ST4 CodePlex site. I'll focus here instead on how to extend them after they are already working. You can download the sample T4 implementation if you want to follow along. Since this is a VB project, I'll be modifying the VBNetDataClasses.tt file here, but the same process could be done with the CSharpDataClasses as well.

First, we need a way to identify if we are adding the read tracking. At the top of the file, we will add a property to the anonymous type setting the options that will be used in the code generation process. Here, we'll add a flag called IncludeReadTracking:

var options = new {
    DbmlFileName = Host.TemplateFile.Replace(".tt",".dbml"), // Which DBML file to operate on (same filename as template)
    SerializeDataContractSP1 = false, // Emit SP1 DataContract serializer attributes
    FilePerEntity = true, // Put each class into a separate file
    StoredProcedureConcurrency = false, // Table updates via an SP require @@rowcount to be returned to enable concurrency    
    EntityFilePath = Path.GetDirectoryName(Host.TemplateFile), // Where to put the files    
    IncludeReadTracking = true // Include audit read tracking ability
};

The code generation is a mixture of C# templating code and VB generated code. The processing in the template is done in C# which is why, although we are modifying the VB template, this anonymous type is declared in C#.

Next, we'll add a common delegate, args and interface that each of the classes will consume. This will mimic the implementation of INotifyPropertyChanging and INotifyPropertyChanged in the underlying .Net Framework. We'll call our interface INotifyPropertyRead which will expose a single PropertyRead event. Here is the code that we want to produce once we're done:


Public Interface INotifyPropertyRead
    Event PropertyRead As PropertyReadEventHandler
End Interface
Public Delegate Sub PropertyReadEventHandler(ByVal sender As Object, ByVal e as PropertyReadEventArgs)
Public Class PropertyReadEventArgs
    Inherits EventArgs
    Public Sub New(ByVal propertyName as String)
        _propertyName = propertyName
    End Sub
    Private ReadOnly _propertyName As String
    Public ReadOnly Property PropertyName As String
        Get
            Return _propertyName
        End Get
    End Property
End Class 

We could create this separately, in our project, however in this case, I'll go ahead and add id dynamically in the code generation process depending on if the flag is set. We do need to be careful when adding it because we only want it added once rather than copied for each table, and outside of the context's namespace. If you have multiple dbml and template files, you will need to move this to a more centralized location in your project. We'll do this right after the header is generated and before the namespace is specified. Locate the following lines near the top of the original T4 template:

<#manager.EndHeader();
if (!String.IsNullOrEmpty(data.ContextNamespace)) {#>

Replace them with  the following:

<# manager.EndHeader(); #>
<#if (options.IncludeReadTracking) {#>
Public Interface INotifyPropertyRead
    Event PropertyRead As PropertyReadEventHandler
End Interface
Public Delegate Sub PropertyReadEventHandler(ByVal sender As Object, ByVal e as PropertyReadEventArgs)
Public Class PropertyReadEventArgs
    Inherits EventArgs
    Public Sub New(ByVal propertyName as String)
        _propertyName = propertyName
    End Sub
    Private ReadOnly _propertyName As String
    Public ReadOnly Property PropertyName As String
        Get
            Return _propertyName
        End Get
    End Property
End Class 
<# } #> 
<#if (!String.IsNullOrEmpty(data.ContextNamespace)) {#>

If you're not familiar with T4, the <# and #> are similar to ASP or MVC's <% %>. Code that is entered inside of the place holders is evaluated and code that is outside of them is considered a string literal. In this case, we have an If block that checks the IncludeReadTracking flag we setup in the options earlier. If the flag is set, then the VB code will be output as the generation process is executed.

Next, we need to modify the class definition corresponding to each table that is being read. The default implementation includes the definition for implements INotifyPropertyChanging, INotifyPropertyChanged. We'll add a designation (if the IncludeReadTracking is set) to also implement our new INotifyPropertyRead as follows:

Implements INotifyPropertyChanging, INotifyPropertyChanged<# if (options.IncludeReadTracking){ #> , INotifyPropertyRead <# } #>

Next, we need to add the actual implementation. This is relatively simple as well. In the #Region for the Property Change Event Handling, add the following:

        <# if (options.IncludeReadTracking){ #>
        Public Event PropertyRead As PropertyReadEventHandler Implements INotifyPropertyRead.PropertyRead
        <#=code.Format(class1.PropertyChangeAccess)#>Sub OnPropertyRead(ByVal propertyName As String)
            RaiseEvent PropertyRead(Me, New PropertyReadEventArgs(propertyName))
        End Sub
        
        <# } #>

Notice here, we will only add this if the flag is set. Also, we check to see if we can override the functionality of OnPropertyRead by checking the class1.PropertyChangeAccess flag.

The last change we make to the template is to modify the code generated for each Property Get which now reads as follows:

            Get
<#                if (options.IncludeReadTracking) { #>
                OnPropertyRead("<#= column.Member #>") 
<#                } #>
                Return <#=column.StorageValue#>
            End Get

That's it for our modifications to the T4 templates. When we save the template, our classes will be regenerated. So how do we consume these? We will need to have a logging implementation which we somehow attach to the new event.  A simple case could be to do something like the following:

Private Logger As New HashSet(Of String)
Sub Main()
    Dim dc As New NWindDataContext
    Dim emps = dc.Employees.ToList

    For Each emp In emps
        AddHandler emp.PropertyRead, AddressOf LogHandler
        Console.WriteLine(emp.FirstName & emp.LastName)
    Next

    Console.WriteLine("Log results")
    For Each item In Logger
        Console.WriteLine(item)
    Next
End Sub

Public Sub LogHandler(ByVal sender As Object, ByVal e As PropertyReadEventArgs)
    Dim value As String = sender.GetHashCode.ToString & e.PropertyName
    If Not Logger.Contains(value) Then Logger.Add(value)
End Sub

Here we are fetching the employees from a context set up against Northwind. As we iterate through the employee list, we add a listener to the PropertyRead event to perform the logging. In the logger we somehow need to identify the object that is being logged and the property being read. Here we just track the object's GetHashCode and the arg's PropertyName. Of course you would need to figure out how to log based on the object's unique key. Since you have a handle on the actual sender object, you could determine this from the LINQ to SQL attributes for the IsPrimaryKey value, or you could use some other implementation. Once you have the items logged, saving them back to your persistence store would need to be an additional step added wherever you are calling submit changes.

This sample also suffers from having to manually add the listener to the PropertyRead event. There are plenty of alternative options here, including using MEF to attach your object to a centralized logger. You would need to make the necessary changes to the T4 template, but hopefully that won't be too hard for you after reading this post.

Also, realize that this technique works when working directly with the LINQ to SQL generated classes. However, if you project into an anonymous type, you will no longer receive read notifications as that generated type won't have the necessary hooks any more. When doing read auditing, the challenges build quickly. You may want to consider instead some third party server based profiling systems for a more robust implementation and leave the logging out of the client tier entirely.

As always, there are plenty of alternatives. Let me know if you thinq of others.

Posted on - Comment
Categories: LINQ -
comments powered by Disqus