Remove duplicate objects based on a list properties - c#

I want to distinct a list of objects just based on some properties. These properties are gotten via reflection and some conditions. I searched a lot but cannot found any code snippets or solutions that are able to do a loop in this lambda expression.
List<PropertyInfo> propList = ...
var distinctList = FullList
.GroupBy(uniqueObj =>
{
//do a loop to iterate all elements in propList
})
.Select(x => x.First());

Ok, took me a while to think this one through.
Basically, you can use the Linq GroupBy operator, but you need to use the overload that accepts a custom IEQualityComparer, because you want to verify equality of the objects based on a subset of all their properties.
The subset of properties is stored in a List<PropertyInfo> that you created somewhere else in your code, or that you receive from a service or whatever.
So, implementing IEqualityComparer, then use it with GroupBy:
//Dummy class representing your data.
//
//Notice that I made the IEqualityComparer as a child class only
//for the sake of demonstration
public class DataObject
{
public string Name { get; set; }
public int Age { get; set; }
public int Grade { get; set; }
public static List<PropertyInfo> GetProps()
{
//Only return a subset of the DataObject class properties, simulating your List<PropertyInfo>
return typeof(DataObject).GetProperties().Where(p => p.Name == "Name" || p.Name == "Grade").ToList();
}
public class DataObjectComparer : IEqualityComparer<DataObject>
{
public bool Equals(DataObject x, DataObject y)
{
if (x == null || y == null)
return false;
foreach (PropertyInfo pi in DataObject.GetProps())
{
if (!pi.GetValue(x).Equals(pi.GetValue(y)))
return false;
}
return true;
}
public int GetHashCode(DataObject obj)
{
int hash = 17;
foreach (PropertyInfo pi in DataObject.GetProps())
{
hash = hash * 31 + pi.GetValue(obj).GetHashCode();
}
return hash;
}
}
}
//Then use that in your code:
//
List<DataObject> lst = new List<DataObject>();
lst.Add(new DataObject { Name = "Luc", Age = 49, Grade = 100 });
lst.Add(new DataObject { Name = "Luc", Age = 23, Grade = 100 });
lst.Add(new DataObject { Name = "Dan", Age = 49, Grade = 100 });
lst.Add(new DataObject { Name = "Dan", Age = 23, Grade = 100 });
lst.Add(new DataObject { Name = "Luc", Age = 20, Grade = 80 });
List<DataObject> dist = lst.GroupBy(p => p, new DataObject.DataObjectComparer()).Select(g => g.First()).ToList();
//The resulting list now contains distinct objects based on the `Name` and `Grade` properties only.
I hope this helps you get closer to your solution.
Cheers

You can create expression using the property name with this method:
public static Expression<Func<T, object>> GetPropertySelector<T>(string propertyName)
{
var arg = Expression.Parameter(typeof(T), "x");
var property = Expression.Property(arg, propertyName);
//return the property as object
var conv = Expression.Convert(property, typeof(object));
var exp = Expression.Lambda<Func<T, object>>(conv, new ParameterExpression[] { arg });
return exp;
}
And use like this:
var exp = GetPropertySelector<Person>("PropertyName");
Now you can make a distinct easily:
List<Person> distinctPeople = allPeople
.GroupBy(exp.Compile())
.Select(g => g.First())
.ToList();

Related

C#- Filter data based on child class or using string query

