How to replace BackgroundServices with a lite Hangfire

This post is part of my Advent of Code 2022.

I like ASP.NET Core BackgroundServices. I’ve used them in one of my client’s projects to run recurring operations outside the main ASP.NET Core API site. Even for small one-time operations, I’ve run them in the same API site.

There’s one catch. We have to write our own retrying, multi-threading, and reporting mechanism. BackgroundServices are a lightweight alternative to run background tasks.

Lite Hangfire

These days, a coworker came up with the idea to use a “lite” Hangfire to replace ASP.NET Core BackgroundServices. By “lite,” he meant an in-memory, single-thread Hangfire configuration.

Let’s create an ASP.NET Core API site and install these NuGet packages:

1. Register Hangfire

In the Program.cs file, let’s register the Hangfire server, dashboard, and recurring jobs. Like this,

using Hangfire;
using LiteHangfire.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.ConfigureHangfire();
//               ^^^^^

var app = builder.Build();

app.UseAuthorization();
app.MapControllers();

app.UseHangfireDashboard();
app.MapHangfireDashboard();
//  ^^^^^
app.ConfigureRecurringJobs();
//  ^^^^^

app.Run();

To make things cleaner, let’s use extension methods to keep all Hangfire configurations in a single place. Like this,

using Hangfire;
using Hangfire.Console;
using Hangfire.InMemory;
using RecreatingFilterScenario.Jobs;

namespace LiteHangfire.Extensions;

public static class ServiceCollectionExtensions
{
    public static void ConfigureHangfire(this IServiceCollection services)
    {
        services.AddHangfire(configuration =>
        {
            configuration.UseInMemoryStorage();
            //            ^^^^^
            // Since we have good memory
            configuration.UseConsole();
            //            ^^^^^

        });
        services.AddHangfireServer(options =>
        {
            options.SchedulePollingInterval = TimeSpan.FromSeconds(5);
            //      ^^^^^
            // For RecurringJobs: Delay between retries.
            // By default: 15sec
            options.WorkerCount = 1;
            //      ^^^^^
            // Number of worker threads.
            // By default: min(processor count * 5, 20)
        });

        GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute
        {
            Attempts = 1
            // ^^^^^
            // Retry count.
            // By default: 10
        });
    }

    public static void ConfigureRecurringJobs(this WebApplication app)
    {
        //var config = app.Services.GetRequiredService<IOptions<MyRecurringJobOptions>>().Value;
        // ^^^^^^^^^
        // To read the cron expression from a config file

        RecurringJob.AddOrUpdate<ProducerRecurringJob>(
            ProducerRecurringJob.JobId,
            x => x.DoSomethingAsync(),
            "0/1 * * * *");
            // ^^^^^^^^^
            // Every minute. Change it to suit your own needs

        RecurringJob.Trigger(ProducerRecurringJob.JobId);
    }
}

Notice that we used the UseInMemoryStorage() method to store jobs in memory instead of a database and the UseConsole() to bring color to our logging messages in the Dashboard.

Car factory
Photo by carlos aranda on Unsplash

2. Change some Hangfire parameters

In the previous step, when we registered the Hangfire server, we used these parameters:

  • SchedulePollingInterval is the time to wait between retries for recurring jobs. By default, it’s 15 seconds. Source
  • WorkerCount is the number of processing threads. By default, it’s the minimum between five times the processor count and 20. Source

As an aside, I also discovered these settings:

  • ServerCheckInterval is how often Hangfire checks for “timed out” servers. By default, it’s 5 minutes. Source
  • ServerTimeout is the time to consider that a server timed out from the last heartbeat. By default, it’s 5 minutes.

Then, we registered the number of retry attempts. By default, Hangfire retries jobs 10 times. Source

3. Write “Producer” and “Consumer” jobs

The next step is to register a recurring job as a “producer.” It looks like this,

using Hangfire;
using Hangfire.Console;
using Hangfire.Server;

