Skip to content

Regression with generics from .NET 8 To .Net 10 #129692

Closed

Description

@AndreasHeisel

Description

I have a generic readonly record struct. It has a single instance member of type System.String. I use it as a strongly typed identifier to various types of objects. To make this customizable, the target object must implement a generic interface with a static virtual property of type StringComparer.

Reproduction Steps

Run the single unit test:

public readonly record struct Id<T>(string Value) : IEquatable<Id<T>> where T : ITarget<T>
{
	public static StringComparer Comparer { get; } = T.IdentifierComparer;

	public override int GetHashCode()
	{
		return Comparer.GetHashCode(Value);
	}

	public bool Equals(Id<T> other)
	{
		return Comparer.Equals(Value, other.Value);
	}

	public override string ToString()
	{
		return Value;
	}
}


public interface ITarget<T> where T : ITarget<T>
{
	static virtual StringComparer IdentifierComparer => StringComparer.Ordinal;

	public Id<T> Id { get; }
}


public record Target(Id<Target> Id) : ITarget<Target>
{
}


[TestClass]
public class Test
{
	[TestMethod]
	public void AssertDictionary()
	{
		var actual = new Dictionary<Id<Target>, Target>();
		var expected = new Dictionary<Id<Target>, Target>();

		var target = new Target(new("1"));
		actual.Add(target.Id, target);
		expected.Add(target.Id, target);

		CollectionAssert.AreEquivalent(expected, actual);
	}
}

Expected behavior

No exception

Actual behavior

Message: 
Test method IdTest.Test.AssertDictionary threw exception:
System.NullReferenceException: Object reference not set to an instance of an object.

  Stack Trace: 
Id`1.get_Comparer() line 7
Id`1.GetHashCode() line 12
ValueType.GetHashCode()
Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
Dictionary`2.set_Item(TKey key, TValue value)
CollectionAssert.GetElementCounts[T](IEnumerable`1 collection, IEqualityComparer`1 comparer, Int32& nullCount)
CollectionAssert.FindMismatchedElement[T](IEnumerable`1 expected, IEnumerable`1 actual, IEqualityComparer`1 comparer, Int32& expectedCount, Int32& actualCount, Object& mismatchedElement)
CollectionAssert.AreEquivalent[T](IEnumerable`1 expected, IEnumerable`1 actual, IEqualityComparer`1 comparer, String message)
CollectionAssert.AreEquivalent(ICollection expected, ICollection actual)
Test.AssertDictionary() line 22
MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Regression?

This worked well in .NET8.

Known Workarounds

None

Configuration

.NET 10, Windows11, x64

Other information

I get a NullReferenceException if the struct reads the interface's static property.

Using the debugger, I see that the runtime uses System.__Canon instead of my type.

Activity

  1. dotnet-policy-service commented on Jun 22, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-system-collections
    See info in area-owners.md if you want to be subscribed.

  2. EgorBo commented on Jun 22, 2026

    @EgorBo
    Member

    Most likely a regression from either #97590 or #98754 cc @huoyaoyuan

    copilot's suggested fix:

    --- a/src/coreclr/vm/comutilnative.cpp
    +++ b/src/coreclr/vm/comutilnative.cpp
    @@ -1918,8 +1918,11 @@ static ValueTypeHashCodeStrategy GetHashCodeStrategy(MethodTable* mt, QCall::Obje
                  else
                  {
    -                // got another value type. Get the type
    -                TypeHandle fieldTH = field->GetFieldTypeHandleThrowing();
    +                // got another value type. Get the exact type (the field type may be
    +                // a shared generic instantiation, e.g. Id<__Canon>, but we need the
    +                // exact type here so that boxing the field for an overridden GetHashCode
    +                // recovers the correct per-instantiation static data, generic context, etc.)
    +                TypeHandle fieldTH = field->GetExactFieldType(TypeHandle(mt));
                     _ASSERTE(!fieldTH.IsNull());
                     MethodTable* fieldMT = fieldTH.GetMethodTable();
                     if (CanCompareBitsOrUseFastGetHashCode(fieldMT)
  3. dotnet-policy-service commented on Jun 22, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @agocke
    See info in area-owners.md if you want to be subscribed.

  4. huoyaoyuan commented on Jun 23, 2026

    @huoyaoyuan
    Member

    The exception is happening in KeyValuePair<Id<Target>, Target>.GetHashCode.

    I'm not sure whether the Copilot fix is related. GetFieldTypeHandleThrowing was already used before #97590.

    Looks like exposed by #98754. The old implementation wasn't calling overridden GetHashCode at all.


    Interestingly, the code will not work correctly on .NET 8 either. Replace the StringComparer.Ordinal with StringComparer.OrdinalIgnoreCase, and test the following code:

            var target = new Target(new("a"));
            actual.Add(new Id<Target>("A"), target);
            expected.Add(new Id<Target>("a"), target);

    The assert will fail, despite "A" and "a" are treated as the same key and actual.Add(new Id<Target>("a"), target) fails.

  5. added a commit that references this issue on Jun 23, 2026
    e785632
  6. added a commit that references this issue on Jul 15, 2026
    b6a3286
  7. locked and limited conversation to collaborators on Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions