5

I've an array of objects DataTestplans from which I try to retrieve records for a particular DataID and ProductID using the LINQ query shown, my current query has Distinct() which distiguishes on all 5 mentioned properties,how do I retrieve distinct records based on properties DataID,TestPlanName,TCIndexList and ProductID?

DataTestplans:-

[
    {
    "DataTestPlanID": 0,
    "DataID": 19148,
    "TestPlanName": "string",
    "TCIndexList": "string",
    "ProductID": 2033915
  },
    {
    "DataTestPlanID": 0,
    "DataID": 19148,
    "TestPlanName": "string",
    "TCIndexList": "string",
    "ProductID": 2033915
  },
      {
    "DataTestPlanID": 0,
    "DataID": 19149,
    "TestPlanName": "string",
    "TCIndexList": "string",
    "ProductID": -2642
  }

]

LINQ

            DataTestPlans_DataID_ProductID = DataTestPlans.Where(c => c.DataID == DataID_ProductID_Record.DataID && c.ProductID == DataID_ProductID_Record.ProductID).Distinct();
user3508811
  • 847
  • 4
  • 19
  • 43
  • 1
    You can write a class that implements `IComparer` and pass an instance of that to `Distinct()`. – itsme86 Dec 22 '16 at 23:00
  • 1
    Possible duplicate of [How to use LINQ Distinct() with multiple fields](http://stackoverflow.com/questions/10719928/how-to-use-linq-distinct-with-multiple-fields) – Klinger Dec 22 '16 at 23:00
  • Klinger - what you pointed doesnt have an accepted answer – user3508811 Dec 22 '16 at 23:09

5 Answers5

4

You could do like this..

DataTestPlans.Where(c => c.DataID == YourInput && c.ProductID == YourInput)
             .GroupBy(x => new {x.DataID,x.TestPlanName,x.TCIndexList,x.ProductID})
             .Select(x => x.First());
Sateesh Pagolu
  • 9,282
  • 2
  • 30
  • 48
  • 1
    Yes, basically that. Note that in this example, the value of the field that wasn't grouped on is essentially random. If you care about that value, you'll have to find some way of aggregating it from the group. Also, `FirstOrDefault()` shouldn't be necessary, since each group will have at least one member or it won't be a group. (So it could just be `First()`.) – Chris Berger Dec 22 '16 at 23:12
  • I like your solution, but would consider replacing `.Select(x => x.FirstOrDefault());` with `.SelectMany(x => x.Take(1))`. It's a clearer approach and prevents spurious default values should you do a filter after the `GroupBy`. – Enigmativity Dec 22 '16 at 23:25
  • @ChrisBerger - There is an issue with using `.First()` after a `.GroupBy(...)` if you refactor and put a `.Select(x => x.Where(...))` between them as this would produce wrong values. – Enigmativity Dec 22 '16 at 23:26
  • can you explain the difference between `FirstOrDefault()` and `First()`.Also between `SelectMany(x => x.Take(1))` and `.Select(x => x.First());` in layman terms...what is the advantage of on over the other – user3508811 Dec 22 '16 at 23:28
  • @user3508811 : FirstOrDefault() returns default value of data type in the context if the sequence is emtpy. For example, `FirstOrDefault()` on empty list of int returns `0`. `First()` fails with an exception if sequence is emtpy. – Sateesh Pagolu Dec 22 '16 at 23:42
  • got it,any reason why you didnt take `x.Take(1)` part of suggestion from Enigmativity – user3508811 Dec 22 '16 at 23:43
  • @user3508811 : Select many flattens the list. For example,` List>` will be converted to `List` – Sateesh Pagolu Dec 22 '16 at 23:44
  • @user3508811: while he got a point there, but it is irrelevant in your case. Both have same impact in your case. So, i just ignore dit. – Sateesh Pagolu Dec 22 '16 at 23:47
  • `.SelectMany(x => x.First());` is throwing an error `The type arguments for method 'Enumerable.SelectMany,Func>) cannot be infterred from usage,Try specifying the type arguments explicity` – user3508811 Dec 22 '16 at 23:49
  • Not ideal - a GroupBy forces enumeration of the whole source before continuing, which is not scalable for large data sets. The answer @Neolisk gives is better recommended because it preserves lazy enumeration. Additionally, using GroupBy only to discard all but the first member of a group means wasted computation, while a custom extension method can (and does) evaluate only what is needed. – Oly Dec 22 '16 at 23:57
  • @user3508811 : SelectMany is not needed. check the updated answer. – Sateesh Pagolu Dec 22 '16 at 23:59
1

There are two ways to do, both highlighted in this question, no need for IComparer. Here is a quick example you can play with (I did not use your actual object, because it's easier to explain this way):

class Program
{
    static void Main(string[] args)
    {
        var persons = Setup();

        //option 1, can stream, option suggested by Jon Skeet
        //https://stackoverflow.com/a/1300116/897326
        var result1 = persons.
            DistinctBy(m => new {m.FirstName, m.LastName});

        //option 2, cannot stream, but does reference to DistinctBy
        //https://stackoverflow.com/a/4158364/897326
        var result2 = persons.
            GroupBy(m => new { m.FirstName, m.LastName }).
            Select(group => group.First());
    }

    class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public string Address { get; set; }
    }

    private static List<Person> Setup()
    {
        var p1 = new Person
        {
            FirstName = "John",
            LastName = "Doe",
            Address = "USA"
        };

        var p2 = new Person
        {
            FirstName = "John",
            LastName = "Doe",
            Address = "Canada"
        };

        var p3 = new Person
        {
            FirstName = "Jane",
            LastName = "Doe",
            Address = "Australia"
        };

        var persons = new List<Person>();
        persons.Add(p1);
        persons.Add(p2);
        persons.Add(p3);

        return persons;
    }
}

public static class LinqExtensions
{
    public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
    {
        HashSet<TKey> knownKeys = new HashSet<TKey>();
        foreach (TSource element in source)
        {
            if (knownKeys.Add(keySelector(element)))
            {
                yield return element;
            }
        }
    }
}
Community
  • 1
  • 1
Victor Zakharov
  • 25,801
  • 18
  • 85
  • 151
  • 1
    Darkknight answers works and is much simpler which is what I was looking for – user3508811 Dec 22 '16 at 23:24
  • @user3508811: Darkknight provided option #2. It does not require any external dependencies but is a bit cryptic. It's not obvious that it's doing Distinct for a casual programmer who doesn't know LINQ. There is also option #1 suggested by Jon Skeet, which supports streaming. Depending on how large you data is, this may be useful. The syntax is cleaner also. – Victor Zakharov Dec 23 '16 at 00:01
0

You would have to use GroupBy(), which will create an IEnumerable<IGrouping<TKey, TElement>> which you can iterate over. You can access records either with group.First() or by some sort of aggregate function over the group.

Chris Berger
  • 557
  • 4
  • 14
0

You can use multiple chained wheres . Make sure you it is IQueryable if you are making db call. An example is below

List<SomeClass> c = new List<SomeClass>();
 var result = c.Where(x => x.ID == 4).Distinct().Where(y => y.Name == "foo").Distinct();
Dan Hunex
  • 5,172
  • 2
  • 27
  • 38
0

You can implement IEqualityComparer and use that instead default in Distinct() method. If you example implement DataTestPlansComparer you can use like in following example:

DataTestPlans_DataID_ProductID = DataTestPlans.Where(c => c.DataID == DataID_ProductID_Record.DataID && c.ProductID == DataID_ProductID_Record.ProductID).Distinct(new DataTestPlansComparer());

Note, your custom comparer should be passed as parameter to Distinct() method.

In your case can be:

        public class DataTestPlanComparer : IEqualityComparer<DataTestPlan>
        {
            public bool Equals(DataTestPlan x, DataTestPlan y)
            {

                if (Object.ReferenceEquals(x, y)) return true;

                if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
                    return false;

                return x.DataID == y.DataID && x.ProductID == y.ProductID;
            }


            public int GetHashCode(DataTestPlan dataTestPlan)
            {
                if (Object.ReferenceEquals(dataTestPlan, null)) return 0;

                int hashDataTestPlanDataID = dataTestPlan.DataID == null ? 0 : dataTestPlan.DataID.GetHashCode();

                int hashDataTestPlanProductID = dataTestPlan.ProductID.GetHashCode();

                return hashDataTestPlanDataID ^ hashDataTestPlanProductID;
            }
        }

Please follow MSDN guide to implement IEqualityComparer.

kat1330
  • 5,134
  • 7
  • 38
  • 61