kaznacheev.tech
← All posts
14 December 2024

Switch Composition in C#

C#PatternsRefactoring

Some time ago, I encountered a situation where I had to use three switch statements, and the results of the first one were used as arguments for the second. I needed this approach for the PhoneNumber primitive in the ToString method to support multiple formats. Here's an example of how that might look:

The entire code is provided as an example to illustrate the underlying motivation behind the approach.

public string ToString(string? format, IFormatProvider? formatProvider)
{
    if (string.IsNullOrEmpty(format))
    {
        format = "e"; //default format
    }
 
    switch (format)
    {
        case "e":
            return Number;
        case "i":
            return Util.Format(_phoneNumber, PhoneNumberFormat.INTERNATIONAL);
        case "r":
            return Util.Format(_phoneNumber, PhoneNumberFormat.RFC3966);
        case "n":
            return Util.Format(_phoneNumber, PhoneNumberFormat.NATIONAL);
        default:
            throw new FormatException("Bad phone format.");
    }
}

At first, I didn't feel satisfied with this code because the responsibilities seemed too muddled. There was no clear separation of concerns: default configuration, format mapping, and actual formatting were all mixed together. So, I started refactoring it and ended up with something like this:

public string ToString(string? format, IFormatProvider? formatProvider)
{
    const char defaultFormat = 'e';
 
    format = format switch
    {
        null or "" => "e",
        var f => f,
    };
 
    var internalFormat = format switch
    {
        "e" => PhoneNumberFormat.E164,
        "i" => PhoneNumberFormat.INTERNATIONAL,
        "r" => PhoneNumberFormat.RFC3966,
        "n" => PhoneNumberFormat.NATIONAL,
        _ => throw new FormatException("Bad phone format.")
    };
 
    return internalFormat switch
    {
        PhoneNumberFormat.E164 => Number,
        var f => Util.Format(_phoneNumber, f)
    };
}

This version clearly separates the default format handling, the mapping of the format to an internal format, and the actual formatting. But after thinking about it more, I wondered if I could further improve it by combining everything into a single composition. The result was this version:

public string ToString(string? format, IFormatProvider? formatProvider)
{
    return format switch
    {
        null or "" => "e",
        var f => f
    }
    switch
    {
        "e" => PhoneNumberFormat.E164,
        "i" => PhoneNumberFormat.INTERNATIONAL,
        "r" => PhoneNumberFormat.RFC3966,
        "n" => PhoneNumberFormat.NATIONAL,
        _ => throw new FormatException("Bad phone format.")
    }
    switch
    {
        PhoneNumberFormat.E164 => Number,
        var f => Util.Format(_phoneNumber, f)
    };
}

It's strange and unusual; even formatting tools have trouble understanding what's going on here. However, it's a fully compilable and functional solution, and I actually like how it looks.