1

I have looked at a lot of articles on this and either the solutions presented did not work for me or perhaps I am just not understanding them clearly enough. Anyway, at risk of being duplicitous I present my problem for your review.

I have a contact database that I want to search ...

Each contact is associated with a single person (ignore the institution relationship for now) Each contact is also associated with multiple email and phone records in other tables (each email and phone are exclusive to the contact record).

enter image description here

I would like to be able to return the contact that satisfies a query against the email records and/or the phone records. So a contact may have multiple email addresses. If my query "hits" on ANY of the emails I want the SINGLE contact returned with ALL the emails (and phones) associated with the contact included in the results.

So I search on the email domain "hollywood". There is one contact with an email domain that equals "hollywood". This contact also has has two other email addresses associated to it. I want to return a SINGLE record containing all these records. (I'm a Morgan Freeman fan so I made up the following data - it's FAKE!)

enter image description here

Of course running this kind of query in SQL Server Management Studio will return you 3 records. One for each email record associated with the contact. However, I am projecting the results into a single object in which the multiple emails (and phones) will be placed so I don't have to deal with multiple objects for the same contact.

The query is constructed in Linq. I added the "group" line thinking that would eliminate the duplicates ...

var query = from rContact in qryContacts
            group rContact by rContact.contact_id into grpContact

            join rPerson in ctx.people
                on grpContact.FirstOrDefault().person_id equals rPerson.person_id

            join rEmail in ctx.emails
                on grpContact.FirstOrDefault().contact_id equals rEmail.contact_id into emailGroup
                from subEmails in emailGroup.DefaultIfEmpty()

            select new PersonalContact
            {
                ID          = grpContact.FirstOrDefault().contact_id,
                Label       = grpContact.FirstOrDefault().label,
                Notes       = grpContact.FirstOrDefault().notes,
                Prefix      = rPerson.prefix,
                FirstName   = rPerson.first_name,
                MiddleName  = rPerson.middle_name,
                LastName    = rPerson.last_name,
                Suffix      = rPerson.suffix,
                AKA         = rPerson.aka,
                DOB         = rPerson.dob,
                IsFemale    = rPerson.is_female,
                Emails      = emailGroup.Select(e=> new Email
                    {
                        ID              = e.email_id,
                        Label           = e.label,
                        Notes           = e.notes,
                        Preferred       = e.is_preferred,
                        Category        = e.email_category,
                        LocalPart       = e.email_local_part,
                        Domain          = e.email_domain,
                        TopLevelDomain  = e.email_top_level_domain,
                    }),
            }

This does OK ... it returns the PersonalContact object, populated as expected ...only it returns 3 of them! I thought that grouping on the Contact would eliminate the duplicates but ... nope.

Here's what the result looks like (I only pasted one but I get back three) ...

{
    "FirstName": "Morgan",
    "LastName": "Freeman",
    "DOB": "1937-06-01T00:00:00",
    "Prefix": "Mr",
    "IsFemale": false,
    "ID": 18,
    "Label": "Personal Contact",
    "Emails": [{
        "ID": 12,
        "LocalPart": "mfreeman",
        "Domain": "hollywood",
        "TopLevelDomain": "com",
        "Category": "Personal",
        "Preferred": true,
        "Display": "mfreeman@hollywood.com"
    },
    {
        "ID": 13,
        "LocalPart": "morgan.freeman",
        "Domain": "studiocity",
        "TopLevelDomain": "net",
        "Category": "Personal",
        "Preferred": false,
        "Display": "morgan.freeman@studiocity.net"
    },
    {
        "ID": 14,
        "LocalPart": "bob.smith",
        "Domain": "gmail",
        "TopLevelDomain": "com",
        "Category": "Private",
        "Preferred": false,
        "Display": "bob.smith@gmail.com"
    }],
}

The problem only compounds as I add multiple phones to the contact as well.

Any help would be appreciated!

UPDATE

I thought it was worthwhile to respond to response to NetMage's question "What is qryContacts? Where did you filter on an email address?"

