Search code examples
c#vb.netlinqgroupinganonymous-types

GroupBy Anonymous Type Differences - VB.net vs C#


I came across a difference in the results of a GroupBy in C# and VB.net when using anonymous types. Specifically, it's that VB.net seems to have trouble correctly grouping items when any of the keys are a nullable type and have no value set.

Say I've got the following model and some data:

    public class Record
    {
        public int? RecordTypeId { get; set; }
        public int? PlayerId { get; set; }
        public int? TeamId { get; set; }
        public int? Salary { get; set; }
    }

    public static class SomeRecords
    {
        public static List<Record> Records{ get; set; } = new List<Record>()
        {
            new Record() {PlayerId = 1, Salary = 100},
            new Record() {PlayerId = 2, Salary = 200},
            new Record() {PlayerId = 3, Salary = 300},
            new Record() {PlayerId = 4, Salary = 400}
        };
    }

The c# output grouping gives me 4 keys, which is what I expect because no items have overlapping values for PlayerId, and they all have the no value set for TeamId or RecordTypeId.

    var cSharpGrouping = SomeCollection.SomeRecords.GroupBy(x => new
            {
                RecordTypeId = x.RecordTypeId.GetValueOrDefault(),
                PlayerId = x.PlayerId.GetValueOrDefault(),
                TeamId  = x.TeamId.GetValueOrDefault()
            });

The VB.net GroupBy gives me only 1 key.

    Dim vbGrouping = SomeCollection.SomeRecords.GroupBy(Function(x) New With { Key _
    .RecordTypeId = x.RecordTypeId.GetValueOrDefault(), _ 
    .PlayerId = x.PlayerId.GetValueOrDefault(), _
    .TeamID = x.TeamId.GetValueOrDefault() _
    })

Yet, if I change the VB.net GroupBy to the following and combine the keys into one stringified key, it works as expected and I get 4 keys:

    Dim stringifiedKeysGrouping = SomeCollection.SomeRecords.GroupBy(Function(x) _ 
    x.RecordTypeId.GetValueOrDefault().ToString() + "-" _
    + x.PlayerId.GetValueOrDefault().ToString() + "-" _
    + x.TeamId.GetValueOrDefault().ToString()
    )

What exactly is going on here? I did a little research and read that VB.net's nullable types are not exactly the same as c#'s, for backwards compatibility reasons, however I don't understand how that would come into play here because I am calling GetValueOrDefault.


Solution

  • In Visual Basic, two anonymous type instances are equal if and only if they are the same type (see below) and their Key properties are all equal. If no Key properties are defined, two seemingly identical instances will compare inequal.

    From the documentation, bolding mine:

    Key properties differ from non-key properties in several fundamental ways:

    • Only the values of key properties are compared in order to determine whether two instances are equal.

    • The values of key properties are read-only and cannot be changed.

    • Only key property values are included in the compiler-generated hash code algorithm for an anonymous type.

    And then continuing:

    Instances of anonymous types can be equal only if they are instances of the same anonymous type. The compiler treats two instances as instances of the same type if they meet the following conditions:

    • They are declared in the same assembly.

    • Their properties have the same names, the same inferred types, and are declared in the same order. Name comparisons are not case-sensitive.

    • The same properties in each are marked as key properties.

    • At least one property in each declaration is a key property.

    GroupBy uses the default equality comparer of the type. For anonymous types it invokes the compiler generator Equals method which (as stated above) only compares the Key properties. In your first example you have only defined one Key property. Every item in your collection has the same RecordTypeId (Nothing which you have coalesced to 0). This means every anonymous object has the same single Key property all with the same value, thus one single grouping.

    The solution is to make all the properties in the grouping Key properties (and not just the first):

    Dim vbGrouping = SomeCollection.SomeRecords.GroupBy(Function(x) New With {  _
       Key .RecordTypeId = x.RecordTypeId.GetValueOrDefault(), _ 
       Key .PlayerId = x.PlayerId.GetValueOrDefault(), _
       Key .TeamID = x.TeamId.GetValueOrDefault() _
    })