Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using LINQ. With two different lists. How can I identify objects that do not match

Tags:

c#

linq

I have three classes:

public partial class Objective{
    public Objective() {
        this.ObjectiveDetails = new List<ObjectiveDetail>();
    }
    public int ObjectiveId { get; set; }
    public int Number { get; set; }
    public virtual ICollection<ObjectiveDetail> ObjectiveDetails { get; set; }
}
public partial class ObjectiveDetail {
    public ObjectiveDetail() {
        this.SubTopics = new List<SubTopic>();
    }
    public int ObjectiveDetailId { get; set; }
    public int Number { get; set; }
    public string Text { get; set; }
    public virtual ICollection<SubTopic> SubTopics { get; set; }
}
public partial class SubTopic {
    public int SubTopicId { get; set; }
    public string Name { get; set; }
}

I have two lists:

IList<ObjectiveDetail> oldObj;
IList<ObjectiveDetail> newObj;

The following LINQ gives me a new list of ObjectiveDetail objects where: the Number or the Text fields for any ObjectiveDetail object in the list differ between oldObj and newObj.

IList<ObjectiveDetail> upd = newObj
    .Where(wb => oldObj
        .Any(db => (db.ObjectiveDetailId == wb.ObjectiveDetailId) && 
                   (db.Number != wb.Number || !db.Text.Equals(wb.Text))))
     .ToList();

How can I modify this so the LINQ gives me a new list of ObjectiveDetail objects where: the Number or the Text fields or the SubTopic collections for any ObjectiveDetail object in the list differ between oldObj and newObj.

In other words I want an ObjectiveDetail to be added to the upd list if:

  • It has Text in oldObj that's different from Text in newObj
  • It has a Number in oldObj that's different from the Number in newObj
  • It has a SubTopics collection with three elements in oldObj and 4 elements in newObj
  • It has a SubTopics collection with no elements in oldObj and 2 elements in newObj
  • It has a SubTopics collection with 2 elements in oldObj and no elements in newObj
  • It has a SubTopics collection with elements with SubTopicId of 1 and 2 in oldObj and 1 and 3 in newObj

I hope someone can come up with just some additional line in the LINQ statement that I already have.

like image 284
Samantha J T Star Avatar asked Jan 22 '14 04:01

Samantha J T Star


People also ask

How can I filter one list from another in C#?

C# filter list with iteration. In the first example, we use a foreach loop to filter a list. var words = new List<string> { "sky", "rock", "forest", "new", "falcon", "jewelry" }; var filtered = new List<string>(); foreach (var word in words) { if (word. Length == 3) { filtered.

What is query expression in LINQ?

LINQ query expressions can be used to conveniently extract and process data from arrays, enumerable classes, XML documents, relational databases, and third-party data sources. Query expressions can be used to query and to transform data from any LINQ-enabled data source. Query expressions have deferred execution.


2 Answers

Instead of creating a huge and hard maintanable LINQ query that will try to find differences, I would create a list of the same objects within both list (intersection) and as a result, take sum of both collection except this intersection. To compare objects you can use IEqualityComparer<> implementation. Here is a draft:

public class ObjectiveDetailEqualityComparer : IEqualityComparer<ObjectiveDetail>
{
    public bool Equals(ObjectiveDetail x, ObjectiveDetail y)
    {
        // implemenation                          
    }

    public int GetHashCode(ObjectiveDetail obj)
    {
        // implementation
    }
}

and then simply:

var comparer = new ObjectiveDetailEqualityComparer();
var common = oldObj.Intersect(newObj, comparer);
var differs = oldObj.Concat(newObj).Except(common, comparer);

This will be much easier to maintain when classes change (new properties etc.).

like image 97
Konrad Kokosa Avatar answered Oct 02 '22 15:10

Konrad Kokosa


This should be what you need:

IList<ObjectiveDetail> upd = newObj.Where(wb =>
            oldObj.Any(db =>
                (db.ObjectiveDetailId == wb.ObjectiveDetailId) &&
                    (db.Number != wb.Number || !db.Text.Equals(wb.Text)
                    || db.SubTopics.Count != wb.SubTopics.Count
                    || !db.SubTopics.All(ds => wb.SubTopics.Any(ws =>
                                     ws.SubTopicId == ds.SubTopicId))
                    ))).ToList();

How It Works

db.SubTopics.Count != wb.SubTopics.Count confirms that the new object being compared (wb) and the old object being compared (db) have the same number of SubTopics. That part is pretty straightforward.

!db.SubTopics.All(ds => wb.SubTopics.Any(ws => ws.SubTopicId == ds.SubTopicId)) is a bit more complicated. The All() method returns true if the given expression is true for all members of the set. The Any() method returns true if the given expression is true for any member of the set. Therefore the entire expression checks that for every SubTopic ds in the old object db there is a Subtopic ws with the same ID in the new object wb.

Basically, the second line ensures that every SubTopic present in the old object is also present in the new object. The first line ensures that the old & new objects have the same number of SubTopics; otherwise the second line would consider an old object with SubTopics 1 & 2 the same as a new object with SubTopics 1, 2, & 3.


Caveats

This addition will not check whether the SubTopics have the same Name; if you need to check that as well, change the ws.SubTopicId == ds.SubTopicId in the second line to ws.SubTopicId == ds.SubTopicId && ws.Name.Equals(ds.Name).

This addition will not work properly if an ObjectiveDetail can contain more than one SubTopic with the same SubTopicId (that is, if SubTopicIds are not unique). If that's the case, you need to replace the second line with !db.SubTopics.All(ds => db.SubTopics.Count(ds2 => ds2.SubTopicId == ds.SubTopicId) == wb.SubTopics.Count(ws => ws.SubTopicId == ds.SubTopicId)). That will check that each SubTopicId appears exactly as many times in the new object as it does in the old object.

This addition will not check whether the SubTopics in the new object & the old object are in the same order. For that you would need to replace the 2nd line with db.SubTopics.Where((ds, i) => ds.SubTopicId == wb.SubTopics[i].SubTopicId).Count != db.SubTopics.Count. Note that this version also handles non-unique SubTopicId values. It confirms that the number of SubTopics in the old object such that the SubTopic in the same position in the new object is the same equals the total number of SubTopics in the old object (that is, that for every SubTopic in the old object, the SubTopic in the same position in the new object is the same).


High Level Thoughts

Konrad Kokosa's answer is better from a maintainability perspective (I've already upvoted it). I would only use a big ugly LINQ statement like this if you don't expect to need to revisit the statement very often. If you think the way you decide whether two ObjectiveDetail objects are equal might change, or the method that uses this statement might need to be reworked, or the method is critical enough that someone new to the code looking at it for the first time needs to be able to understand it quickly, then don't use a big long blob of LINQ.

like image 30
Oblivious Sage Avatar answered Oct 02 '22 13:10

Oblivious Sage