Search code examples
c#.netloggingilogger

How do I pass ILogger to a static class, so it's suitable for unit tests as well


How do I pass ILogger<T> to the static class Policies and its static method RateLimit(...), so it's suitable for both the Client.TooManyRequestsAsync and the PoliciesTests.Test1? The issue is that I cannot pass NullLogger.Instance in the unit tests.

public class PoliciesTests
{
    [Fact]
    public async Task Test1()
    {
        var client = new RestClient("https://httpstat.us/429");
        var request = new RestRequest();

        // TODO: Compile time error here
        var response = await Policies.RateLimit(NullLogger.Instance).ExecuteAsync(() => client.ExecuteAsync(request));
    }
}

public class Client : IDisposable
{
    public Client(ILoggerFactory loggerFactory)
    {
        _restClient = new RestClient(restApiUrl);
        _logger = loggerFactory.CreateLogger<Client>();
    }

    public async Task TooManyRequestsAsync()
    {
        var client = new RestClient("https://httpstat.us/429");
        var request = new RestRequest();
        var response = await Policies.RateLimit(_logger).ExecuteAsync(() => client.ExecuteAsync(request));
    }
}

public static class Policies
{
    private const int RateLimitRetryCount = 2;

    public static AsyncRetryPolicy<RestResponse> RateLimit<T>(ILogger<T> logger)
    {
        return Policy.HandleResult<RestResponse>(response => response.StatusCode == HttpStatusCode.TooManyRequests)
            .WaitAndRetryAsync(RateLimitRetryCount,
                (attemptCount, restResponse, _) =>
                {
                    var retryAfterHeader = restResponse?.Result?.Headers?.SingleOrDefault(h => h.Name == "Retry-After");
                    double secondsInterval = 0;

                    if (retryAfterHeader != null)
                    {
                        var value = retryAfterHeader.Value?.ToString();
                        if (!double.TryParse(value, out secondsInterval))
                        {
                            secondsInterval = Math.Pow(2, attemptCount);
                        }
                    }

                    return TimeSpan.FromSeconds(secondsInterval);
                },
                (response, timeSpan, retryCount, _) =>
                {
                    logger.LogTrace(
                        "The API request has been rate limited. HttpStatusCode={StatusCode}. Waiting {Seconds} seconds before retry. Number attempt {RetryCount}. Uri={Url}; RequestResponse={Content}",
                        response.Result.StatusCode, timeSpan.TotalSeconds, retryCount, response.Result.ResponseUri, response.Result.Content);

                    return Task.CompletedTask;
                });
    }
}

Solution

  • You can create an instance of ILogger using NullLoggerFactory.Instance.CreateLogger().

    So your example will looks like:

    public class PoliciesTests
    {
        private readonly ILogger<PoliciesTests> _logger = 
            NullLoggerFactory.Instance.CreateLogger<PoliciesTests>();
    
        [Fact]
        public async Task Test1()
        {
            var client = new RestClient("https://httpstat.us/429");
            var request = new RestRequest();
            var response = await Policies.RateLimit(_logger).ExecuteAsync(() => client.ExecuteAsync(request)); // No compile time error
        }
    }