namespace LiteHangfire.Jobs;

public class ProducerRecurringJob
{
    public const string JobId = nameof(ProducerRecurringJob);

    private readonly IBackgroundJobClient _backgroundJobClient;
    private readonly ILogger<ProducerRecurringJob> _logger;

    public ProducerRecurringJob(IBackgroundJobClient backgroundJobClient,
                                ILogger<ProducerRecurringJob> logger)
    {
        _backgroundJobClient = backgroundJobClient;
        _logger = logger;
    }

    public async Task DoSomethingAsync()
    {
        _logger.LogInformation("Running recurring job at {now}", DateTime.UtcNow);

        // Beep, beep, boop...
        await Task.Delay(1_000);

        // We could read pending jobs from a database, for example
        foreach (var item in Enumerable.Range(0, 5))
        {
            _backgroundJobClient.Enqueue<WorkerJob>(x => x.DoSomeWorkAsync(null));
            // ^^^^^
        }
    }
}

Inside this recurring job, we can read pending jobs from a database and enqueue a new worker job for every pending job available.

And a sample worker job that uses Hangfire.Console looks like this,

public class WorkerJob
{
    public async Task DoSomeWorkAsync(PerformContext context)
    {
        context.SetTextColor(ConsoleTextColor.Blue);
        context.WriteLine("Doing some work at {0}", DateTime.UtcNow);

        // Beep, beep, boop...
        await Task.Delay(3_000);
    }
}

Notice that we expect a PerformContext as a parameter to change the color of the logging message. When we enqueued the worker jobs, we passed null as context, then Hangfire uses the right instance when running our jobs. Source.

Voilà! That’s how to use a lite Hangfire to replace BackgroundServices without adding too much overhead or a new database to store jobs. With the advantage that Hangfire has recurring jobs, retries, and a Dashboard out of the box.

After solving a couple of issues, I learned some lessons when working with Hangfire.

To read more content about ASP.NET Core, check how to write tests for HttpClient and how to test an ASP.NET filter.

Happy coding!

I don't use 'Pushy' questions in code reviews anymore. This is what I do instead

This post is part of my Advent of Code 2022.

“Ask questions” is common advice for better code reviews.

At some point, we followed that advice and started using what I call “leading” or “pushy” questions. Questions that only hint a request for a code change.

After working on a remote software team for a couple years, I stopped using “pushy” questions on code reviews. Here’s why it’s a bad idea.

“Pushy” Questions Are Time-Consuming

Let’s imagine we’ve written a method and forgot to check for nulls. Something like this,

public void DoSomething(OneParam oneParam, AnotherParam anotherParam)
{
    var someResult = AMethodThatUsesOneParam(oneParam.SomeProperty);
    // ...
    // Beep, beep, boop...
}

If we follow the advice to ask “pushy” questions, we might leave and receive comments like “What if oneParam is null?” or “Could oneParam or anotherParam be null?”

The problem with those types of comments is we can’t tell if they’re genuine questions or actionable items. Is the reviewer asking a clarification question or “pushing” us in a different direction? We can’t tell.

Behind those comments, there’s a hidden change request. How is the code author supposed to know the reviewer is asking for a change?

While working on a remote team, it happened more than once that I had to reach out to reviewers via email or chat to ask them to clarify their intentions behind those comments. But some reviewers were in different time zones or even on the other side of the world. All interactions took about ~24 hours between my initial message and their response.

It was frustrating and time-consuming. Arrrggg!

When it was my turn to be a code reviewer, I chose a different approach: I stopped asking those questions.

The Thinker
That's a tricky question. Let me think about it. Photo by Tingey Injury Law Firm on Unsplash

Use Unambiguous and Intentional Comments

Instead of asking “pushy” questions, let’s leave actionable and unambiguous comments that distinguish between questions, to-dos, and nice-to-haves.

