Using Exception.Data property to log user-defined information about exceptions

Anton Antonov
4 min readAug 15, 2022

--

Hi! I’m Anton Antonov, a full stack developer. I’m going to tell you what helps me fix bugs in code.

A good and detailed exception is a powerful tool for figuring out what the issue is and how to resolve it. That is why you may need to define more details in an exception.

One method is to define the information inside an exception message, just like in the code below.

try
{
return await _ordersRepository.Get(id, cancellationToken);
}
catch (Exception exception)
{
throw new Exception($"Unable to get order info, user {userName}, order id {id}", exception);
}

This approach works, but it leads to a lot of extra work to create messages. Also, it isn’t safe, because an exception message can be sent to the user in unhandled cases. You should avoid sending information like IDs to the user. All that you have to do to fix an issue is log the user-defined information and the original exception that caused the error on the server-side.

Another approach is to create a custom exception with defined properties. However, it is quite complicated to create a lot of exception classes, and the main problem is configuring a logger to log their custom properties.

The .NET Framework Exception base class itself has a Data property that already provides the ability to save additional user-defined information as a collection of key/value pairs. Because this is a property of the base class, I’m sure you can configure a logger to log it. In our examples below, we will use NLog, which can be configured pretty easily. To avoid key conflicts and to handle the error more appropriately, I recommend creating your own custom exception.

Half of the .NET Framework exception classes contain custom properties that aren’t logged, so you can add this data to the Exception.Data property of your new exception. The InnerException property will contain a reference to the original exception.

The following code shows how to use the Exception.Data property.

try
{
return await _ordersRepository.Get(id, cancellationToken);
}
catch (Exception exception)
{
const string message = "Unable to get order info";
var yourException = new YourAppException(message, exception);
yourException.Data[nameof(userName)] = userName;
yourException.Data[nameof(id)] = id;
throw yourException;
}

Oops. It looks like we added a lot of extra code, but we can fix it by adding some extensions that allow us to use the Fluent Interface pattern. The following example shows how our code can become more readable and easier to use.

try
{
return await _ordersRepository.Get(id, cancellationToken);
}
catch (Exception exception)
{
throw exception.With("Unable to get order info")
.DetailData(nameof(userName), userName)
.DetailData(nameof(id), id);
}

Let’s see how we can configure the NLog layout to log the Exception.Data property.

${shortdate} ${time} [${level:uppercase=true}]: ${message:withException=true}${when:when=length('${exception:format=Data}')>0:Inner=${newline}--- Exception Data ---${newline}${exception:format=Data:exceptionDataSeparator=,\r\n}}

How it looks in the console.

This example works perfectly if we add simple data structs, because the NLog calls ToString() to write string values to targets, so we can log objects properly only if they override the ToString() method correctly.

However, it is quite complicated to override ToString() for all classes. A straightforward way to represent an object as a string is by serializing the object to JSON. The following code adds a C# class that does this.

/// <summary>
/// Defines a value/json pair to represent an exception data value as JSON
/// </summary>
public record ExceptionDataEntry
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = {new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)},
WriteIndented = true
};

private ExceptionDataEntry(in object value, in string json)
{
Value = value;
Json = json;
}

public object Value { get; }
public string Json { get; }

public static ExceptionDataEntry FromValue(in object value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}

var json = JsonSerializer.Serialize(value, SerializerOptions);
return new ExceptionDataEntry(value, json);
}

/// <summary>
/// Represents an exception data value as JSON
/// </summary>
public override string ToString() => Json;
}

The following code adds an extension.

public static YourAppException DetailData(this YourAppException exception, in string key, in object value)
{
try
{
exception.Data[key] = ExceptionDataEntry.FromValue(value);
}
catch
{
// ignored, because we use it inside another exception catch block
// so, we should avoid throwing a new exception to keep the original exception
}

return exception;
}

How it looks in the console in our example.

I hope this approach to using the Exception.Data property to log user-defined information will help you to support apps. If you have any ideas on how to improve the approach, please let me know in the comments.

If you find my work valuable, please consider sponsoring me on GitHub. Your support will help me create more content and share more code.

Here’s the link to sponsor me: My GitHub Profile

Thank you for considering! Your support means a lot to me.

--

--

Anton Antonov

I have more than 14 years of experience. I started with .NET Framework 3.5. Now I’m an expert in C#, T-SQL, HTML, CSS, TypeScript, Angular, and Docker