Hi I have a scenario to filter the data based sub-object field please help me. From controller as query I pass Expression String.
class MasterDocument
{
private Id;
public ICollection<SubDocument> documents { get; set; }
}
class SubDocument
{
private Id;
public int Age { get; set; }
}
var filterQuery = "documents.Age == 25";
var filteredResult = MasterDocument.Where(filterQuery).ToList();
to filter the Data 
how to create Expression from string to filter data from Substructure.
Well, that's quite complicated topic, but i will first give code example and later focus on caveats:
I would follow approach and define it as another extension method:
using System.Linq.Expressions;
namespace ConsoleApp2;
public static class WhereExtensions
{
public static IEnumerable<T> Where<T>(
this IEnumerable<T> collection,
string filterExpression)
{
// Most probably you'd like to have here more sophisticated validations.
var itemsToCompare = filterExpression.Split("==")
.Select(x => x.Trim())
.ToArray();
if (itemsToCompare.Length != 2)
{
throw new InvalidOperationException();
}
var source = Expression.Parameter(typeof(T));
var property = itemsToCompare[0];
var valueToCompareAgainst = itemsToCompare[1];
var memberExpr = source.GetMemberExpression(property);
var comparisonExpr = Expression.Equal(
Expression.Call(memberExpr, typeof(object).GetMethod("ToString")),
Expression.Constant(valueToCompareAgainst)
);
var predicate = Expression.Lambda<Func<T, bool>>(comparisonExpr, source);
return collection.Where(predicate.Compile());
}
public static MemberExpression GetMemberExpression(
this ParameterExpression parameter,
string memberExpression)
{
var properties = memberExpression.Split('.');
if (!properties.Any())
{
throw new InvalidOperationException();
}
var memberExpr = Expression.PropertyOrField(parameter, properties[0]);
foreach (var property in properties.Skip(1))
{
memberExpr = Expression.PropertyOrField(memberExpr, property);
}
return memberExpr;
}
}
and the usage would be:
using ConsoleApp2;
var example = new[]
{
new TestClass() { Id = 1, Description = "a" },
new TestClass() { Id = 2, Description = "a" },
new TestClass() { Id = 3, Description = "b" },
new TestClass() { Id = 4, Description = "b" },
};
var result1 = example.Where("Id == 1").ToList();
var result2 = example.Where("Description == b").ToList();
Console.WriteLine("Items with Id == 1");
result1.ForEach(x => Console.WriteLine($"Id: {x.Id} , Descr: {x.Description}"));
Console.WriteLine("Items with Description == b");
result2.ForEach(x => Console.WriteLine($"Id: {x.Id} , Descr: {x.Description}"));
class TestClass
{
public int Id { get; set; }
public string Description { get; set; }
}
This codes returns:
NOW, THE CAVEATS
It's very tricky to cast value to compare against to an arbitrary type T, that's why i reversed the problem, and I call "ToString" on whetever member we want to compare
Expression.Call(memberExpr, typeof(object).GetMethod("ToString"))
But this also could have it's own issues, as often "ToString" returns default tpye name. But works well with integers and simple value types.

Combine two list elements into single element

