简体   繁体   中英

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

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.

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.).

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.

Normally I would go with @Konrad Kokosa way. But it looks like you need a fast solution.

I tried it with some data. It gives the expected result. I am sure that you can modify the code for desired results.

var updatedObjects = oldObj.Join(newObj,
    x => x.ObjectiveDetailId,
    y => y.ObjectiveDetailId,
    (x, y) => new
    {
        UpdatedObject = y,
        IsUpdated = !x.Text.Equals(y.Text) || x.Number != y.Number //put here some more conditions
    })
    .Where(x => x.IsUpdated)
    .Select(x => x.UpdatedObject);

Problems

Your LINQ query was not that bad but some proplems needed to be solved:

  • Using .Any() in a .Where() means that the query is much slower than needed . This is because for every item in objNew , you iterate the items of objOld .
  • !db.Text.Equals(wb.Text) throws an exception when db.Text is null .
  • Your code doesn't detect new items added to objNew that doesn't exists in objOld . I don't know if that is a problem because you didn't told us if that is possible.

Solution

If you compare collections, it would be a good idea to override the Equals() and GetHashcode() methods:

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 override bool Equals(Object obj)
    {
        var typedObj = obj as ObjectiveDetail;
        return Equals(typedObj);
    }

    public bool Equals(ObjectiveDetail obj)
    {
        if ((object)obj == null) return false;

        return ObjectiveDetailId == obj.ObjectiveDetailId &&
               Number == obj.Number &&
               Text == obj.Text &&
               SubTopics != null && obj.SubTopics != null && // Just in the unlikely case the list is set to null
               SubTopics.Count == obj.SubTopics.Count;
    }

    public override int GetHashCode()
    {
        return new { A = ObjectiveDetailId, B = Number, C = Text }.GetHashCode();
    }
}

Then it is easy:

var dictionary = oldObj.ToDictionary(o => o.ObjectiveDetailId);

IList<ObjectiveDetail> upd = newObj
    .Where(n => !EqualsOld(n, dictionary))
    .ToList();

using this method:

private bool EqualsOld(ObjectiveDetail newItem, Dictionary<int, ObjectiveDetail> dictionary)
{
    ObjectiveDetail oldItem;
    var found = dictionary.TryGetValue(newItem.ObjectiveDetailId, out oldItem);
    if (!found) return false; // This item was added to the new list
    return oldItem.Equals(newItem);
}

If I get it right, you want to make a deep comparison between two .NET objects, regardless of LINQ. Why don't you use something like comparenetobjects ?

Trying to implement a deep comparison through LINQ would probably be slower and more complex than making the comparison in the memory. Even if you chose to do it in LINQ realm, you would finally retrieve the whole object and perhaps you would do it with more than one queries, adding performance overhead. Therefore, I would suggest to eagerly load your data object from database and make the deep comparison without a specific linq query.

Hope I helped!

Find the entities that are not updated then exclude:

IEnumerable<ObjectiveDetail> newOds = ...;
IEnumerable<ObjectiveDetail> oldOds = ...;

// build collection of exclusions
// start with ObjectiveDetail entities that have the same properties
var propertiesMatched = oldOds.Join( newOds,
    o => new { o.ObjectiveDetailId, o.Number, o.Text },
    n => new { n.ObjectiveDetailId, n.Number, n.Text },
    ( o, n ) => new { Old = o, New = n } );

// take entities that matched properties and test for same collection
//  of SubTopic entities
var subTopicsMatched = propertiesMatched.Where( g =>
    // first check SubTopic count
    g.Old.SubTopics.Count == g.New.SubTopics.Count &&
    // match
    g.New.SubTopics.Select( nst => nst.SubTopicId )
        .Intersect( g.Old.SubTopics.Select( ost => ost.SubTopicId ) )
        .Count() == g.Old.SubTopics.Count )
    // select new ObjectiveDetail entities
    .Select( g => g.New );

// updated ObjectiveDetail entities are those not found
// in subTopicsMatched
var upd = newOds.Except( subTopicsMatched );

This would work w/ EF and run completely server-side if newOds and oldOds are IQueryable<ObjectiveDetail> s from a DbContext

I have tried what you wanted but it is not too "neat" and it was not possible for me to make "one-liner-linq-expression" type code. Check it out and see if it is acceptable to you.

Also you need to check the performance but as you said there are not many objects so performance might not be of concern.

Also I have not tested it properly so if you wish to accept it then please do testing.

        var oldObj = _objectiveDetailService.GetObjectiveDetails(id);
        var newObj = objective.ObjectiveDetails.ToList();

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

        newObj.ForEach(wb =>
        {
            var comOld = oldObj.Where(db => wb.ObjectiveDetailId == db.ObjectiveDetailId &&
                           db.Number == wb.Number && db.Text.Equals(wb.Text)).FirstOrDefault();
            if (comOld != null && wb.SubTopics.Any(wb2 => comOld.SubTopics.Where(oldST => wb2.SubTopicId == oldST.SubTopicId).Any(a => !a.Name.Equals(wb2.Name))))
            {
                upd.Add(wb);
            }
        });

You can write similar code to add and delete as well.

Hope this helps.

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


||!oldObj.Any(o=>o.DetailId == wb.DetailId) //check if it's there or a new one
       //check count
    || ((wb.SubTopics.Count!= oldObj.FirstOrDefault(o=>o.DetailId == wb.DetailId).SubTopics.Count 
     || //check Ids match, or you can add more properties with OR
       wb.SubTopics.Any(wbs=>oldObj.FirstOrDefault(o=>o.DetailId == wb.DetailId)
      .SubTopics.Any(obs=>obs.SubTopicId !=wbs.SubTopicId))))
      ).ToList();

Have a look at below code. I created this function to compare two object then returns matched properties fields as an object.It may help full to you.

/// <summary>
    /// Compare two objects, returns destination object with matched properties, values. simply Reflection to automatically copy and compare properties of two object
    /// </summary>
    /// <param name="source"></param>
    /// <param name="destination"></param>
    /// <returns>destination</returns>
    public static object CompareNameAndSync(object source, object destination)
    {
        Type stype = source.GetType();
        Type dtype = destination.GetType();
        PropertyInfo[] spinfo = stype.GetProperties();
        PropertyInfo[] dpinfo = dtype.GetProperties();
        foreach (PropertyInfo des in dpinfo)
        {
            foreach (PropertyInfo sou in spinfo)
            {
                if (des.Name == sou.Name)
                {
                    des.SetValue(destination, sou.GetValue(source));
                }
            }
        }
        return destination;

    }

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