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.
Let’s make a start.
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.