How can I avoid this pattern? I wish to capture an illegal state, such as found in the contrived example below. Log a structured message followed by throwing an exception containing the same message.
public async Task<int> DoSomeWork(int numerator, int denominator)
{
if (denominator == 0)
{
Logger.LogError("The division : {Numerator}/{Denominator} is invalid as the denominator is equal to zero", numerator, denominator);
throw new ApplicationException($"The division : {numerator}/{denominator} is invalid as the denominator is equal to zero.");
}
//Yes the solution must work with async methods
await Task.Delay(TimeSpan.FromSeconds(1));
//this would have thrown a DivideByZeroException
return (numerator / denominator);
}
I have the above pattern all over my code and it seems crazy, yet I can't find an alternative.
I want the goodness of structured logging, and I also want my Exception messages to align with the log message. Yet I don't want to have to duplicate my error message template strings as seen above.
One approach is to add a custom exception that allows an args collection to be supplied, which can in turn be used with the structured logging. A delegate to the log action can also be added so that whatever handles the exception can call the action supplying an ILogger
instance.
public abstract class BaseStructuredLoggingException : Exception
{
private readonly object[] _args;
protected BaseStructuredLoggingException(string message, params object[] args)
: base(message)
{
_args = args;
}
public Action<ILogger<T>> LogAction<T>()
{
return l => l.LogError(this, Message, _args);
}
}
public sealed class DivideException : BaseStructuredLoggingException
{
public DivideException(string message, params object[] args)
: base(message, args)
{ }
}
Then in whatever class is handling the exception
private void HandleException(Exception ex)
{
if (ex is BaseStructuredLoggingException exception)
{
var log = exception.LogAction<ErrorHandler>();
log(_logger);
}
else
{
_logger.LogError(ex, ex.Message);
}
}
and finally your application code
public async Task<int> DoSomeWork(int numerator, int denominator)
{
if (denominator == 0)
{
throw new DivideException("The division : {Numerator}/{Denominator} is invalid as the denominator is equal to zero", numerator, denominator);
}
//Yes the solution must work with async methods
await Task.Delay(TimeSpan.FromSeconds(1));
//this would have thrown a DivideByZeroException
return (numerator / denominator);
}