I have a list with two elements
element 1:
no:1,
vendor: a,
Description: Nice,
price :10
element 2:
no:1
vendor:a,
Description: Nice,
price:20
i have lot more fields in list elements so i cannot use new to sum the price
if everything is same except price i need to combine two elements into a single element by summing price.
o/p element 1:
no:1,
vendor:a,
Description:Nice,
price:30
Tried below one but not sure how to sum the price and return the entire fields with out using new
list.GroupBy(y => new { y.Description,y.vendor, y.no})
.Select(x => x.ToList().OrderBy(t => t.Price)).FirstOrDefault()
If you prefer LINQ query expressions:
var groupedElements = from element in elements
group element by new
{
element.no,
element.Description,
element.vendor
}
into grouped
select new {grouped, TotalPrice = grouped.Sum(x => x.price)};
The total price is calculated with the final .Sum method call on the grouped elements.
Try following :
class Program
{
static void Main(string[] args)
{
List<Element> elements = new List<Element>() {
new Element() { no = 1, vendor = "a", Description = "Nice", price = 10},
new Element() { no = 1, vendor = "a", Description = "Nice", price = 20}
};
List<Element> totals = elements.GroupBy(x => x.no).Select(x => new Element()
{
no = x.Key,
vendor = x.FirstOrDefault().vendor,
Description = x.FirstOrDefault().Description,
price = x.Sum(y => y.price)
}).ToList();
}
}
public class Element
{
public int no { get;set; }
public string vendor { get;set; }
public string Description { get;set; }
public decimal price { get;set; }
}
Try following using Clone
using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
List<Element> elements = new List<Element>() {
new Element() { no = 1, vendor = "a", Description = "Nice", price = 10},
new Element() { no = 1, vendor = "a", Description = "Nice", price = 20}
};
var groups = elements.GroupBy(x => x.no).ToList();
List<Element> totals = new List<Element>();
foreach (var group in groups)
{
Element newElement = (Element)group.FirstOrDefault().Clone();
newElement.price = group.Sum(x => x.price);
totals.Add(newElement);
}
}
}
public class Element : ICloneable
{
public int no { get;set; }
public string vendor { get;set; }
public string Description { get;set; }
public decimal price { get;set; }
public object Clone()
{
return this;
}
}
}
Willy-nilly you have to create Key which has 3 properties;
If you don't like the current solution with anonymous class
list
.GroupBy(y => new {
y.Description,
y.vendor,
y.no}
)
...
You can do it in different way, e.g. with a help of unnamed tuple:
list
.GroupBy(y => Tuple.Create(
y.Description,
y.vendor,
y.no)
)
...
Or named tuple (see https://learn.microsoft.com/en-us/dotnet/csharp/tuples for details):
list
.GroupBy(y => (
Description : y.Description,
vendor : y.vendor,
no : y.no)
)
...
Or even tailored class. What's matter the most, however, is that you can't just get First item from the group
but should create a new instance. Another issue is premature materialization: .ToList() when you then get rid of this new born list and keep on querying with .OrderBy(...)
var result = result
.GroupBy(y => new {
y.Description,
y.vendor,
y.no}
)
.Select(group => MyObject() { //TODO: put the right syntax here
Description = group.Key.Description,
vendor = group.Key.vendor,
no = group.Key.no,
price = group.Sum(item => item.price) // you want to sum prices, right?
});
You need to create a custom IEqualityComparer, which when passed into the GroupBy clause, will group the items according to your needs.
Asuming the following sample class:
public class Element
{
public int no { get; set; }
public string vendor { get; set; }
public string Description { get; set; }
public decimal price { get; set; }
}
You can implement the following IEqualityComparer which using Reflection will compare every Propertypresent in the Element class except the ones defined in the Linq Where clause, in this case "price". Bear in mind further customizations could be required.
public class ElementComparer : IEqualityComparer<Element>
{
public bool Equals(Element a, Element b) => typeof(Element).GetProperties()
.Where(p => p.Name != "price")
.All(p => p.GetValue(a).Equals(p.GetValue(b)));
public int GetHashCode(Element obj) => obj.no.GetHashCode();
}
Then simply group them this way
list.GroupBy(x => x, new ElementComparer()).Select(g =>
{
// Here you need to either clone the first element of the group like
// #jdweng did, or create a new instance of Element like I'm doing below
Element element = new Element();
foreach (var prop in element.GetType().GetProperties())
{
if (prop.Name == "price")
{
prop.SetValue(element, g.Sum(y => y.price));
}
else
{
prop.SetValue(element, prop.GetValue(g.First()));
}
}
return element;
});
I think what you're trying to do is write dynamic code that groups by all properties except for the property you want to sum. This solution should work, though I loath to use reflection. A more performant method would be to use expression trees to generate an aggregation delegate that you reuse, but that is very involved. This should do the trick:
Edit: There's another answer that also seems to work. Mine assumes you will want to do this with any collection regardless of type. Doesn't require ICloneable or a type-specific IEqualityComparer<T>, though, as a slight trade-off, the other one will likely perform better in very large datasets.
static T[] GetGroupSums<T>(IEnumerable<T> collection, string sumPropertyName) where T : new()
{
//get the PropertyInfo you want to sum
//var sumProp = (PropertyInfo)((MemberExpression)((UnaryExpression)memberExpression.Body).Operand).Member;
var sumProp = typeof(T).GetProperty(sumPropertyName);
//get all PropertyInfos that are not the property to sum
var groupProps = typeof(T).GetProperties().Where(x => x != sumProp).ToArray();
//group them by a hash of non-summed properties (I got this hash method off StackExchange many years back)
var groups = collection
.GroupBy(x => GetHash(groupProps.Select(pi => pi.GetValue(x)).ToArray()))
.Select(items =>
{
var item = new T();
var firstItem = items.First();
//clone the first item
foreach (var gp in groupProps)
{
gp.SetValue(item, gp.GetValue(firstItem));
}
//Get a decimal sum and then convert back to the sum property type
var sum = items.Sum(_item => (decimal)Convert.ChangeType(sumProp.GetValue(_item), typeof(decimal)));
sumProp.SetValue(item, Convert.ChangeType(sum, sumProp.PropertyType));
//If it will always be int, just do this
//var sum = items.Sum(_item => (int)sumProp.GetValue(_item));
//sumProp.SetValue(item, sum);
return item;
});
return groups.ToArray();
}
//I got this hash method off StackExchange many years back
public static int GetHash(params object[] args)
{
unchecked
{
int hash = 17;
foreach (object arg in args)
{
hash = hash * 23 + arg.GetHashCode();
}
return hash;
}
}
Use it like this:
List<Element> elements = new List<Element>() {
new Element() { no = 1, vendor = "a", Description = "Nice", price = 10},
new Element() { no = 2, vendor = "a", Description = "Nice", price = 15},
new Element() { no = 2, vendor = "b", Description = "Nice", price = 10},
new Element() { no = 1, vendor = "a", Description = "Nice", price = 20}
};
var groups = GetGroupSums(elements, nameof(Element.price));

Using an expression tree to generate a select statement with a sum after a group by C#

Apologies if the question doesn't really make sense,
We use C#, Entity Framework & Linq for some context.
So the problem is:
A user can select a number of fields to be totaled up e.g. gross, net, vat across a data set, then when running a standard query to return the data the query has to total these columns for the search.
I'm trying to translate these statements into a dynamic expression tree.
List<string> propFrom = new List<string>()
{
"Class1.PropertyName",
"Class1.AnotherPropertyName"
}
Expression<Func<Grouped<Class1>, Class2>> select = (x => new Class2()
{
Class1Prop = x.Sum(s => s.Class2Prop),
Class1AntoherProp = x.Sum(s => s.Class2AnotherProp)
})
The properties on Class1 match Class2 and only the properties in the string list will be summed and selected in the select list.
I understand how to generate an expression tree for a standard select statement just not how to do one after a group by and using a sum.
If anyone can think on how to rephrase the question or any help as to the answer it is much appreciated.
-- Further Detail --
The application displays a table of data around invoices
Table looks something like this
invoiceNo,
Gross,
Vat,
Net....Other Fields
The table brings back all rows, the user can additionally say I wish to total any of the fields e.g. I want to sum up all vat and or all net, or just gross alone, problem is we don't know which of the fields they want totalling, if we did we could do
query.Sum(x => x.net)
but we have multiple sums of unknown columns so we're trying to use the property names to map to an a select statement as shown above
Data example,
invoices Net Gross Vat
1 10 10 10
2 20 20 20
3 10 30 30
4 15 40 40
5 50 50 50
6 5 60 60
Users specifies total gross and vat there for get
gross - 210, Vat - 210
along side all of their results
and yes the query groups by 1 to get the aggregate to calculate the sum, but thats done before e.g.
query.groupBy(x => 1).select(insertDynamicSelectHere);
If I understood your requrement right, here is the code:
public static class LinqExtensions
{
public static Expression<Func<IGrouping<int, TInput>, TOutput>> AggregateExpression<TInput, TOutput>(string[] strings) where TInput: new()
{
ParameterExpression p = Expression.Parameter(typeof(IGrouping<int, TInput>));
// Create object using Member Initialization; for example `new XXX { A = a.Sum(b => b.A), B = a.Sum(b => b.B) }`
MemberInitExpression body = Expression.MemberInit(
Expression.New(typeof(TOutput).GetConstructor(Type.EmptyTypes)),
strings.Select(CreateMemberAssignment).ToArray()
);
// Create lambda
return Expression.Lambda<Func<IGrouping<int, TInput>, TOutput>>(body, p);
// Create single member assignment for MemberInit call
// For example for expression `new XXX { A = a.Sum(b => b.A), B = a.Sum(b => b.B) }` it can be `A = a.Sum(b => b.A)` or `B = a.Sum(b => b.B)`
MemberAssignment CreateMemberAssignment(string prop)
{
// If needed you can map TInput.Prop to TOutput.Prop names here
PropertyInfo propInfo = typeof(TOutput).GetProperty(prop);
return Expression.Bind(
propInfo,
Expression.Convert(
Expression.Call(
typeof(Enumerable),
"Sum",
new[] {typeof(TInput)},
new[] {p, CreateSumLambda(prop)}
),
propInfo.PropertyType
)
);
}
// Create Lambda to be passed to Sum method
Expression CreateSumLambda(string prop)
{
ParameterExpression q = Expression.Parameter(typeof(TInput));
return Expression.Lambda(Expression.Property(q, prop), q);
}
}
}
So instead of calling
invoices.GroupBy(x => 1)
.AsQueryable()
.Select(i => new AggregatedInvoice
{
Net = i.Sum(x => x.Net),
Gross = i.Sum(x => x.Gross)
})
you can call
invoices.GroupBy(x => 1)
.AsQueryable()
.Select(LinqExtensions.AggregateExpression<Invoice, AggregatedInvoice>(new[] { "Net", "Gross" }));
For following models:
public class Invoice
{
public int Id { get; set; }
public decimal Net { get; set; }
public decimal Gross { get; set; }
public decimal Vat { get; set; }
}
public class AggregatedInvoice
{
public decimal? Net { get; set; }
public decimal? Gross { get; set; }
public decimal? Vat { get; set; }
}
Method accepts 2 type parameters TInput and TOutput. You can use the same class for both if you want.
Only limitation is that both TInput and TOutput must have properties with same names.
Is this what you are looking to do?
public class Data
{
public int Value1 { get; set; }
public int Value2 { get; set; }
public int Value3 { get; set; }
}
public class DataContainer
{
public IQueryable<Data> Data { get; set; }
}
class Program
{
static void Main(string[] args)
{
var list = MakeList();
// Hardcoded
var sumExpression1 = (Expression<Func<Data, Int32>>)(d => d.Value1);
// Generate property expression
var sumExpression2 = GeneratePropertyExpression<Data>("Value2"); // THIS?
// Generate Sum expression
var sumExpression3 = GenerateSumExpression<DataContainer, Data>("Value3"); // OR THIS?
var selected = list.Select(c => new
{
Sum1 = c.Data.Sum(sumExpression1),
Sum2 = c.Data.Sum(sumExpression2),
// will not need the compile() if you generate the entire select expression.
Sum3 = sumExpression3.Compile()(c)
}); ;
var first = selected.First();
var last = selected.Last();
Console.WriteLine($"First().Sum1 = {first.Sum1}"); // 2
Console.WriteLine($"Last().Sum2 = {last.Sum2}"); // 6
Console.WriteLine($"Last().Sum3 = {last.Sum3}"); // 9
}
private static Expression<Func<TParent, Int32>> GenerateSumExpression<TParent, TChild>(String field)
{
var parameter = Expression.Parameter(typeof(TParent), "c");
// I don't know a cleaner way to get the LINQ methods than this.
// We use Enumerable because it wants a concrete type.
var sumMethod =
(
from m in typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)
where m.Name == "Sum"
let p = m.GetParameters()
where p.Length == 2
&& p[0].ParameterType.IsGenericType
&& p[0].ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>)
&& p[1].ParameterType.IsGenericType
&& p[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>)
select m
).FirstOrDefault()
.MakeGenericMethod(typeof(TChild)); // Enumerable is not generic, so we'll make it a generic with this reflection method.
var nestedLambda = GeneratePropertyExpression<TChild>(field);
// 'parameter' is our DataContainer object, while 'Data' is the
// property we want.
var prop = Expression.PropertyOrField(parameter, "Data");
// Null is the first parameter because it's a static extension method.
var body = Expression.Call(null, sumMethod, prop, nestedLambda);
return Expression.Lambda<Func<TParent, Int32>>(body, parameter);
}
private static Expression<Func<T, Int32>> GeneratePropertyExpression<T>(String field)
{
var parameter = Expression.Parameter(typeof(T), "d");
var body = Expression.PropertyOrField(parameter, field);
return Expression.Lambda<Func<T, Int32>>(body, parameter);
}
public static IQueryable<DataContainer> MakeList()
{
return new List<DataContainer>()
{
new DataContainer()
{
Data = new List<Data>()
{
new Data() { Value1 = 1, Value2 = 2, Value3 = 3 },
new Data() { Value1 = 1, Value2 = 2, Value3 = 3 }
}.AsQueryable()
},
new DataContainer()
{
Data = new List<Data>()
{
new Data() { Value1 = 1, Value2 = 2, Value3 = 3 },
new Data() { Value1 = 1, Value2 = 2, Value3 = 3 },
new Data() { Value1 = 1, Value2 = 2, Value3 = 3 }
}.AsQueryable()
}
}.AsQueryable();
}

