Compile Time Checking of String Parameters in C#

January 13, 2026    Development C# Functional Programming

Compile Time Checking of String Parameters in C#

In my earlier article Functional Programming Overview , I wrote about several principles of functional programming that can enhance code quality and maintainability. One of these principles is to make unrepresentable states, unrepresentable". This can be particularly useful when dealing with string parameters in C# methods.

Another benefit is that the Find All References feature in Visual Studio works better with types than with strings, making it easier to track where specific parameters are used.

The Challenge

Using Functional Programming principles, in a console application, injected the dependencies from the Program.cs. I then used some static classes and methods when I didn’t need to swap out the implementation for testing.

Original Approach

I passed in the paths as string parameters to the methods. However, this approach has a significant drawback: there’s no compile-time checking to ensure that the correct path is passed in. What if I accidentally swapped the input and output paths? The compiler wouldn’t catch this mistake, leading to huge runtime errors.

// Program.cs
var services = new ServiceCollection();
services.Configure<AppSettings>(options => configuration.GetSection("AppSettings").Bind(options));
var appSettings =
    configuration.Get<AppSettings>() ?? throw new MyAppException("AppSettings!");
var fileInteractor = new FileInteractor(
    // ... more dependencies could go here,
    System.TimeProvider.System, // fakeable time 🎉
    appSettings.InPath
);

await Orchestrator.Start(
    fileInteractor,
    // ... more dependencies could go here,
    appSettings.OutPath
);

A Solution

To address this issue, I created two distinct types to represent the input and output paths. By doing so, I ensured that the compiler would enforce the correct usage of these parameters, preventing accidental swaps.

Defining the Types


internal sealed class AppSettings
{
    public string InPath { get; set; } = "";
    public string OutPath { get; set; } = "";
}

// using these types, we can avoid passing in the wrong string
//public record struct FileName(string Value);

public sealed record InPath;

public sealed record OutPath;

public sealed record AppSettingPath<T>(string Value);

Using those types

// Program.cs
var services = new ServiceCollection();
services.Configure<AppSettings>(options => configuration.GetSection("AppSettings").Bind(options));
var appSettings =
    configuration.Get<AppSettings>() ?? throw new MyAppException("AppSettings!");
// Note: all of the appsettings.json files are strings and the AppSettings class has string properties

var fileInteractor = new FileInteractor(
    // ... more dependencies could go here,
    System.TimeProvider.System, // fakeable time 🎉
    // take the string from appsettings and wrap it in the type
    new AppSettingPath<InPath>(appSettings.InPath)
);

await Orchestrator.Start(
        fileInteractor,
        // ... more dependencies could go here,
    new AppSettingPath<OutPath>(appSettings.OutPath)
);

FileInteractor Constructor Comparison

Using string (the old way)

public class FileInteractor
{
    public FileInteractor(
        // ... more dependencies could go here,
        System.TimeProvider timeProvider,
        string inPath
    )
    {
        // fill in your code here
    }
}

Using the types

public class FileInteractor
{
    public FileInteractor(
        // ... more dependencies could go here,
        System.TimeProvider timeProvider,
        AppSettingPath<InPath> inPath
    )
    {
        // fill in your code here
    }
}

Accessing the Path Value

To get the value of the path, you can access the Value property of the AppSettingPath<T> instance.

var inPath = inPathParameter.Value; // where inPathParameter is of type AppSettingPath<InPath>

Alternative

I could have injected or passed around my AppSettings class that was loaded from the appsettings.json file, but staying with the strings for paths as parameters seemed more straightforward.

Thanks

Shout out to twopoint.dev for giving me this code example after I asked him about what to do.