2

I am trying to group a list of common objects by the Email property but I am not getting the results I'm expecting.

var users = new List<Profile>
            {
                new SharePointProfile
                {
                    Id = 1,
                    LoginName = "Login1",
                    Title = "Login1 SharePoint",
                    Email = "Login1@sharepoint.com",
                    IsSiteAdmin = false
                },
                new SharePointProfile
                {
                    Id = 2,
                    LoginName = "Login2",
                    Title = "Login2 SharePoint",
                    Email = "Login2@sharepoint.com",
                    IsSiteAdmin = true
                },
                new Auth0Profile
                {
                    Email = "Login2@sharepoint.com",
                    Name = "Login2 SharePoint Auth0"
                },
                new Auth0Profile
                {
                    Email = "test@test.com",
                    Name = "Test User Auth0"
                }
            };

var userGroups = users.GroupBy(m => m.Email)

enter image description here

I am expecting to see three groups:

  • Group #1 - Login1@sharepoint.com - 1 SharePointProfile object
  • Group #2 - Login2@sharepoint.com - 1 SharePointProfile object and 1 Auth0Profile object
  • Group #3 - test@test.com - 1 Auth0Profile object

What am I doing wrong here?

public abstract class Profile : IEquatable<Profile>
    {
        public string Name { get; set; }
        public string Email { get; set; }

        public bool Equals(Profile other)
        {
            return this.Email == other.Email;
        }
    }

    public class Auth0Profile : Profile
    {
        public new string Email { get; set; }
        public new string Name { get; set; }
        public string Connection { get; set; }
        public string Password { get; set; }
        public bool VerifyEmail { get; set; }
    }

    public class SharePointProfile : Profile
    {
        public int Id { get; set; }
        public string LoginName { get; set; }
        public string Title { get; set; }
        public new string Email { get; set; }
        public bool IsSiteAdmin { get; set; }
    }
Adam Chubbuck
  • 1,612
  • 10
  • 27
  • 1
    Can you post a (shortened) version of your 3 classes (Profile, SharePointProfile, and Auth0Profile)? Is there any overriding / manipulating of the Email property from child classes? – gunr2171 Sep 03 '19 at 19:20
  • @gunr2171 Thanks for commenting. I've added those so that you can review. – Adam Chubbuck Sep 03 '19 at 19:22
  • 4
    Humor me, comment out the Email properties from Auth0Profile and SharePointProfile (but not Profile) and try it again. – gunr2171 Sep 03 '19 at 19:24
  • @gunr2171 That solved it. Thank you! – Adam Chubbuck Sep 03 '19 at 19:25
  • 1
    Yeah, so it was the fact that you're using `new` in your properties. I don't have a good explanation for that, so that's where someone can jump on in here and make a better answer post. Bottom line - don't use `new` in properties (or inheritance) if you don't have a good reason. – gunr2171 Sep 03 '19 at 19:26
  • 2
    When you add `new`, it creates a new property (also named `Email`) that is completely unrelated to the base class' `Email` property. By getting rid of the `new` version, all of your classes now have the same `Email` property, and so their instances can be properly grouped together. If you had left in the `new`, your subclasses would have had two, unrelated `Email` properties, one accessible via `base.Email`, the other via `this.Email`. You could set them differently – Flydog57 Sep 03 '19 at 19:30

3 Answers3

4

The problem is that you have objects of type Profile in your list. But in your definition of the objects you create new properties called email on each of those objects. The linq looks at a property that is not being used.

Hogan
  • 69,564
  • 10
  • 76
  • 117
3

In your code you have several problems with inheritance. Update your classes to:

   public abstract class Profile : IEquatable<Profile>
  {
    public string Name { get; set; }
    public string Email { get; set; }

    public bool Equals(Profile other)
   {
       return this.Email == other.Email;
    }
 }

public class Auth0Profile : Profile
{

   public string Connection { get; set; }
   public string Password { get; set; }
   public bool VerifyEmail { get; set; }
}

public class SharePointProfile : Profile
{
   public int Id { get; set; }
   public string Title { get; set; }
   public bool IsSiteAdmin { get; set; }
}

and grouping will work

grouping result

Yuri
  • 2,820
  • 4
  • 28
  • 40
1

The other answers leaned more on what to do to fix the problem but they were a bit skinny on detail as to why it was a problem in the first place

Consider this simple set of classes:

class Foo:IEquatable<Foo>{
  public string A="foo";
  public bool Equals(Foo other){
    return this.A == other.A;
  }
}
class Bar:Foo{
  public new string A="bar";
}
class Baz:Foo{
  public new string A="baz";
}

Both Bar and Baz can be boxed as Foo:

Foo br = new Bar();
Foo bz = new Baz();

And this is also what happens when you put them in a List typed as Profiles/Foos

Anyways, even though our Bar and Baz have their own values for A, when they're boxed as a Foo they will both use Foo's A. They have their own values for A because A is declared as new in the subclass. You can then choose which of the two A they have depending on the type they're boxed as:

Foo b = new Bar(); //print(b.A) would show "foo"
Bar b = new Bar(); //print(b.A) would show "bar"

If they're cast/boxed as Bar/Baz then they use the A in Bar. If they're cast/boxed as Foo then they use the A in Foo

This also affects your Equals comparison:

Bar br = new Bar(); 
Baz bz = new Baz();
br.Equals(bz); //true, the comparison is done inside Foo, using objects that are boxed as Foo
               //this means that it is "foo" == "foo" that is compared (true) 
               //rather than "bar" == "baz" (false) 

Your grouping ended up with a single group of 4 entities because they all used the A in Foo and ended up grouped under "foo" (or, in your case the null Profile.Email version and thus ended up grouped under null)

By removing the new keyword from Bar and Baz you'd get the compiler complaining that A had been declared in the parent and the child. It's a warning, not an error, because the A in the child hides A in the parent just like if you use the new keyword - the only thing new really does in this context is effectively state "I know what I'm doing, stop warning me that Bar.A hides Foo.A".

Instead of adding new to stop the warning you should remove the definition of A from Bar and Baz so they would use the single existence of A declared in the parent Foo. This way both Bar and Baz would have an A property thanks to their parent having that property, rather than having their own A. Because Bar.A and Baz.A would actually only be the A in Foo it means there isn't a possibility for Bar/Baz to have a value of A that differs from Foo.A - so when Bar.A is set to "bar" and Baz.A is set to "baz" the grouping will see the two different values when considering a list of Foos, and you'll end up with things grouped under both "bar" and "baz"

Ultimately new kinda violates the way that inheritance is supposed to work; common properties should exist on the parent class, abstract Vehicle should have NumberOfWheels, and the Car subclass should set it to 4, rather than re declare it's own NumberOfWheels. The valid use cases for new are very narrow; as a rule of thumb you'll probably never need to use it - if you think you do, there's more likely a problem elsewhere. See Overriding vs method hiding for some more discussion on why you might use it. I've never needed to in my entire career, and I'm quite old

Caius Jard
  • 72,509
  • 5
  • 49
  • 80