简体   繁体   中英

Linq to get list of custom objects where a certain value contained within dictionary property equals a specific value

I am struggling to solve this issue and have searched multiple ways and cannot seem to find an answer. I inherited this app from someone else and need to add a couple features to the app. I have not worked much with dictionaries and linq before, so I have been searching and trying to gain knowledge to do what I need to do.

There is a class with the following properties(removed some properties not necessary for this discussion):

class EmailRecord
{
    public Dictionary<string, List<string>> emails = new Dictionary<string, List<string>>();
    public string RecordID { get; set; }

[followed by additional properties and constructors...]

When the objects are created, the emails Property would have a template string in the key, and a list of strings containing email addresses in the values. For my purposes, I do not need to know what is in the key.

I have a list of EmailRecord objects called allRecords. I need to query allRecords to get a list of all EmailRecord objects where the emails dictionary property's list of values contains a specific email address I have stored in a variable called recipientEmail. The key doesn't matter, and it doesn't matter how many times the email shows up. I just need the instance of the object included in the results if the email shows up anywhere in the values of the emails property. In an instance of EmailRecord, the emails dictionary property may have two keys and within each of those keys, multiple emails in a list of strings for the value. I don't need to limit to a specific key, I just need to know if an email exists anywhere within the list of email strings anywhere in that dictionary.

I've tried a few things, with the latest being this (which doesn't work):

var results = EmailRecords
    .SelectMany(x => x.emails)
    .Where(x => x.Value.Contains(recipientEmail));

The above just seems to be returning the dictionary property, not the entire object.

I want to be able to loop through the results with something like this:

foreach (EmailRecord foundRecord in results) {
    ...do work here
}

Any thoughts or suggestions to assist me as I am trying to learn Linq? Thank you in advance for any help you can provide.

If you want to loop through EmailRecord objects which one of its emails property values contains recipientEmail , then you need to have a list of EmailRecord first. Then search throught them. following should do the trick.

List<EmailRecord> EmailRecords = new List<EmailRecord>();

//Fill the EmailRecords somewhere

foreach (KeyValuePair<string, List<string>> emailfoundRecord in 
                  EmailRecords.emails.Where(x => x.Value.Contains(recipientEmail)))
 {
   //do work here       
 }

When you call EmailRecords.SelectMany(x => x.Emails) what you get back is an IEnumerable<KeyValuePair<string, List<string>>> or similar. Obviously this is not what you're after for your result since it strips away all that other information.

With LINQ the first thing to consider at each stage is what you are expecting to get out of the query. In this case the query results should be an enumeration of EmailRecord instances which is also what we're feeding in. Filtering that list is most simply done with the Where method, so that's where you should do all the work

Next decide on your filter criteria and write the filter predicate to suit. For any given EmailRecord we want to find out if any of the dictionary entries contains a particular email address. Since the dictionary values are lists we'll use Contains to do the actual comparison, and Any to test the dictionary itself.

Which looks like this:

var filtered = EmailRecords.Where(e => 
    e.Emails.Any(kv => 
        kv.Value.Contains(recipientEmail)
    )
);

This works because a dictionary is also an enumerable, with each entry in the enumeration being a key/value pair.

Using Any will stop when it finds a single matching entry instead of continuing to the end of the Emails dictionary for every EmailRecord instance. If there are a lot of emails and you're expecting a high number of selections then this might save some time. Probably not however, since generally this sort of structure doesn't have a lot of duplicate email addresses in it.

Depending on how often you want to do this however it might be quicker to build a lookup and query that. Assuming that your EmailRecords list changes infrequently and you are doing a lot of this sort of lookup, you could get a large speedup.

I'll use a Dictionary<string, EmailRecord[]> for the lookup because it's (fairly) simple to build once we get a list of all of the pairs of email address and EmailRecord objects:

var emailReferences = EmailRecords.SelectMany(e => 
    e.Emails.SelectMany(kv => 
        kv.Value.Select(v => 
            new { address = v, record = e }
        )
    )
);

var lookup = 
    emailReferences
    .GroupBy(i => i.address, i => i.record)
    .ToDictionary(g => g.Key, g => g.ToArray());
;

From this you will be able to locate an email address and get its referencing EmailRecord instances fairly simply and quickly:

EmailRecord[] filtered = null;
lookup.TryGetValue(recipientEmail, out filtered);

This will be faster per lookup than the LINQ equivalent above, but the setup could consume a fair amount of time and memory for large lists. If you have small or frequently changing lists (since the lookup has to be regenerated or at least invalidated at each change) then this won't improve your program's speed.


As a completely unsolicited aside, here's an extension method I use when dealing with dictionaries that have List<> as the value:

public static partial class extensions
{
    public static Dictionary<TKey, List<TElem>> Add<TKey, TElem>(this Dictionary<TKey, List<TElem>> dict, TKey key, TElem value)
    {
        List<TElem> list;
        if (dict.ContainsKey(key))
            list = dict[key];
        else
            dict[key] = list = new List<TElem>();

        list.Add(value);
        return dict;
    }
}

It helps make your adds simpler and easier to read.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM