1
\$\begingroup\$

My code writes data and then receives data back. I want to log that this has happened, and I also want to inject a delegate so that I can get a callback to trace the data that is going in and out. This is example of the classes for reading and writing data in a request-response style.

public class IOExample
{
    private readonly ILogger _logger;
    private readonly Action<DataPacket> _writeData;
    private readonly Func<DataPacket> _readData;

    public IOExample(
        ILoggerFactory loggerFactory,
        Action<DataPacket> writeData,
        Func<DataPacket> readData
        )
    {
        _logger = loggerFactory.CreateLogger<IOExample>();
        _writeData = writeData;
        _readData = readData;
    }

    public DataPacket WriteAndRead(DataPacket writePacket)
    {
        _writeData(writePacket);
        _logger.LogTrace(new Trace { IsWrite = true, DataPacket = writePacket });
        var readPacket = _readData();
        _logger.LogTrace(new Trace { IsWrite = false, DataPacket = readPacket });
        return readPacket;
    }
}

The LogTrace method is an extension method on ILogger:

public static class LoggingExtensions
{
    public static void LogTrace<T>(this ILogger logger, T state)
    {
        if (logger == null) throw new ArgumentNullException(nameof(logger));

        logger.Log(LogLevel.Trace, default, state, null, (a, b) => $"Trace\r\nState: {state}");
    }
}

Here's where it gets tricky. I really don't feel like I should have to write this code. It feels like there should be a class to do this already. I basically just want a logger that calls back when a log occurs. But, anyway, this is the code so far:

public class DelegateLogger : ILogger
{
    #region Fields
    private readonly Action<LogMessage> _action;
    private readonly ConcurrentDictionary<Type, DelegateLoggerScope> _currentScopes = new ConcurrentDictionary<Type, DelegateLoggerScope>();
    #endregion

    #region Constructor
    public DelegateLogger(Action<LogMessage> action)
    {
        _action = action ?? throw new ArgumentNullException(nameof(action));
    }
    #endregion

    #region Implementation
    public IDisposable BeginScope<TState>(TState state)
    {
        if (_currentScopes.ContainsKey(typeof(TState)))
        {
            throw new ScopeExistsException($"Scope already exists for type of {typeof(TState)}");
        }

        var scope = GetScope(state);

        Callback(null, default, state, null, true);
        return scope;
    }

    public bool IsEnabled(LogLevel logLevel) => true;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (_currentScopes.ContainsKey(typeof(TState)))
        {
            var delegateLoggerScope = (DelegateLoggerScope<TState>)_currentScopes[typeof(TState)];
            Callback(null, default, delegateLoggerScope.State, null, true);
        }

        Callback(logLevel, eventId, state, exception, false);
    }
    #endregion

    #region Non Public Methods
    internal void ReleaseScope<T>() => _currentScopes.Remove(typeof(T), out _);

    private void Callback<TState>(LogLevel? logLevel, EventId eventId, TState state, Exception? exception, bool isScope)
    {
        if (state == null) throw new ArgumentNullException(nameof(state));
        _action(new LogMessage(logLevel, eventId, state, exception, isScope, this));
    }

    private DelegateLoggerScope<TState> GetScope<TState>(TState state) => (DelegateLoggerScope<TState>)_currentScopes.GetOrAdd(typeof(TState), (t) => new DelegateLoggerScope<TState>(_action, state, this));
    #endregion
}

These are the scopes:

internal class DelegateLoggerScope
{
    protected Action<LogMessage> _action;
    protected DelegateLogger _DelegateLogger;

    public DelegateLoggerScope(
    Action<LogMessage> action,
    DelegateLogger delegateLogger
    )
    {
        _action = action;
        _DelegateLogger = delegateLogger;
    }

}

internal class DelegateLoggerScope<TState> : DelegateLoggerScope, IDisposable
{
    public TState State { get; private set; }

    public DelegateLoggerScope(
        Action<LogMessage> action,
        TState state,
        DelegateLogger delegateLogger
        ) : base(action, delegateLogger)
    {
        State = state;
    }

    public void Dispose()
    {
        _DelegateLogger.ReleaseScope<TState>();
        _action = null;
        _DelegateLogger = null;
        State = default;
    }
}

This is a generic provider:

public class GenericLoggerProvider : ILoggerProvider
{
    private Func<string, ILogger> _createLogger;

    public GenericLoggerProvider(Func<string, ILogger> createLogger)
    {
        _createLogger = createLogger;
    }

    public ILogger CreateLogger(string categoryName) => _createLogger(categoryName);

    public void Dispose() => _createLogger = null;
}

Here's a very basic unit test that shows how it can be used:

[TestMethod]
public void TestBeginScope()
{
    Dog? state = null;
    var scopeCount = 0;
    var logCount = 0;

    using var loggerFactory = LoggerFactory.Create((builder) =>
        {
            _ = builder.SetMinimumLevel(LogLevel.Trace)
            .AddDebug()
            .AddProvider(new GenericLoggerProvider((name) =>
            new DelegateLogger((m) =>
            {
                state = (Dog)m.State;

                if (m.IsScope)
                {
                    scopeCount++;
                }
                else
                {
                    logCount++;
                }
            }
            )
            ));
        });
    var callbackLogger = loggerFactory.CreateLogger<UnitTest1>();
    using (var scope = callbackLogger.BeginScope(new Dog { Name = dogName }))
    {
        callbackLogger.LogTrace(new Dog { Name = dogName });
    }

    callbackLogger.LogTrace(new Dog { Name = dogName });

    if (state == null) throw new Exception();
    Assert.AreEqual(dogName, state.Name);

    Assert.AreEqual(2, logCount);
    Assert.AreEqual(2, scopeCount);
}

Isn't there some out of the box way of doing this? Why do I have to write all this code?

If not, please help me to poke some holes in this so I can feel confident in using it.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.