When developing web applications using ASP.NET Core, we frequently encounter scenarios where we need to manage collections of strings. These collections can originate from various sources, such as HTTP headers, query strings, form data, and configuration settings. Efficiently handling these collections is crucial for maintaining the performance and scalability of our web applications. That’s where StringValues can become handy.

StringValues is a specialized type in ASP.NET Core designed to handle multiple string values efficiently. StringValues is a read-only struct that uses a single internal object to represent zero, one, or many strings. In this article, we’ll explore the concept of StringValues in ASP.NET Core.

To download the source code for this article, you can visit our GitHub repository.

Let’s make a start.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Naive Implementation Approach

In the early stages of handling HTTP headers, we might consider using arrays to store multiple values for a single header key. While this approach appears straightforward, it introduces several drawbacks that can affect performance and complexity.

When we use arrays to store header values, we encounter increased memory allocations. For instance, storing a single value requires allocating an array, even when unnecessary. This leads to excessive memory usage, which can degrade performance, especially in high-traffic web applications.

Now, let’s create a console app and add a NaiveImplementation class:

public class NaiveImplementation
{
    private readonly Dictionary<string, string[]> _headers = [];

    public void AddHeader(string key, params string[] values)
    {
        if (_headers.TryGetValue(key, out var existingValues))
        {
            var newValues = new string[existingValues.Length + values.Length];
            existingValues.CopyTo(newValues, 0);
            values.CopyTo(newValues, existingValues.Length);
            _headers[key] = newValues;
        }
        else
        {
            _headers[key] = values;
        }
    }
}

Here, we declare a private field _headers of type Dictionary<string, string[]>. Then, we define a AddHeader() method that accepts a key and a variable number of string values using the params keyword. We use the TryGetValue() method to check if the dictionary _headers already contains the specified key. This avoids the need for multiple lookups.

If the key exists, we create a new array that combines existing and new values. We copy the existing values to the beginning of the new array and then copy the new values to the remaining positions. Finally, we update the dictionary with the combined array.

This implementation uses a Dictionary<string, string[]> to store header values. When adding new values, a new array is allocated, existing values are copied over, and the new values are appended.  Although this method is straightforward, it leads to increased memory allocations due to frequent array resizing, causing performance issues, especially in high-traffic scenarios.

Legacy Solution: NameValueCollection

Before the introduction of StringValues, we used NameValueCollection to manage headers with multiple values. This class helps us store and retrieve header values but also has some challenges.

NameValueCollection, found in the System.Collections.Specialized namespace, lets us store multiple values under a single key. It also offers additional methods for adding values and retrieving them as a single comma-delimited string or an array of strings.

Let’s create another class LegacyImplementation in the console app:

public class LegacyImplementation
{
    private readonly NameValueCollection _headers = [];

    public void AddHeader(string key, params string[] values)
    {
        foreach (var value in values)
        {
            _headers.Add(key, value);
        }
    }
}

Here, we declare a private field _headers of type NameValueCollection. Similarly, we define an AddHeader() method. Inside the method, we iterate through the values array and add it to the _headers collection with the specified key.

This implementation utilizes the NameValueCollection from the System.Collections.Specialized namespace. This approach allows multiple values per key, simplifying header management. However, due to internal array handling, it requires higher memory allocations, making it less optimal for modern high-traffic applications.

Understanding the StringValues

Understanding StringValues is particularly important for us when working with ASP.NET Core. Many core components and middleware in ASP.NET Core employ it to manage collections of strings. For instance, HTTP headers and query string parameters are represented as StringValues in the HttpRequest and HttpResponse classes.

StringValues is a struct type, i.e. a value type. When allocated locally within a method, value types are typically stored on the stack, making them faster to allocate and deallocate compared to heap allocations. This, in turn, reduces memory allocations and thus reduces the need for garbage collection. For a deeper dive into records, classes, and structs, please refer to our article: Should We Use Records or Classes or Structs in C#.

We use StringValues to store values using a single internal object that can hold null, a single string, or an array of strings. This design allows us to handle different scenarios without unnecessary memory allocations. For instance, we store a single header value as a string, while we store multiple values in an array.

Using a single object to store values, StringValues helps us minimize memory allocations, enhancing application performance. This is particularly beneficial in high-traffic web applications where efficient memory management is crucial.

StringValues Constructors

