A Reflected Property formatter token for the Logging Application Block

In my last post I cracked open the Logging Application Block to extend the Text Formatter so it could log timestamps in either local or UTC time. Since I already had my hands dirty, I thought I'd have a go at another useful extension that we unfortunately didn't get time to include in Enterprise Library for .NET 2.0.

One interesting (but often overlooked) feature of this block is that you can extend the LogEntry class to include additional properties that make sense for certain types of events. For example, you can subclass LogEntry into classes like DataLayerLogEntry, BusinessLayerLogEntry and AuditLogEntry, each with different strongly-typed properties that you want to collect when different things happen, such as reporting the database server name and stored procedure name in every event raised from your data access layer.

Unfortunately, just building these new LogEntry classes isn't enough. This is because the TextFormatter and the various TraceListeners don't know anything about these new properties that you've added. One solution would be to modify the TraceListener classes to deal with your new types and properties, but given how many TraceListeners we have, it's not a very attractive solution. Instead, I built a new Token class that works with the existing TextFormatter class that uses reflection so it can deal with any new property in any derived or modified LogEntry.

Before I get into the solution, let me explain the goals by way of an example. Suppose I built a new LogEntry-derived class like this:

public class DataLayerLogEntry : LogEntry{    private string databaseServer;    private string command; // Add as many (or as few) constructors as you want!    public DataLayerLogEntry() : base()    {    } public string DatabaseServer    {        get { return databaseServer; }        set { databaseServer = value; }    } public string Command    {        get { return command; }        set { command = value; }    }}

Now I can easily raise new events of this class from my code, like this (of course you wouldn't hard-code the values in real life, but you get my drift...):

DataLayerLogEntry logEntry = new DataLayerLogEntry();
logEntry.EventId = 123;
logEntry.Message = "Something happened in the data layer";
logEntry.Categories.Add("Data");
logEntry.DatabaseServer = "TOMHOLL1\\SQLEXPRESS";
logEntry.Command = "spDoStuff";
Logger.Write(logEntry);

So far so good, but if I sent this to any TraceListener via the out-of-the-box TextFormatter, I could get my custom properties out of it. However it's really easy to solve this generically. Again, I chose to just modify the original EntLib solution file, although you could probably separate the code into your own assembly if you're a purist and don't mind working out which code you need to copy or subclass. Also to do it properly you'd probably want to externalize some of the strings to make it localizable. But anyway, here's my new class ReflectedPropertyToken:

public

class ReflectedPropertyToken : TokenFunction
{
    /// <summary>
    /// Constructor that initializes the token with the token name
    /// </summary>

public ReflectedPropertyToken() : base("{property(")
    {
    }

/// <summary>
    /// Searches for the reflected property and returns its value as a string
    /// </summary>
    public override string FormatToken(string tokenTemplate, LogEntry log)
    {
        // find the property with this name on the log entry
        Type logType = log.GetType();
        PropertyInfo property = logType.GetProperty(tokenTemplate);
        if (property != null)
        {
            return property.GetValue(log, null).ToString();
        }
        else
        {
            return String.Format("<Error: property {0} not found>", tokenTemplate);
        }
    }
}

The only other thing I needed to do is modify TextFormatter.RegisterTokenFunctions to tell it about my new token. This just involved adding one more line to the end:

tokenFunctions.Add(new ReflectedPropertyToken());

So how does it work? Using this new token function, you can add the {property(propertyname)} token into your templates. To continue my example, I modified my TextFormatter template to include this:

Message: {message}
Database Server: {property(DatabaseServer)}
Database Command: {property(Command)}

And the result, of course, looks like this:

Message: Something happened in the data layer
Database Server: TOMHOLL1\SQLEXPRESS
Database Command: spDoStuff

The cool thing about this is that it's now easy to use any custom log schemas with (practically) any TraceListener. Also, while I only tested this with the new January 2006 .NET 2.0 version, it should be possible to use very much the same solution with the .NET 1.1 releases of the block too. I hope you find it useful for your applications!

This posting is provided "AS IS" with no warranties, and confers no rights.