A closer look at StringValues in C#

17/06/2024
C#StringValues

A closer look at StringValues type in C#

In C# we have a few types that carry string types like string, String, and StringBuilder (or maybe char[] 😃) but what is StringValues?

The MS doc says StringValues Represents zero/null, one, or many strings in an efficient way! huh?

Based on the definition it's like an interesting type which can be string or string[] at the same time (or null of course).

Before going to check what is that, let's talk a little about the reason and use cases for creating such a type.

Usually we see the StringValues when we get an HTTP Header, right?

This is one of the HTTP features that you can have the same header key but multiple values, so at the same time a header can have one or multiple values (see rfc9110)

When a field name is only present once in a section, the combined "field value" for that field consists of the corresponding field line value. When a field name is repeated within a section, its combined field value consists of the list of corresponding field line values within that section, concatenated in order, with each field line value separated by a comma.

So how we can handle this situation? you may say simply it's possible to use Dictionary<string, string[]>, while it's correct but it has some downsides like memory allocation.

.NET team created the StringValues as a struct:

public readonly struct StringValues : IList<string?>, IReadOnlyList<string?>, IEquatable<StringValues>, IEquatable<string?>, IEquatable<string?[]?>
    { }

To provide easy to use, it implements IList and also IEquatable interface for comparing values. I don't want to check all of the implementation of StringValues but there are some interesting points in it.

First, let's see how we can create a new instance of StringValues, yes I mean constructor:

private readonly object? _values; //<------ object!

/// <summary>
/// Initializes a new instance of the <see cref="StringValues"/> structure using the specified string.
/// </summary>
/// <param name="value">A string value or <c>null</c>.</param>
public StringValues(string? value)
{
    _values = value;
}

/// <summary>
/// Initializes a new instance of the <see cref="StringValues"/> structure using the specified array of strings.
/// </summary>
/// <param name="values">A string array.</param>
public StringValues(string?[]? values)
{
    _values = values;
}

We can create either by one or multiple strings but they set them to an object property called values, later down we know why:)

We can call Count method, curious to know how they implement that, so here is the code:

/// <summary>
/// Gets the number of <see cref="string"/> elements contained in this <see cref="StringValues" />.
/// </summary>
public int Count
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
        // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory
        object? value = _values;
        if (value is null)
        {
            return 0;
        }
        if (value is string)
        {
            return 1;
        }
        else
        {
            // Not string, not null, can only be string[]
            return Unsafe.As<string?[]>(value).Length;
        }
    }
}

.NET team is using pattern matching to check what is the actual type of StringValues, above mentioned no matter how you call the constructor with one or with multiple strings it will assing to an object type called values, so here it checks based on that object. I really like how simple and nice it is:)

Just a point here in case of multiple strings, they are using Unsafe.As to cast value to string[], this API is used to cast an object to the given type, suppressing the runtime's normal type safety checks. It is the caller's responsibility to ensure that the cast is legal. No InvalidCastException will be thrown.

Another interesting method is IsNullOrEmpty:

/// <summary>
/// Indicates whether the specified <see cref="StringValues"/> contains no string values.
/// </summary>
/// <param name="value">The <see cref="StringValues"/> to test.</param>
/// <returns>true if <paramref name="value">value</paramref> contains a single null or empty string or an empty array; otherwise, false.</returns>
public static bool IsNullOrEmpty(StringValues value)
{
    object? data = value._values;
    if (data is null)
    {
        return true;
    }
    if (data is string[] values)
    {
        return values.Length switch
        {
            0 => true,
            1 => string.IsNullOrEmpty(values[0]),
            _ => false,
        };
    }
    else
    {
        // Not array, can only be string
        return string.IsNullOrEmpty(Unsafe.As<string>(data));
    }
}

You see how nice it uses pattern matching to handle different situations. As last one let's look at Concat method:

/// <summary>
/// Concatenates two specified instances of <see cref="StringValues"/>.
/// </summary>
/// <param name="values1">The first <see cref="StringValues"/> to concatenate.</param>
/// <param name="values2">The second <see cref="StringValues"/> to concatenate.</param>
/// <returns>The concatenation of <paramref name="values1"/> and <paramref name="values2"/>.</returns>
public static StringValues Concat(StringValues values1, StringValues values2)
{
    int count1 = values1.Count;
    int count2 = values2.Count;

    if (count1 == 0)
    {
        return values2;
    }

    if (count2 == 0)
    {
        return values1;
    }

    var combined = new string[count1 + count2];
    values1.CopyTo(combined, 0);
    values2.CopyTo(combined, count1);
    return new StringValues(combined);
}

If you are curious more about checking whole code, find it here: StringValues.cs

This post was inspired by Andrew Lock post.

Thanks for reading and Code Simple!

11
An error has occurred. This application may no longer respond until reloaded. Reload x