StringValues provides constructors that offer flexibility in initializing and using it. To begin with, let’s install the Microsoft.Extensions.Primitives package:

dotnet add package Microsoft.Extensions.Primitives

First, let’s create a StringValues containing a single string:

StringValues singleValue = new StringValues("value1");

This constructor initializes the StringValues object with only a single string. This constructor creates an object that efficiently stores the single string itself without allocating an array.

Next, let’s initialize a StringValues with an array of strings:

StringValues multipleValues = new StringValues(new[] { "value1", "value2" });

Here, we use the array constructor of StringValues to initialize it with an array of strings. This approach is beneficial when we need to handle collections of strings, such as multiple HTTP headers or query parameters in ASP.NET Core. The StringValues struct helps us manage these collections with minimal memory overhead and provides convenient methods for accessing and manipulating the string values.

Finally, let’s see how to initialize a StringValues object with an empty or null value:

StringValues emptyValue = new StringValues();

StringValues nullValue = new StringValues((string)null);

Here, we initialize the StringValues constructor with empty and null value. These constructors handle scenarios where there are no values, ensuring StringValues can gracefully manage empty or null inputs.

Implicit Conversion and Comma-Separated String Representation of StringValues

StringValues supports implicit conversion from a single string or an array of strings, making it easy to initialize. When StringValues contains multiple strings, it can represent them as a single comma-separated string, simplifying the handling of numerous values in scenarios like HTTP headers:

StringValues implicitSingle = "value1";
Console.WriteLine($"Implicit Single Value: {implicitSingle}");

StringValues implicitMultiple = new[] { "value1", "value2" };
Console.WriteLine($"Implicit Multiple Values: {implicitMultiple}");

StringValues values = new StringValues(new[] { "value1", "value2" });
Console.WriteLine($"Comma-Separated Values: {values}");

Here, we initialize StringValues with a single string value. The implicit conversion feature of StringValues allows us to assign a string. Next, we initialize StringValues with an array of strings. Again implicit conversion allows us to assign an array to a variable directly. Finally, we explicitly initialize StringValues with an array of strings.

Now, let’s inspect the output:

Implicit Single Value: value1
Implicit Multiple Values: value1,value2
Comma-Separated Values: value1,value2

When a single string, like “value1”, is implicitly converted to StringValues, it displays the string directly. For multiple strings, such as “value1,value2”, the array is converted into a StringValues object and displayed as a comma-separated list. Similarly, the output for comma-separated values, “value1,value2”, demonstrates how StringValues handles multiple strings efficiently, combining them into a single, readable format.

Practical Usage of StringValues

Next, let’s create a StringValuesImplementation class:

public class StringValuesImplementation
{
    private readonly Dictionary<string, StringValues> _headers = new();

    public void AddHeader(string key, params string[] values)
    {
        if (_headers.TryGetValue(key, out var existingValues))
        {
            _headers[key] = StringValues.Concat(existingValues, new StringValues(values));
        }
        else
        {
            _headers[key] = new StringValues(values);
        }
    }
}

Here, we declare a private field _headers of type Dictionary<string, StringValues>. Then, we check if the _headers dictionary already contains the key using TryGetValue() method. If the key exists, we concatenate the new value to the existing StringValues object using StringValues.Concat() method else we create a new StringValues instance with the value and add it to the dictionary.

Moving on, let’s define another method inside the StringValuesImplementation class to display the header values:

public void DisplayHeaders()
{
    foreach (var (key, value) in _headers)
    {
        Console.WriteLine($"{key}: {value}");
    }
}

Here, we use a foreach loop to iterate through each key-value pair in the _headers dictionary. Each _header in the loop is a KeyValuePair<string, StringValues>. The ToString() method of StringValues is automatically called, which conveniently converts the stored values into a single, comma-separated string if there are multiple values.

Conclusion

In this article, we explored StringValues to manage HTTP header values in modern ASP.NET Core applications. We briefly looked at both a naïve implementation as well as the standard ASP.NET legacy implementation using NameValueCollection. While our naïve implementation was easy to understand and implement, it suffers from performance inefficiencies due to increased memory allocations. The legacy NameValueCollection implementation simplifies value management but is less efficient than StringValues. For developing high-performance ASP.NET Core applications, leveraging StringValues offers significant advantages, making it the preferred solution for managing multiple HTTP header values.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!