Dynamically add Or conditions

I have a collection where I want to programatically add OR condtions to a linq query. I know how to add AND condtions like the following:
var mySet = SomeFactory.GetData();
foreach(var nameFilter in nameFilters)
{
mySet = mySet.Where(item => item.Name == nameFilter);
}
foreach(var ageFilter in ageFilters)
{
mySet = mySet.Where(item => item.Age == ageFilter)
}
however, if I wanted these to be OR conditions rather than be 'AND' together, how would I do that? I.E. I can do this if I know I will always have both and never an array of different values:
mySet.Where(item => item.Name == nameFilter[0] || item.Name == nameFilter[1] ... || item.Age == ageFilter[0] || item.Age == ageFilter[1] || ...);
TL;DR: I want to be able to chain an unknown number of boolean checks into a single expression evaluated with OR statements. For example, if I have a cross reference of People named Mary or Jim who are either 32 or 51.
PredicateBuilder would help you to apply where clauses in flexibility. You can find extension method here.
var filterOfNames = new List<string> {"Sample", "Sample2"};
var filterOfAges = new List<int> { 21, 33, 45 };
var mySet = SomeFactory.GetData();
var predicate = PredicateBuilder.True<TypeOfmySet>();
foreach (var filterOfName in filterOfNames)
{
//If it is the first predicate, you should apply "And"
if (predicate.Body.NodeType == ExpressionType.Constant)
{
predicate = predicate.And(x => x.Name == filterOfName);
continue;
}
predicate = predicate.Or(x => x.Name == filterOfName);
}
foreach (var filterOfAge in filterOfAges)
{
//If it is the first predicate, you should apply "And"
if (predicate.Body.NodeType == ExpressionType.Constant)
{
predicate = predicate.And(x => x.Age == filterOfAge);
continue;
}
predicate = predicate.Or(x => x.Age == filterOfAge);
}
//I don't know the myset has which type in IQueryable or already retrieved in memory collection. If it is IQueryable, don't compile the predicate otherwise compile it.
//var compiledPredicate = predicate.Compile();
mySet = mySet.Where(predicate);
There is no additive "or" in LINQ. Combining "Where" expressions is always evaluated as "and".
However, you can build predicates, add them to a list, then test them. I think this code does what you want:
using System;
using System.Collections.Generic;
class Item
{
internal string Name { get; set; }
internal short Age { get; set; }
internal string City { get; set; }
}
public static class ExtensionMethods
{
public static List<T> FindAll<T>(this List<T> list, List<Predicate<T>> predicates)
{
List<T> L = new List<T>();
foreach (T item in list)
{
foreach (Predicate<T> p in predicates)
{
if (p(item)) L.Add(item);
}
}
return L;
}
}
class Program
{
static void Main(string[] args)
{
List<Item> items = new List<Item>();
items.Add(new Item { Name = "Bob", Age = 31, City = "Denver" });
items.Add(new Item { Name = "Mary", Age = 44, City = "LA" });
items.Add(new Item { Name = "Sue", Age = 21, City = "Austin" });
items.Add(new Item { Name = "Joe", Age = 55, City = "Redmond" });
items.Add(new Item { Name = "Tom", Age = 81, City = "Portland" });
string nameFilter = "Bob,Mary";
//string ageFilter = "55,21";
string ageFilter = null;
string cityFilter = "Portland";
List<Predicate<Item>> p = new List<Predicate<Item>>();
if (nameFilter != null)
{
p.Add(i => nameFilter.Contains(i.Name));
}
if (cityFilter != null)
{
p.Add(i => cityFilter.Contains(i.City));
}
if (ageFilter != null)
{
p.Add(i => ageFilter.Contains(i.Age.ToString()));
}
var results = items.FindAll(p);
foreach (var result in results)
{
Console.WriteLine($"{result.Name} {result.Age} {result.City}");
}
}
}

