Skip to content

StringValues.ToArray does not return a copy if the StringValues contains an array internally. #36131

Open
@marcelltoth

Description

@marcelltoth

Describe the bug

The StringValues class is an IEnumerable<string>, but it has its own ToArray() method that shadows the well-known extension method with the same name. This would not be a problem by itself, but this ToArray works surprisingly differently than the extension: If the StringValues contains an array internally it simply returns that array. This greatly violates the principle of least astonishment and causes hard-to-localize bugs in user code such as: dotnet/aspnetcore#11016

To Reproduce

Steps to reproduce the behavior:

  1. Using the latest master version of assembly Microsoft.Extensions.Primitives (the problem existed for a long time though)
  2. Run these two tests:
[Fact]
public void ToArray_ConstructedFromString_CopiesValues()
{
	var singleStringValue = new StringValues("abc");

	var array = singleStringValue.ToArray();
	array[0] = "bcd";

	Assert.Equal("abc", singleStringValue[0]);
}

[Fact]
public void ToArray_ConstructedFromArray_CopiesValues()
{
	var singleStringValue = new StringValues(new [] {"abc"});

	var array = singleStringValue.ToArray();
	array[0] = "bcd";

	Assert.Equal("abc", singleStringValue[0]);
}
  1. The first one succeeds, as expected.
  2. The second one fails, it allows the mutation of singleStringValue.

Expected behavior

Both tests succeed.

Rationale

  1. Seeing a method called ToArray, I can't blame anyone that assumes that it creates a copy. I definitely would.
  2. This struct is defined as read only at every possible place (ICollection<string>.IsReadOnly, all the NotSupportedException-s, etc.), it should be immutable as much as practical.
  3. Because of the shadowing, the current situation makes this innocent change a breaking chage:
    IEnumerable<string> values = someStringValues;
    var arrayOfValues = values.ToArray();
    Array.Sort(arrayOfValues, StringComparer.Ordinal);
    
    versus:
    var values = someStringValues;
    var arrayOfValues = values.ToArray();
    Array.Sort(arrayOfValues, StringComparer.Ordinal);
    
    The first one copies, the second one mutates. Very hard to catch.

Disclaimer

The proposed fix is a change of behavior in the public-facing api of the StringValues class.
However I do not think anyone has used this behavior intentionally because:

  • StringValues is an IList by itself, if someone were to attempt to mutate it they just used that interface (and realized that it does not support mutations intentionally).
  • If you really really need a mutable array out of the struct, it has an implicit string[] conversion operator which does exactly that.

Further considerations

It should be worth considering whether the implicit operator string[] conversion operator and string[]-based constructor should create a copy, too.

I am assuming that these two are implemented without copies for performance reasons, and I left them alone for now in the fix I prepared for the ToArray problem. But still, these two points on the API allow simple (and perhaps unintentional) mutation of a struct that is otherwise acts perfectly immutable. It might be worth considering whether the performance gain is worth the integrity risk.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions