1

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.

  • 1
    Catch the exception in an outer scope and log that. You should probably be doing that anyway, and if you do you'd currently end up logging things twice. – Jeroen Mostert Nov 18 '22 at 13:08
  • If your emphasis is on the _structured_ logging (so you can have numerator and denominator explicitly as values) you could derive a specific custom Exception that has the two values as Fields and handle it explicitly in the outer scope, that Jeroen mentions. – Fildor Nov 18 '22 at 13:18
  • @JeroenMostert Understand, yet at the point of catching it in the outer scope, I no longer have the variables required so create a structured log message. – Angus Millar Nov 18 '22 at 13:27
  • @AngusMillar You can create a dedicated Exception type for that: https://dotnetfiddle.net/KIXKFV – Fildor Nov 18 '22 at 13:38
  • 2
    If you are using C#10 then you should the following article: https://habr.com/en/post/591171/ – Peter Csala Nov 18 '22 at 13:50
  • @Fildor I'm looking for a general solution to use everywhere, not just some special case that happens to have these two variables. Just as when you write the structured log, the template can have any number of variables and you need to pass each in as parameters. – Angus Millar Nov 18 '22 at 15:21
  • 1
    @Fildor You were right, that made no sense; though it was unrelated. I have edited the code to remove that distraction. Thanks for pointing it out. – Angus Millar Nov 18 '22 at 15:46
  • @Fildor I looked at your [https://dotnetfiddle.net/KIXKFV](https://dotnetfiddle.net/KIXKFV) linked fiddle. It's not the same as the example I gave. You are throwing a custom exception with custom properties, then catching that exception and structured logging those properties. However, I want to write and structured log message, and then throw an exception that has the same message. – Angus Millar Nov 18 '22 at 15:52
  • Thanks, @PeterCsala that was a great link. Yet I don't feel it's a solution. They have found a way to pass an Interpolated String to a logging method which still plays nice with structured logging. Yet how does that eliminate the need to still have two message templates in my code, one when calling the Log message, and the other to set the Exception's message? – Angus Millar Nov 18 '22 at 15:59
  • @AngusMillar Good point :) Don't know unfortunately. – Peter Csala Nov 18 '22 at 16:28
  • Maybe you can create a function which returns the interpolated string and pass that function to both places. Haven't tried it, maybe compiler would inline it. I guess it worths to give it a try. – Peter Csala Nov 18 '22 at 16:43

1 Answers1

0

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);
}
Mike
  • 123
  • 8