Filter list based on KeyValuePairs

I have a list that i would like to filter based on a List of KeyValuePairs. All the keys in the KeyValuePair exist in the object.
So let's say i have a list of objects from this class:
public class filters
{
public string Name { get; set; }
public string Age { get; set; }
public string Country { get; set; }
}
And I have a KeyValuePair with:
Key: "Name", Value: "test"
Key: "Country", Value: "SE"
Is it possible to generate some kind of LINQ predicate from the KeyValuePair that is usable as list.Where(predicate), and the predicate would be the same as if I would have written list.Where(c => c.Name == "test" && c.Country == "SE") ?
Or how should I approach this?
As a one-liner:
var filters = new Dictionary<string, string>{{"Name", "test"}, {"Country", "SE"}};
var result = list.Where(item => filters.All(f => (string)(item.GetType().GetProperty(f.Key)?.GetValue(item)) == f.Value));
This enables you to have an unlimited number of filters.
For each item in your list the All predicate will check the validity for each filter. item.GetType() gets the Type (ie information about your class) for your item. GetProperty(f.Key) gets information for the specific property, named by the Key of the current filter f. GetValue(item) gets the value of the property for the current item. The ? is a new feature of c# 6, ie it's an inline check for null, ie if the property is not found, it does not try to execute GetValue -- which would raise a NullReferenceException -- but returns null. You then have to cast the property value to string and compare it to the Value of the current filter. You can also use String::Compare (or any other comparison method you prefer).
All only returns true if all filters are met and false otherwise. So the result of this query will contain all elements which meet all the filters in your Dictionary
Something like this? Getting the Propertyname through reflection and do an equality check.
Func<filters, IEnumerable<KeyValuePair<string, string>>, bool> filter = (filters, pairs) =>
{
foreach (var valuePair in pairs)
{
if (filters.GetType().GetProperty(valuePair.Key).GetValue(filters) != valuePair.Value)
{
return false;
}
}
return true;
};
List<filters> list = new List<filters>();
list.Add(new filters() { Name = "Name1", Country = "DE"});
list.Add(new filters() { Name = "Name2", Country = "SE"});
var element = list.FirstOrDefault(x => filter(x, new List<KeyValuePair<string, string>>() {
new KeyValuePair<string, string>("Name", "Name2"),
new KeyValuePair<string, string>("Country", "SE"),
}));
Console.WriteLine(element.Name);
I might have mis understood you here but does this do what you want:
// say I have a list of ilters like this
// assume there are actually some filters in here though
var filterCollection = new List<filters>()
// build dictionary of key values
var keyedSet = filterCollection.ToDictionary(i => i.Name + i.Country, i => i);
// query them using key ...
var filterItem = keyedSet["testSE"];
... or you can wrap the predicate in an extension method ...
public IEnumerable<filters> ByNameAndCountry(this IEnumerable<filters> collection, string name, string country)
{
return collection.Where(i => i.Name == name && i => i.Country == country);
}
... having done that you can filter the original list like this ...
var result = filterCollection.ByNameAndCountry("test", "ES");
This should do the trick:
void Main()
{
List<Filter> filters = new List<Filter>() {
new Filter {Name = "Filter1", Age = 1, Country ="De"},
new Filter {Name = "Filter2", Age = 2, Country ="Fr"},
new Filter {Name = "Filter3", Age = 3, Country ="It"},
new Filter {Name = "Filter4", Age = 4, Country ="Es"},
};
KeyValuePair<string, string> kvp = new KeyValuePair<string, string>("Filter1", "De");
var result = filters.AsQueryable().Where (GetPredicate(kvp));
result.Dump();
}
//Create the predicate as an expression, which takes a Filter as input and a kvp as a parameter
private static Expression<Func<Filter, bool>> GetPredicate(KeyValuePair<string,string> kvp)
{
return (f) => f.Name == kvp.Key && f.Country == kvp.Value;
}
Result:
You want a Predicate<filters> generated from KeyValuePair, which is IEnumerable<KeyValuePair<string, string>>. So it's
Func<IEnumerable<KeyValuePair<string, string>>, Predicate<filters>>
Use reflection to enumerate All properties listed in KeyValuePair.Key and check if each property value match KeyValuePair.Value. Full code is like
var lists = new List<filters> { new filters { Name = "abc", Country = "def" } };
Func<IEnumerable<KeyValuePair<string, string>>, Predicate<filters>> predicateBuilder =
( keyValueParis ) => filter => ( from kp in keyValueParis
let pi = typeof( filters ).GetProperty( kp.Key )
select pi.GetValue( filter ) == kp.Value )
.All( r => r );
var predicates = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("Name", "abc" ),
new KeyValuePair<string, string>("Country", "def")
};
Predicate<filters> predicate = predicateBuilder( predicates );
Console.WriteLine( lists.FindAll(predicate).Count);
You could use an approach like this:
void Main()
{
var keyValuePairs = new List<KeyValuePair>
{
new KeyValuePair {Key = "Name", Value = "Test"},
new KeyValuePair {Key = "Age", Value = "42"},
new KeyValuePair {Key = "Country", Value = "USA"}
};
var list = new List<Filter>();
list.Add(new Filter { Name = "Test", Age = "42", Country = "USA" });
list.Where(keyValuePairs.ToPredicate<Filter>()).Dump();
}
public class Filter
{
public string Name { get; set; }
public string Age { get; set; }
public string Country { get; set; }
}
public class KeyValuePair
{
public string Key { get; set; }
public string Value { get; set; }
}
public static class KeyValuePairExtensions
{
public static Func<T, bool> ToPredicate<T>(this IEnumerable<KeyValuePair> keyValuePairs)
{
if (keyValuePairs == null || !keyValuePairs.Any())
return t => false; // default value in case the enumerable is empty
var parameter = Expression.Parameter(typeof(T));
var equalExpressions = new List<BinaryExpression>();
foreach (var keyValuePair in keyValuePairs)
{
var propertyInfo = typeof(T).GetProperty(keyValuePair.Key);
var property = Expression.Property(parameter, propertyInfo);
var value = Expression.Constant(keyValuePair.Value, propertyInfo.PropertyType);
var equalExpression = Expression.Equal(property, value);
equalExpressions.Add(equalExpression);
}
var expression = equalExpressions.First();
if (equalExpressions.Count > 1)
{
// combine expression with and
expression = Expression.AndAlso(equalExpressions[0], equalExpressions[1]);
for (var i = 2; i < equalExpressions.Count; i++)
{
expression = Expression.AndAlso(expression, equalExpressions[i]);
}
}
var lambda = (Func<T, bool>)Expression.Lambda(expression, parameter).Compile();
return lambda;
}
}
You could also extend the ToPredicate method to combine the KeyValuePairs with something else than and.

Categories