Related
I have list of data and user defined sort list and I need to sort my source list based on total numbers defined in the sort list object. It should be dynamic so that the user can freely create his own sort list preference. In my sample code I used Person class. The class may have more properties in the future that's why I want that my sort expression is dynamic too. I used PropertyName to convey a lookup for property. In my example below I have list of person and I have list of sort preference. In my first example I want to sort the person list by Name ascending, then by Age descending. Can someone help me have a LINQ extension? I saw an example in this post Dynamic Linq Order By Variable
The scenario in that post is quite similar to mine except this one is using fixed properties. What I want to achieve is dynamic like the following.
Sort expression is dynamic that is I need to look up for property name that has matching in my sort expression. If any found sort based on sort direction.
Sort execution should be based on how many sort items are defined in the sort list. For example loop through the sort list and do OrderBy (if ascending), OrderByDescending (if descending), ThenBy, ThenBy so on and so fort. For example I have 2 sort order then the source list should be ordered by then by. If I have 5 then the list should sorted in 1 "OrderBy (desc or asc)" and 4 "ThenBy (desc or asc)". The chain should not be broken that for example given 4 sort order and all are ascending it will become persons.OrderBy(prop1).ThenBy(prop2).ThenBy(prop3).ThenBy(prop4).
C# Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SortDemo
{
class Program
{
static void Main(string[] args)
{
var persons = new List<Person>();
persons.Add(new Person { Name="George", Age=25, Group="A", State="LA"});
persons.Add(new Person { Name = "Anna", Age = 20, Group = "B", State = "CA" });
persons.Add(new Person { Name = "Xenna", Age = 30, Group = "A", State = "DC" });
persons.Add(new Person { Name = "Sam", Age = 40, Group = "C", State = "IL" });
persons.Add(new Person { Name = "Elise", Age = 21, Group = "B", State = "LA" });
persons.Add(new Person { Name = "Josh", Age = 29, Group = "C", State = "MI" });
persons.Add(new Person { Name = "Mike", Age = 34, Group = "A", State = "NY" });
persons.Add(new Person { Name = "Bernard", Age = 27, Group = "C", State = "WY" });
var sorts = new List<Sort>();
sorts.Add(new Sort { PropertyName = "Age", SortOrder = 2, Direction = "Descending" });
sorts.Add(new Sort { PropertyName="Name", SortOrder=1, Direction="Ascending"});
//sort by two properties
foreach(var sort in sorts.OrderBy(x=>x.SortOrder))
{
//OrderBy if sort direction is Ascending
//OrderByDescending if sort direction is Descending
var sortedPersons = persons.OrderBy(x=>PropertyName==sort.PropertyName);
//expected results
//order persons by Name ascending
//then by Age Descending
}
//another example
var sorts1 = new List<Sort>();
sorts1.Add(new Sort { PropertyName = "Name", SortOrder = 4, Direction = "Descending" });
sorts1.Add(new Sort { PropertyName = "Age", SortOrder = 1, Direction = "Ascending" });
sorts1.Add(new Sort { PropertyName = "State", SortOrder = 3, Direction = "Ascending" });
sorts1.Add(new Sort { PropertyName = "Group", SortOrder = 2, Direction = "Ascending" });
//sort by four properties
foreach (var sort in sorts1.OrderBy(x => x.SortOrder))
{
//OrderBy if sort direction is Ascending
//OrderByDescending if sort direction is Descending
var sortedPersons1 = persons.OrderBy(x => PropertyName == sort.PropertyName);
//expected results
//order persons by Age Ascending
//then by Group Ascending
//then by State Ascending
//then by Name Descending
}
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Group { get; set; }
public string State { get; set; }
}
public class Sort
{
public string PropertyName { get; set; }
public int SortOrder { get; set; }
public string Direction { get; set; }
}
}
This is my original answer which has IQueryable implementation - good for LINQ to Entities queries, but also can be performant for LINQ to Objects.
In your case it can be used in this way:
var sorts = new List<Sort>();
sorts.Add(new Sort { PropertyName = "Age", SortOrder = 2, Direction = "Descending" });
sorts.Add(new Sort { PropertyName = "Name", SortOrder = 1, Direction = "Ascending"});
var orderByInfo = sorts.OrderBy(s => s.SortOrder)
.Select(s => Tuple.Create(s.PropertyName, s.Direction == "Descending"));
var sordedPersons = persons.AsQueryable().ApplyOrderBy(orderByInfo).ToList();
Try this to sort an in-memory list.
List<Person> SortDynamically(IEnumerable<Person> persons, IList<Sort> sorts)
{
// this line to get an IOrderedEnumerable<T> so that we can chain ThenBy(s)
var sortedPersons = persons.OrderBy(x => 1);
foreach(var sort in sorts.OrderBy(x => x.SortOrder))
{
sortedPersons = sort.Direction switch
{
"Ascending" => sortedPersons
.ThenBy(x => x.GetType().GetProperty(sort.PropertyName)?.GetValue(x, null)),
"Descending" => sortedPersons
.ThenByDescending(x => x.GetType().GetProperty(sort.PropertyName)?.GetValue(x, null)),
_ => throw new ArgumentException("Sort Direction must be Ascending or Descending")
};
}
return sortedPersons.ToList();
}
Alternatively, if you do not like the persons.OrderBy(x => 1) trick, you could the call OrderBy and ThenBy separately.
List<Person> SortDynamicallyAlt(IEnumerable<Person> persons, IList<Sort> sorts)
{
if(sorts.Count == 0)
{
return persons.ToList();
}
var firstSort = sorts.OrderBy(x => x.SortOrder).First();
var sortedPersonsAlt = firstSort.Direction switch
{
"Ascending" => persons
.OrderBy(x => x.GetType().GetProperty(firstSort.PropertyName)?.GetValue(x, null)),
"Descending" => persons
.OrderByDescending(x => x.GetType().GetProperty(firstSort.PropertyName)?.GetValue(x, null)),
_=> throw new ArgumentException("Sort Direction must be Ascending or Descending")
};
foreach(var sort in sorts.OrderBy(x => x.SortOrder).Skip(1))
{
sortedPersonsAlt = sort.Direction switch
{
"Ascending" => sortedPersonsAlt
.ThenBy(x => x.GetType().GetProperty(sort.PropertyName)?.GetValue(x, null)),
"Descending" => sortedPersonsAlt
.ThenByDescending(x => x.GetType().GetProperty(sort.PropertyName)?.GetValue(x, null)),
_=> throw new ArgumentException("sort Direction must be Ascending or Descending")
};
}
return sortedPersonsAlt.ToList();
}
The best way I can describe what I'm trying to do is "Nested DistinctBy".
Let's say I have a collection of objects. Each object contains a collection of nicknames.
class Person
{
public string Name { get; set; }
public int Priority { get; set; }
public string[] Nicknames { get; set; }
}
public class Program
{
public static void Main()
{
var People = new List<Person>
{
new Person { Name = "Steve", Priority = 4, Nicknames = new string[] { "Stevo", "Lefty", "Slim" }},
new Person { Name = "Karen", Priority = 6, Nicknames = new string[] { "Kary", "Birdie", "Snookie" }},
new Person { Name = "Molly", Priority = 3, Nicknames = new string[] { "Mol", "Lefty", "Dixie" }},
new Person { Name = "Greg", Priority = 5, Nicknames = new string[] { "G-man", "Chubs", "Skippy" }}
};
}
}
I want to select all Persons but make sure nobody selected shares a nickname with another. Molly and Steve both share the nickname 'Lefty' so I want to filter one of them out. Only the one with highest priority should be included. If there is a highest priority tie between 2 or more then just pick the first one of them. So in this example I would want an IEnumerable of all people except Steve.
EDIT: Here's another example using music album instead of person, might make more sense.
class Album
{
string Name {get; set;}
int Priority {get;set;}
string[] Aliases {get; set;}
{
class Program
{
var NeilYoungAlbums = new List<Album>
{
new Person{ Name = "Harvest (Remastered)", Priority = 4, Aliases = new string[] { "Harvest (1972)", "Harvest (2012)"}},
new Person{ Name = "On The Beach", Priority = 6, Aliases = new string[] { "The Beach Album", "On The Beach (1974)"}},
new Person{ Name = "Harvest", Priority = 3, Aliases = new string[] { "Harvest (1972)"}},
new Person{ Name = "Freedom", Priority = 5, Aliases = new string[] { "Freedom (1989)"}}
};
}
The idea here is we want to show his discography but we want to skip quasi-duplicates.
I would solve this using a custom IEqualityComparer<T>:
class Person
{
public string Name { get; set; }
public int Priority { get; set; }
public string[] Nicknames { get; set; }
}
class PersonEqualityComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
if (x == null || y == null) return false;
return x.Nicknames.Any(i => y.Nicknames.Any(j => i == j));
}
// This is bad for performance, but if performance is not a
// concern, it allows for more readability of the LINQ below
// However you should check the Edit, if you want a truely
// LINQ only solution, without a wonky implementation of GetHashCode
public int GetHashCode(Person obj) => 0;
}
// ...
var people = new List<Person>
{
new Person { Name = "Steve", Priority = 4, Nicknames = new[] { "Stevo", "Lefty", "Slim" } },
new Person { Name = "Karen", Priority = 6, Nicknames = new[] { "Kary", "Birdie", "Snookie" } },
new Person { Name = "Molly", Priority = 3, Nicknames = new[] { "Mol", "Lefty", "Dixie" } },
new Person { Name = "Greg", Priority = 5, Nicknames = new[] { "G-man", "Chubs", "Skippy" } }
};
var distinctPeople = people.OrderBy(i => i.Priority).Distinct(new PersonEqualityComparer());
EDIT:
Just for completeness, this could be a possible LINQ only approach:
var personNicknames = people.SelectMany(person => person.Nicknames
.Select(nickname => new { person, nickname }));
var groupedPersonNicknames = personNicknames.GroupBy(i => i.nickname);
var duplicatePeople = groupedPersonNicknames.SelectMany(i =>
i.OrderBy(j => j.person.Priority)
.Skip(1).Select(j => j.person)
);
var distinctPeople = people.Except(duplicatePeople);
A LINQ-only solution
var dupeQuery = people
.SelectMany( p => p.Nicknames.Select( n => new { Nickname = n, Person = p } ) )
.ToLookup( e => e.Nickname, e => e.Person )
.SelectMany( e => e.OrderBy( p => p.Priority ).Skip( 1 ) );
var result = people.Except( dupeQuery ).ToList();
See .net fiddle sample
This works once, then you have to clear the set. Or store the results in a collection.
var uniqueNicknames = new HashSet<string>();
IEnumerable<Person> uniquePeople = people
.OrderBy(T => T.Priority) // ByDescending?
.Where(T => T.Nicknames.All(N => !uniqueNicknames.Contains(N)))
.Where(T => T.Nicknames.All(N => uniqueNicknames.Add(N)));
I would assume there's a simple LINQ query to do this, I'm just not exactly sure how. Please see code snippet below, the comment explains what I'd like to do:
class Program
{
static void Main(string[] args)
{
List<Person> peopleList1 = new List<Person>();
peopleList1.Add(new Person() { ID = 1 });
peopleList1.Add(new Person() { ID = 2 });
peopleList1.Add(new Person() { ID = 3 });
peopleList1.Add(new Person() { ID = 4});
peopleList1.Add(new Person() { ID = 5});
List<Person> peopleList2 = new List<Person>();
peopleList2.Add(new Person() { ID = 1 });
peopleList2.Add(new Person() { ID = 4});
//I would like to perform a LINQ query to give me only
//those people in 'peopleList1' that are in 'peopleList2'
//this example should give me two people (ID = 1& ID = 4)
}
}
class Person
{
public int ID { get; set; }
}
var result = peopleList2.Where(p => peopleList1.Any(p2 => p2.ID == p.ID));
You can do this using Where:
var result = peopleList1.Where(p => peopleList2.Any(p2 => p2.ID == p.ID));
You can also use Intersect (var result = peopleList1.Intersect(peopleList2);), but that would need you to implement an extra IEqualityComparer<Person> or override Person's Equals and GetHashCode methods in a way that two Person instances with the same ID are regarded equal. Intersect would otherwise perform reference equality.
I would join both lists on ID:
var inboth = from p1 in peopleList1
join p2 in peopleList2
on p1.ID equals p2.ID
select p1;
List<Person> joinedList = inboth.ToList();
Related: Why is LINQ JOIN so much faster than linking with WHERE?
If you would override Equals + GetHashCode you could use Intersect:
List<Person> joinedList = peopleList1.Intersect(peopleList2).ToList();
or you could provide a custom IEqualityComparer<Person> for Intersect:
public class PersonIdComparer: IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
if(object.ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
return x.ID == y.ID;
}
public int GetHashCode(Person obj)
{
return obj == null ? int.MinValue : obj.ID;
}
}
Now you can use it in this way:
List<Person> joinedList = peopleList1
.Intersect(peopleList2, new PersonIdComparer())
.ToList();
Both, Enumerable.Join and Enumerable.Intersect are efficient since they are using a set.
Product[] fruits1 = { new Product { Name = "apple", Code = 9 },
new Product { Name = "orange", Code = 4 },
new Product { Name = "lemon", Code = 12 } };
Product[] fruits2 = { new Product { Name = "apple", Code = 9 } };
//Get all the elements from the first array
//except for the elements from the second array.
IEnumerable<Product> except =
fruits1.Except(fruits2);
foreach (var product in except)
Console.WriteLine(product.Name + " " + product.Code);
/*
This code produces the following output:
orange 4
lemon 12
*/
You can use Enumerable.Intersect
var common = peopleList1.Intersect(peopleList2);
You just can use the LINQ Intersect Extension Method.
http://msdn.microsoft.com/en-us/library/bb460136(v=VS.100).aspx
So you would do it like that:
class Program
{
static void Main(string[] args)
{
List<Person> peopleList1 = new List<Person>();
peopleList1.Add(new Person() { ID = 1 });
peopleList1.Add(new Person() { ID = 2 });
peopleList1.Add(new Person() { ID = 3 });
peopleList1.Add(new Person() { ID = 4});
peopleList1.Add(new Person() { ID = 5});
List<Person> peopleList2 = new List<Person>();
peopleList2.Add(new Person() { ID = 1 });
peopleList2.Add(new Person() { ID = 4});
var result = peopleList1.Intersect(peopleList2);
}
}
Just override the Equals Method in the Person-Class. I think you could compare the ids there.
I have a simple LINQ query here:
var Staffs = new[]
{
new { id = 1, name = "Jefferson", age = 42},
new { id = 2, name = "Jacobson", age = 54},
new { id = 3, name = "Zhang", age = 34}
};
var payroll = new[]
{
new { pid = 1, wage = 5000},
new { pid = 2, wage = 6500},
new { pid = 3, wage = 6700}
};
var q = from stf in Staffs
from pay in payroll
where stf.id == pay.pid
select new
{
stfObj = stf,
pay.pid,
pay.wage
};
Here, stfObj would be an object containing the id, name and age fields
Here comes the question:
Is it possible to turn the object into the fields themselves without explicitly hard-coding the field names like this:
select new
{
stf.id,
stf.name,
stf.age,
pay.pid,
pay.wage
};
In this way, there will be no need to change the select new block when I add a new field to Staffs, like Gender for example
Is that possible?
(ok, this looks like the question here... anyway, hoping to get better answers here)
Is this what you want!
select new
{
sId=stfObj.id,
sName=stfObj.name,
sAge=stdObj.age,
pId=pay.pid,
pWage=pay.wage
};
Why not simply embed your object ?
select new {
staff = stf,
pay = pay
};
I do not know, what you need this for. But you could try to use Dictionary<> for this. Say, we have a class:
public class Test
{
public string Name { get; set; }
public string Desc { get; set; }
}
So you can do the following:
List<Test> list = new List<Test>
{
new Test
{
Name = "Test 1",
Desc = "Desc 1"
}
};
var temp = list.Select(t =>
{
Dictionary<string, object> values = new Dictionary<string, object>();
foreach (PropertyInfo pi in t.GetType().GetProperties())
values[pi.Name] = pi.GetValue(t, null);
return values;
})
.FirstOrDefault();
temp.ToList().ForEach(p => Console.WriteLine(string.Format("{0}:\t{1}", p.Key, p.Value)));
So if you add a property to the Test class, say, like this:
public bool Extra { get; set; }
You'll get it in the dictionary automatically. Probably you'll have to work with reflection methods overloads to get exactly what you need...
i have a two list
List<Sent> SentList;
List<Messages> MsgList;
both have the same property called MsgID;
MsgList SentList
MsgID Content MsgID Content Stauts
1 aaa 1 aaa 0
2 bbb 3 ccc 0
3 ccc
4 ddd
5 eee
i want to compare the MsgID in Msglist with the sentlist and need items which are not in the sent list using linq
Result
MsgID Content
2 bbb
4 ddd
5 eee
You could do something like:
HashSet<int> sentIDs = new HashSet<int>(SentList.Select(s => s.MsgID));
var results = MsgList.Where(m => !sentIDs.Contains(m.MsgID));
This will return all messages in MsgList which don't have a matching ID in SentList.
The naive approach:
MsgList.Where(x => !SentList.Any(y => y.MsgID == x.MsgID))
Be aware this will take up to m*n operations as it compares every MsgID in SentList to each in MsgList ("up to" because it will short-circuit when it does happen to match).
Well, you already have good answers, but they're most Lambda. A more LINQ approach would be like
var NotSentMessages =
from msg in MsgList
where !SentList.Any(x => x.MsgID == msg.MsgID)
select msg;
You can do like this,this is the quickest process
Var result = MsgList.Except(MsgList.Where(o => SentList.Select(s => s.MsgID).ToList().Contains(o.MsgID))).ToList();
This will give you expected output.
You can do something like
var notSent = MsgSent.Except(MsgList, MsgIdEqualityComparer);
You will need to provide a custom equality comparer as outlined on MSDN
http://msdn.microsoft.com/en-us/library/bb336390.aspx
Simply have that equality comparer base equality only on MsgID property of each respective type. Since the equality comparer compares two instances of the same type, you would need to define an interface or common base type that both Sent and Messages implement that has a MsgID property.
Try,
public class Sent
{
public int MsgID;
public string Content;
public int Status;
}
public class Messages
{
public int MsgID;
public string Content;
}
List<Sent> SentList = new List<Sent>() { new Sent() { MsgID = 1, Content = "aaa", Status = 0 }, new Sent() { MsgID = 3, Content = "ccc", Status = 0 } };
List<Messages> MsgList = new List<Messages>() { new Messages() { MsgID = 1, Content = "aaa" }, new Messages() { MsgID = 2, Content = "bbb" }, new Messages() { MsgID = 3, Content = "ccc" }, new Messages() { MsgID = 4, Content = "ddd" }, new Messages() { MsgID = 5, Content = "eee" }};
int [] sentMsgIDs = SentList.Select(v => v.MsgID).ToArray();
List<Messages> result1 = MsgList.Where(o => !sentMsgIDs.Contains(o.MsgID)).ToList<Messages>();
Hope it should help.
In .NET 6 you can take advantage of .ExceptBy(), which lets you define which property of the first list to compare the items in the second list by:
List<Message> result = messages
.ExceptBy(sentList.Select(msg => msg.MsgID), msg => msg.MsgID)
.ToList();
messages is the first list, whereas a collection of the MsgID properties from sentList is the second list.
Example fiddle here.
Note:
.ExceptBy() produces the set difference between the two collections --> only distinct values will be in the resulting collection. This means that if messages contains the same value more than once (e.g. { "aaa", "bbb", "ccc", "ddd", "ddd", "eee" }), any duplicates will be removed in the resulting collection (--> { "bbb", "ddd", "eee" }).
As an extension method
public static IEnumerable<TSource> AreNotEqual<TSource, TKey, TTarget>(this IEnumerable<TSource> source, Func<TSource, TKey> sourceKeySelector, IEnumerable<TTarget> target, Func<TTarget, TKey> targetKeySelector)
{
var targetValues = new HashSet<TKey>(target.Select(targetKeySelector));
return source.Where(sourceValue => targetValues.Contains(sourceKeySelector(sourceValue)) == false);
}
eg.
public class Customer
{
public int CustomerId { get; set; }
}
public class OtherCustomer
{
public int Id { get; set; }
}
var customers = new List<Customer>()
{
new Customer() { CustomerId = 1 },
new Customer() { CustomerId = 2 }
};
var others = new List<OtherCustomer>()
{
new OtherCustomer() { Id = 2 },
new OtherCustomer() { Id = 3 }
};
var result = customers.AreNotEqual(customer => customer.CustomerId, others, other => other.Id).ToList();
Debug.Assert(result.Count == 1);
Debug.Assert(result[0].CustomerId == 1);
List<Person> persons1 = new List<Person>
{
new Person {Id = 1, Name = "Person 1"},
new Person {Id = 2, Name = "Person 2"},
new Person {Id = 3, Name = "Person 3"},
new Person {Id = 4, Name = "Person 4"}
};
List<Person> persons2 = new List<Person>
{
new Person {Id = 1, Name = "Person 1"},
new Person {Id = 2, Name = "Person 2"},
new Person {Id = 3, Name = "Person 3"},
new Person {Id = 4, Name = "Person 4"},
new Person {Id = 5, Name = "Person 5"},
new Person {Id = 6, Name = "Person 6"},
new Person {Id = 7, Name = "Person 7"}
};
var output = (from ps1 in persons1
from ps2 in persons2
where ps1.Id == ps2.Id
select ps2.Name).ToList();
Person class
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
If u wanna Select items of List from 2nd list:
MainList.Where(p => 2ndlist.Contains(p.columns from MainList )).ToList();
make single list and
var result = list.GroupBy(x => x.BillId).Where(x => x.Count() == 1).Select(x => x.First());
List<Car> cars = new List<Car>() { new Car() { Name = "Ford", Year = 1892, Website = "www.ford.us" },
new Car() { Name = "Jaguar", Year = 1892, Website = "www.jaguar.co.uk" },
new Car() { Name = "Honda", Year = 1892, Website = "www.honda.jp"} };
List<Factory> factories = new List<Factory>() { new Factory() { Name = "Ferrari", Website = "www.ferrari.it" },
new Factory() { Name = "Jaguar", Website = "www.jaguar.co.uk" },
new Factory() { Name = "BMW", Website = "www.bmw.de"} };
foreach (Car car in cars.Where(c => !factories.Any(f => f.Name == c.Name))) {
lblDebug.Text += car.Name;
}