Let’s go back to the previous example and leave an unambiguous comment. Like this one: “Is it possible that oneParam could be null? If that’s the case, please let’s add the appropriate null checks. Something like if (oneParam == null) throw ...

With that comment, it’s clear we’re suggesting a change.

To better show the intention behind our comments, we can use Conventional Comments.

With that convention, we add keywords like “question,” “suggestion” or “nitpick” to clarify the purpose of our comments.

I used it for months in one of my client’s projects and other reviewers started to use it too.

For example, we can turn our previous “pushy” comment into these two depending on our intention:

  1. A clarification question: question: Is it possible that oneParam could be null?”
  2. A change request: suggestion (blocking): Let’s add the appropriate null checks if oneParam could be null.”

Now it’s clear we’re referring to two different actions.

Voilà! That’s why I don’t like “pushy” questions in code reviews. Let’s always prefer clear and direct comments without forgetting good manners of course. And let’s remember we review code from people with different experience levels and even non-native speakers of our language.

After this experience, my rule of thumb for better code reviews is to write unambiguous comments and always include a suggestion with each comment.

If you want to read more about code reviews, check these Tips and Tricks for Better Code Reviews and these lessons I learned about as code reviewer. And if you’re interested in unit testing, this lesson came up during a code review session: how to use simple test values to write good unit tests.

Happy coding!

On Unit Testing Logging Messages

This post is part of my Advent of Code 2022.

These days I had to review some code that expected a controller to log the exceptions thrown in a service. This is how that controller looked and what I learned about testing logging messages.

When writing unit tests for logging, assert that actual log messages contain keywords like identifiers or requested values. Don’t assert that actual and expected log messages are exactly the same.

Don’t expect identical log messages

The controller I reviewed looked like this,

using Microsoft.AspNetCore.Mvc;
using OnTestingLogMessages.Services;

namespace OnTestingLogMessages.Controllers;

[ApiController]
[Route("[controller]")]
public class SomethingController : ControllerBase
{
    private readonly IClientService _clientService;
    private readonly ILogger<SomethingController> _logger;

    public SomethingController(IClientService clientService,
                               ILogger<SomethingController> logger)
    {
        _clientService = clientService;
        _logger = logger;
    }

    [HttpPost]
    public async Task<IActionResult> PostAsync(AnyPostRequest request)
    {
        try
        {
            // Imagine that this service does something interesting...
            await _clientService.DoSomethingAsync(request.ClientId);

            return Ok();
        }
        catch (Exception exception)
        {
            _logger.LogError(exception, "Something horribly wrong happened. ClientId: [{clientId}]", request.ClientId);
            //      ^^^^^^^^
            // Logging things like good citizens of the world...

            return BadRequest();
        }
    }

    // Other methods here...
}

Nothing fancy. It called an IClientService service and logged the exception thrown by it. Let’s imagine that the controller logged a more helpful message to troubleshoot later. I wrote a funny log message here. Yes, exception filters are a better idea, but bear with me.

To test if the controller logs exceptions, we could write a unit test like this,

using Microsoft.Extensions.Logging;
using Moq;
using OnTestingLogMessages.Controllers;
using OnTestingLogMessages.Services;

namespace OnTestingLogMessages.Tests;

[TestClass]
public class SomethingControllerTests
{
    [TestMethod]
    public async Task PostAsync_Exception_LogsException()
    {
        var clientId = 123456;

        var fakeClientService = new Mock<IClientService>();
        fakeClientService
            .Setup(t => t.DoSomethingAsync(clientId))
            .ThrowsAsync(new Exception("Expected exception..."));
            //           ^^^^^
            // 3...2...1...Boom...
        var fakeLogger = new Mock<ILogger<SomethingController>>();
        var controller = new SomethingController(fakeClientService.Object, fakeLogger.Object);
        //                                       ^^^^^

        var request = new AnyPostRequest(clientId);
        await controller.PostAsync(request);

        var expected = $"Something horribly wrong happened. ClientId: [{clientId}]";
        //  ^^^^^^^^
        // We expect exactly the same log message from the PostAsync
        fakeLogger.VerifyWasCalled(LogLevel.Error, expected);
    }
}

