Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hash code of string is broken in .NET Core 2.1, but works in 2.0

I recently upgraded one of my projects from .NET Core 2.0 to .NET Core 2.1. After doing so several of my tests started to fail.

After narrowing this down I've found that in .NET Core 2.1 it is not possible to compute the hash code of a string using a culture aware comparer with the string sort compare option.

I've created a test that reproduce my problem:

[TestMethod]
public void Can_compute_hash_code_using_invariant_string_sort_comparer()
{
    var compareInfo = CultureInfo.InvariantCulture.CompareInfo;
    var stringComparer = compareInfo.GetStringComparer(CompareOptions.StringSort);
    stringComparer.GetHashCode("test"); // should not throw!
}

I've tested it on a couple of frameworks with the following results:

  • .NET Core 2.0: ✔ PASS
  • .NET Core 2.1: ✖ FAIL
  • .NET Framework 4.7: ✖ FAIL
  • .NET Framework 4.6.2: ✖ FAIL

When failing an ArgumentException is thrown from CompareInfo.GetHashCodeOfString saying:

Value of flags is invalid

Now, to my questions:

  1. Why is it not allowed to use CompareOptions.StringSort when computing a hash code?

  2. Why was it allowed in .NET Core 2.0?`

As far as I understand CompareOptions.StringSort only affects the relative sort order of strings and should not affect hash code computation. MSDN says:

StringSort Indicates that the string comparison must use the string sort algorithm. In a string sort, the hyphen and the apostrophe, as well as other nonalphanumeric symbols, come before alphanumeric characters.

like image 357
Mårten Wikström Avatar asked Oct 30 '18 05:10

Mårten Wikström


1 Answers

The corefx team has confirmed that this is a bug in .NET Core 2.1 and also in the full .NET Framework as of 4.6+.

They also acknowledge that it will be hard to change this behavior in the full framework and may therefore consider to keep the behavior as-is in .NET Core 2.1+ to maintain consistency between .NET Core and the full framework.

A possible workaround is to use a class like this:

internal sealed class CultureAwareStringSortComparer : StringComparer
{
    public CultureAwareStringSortComparer(
        CompareInfo compareInfo, 
        CompareOptions options = CompareOptions.StringSort)
    {
        Requires.ArgNotNull(compareInfo, nameof(compareInfo));
        this.SortComparer = compareInfo.GetStringComparer(options);
        this.HashCodeComparer = compareInfo.GetStringComparer(
            options & ~CompareOptions.StringSort);
    }

    internal StringComparer SortComparer { get; }

    internal StringComparer HashCodeComparer { get; }

    public override int Compare(string x, string y) => this.SortComparer.Compare(x, y);

    public override bool Equals(string x, string y) => this.SortComparer.Equals(x, y);

    public override int GetHashCode(string obj) => this.HashCodeComparer.GetHashCode(obj);
}
like image 74
Mårten Wikström Avatar answered Nov 03 '22 13:11

Mårten Wikström