I came up with the following pattern, which allows me to define a LINQ query dynamically ... sort of. I create a variable which holds a LIST of expressions. This allows me to add whatever criteria I want at runtime.

var contactFilter = new List<Expression<Func<contact, bool>>>();
    contactFilter.Add(e => e.emails.Where(x=>x.email_domain == "gmail").FirstOrDefault() != null);

I then get an IQueryable which represents the entire table.

var qryContacts = ctx.contacts;

Now I add the filter contents to the query.

if (contactFilter != null)
{
    foreach (var item in contactFilter)
    {
        qryContacts = qryContacts.Where(item);
    }
}

Then the query is constructed on top of that as shown in the original post. Hope that's helpful. It has some limitations, but It's been a very productive pattern for me, easing the task of writing queries where you don't know exactly what fields will be defined. My understanding of the underpinnings of Linq and Expressions is very limited so I've found this technique much easier to implement than some of the "Builders" out there. What do you think about this technique? Would love to hear some feedback.

Gary O. Stenstrom
  • 2,284
  • 9
  • 38
  • 59
  • 1
    The group by effectively does nothing - you are grouping contacts by `contact_id` and there should be only one entry for each `contact_id` already, unless you already received multiple rows somehow. What is `qryContacts`? Where did you filter on an email address? – NetMage Jan 23 '20 at 20:22
  • Updated the OP with a response. Thanks for your feedback!! – Gary O. Stenstrom Jan 23 '20 at 21:03
  • Quick note: instead of `FirstOrDefault() != null` use `Any()`. You can also fold the predicate into the `Any`: `e => e.emails.Any(x => x.email_domain == "gmail")`. – NetMage Jan 23 '20 at 22:27
  • Based on your `qryContacts` construction, the `GroupBy` is doing nothing for you. You are getting a filtered list of `ctx.contacts`, which will start with only one record per `contact_id`. – NetMage Jan 23 '20 at 22:31

1 Answers1

0

The solution to the problem (for me) was the removal of the from subEmails in emailGroup.DefaultIfEmpty() line ... I have to be honest, I am not entirely sure why that made a difference but everything works as expected now.

var query = from rContact in qryContacts
    group rContact by rContact.contact_id into grpContact

    join rPerson in ctx.people
        on grpContact.FirstOrDefault().person_id equals rPerson.person_id

    join rEmail in ctx.emails
        on grpContact.FirstOrDefault().contact_id equals rEmail.contact_id into emailGroup
        // from subEmails in emailGroup.DefaultIfEmpty() <!-- REMOVE THIS LINE

    select new PersonalContact
    {
        ID          = grpContact.FirstOrDefault().contact_id,
        Label       = grpContact.FirstOrDefault().label,
        Notes       = grpContact.FirstOrDefault().notes,
        Prefix      = rPerson.prefix,
        FirstName   = rPerson.first_name,
        MiddleName  = rPerson.middle_name,
        LastName    = rPerson.last_name,
        Suffix      = rPerson.suffix,
        AKA         = rPerson.aka,
        DOB         = rPerson.dob,
        IsFemale    = rPerson.is_female,
        Emails      = emailGroup.Select(e=> new Email
            {
                 ID              = e.email_id,
                 Label           = e.label,
                 Notes           = e.notes,
                 Preferred       = e.is_preferred,
                 Category        = e.email_category,
                 LocalPart       = e.email_local_part,
                 Domain          = e.email_domain,
                 TopLevelDomain  = e.email_top_level_domain,
            }),
    }

One person voted this OP up so hopefully they mange to find something useful in this. Best of luck!

Gary O. Stenstrom
  • 2,284
  • 9
  • 38
  • 59
  • 1
    The `from`...`DefaultIfEmpty()` is to convert a join into a left join. Perhaps my [SQL to LINQ Recipe](https://stackoverflow.com/questions/49245160/sql-to-linq-with-multiple-join-count-and-left-join/49245786#49245786) might help you. – NetMage Jan 23 '20 at 20:25