In that test, we used Moq to create fakes for our dependencies, even for the ILogger itself. I prefer to call them fakes. There’s a difference between stubs and mocks.

By the way, .NET 8.0 added a FakeLogger, a logging provider for unit testing, so we don’t have to rely on fakes to test logging.

In our test, we’re expecting the actual log message to be exactly the same as the one from the SomethingController. Can you already spot the duplication? In fact, we’re rebuilding the log message inside our tests. We’re duplicating the logic under test.

Also, let’s notice we used a custom assertion method to make our assertions less verbose. VerifyWasCalled() is an extension method that inspects the Moq instance to check if the actual and expected messages are equal. Here it is,

public static class MoqExtensions
{
    public static void VerifyWasCalled<T>(this Mock<ILogger<T>> fakeLogger, LogLevel logLevel, string message)
    {
        fakeLogger.Verify(
            x => x.Log(
                logLevel,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((o, t) =>
                    string.Equals(message, o.ToString(), StringComparison.InvariantCultureIgnoreCase)),
                    //     ^^^^^
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.Once);
    }
}
Pile of tree logs
Don't expect identical log...messages. Photo by Chandler Cruttenden on Unsplash

Instead, expect log messages to contain keywords

To make our unit tests more maintainable, let’s check that log messages contain keywords or relevant substrings, like identifiers and values from input requests. Let’s not check if they’re identical to the expected log messages. Any changes in casing, punctuation, spelling or any other minor changes in the message structure will make our tests break.

Let’s rewrite our test,

using Microsoft.Extensions.Logging;
using Moq;
using OnTestingLogMessages.Controllers;
using OnTestingLogMessages.Services;

namespace OnTestingLogMessages.Tests;

[TestClass]
public class SomethingControllerTests
{
    [TestMethod]
    public async Task PostAsync_Exception_LogsException()
    {
        var clientId = 123456;

        var fakeClientService = new Mock<IClientService>();
        fakeClientService
            .Setup(t => t.DoSomethingAsync(clientId))
            .ThrowsAsync(new Exception("Expected exception..."));
            //           ^^^^^
            // 3...2...1...Boom...
        var fakeLogger = new Mock<ILogger<SomethingController>>();
        var controller = new SomethingController(fakeClientService.Object, fakeLogger.Object);

        var request = new AnyPostRequest(clientId);
        await controller.PostAsync(request);
        
        fakeLogger.VerifyMessageContains(LogLevel.Error, clientId.ToString());
        //         ^^^^^^^^
        // We expect the same log message to only contain the clientId
    }
}

This time, we rolled another extension method, VerifyMessageContains(), removed the expected log message and asserted that the log message only contained only relevant subtrings: the clientId.

Here it is the new VerifyMessageContains(),

public static class MoqExtensions
{
    public static void VerifyMessageContains<T>(this Mock<ILogger<T>> fakeLogger, LogLevel logLevel, params string[] expected)
    {
        fakeLogger.Verify(
            x => x.Log(
                logLevel,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((o, t) =>
                    expected.All(s => o.ToString().Contains(s, StringComparison.OrdinalIgnoreCase))),
                    // ^^^^^
                    // Checking if the log message contains some keywords, instead
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.Once);
    }
}

Voilà! That’s how to make our test that checks logging messages more maintainable. By not rebuilding log messages inside tests and asserting that they contain keywords instead of expecting to be exact matches.

Here we dealt with logging for diagnostic purposes (logging to make troubleshooting easier for developers). But if logging were a business requirement, we should have to make it a separate “concept” in our code. Not in logging statements scatter all over the place. I learned by distinction about logging when reading Unit Testing Principles, Practices, and Patterns.

If you want to read more about unit testing, check How to write tests for HttpClient using Moq, How to test an ASP.NET Authorization Filter and my Unit Testing 101 series where we cover from what a unit test is, to fakes and mocks, to best practices.

Want to write readable and maintainable unit tests in C#? Join my course Mastering C# Unit Testing with Real-world Examples on Udemy and learn unit testing best practices while refactoring real unit tests from my past projects. No more tests for a Calculator class.

Happy testing!

TIL: How to test an ASP.NET Authorization Filter

This post is part of my Advent of Code 2022.

These days I needed to work with a microservice for one of my clients. In that microservice, instead of validating incoming requests with the built-in model validations or FluentValidation, they use authorization filters. I needed to write some tests for that filter. This is what I learned.

Apart from validating the integrity of the incoming requests, the filter also validated that the referenced object in the request body matched the same “client.”

A weird filter scenario

The filter looked something like this,

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using MyWeirdFilterScenario.Controllers;

namespace MyWeirdFilterScenario.Filters;

public class MyAuthorizationFilter : IAsyncAuthorizationFilter
{
    private readonly Dictionary<string, Func<AuthorizationFilterContext, Task<bool>>> _validationsPerEndpoint;

    private readonly IClientRepository _clientRepository;
    private readonly IOtherEntityRepository _otherEntityRepository;

    public MyAuthorizationFilter(IClientRepository clientRepository,
                                 IOtherEntityRepository otherEntityRepository)
    {
        _clientRepository = clientRepository;
        _otherEntityRepository = otherEntityRepository;

        // Register validations per action name here
        // vvvvv
        _validationsPerEndpoint = new Dictionary<string, Func<AuthorizationFilterContext, Task<bool>>>(StringComparer.OrdinalIgnoreCase)
        {
            { nameof(SomethingController.Post),  ValidatePostAsync },
            // Register validations for other methods here...
        };
    }

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        var actionName = ((ControllerActionDescriptor)context.ActionDescriptor).ActionName;

        try
        {
            var validation = _validationsPerEndpoint[actionName];
            var isValid = await validation(context);
            //                  ^^^^^^^^^^
            // Grab and run the validation for the called endpoint
            if (!isValid)
            {
                context.Result = new BadRequestResult();
                return;
            }
        }
        catch (Exception)
        {
            // Log bad things here...
            context.Result = new BadRequestResult();
        }
    }

    private async Task<bool> ValidatePostAsync(AuthorizationFilterContext context)
    {
        var request = await GetRequestBodyAsync<AnyPostRequest>(context);
        //                  ^^^^^^^^^^^^^^^^^^^
        // Grab the request body
        if (request == null || request.ClientId == default)
        {
            return false;
        }

        var client = await _clientRepository.GetByIdAsync(request.ClientId);
        //  ^^^^^^
        // Check our client exists...
        if (client == null)
        {
            return false;
        }

        var otherEntity = await _otherEntityRepository.GetByIdAsync(request.OtherEntityId);
        if (otherEntity == null || otherEntity.ClientId != client.Id)
        //  ^^^^^^^^^^^
        // Check we're updating our own entity...
        {
            return false;
        }

        // Doing something else here...

        return true;
    }

    // A helper method to grab the request body from the AuthorizationFilterContext
    private static async Task<T?> GetRequestBodyAsync<T>(AuthorizationFilterContext context)
    {
        var request = context.HttpContext.Request;
        request.EnableBuffering();
        request.Body.Position = 0;

        var body = new StreamReader(request.Body);
        var requestBodyJson = await body.ReadToEndAsync();

        request.Body.Position = 0;

        if (string.IsNullOrEmpty(requestBodyJson))
        {
            return default;
        }

        var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
        var requestBody = JsonConvert.DeserializeObject<T>(requestBodyJson, settings);
        return requestBody;
    }
}

On the OnAuthorizationAsync() method, this filter grabbed the validation method based on the called method name. And, inside the validation method, it checked that the request had a valid “clientId” and the referenced entity belonged to the same client. This is to prevent any client from updating somebody else’s entities.

Also, notice we needed to use the EnableBuffering() and reset the body’s position before and after reading the body from the AuthorizationFilterContext.

On the controller side, we registered the filter with an attribute like this,

using Microsoft.AspNetCore.Mvc;
using RecreatingFilterScenario.Filters;

namespace MyAuthorizationFilter.Controllers;

[ApiController]
[Route("[controller]")]
[ServiceFilter(typeof(MyAuthorizationFilter))]
//                    ^^^^^^^^^^^
public class SomethingController : ControllerBase
{
    [HttpPost]
    public void Post(AnyPostRequest request)
    {
        // Beep, beep, boop...
        // Doing something with request
    }

    // Other methods here...
}

And, to make it work, we also need to register our filter in the dependencies container.

Morning Brew
Photo by Kris Gerhard on Unsplash

How to test an ASP.NET async authorization filter

To test an ASP.NET async filter, create a new instance of the filter passing the needed dependencies as stubs. Then, when calling the OnAuthorizationAsync() method, create a AuthorizationFilterContext instance attaching the request body inside a DefaultHttpContext.

Like this,

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Moq;
using Newtonsoft.Json;
using RecreatingFilterScenario.Controllers;
using RecreatingFilterScenario.Filters;
using System.Text;

namespace MyWeirdFilterScenario.Tests;

[TestClass]
public class MyAuthorizationFilterTests
{
    [TestMethod]
    public async Task OnAuthorizationAsync_OtherEntityWithoutTheSameClient_ReturnsBadRequest()
    {
        var sameClientId = 1;
        var otherClientId = 2;
        var otherEntityId = 123456;

        var fakeClientRepository = new Mock<IClientRepository>();
        fakeClientRepository
            .Setup(t => t.GetByIdAsync(sameClientId))
            .ReturnsAsync(new Client(sameClientId));

        var fakeOtherEntityRepository = new Mock<IOtherEntityRepository>();
        fakeOtherEntityRepository
            .Setup(t => t.GetByIdAsync(otherEntityId))
            .ReturnsAsync(new OtherEntity(otherClientId));

        var filter = new MyAuthorizationFilter(fakeClientRepository.Object, fakeOtherEntityRepository.Object);
        //  ^^^^^^
        // Create an instance of our filter with two fake dependencies

        var request = new AnyPostRequest(sameClientId, otherEntityId);
        var context = BuildContext(request);
        //            ^^^^^^^^^^^^
        // Create an AuthorizationFilterContext
        await filter.OnAuthorizationAsync(context);

        Assert.IsNotNull(context.Result);
        Assert.AreEqual(typeof(BadRequestResult), context.Result.GetType());
    }

    private AuthorizationFilterContext BuildContext(AnyPostRequest? request)
    {
        var httpContext = new DefaultHttpContext();

        var json = JsonConvert.SerializeObject(request);
        var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
        httpContext.Request.Body = stream;
        httpContext.Request.ContentLength = stream.Length;
        httpContext.Request.ContentType = "application/json";
        // ^^^^^^^^
        // Attach a JSON body

        var actionDescriptor = new ControllerActionDescriptor
        {
            ActionName = nameof(SomethingController.Post)
            // ^^^^^^^
            // Use the endpoint name
        };
        var actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor);
        return new AuthorizationFilterContext(actionContext, new List<IFilterMetadata>());
    }
}

Let’s unwrap it. First, we created an instance of MyAuthorizationFilter passing the dependencies as fakes using Moq. As stubs, to be precise.

To call the OnAuthorizationAsync() method, we needed to create an AuthorizationFilterContext. This context required an ActionContext. We used a Builder method, BuildContext(), to keep things clean.

Then, to create an ActionContext, we needed to attach the request body as JSON to a DefaultHttpContext and set the action descriptor with our method name. Since we didn’t read any route information, we passed a default RouteData instance.

Notice that we needed to use a MemoryStream to pass our request object as JSON and set the content length and type. Source.

With the BuildContext() method in place, we got the Arrange and Act parts of our sample test. The next step was to assert on the context result.

Voilà! That’s what I learned about unit testing ASP.NET authorization filters. Again, a Builder method helped to keep things simple and easier to reuse.

If you want to read more about unit testing, check How to write tests for HttpClient using Moq and my Unit Testing 101 series where we cover from what a unit test is, to fakes and mocks, to best practices.

Want to write readable and maintainable unit tests in C#? Join my course Mastering C# Unit Testing with Real-world Examples on Udemy and learn unit testing best practices while refactoring real unit tests from my past projects. No more tests for a Calculator class.

Happy testing!

TIL: Always check for missing configuration values inside constructors

This post is part of my Advent of Code 2022.

This is a lesson I learned after trying to use a shared NuGet package in one of my client’s projects and getting an ArgumentNullException. I had no clue that I needed some configuration values in my appsettings.json file. This is what I learned.

Always check for missing configuration values inside constructors. In case they’re not set, throw a human-friendly exception message showing the name of the expected configuration value. For example: ‘Missing Section:Subsection:Value in config file’.

A missing configuration value

This is what happened. I needed to import a feature from a shared Nuget package. It had a method to register its dependencies. Something like services.AddFeature().

When calling an API endpoint that used that feature, I got an ArgumentNullException: “Value cannot be null. (Parameter ‘uriString’).” It seemed that I was missing a URI. But what URI?

Without any XML docstrings on the AddFeature() method, I had no other solution than to decompile that DLL. I found a service like this one,

public class SomeService : ISomeService
{
    private readonly Uri _anyUri;

    public SomeService(IOptions<AnyConfigOptions> options, OtherParam otherParam)
    {
        _anyUri = new Uri(options.Value.AnyConfigValue);
        //                ^^^^^^^
        // System.ArgumentNullException: Value cannot be null. (Parameter 'uriString')
    }

    public async Task DoSomethingAsync()
    {
        // Beep, beep, boop...
        // Doing something here...
    }
}

There it was! The service used the IOptions pattern to read configuration values. And I needed an URL inside a section in the appsettings.json file. How was I supposed to know?

Black and brown jigsaw puzzle
Missing one value... Photo by Sigmund on Unsplash

A better exception message

Then I realized that a validation inside the constructor with a human-friendly message would have saved me (and any other future developer using that NuGet package) some time. And it would have pointed me in the right direction. I mean having something like,

public class SomeService : ISomeService
{
    private readonly Uri _anyUri;

    public SomeService(IOptions<AnyConfigOptions> options, OtherParam otherParam)
    {
        //  vvvvvvv
        if (string.IsNullOrEmpty(options?.Value?.AnyConfigValue))
        {
            throw new ArgumentNullException("Missing 'AnyConfigOptions:AnyConfigValue' in config file.");
            //                              ^^^^^^^^
            // I think this would be a better message
        }

        _anyUri = new Uri(options.Value.AnyConfigValue);
    }

    public async Task DoSomethingAsync()
    {
        // Beep, beep, boop...
        // Doing something here again...
    }
}

Even better, what if the AddFeature() method had an overload that receives the expected configuration value? Something like AddFeature(AnyConfigOptions options). This way, the client of that package could decide the source of those options. Either read them from a configuration file or hardcode them.

The book “Growing Object-Oriented Software Guided by Tests” suggests having a StupidProgrammerMistakeException or a specific exception for this type of scenario: missing configuration values. This would be a good use case for that exception type.

Voilà! That’s what I learned today: always validate configuration values inside constructors and use explicit error messages when implementing the Options pattern. It reminded me of “The given key was not present in the dictionary” and other obscure error messages. Do you write friendly and clear error messages?

To read more content about ASP.NET Core, check how to add caching with Redis and how to read configuration values.

Happy coding!