Pass aggregate function as parameter - c#

I have a simple object:
public class Machine
{
public string Name { get; set; }
public int Power { get; set; }
public int Type { get; set; }
}
Then there's a class which holds a List of these objects:
public class Aggregations
{
private List<Machine> _machines;
public Aggregations()
{
_machines = new List<Machine>
{
new Machine { Name = "XLR1", Power = 111, Type = 1 },
new Machine { Name = "XLR2", Power = 222, Type = 1 },
new Machine { Name = "XLR3", Power = 333, Type = 1 },
new Machine { Name = "XLR4", Power = 444, Type = 1 },
new Machine { Name = "XLR5", Power = 555, Type = 2 },
new Machine { Name = "XLR6", Power = 666, Type = 2 }
};
}
// ...
}
There're two functions which return lists of machines with specific criteria:
public IEnumerable<Machine> MaxPower(IEnumerable<Machine> machines)
{
var maxPowerMachinesPerType = new List<Machine>();
var groups = machines.GroupBy(m => m.Type);
foreach (var g in groups)
{
var max = g.Max(m => m.Power);
var machine = g.First(m => m.Power == max);
maxPowerMachinesPerType.Add(machine);
}
return maxPowerMachinesPerType;
}
public IEnumerable<Machine> MinPower(IEnumerable<Machine> machines)
{
var minPowerMachinesPerType = new List<Machine>();
var groups = machines.GroupBy(m => m.Type);
foreach (var g in groups)
{
var min = g.Min(m => m.Power);
var machine = g.First(m => m.Power == min);
minPowerMachinesPerType.Add(machine);
}
return minPowerMachinesPerType;
}
}
As you can see, the two functions are almost equal. Only "max" and "min" differ.
These functions are called like this:
IEnumerable<Machine> maxPowerMachines = MaxPower(_machines);
IEnumerable<Machine> minPowerMachines = MinPower(_machines);
Because my actual program is slightly more complex and I although would like to call other aggregate functions, I would like to pass the aggregate-function to be used as a parameter: (Pseudo-code)
IEnumerable<Machine> maxPowerMachines = SuperFunction(_machines, m => m.Max);
IEnumerable<Machine> minPowerMachines = SuperFunction(_machines, m => m.Min);
IEnumerable<Machine> averagePowerMachines = SuperFunction(_machines, m => m.Average);
I hope you get the intent.

Since Min and Max have the same signature, i.e. they both take IEnumerable<T> and produce a T, you can do it like this:
public IEnumerable<Machine> SelectPower(
IEnumerable<Machine> machines
, Func<IEnumerable<int>,int> powerSelector
) {
var res = new List<Machine>();
var groups = machines.GroupBy(m => m.Type);
foreach (var g in groups) {
var targetPower = powerSelector(g.Select(m => m.Power));
var machine = g.First(m => m.Power == targetPower);
res.Add(machine);
}
return res;
}
Now you can call your method like this:
IEnumerable<Machine> maxPowerMachines = SuperFunction(_machines, m => m.Max());
IEnumerable<Machine> minPowerMachines = SuperFunction(_machines, m => m.Min());

Func<T> sounds like what you're looking for. The leading types <T> show the input, and the last <T> shows the return value, so you'd be looking for something like this:
Func<IEnumerable<Machine>, IEnumerable<Machine>> aggregateFunction = MaxPower;
//Now you can pass aggregateFunction around as a variable. You can call it like so:
var machines = Aggregations();
aggregateFunction.Invoke(machines);

Related

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

Get result from list inside list

I have a structure like
class a
{
public IList<b> bs{ get; set; }
public class b
{
public string r{ get; set; }
public IList<sl> sls{ get; set; }
public class sl
{
public string sn{ get; set; }
public string st{ get; set; }
}
}
}
the query is like if sn == "abc" then get r
I have done
a aobj = new a();
var aa = aobj.bs.Where(c => c.sl != null).Select(c => c).ToList(); // here I get `r = "qwerty", sls will have data like sn = "qwerty0", st= "1" ; sn = "asdf" , st="2"; sn = "zxc" st = "abc"; sn="me" , st = "abc"
var bb = aa.where(c => c.sl.Select(dr => dr.st.ToLower().Contains("abc"))); // I 'm here checking that `sn` contain abc or not
var cc = bb.Select(c => c.r).ToList(); // result
my expected output of query is "zxc", "me"
but I am getting all the list not only contains abc.. can anyone suggest me what should I do? I am partitioning this query to debug.
Thank you
You'll need to use the Any operator to check if an enumerable collection has an item that meets a criteria.
You can't use Select as that only projects an item, it isn't returning an predicate and as such has no function in a where clause.
Here is your (fixed for syntax errors) changed code:
var aa = aobj.bs.Where(c => c.sls != null).Select(c => c).ToList();
// use Any here
var bb = aa.Where(c => c.sls.Any(dr => dr.sn.ToLower().Contains("abc")));
var cc = bb.Select(c => c.r).ToList();
And here is the test set I used:
a aobj = new a();
aobj.bs = new List<b>();
aobj.bs.Add(new b {
r ="bar",
sls = new List<sl>{
new sl { sn="tets"},
new sl { sn="no"}
}
});
aobj.bs.Add(new b {
r ="foo",
sls = new List<sl>{
new sl { sn="no"},
new sl { sn="abc"}
}
});
aobj.bs.Add(new b {
r ="fubar",
sls = new List<sl>{
new sl { sn="no"},
new sl { sn="abc"}
}
});
This will output:
foo
fubar
If you combine all operators together you'll get:
var merged = aobj
.bs
.Where(c => c.sls != null
&& c.sls.Any(dr => dr.sn.ToLower().Contains("abc")))
.Select(c => c.r);
I think you can use a code like this:
var cc = a.bs
.Where(w => w.sls?.Any(s => s.st?.ToLower().Contains("abc") ?? false) ?? false)
.Select(c => c.r);

Finding many duplicates based on multiple properties in one database hit

I'm trying to perform a check on the database to see if the combination of two properties exists in the database check (pre-Unique constraint check for better UX). Doing the check with a single property is easy, no matter how many you're trying to check. I'm unable to find how to do it with multiple properties in an enumerable.
public class Foo
{
public int Id { get; set; }
public int Bar { get; set; }
public int Bat { get; set; }
public string Name { get; set; }
//...
}
public class FooDupeCheckModel
{
public int Bar { get; set; }
public int Bat { get; set; }
}
public IEnumerable<FooDupeCheckModel> MatchExists(IEnumerable<FooDupeCheckModel> matches)
{
return _db.Foos
.Where(f => matches.Any(m => m.BarId == f.BarId &&
m.BatId == f.BatId))
.Select(f => new FooDupeCheckModel
{
BarId = f.BarId,
BatId = f.BatId
});
}
This unfortunately gives an exception because the complex property matches cannot be converted into a SQL script. Only primitive types can be included into a query.
I also tried converting matches to a multidimensional array before using it within the query, but indexes are not supported within a query. .First() is not allowed to be used their either.
public IEnumerable<FooDupeCheckModel> MatchExists(IEnumerable<FooDupeCheckModel> matches)
{
var matchArray = matches.Select(m => new [] {m.BarId, m.BatId})
.ToArray();
return _db.Foos
.Where(f => matchArray.Any(m => m[0] == f.BarId &&
m[1] == f.BatId))
.Select(f => new FooDupeCheckModel
{
BarId = f.BarId,
BatId = f.BatId
});
}
This may be one of those situations that is such a niche case, or requires a SQL query that is too complex for Entity Framework that it is not possible. If it is possible, then this would be very helpful if someone else runs into the same issue.
I did get around this by looping through and calling the database for each element in matches, but if I could do this in one database call, that'd be quicker.
SQL Server doesn't support comparing tuples. However, you can compare two+ properties by OR-ing together comparisons:
SELECT *
FROM Foo f
WHERE
(
(f.Bar = 1 AND f.Bat = 1)
OR
(f.Bar = 3 AND f.Bat = 2)
)
Unfortunately, there's no easy way to build up an IQueryable<T> involving an OR. You can, however, build it up using Expression tree builders:
var models = new FooDupeCheckModel[]
{
new FooDupeCheckModel() { Bar = 1, Bat = 2 },
new FooDupeCheckModel() { Bar = 1, Bat = 3 }
};
var comparison = getComparison(models);
IQueryable<Foo> foos = new Foo[]
{
new Foo() { Bar = 1, Bat = 1 },
new Foo() { Bar = 1, Bat = 2 },
new Foo() { Bar = 1, Bat = 3 },
new Foo() { Bar = 1, Bat = 4 }
}.AsQueryable();
var results = foos.Where(comparison).ToArray();
...
private static Expression<Func<Foo, bool>> getComparison(IEnumerable<FooDupeCheckModel> models)
{
ParameterExpression pe = Expression.Parameter(typeof(Foo), "f");
var ands = models.Select(m =>
{
// Compare Bars
Expression pBarId = Expression.Property(pe, "Bar");
Expression vBarId = Expression.Constant(m.Bar);
Expression bar = Expression.Equal(pBarId, vBarId);
// Compare Bats
Expression pBatId = Expression.Property(pe, "Bat");
Expression vBatId = Expression.Constant(m.Bat);
Expression bat = Expression.Equal(pBatId, vBatId);
Expression and = Expression.And(bar, bat);
return and;
}).ToArray();
if (ands.Length == 0)
{
return Expression.Lambda<Func<Foo, bool>>(Expression.Constant(true), pe);
}
else
{
Expression ors = ands.First();
foreach (Expression and in ands.Skip(1))
{
ors = Expression.OrElse(ors, and);
}
return Expression.Lambda<Func<Foo, bool>>(ors, pe);
}
}
This works against in-memory data structures. Test it again SQL Server; it should generate the corresponding SQL.
Here is a version that supports an arbitrary number of properties with any names:
public class Foo
{
public int Id { get; set; }
public int Bar { get; set; }
public int Bat { get; set; }
public string Name { get; set; }
}
public class FooDupeCheckModel
{
public int Bar { get; set; }
public int Bat { get; set; }
}
static void Main(string[] args)
{
var models = new FooDupeCheckModel[]
{
new FooDupeCheckModel() { Bar = 1, Bat = 2 },
new FooDupeCheckModel() { Bar = 1, Bat = 3 }
};
var comparison = getComparison<Foo, FooDupeCheckModel>(
models,
compare((Foo f) => f.Bar, (FooDupeCheckModel f) => f.Bar),
compare((Foo f) => f.Bat, (FooDupeCheckModel f) => f.Bat)
);
IQueryable<Foo> foos = new Foo[]
{
new Foo() { Bar = 1, Bat = 1 },
new Foo() { Bar = 1, Bat = 2 },
new Foo() { Bar = 1, Bat = 3 },
new Foo() { Bar = 1, Bat = 4 }
}.AsQueryable();
var query = foos.Where(comparison);
var results = query.ToArray();
}
private class PropertyComparison
{
public PropertyInfo FromProperty { get; set; }
public PropertyInfo ToProperty { get; set; }
}
private static PropertyComparison compare<TFrom, TFromValue, TTo, TToValue>(
Expression<Func<TFrom, TFromValue>> fromAccessor,
Expression<Func<TTo, TToValue>> toAccessor)
{
MemberExpression fromMemberAccessor = (MemberExpression)fromAccessor.Body;
PropertyInfo fromProperty = (PropertyInfo)fromMemberAccessor.Member;
MemberExpression toMemberAccessor = (MemberExpression)toAccessor.Body;
PropertyInfo toProperty = (PropertyInfo)toMemberAccessor.Member;
return new PropertyComparison() { FromProperty = fromProperty, ToProperty = toProperty };
}
private static Expression<Func<TFrom, bool>> getComparison<TFrom, TTo>(
IEnumerable<TTo> models,
params PropertyComparison[] comparisons)
{
ParameterExpression pe = Expression.Parameter(typeof(TFrom), "f");
if (!models.Any() || !comparisons.Any())
{
return Expression.Lambda<Func<TFrom, bool>>(Expression.Constant(true), pe);
}
var ands = models.Select(m =>
{
var equals = comparisons.Select(p =>
{
PropertyInfo fromProperty = p.FromProperty;
PropertyInfo toProperty = p.ToProperty;
object value = toProperty.GetValue(m);
Expression fromValue = Expression.Property(pe, fromProperty);
Expression toValue = Expression.Constant(value);
Expression equal = Expression.Equal(fromValue, toValue);
return equal;
}).ToArray();
var and = equals.First();
foreach (var equal in equals.Skip(1))
{
and = Expression.AndAlso(and, equal);
}
return and;
}).ToArray();
Expression ors = ands.First();
foreach (Expression and in ands.Skip(1))
{
ors = Expression.OrElse(ors, and);
}
return Expression.Lambda<Func<TFrom, bool>>(ors, pe);
}
Since Foo is a type that belongs to the model, you could project your matches to IEnumerable<Foo>, mapping only the two properties of interest, then issue the query. That should make it work.
public IEnumerable<FooDupeCheckModel> MatchExists(IEnumerable<FooDupeCheckModel> matches)
{
//Convert small set to check for dups to objects recognized by the EF
var fooMatches = matches.Select(m => new Foo() { BarId = m.BardId, BatId = m.BatId });
//This should now work
return _db.Foos
.Where(f => fooMatches.Any(m => m.BarId == f.BarId &&
m.BatId == f.BatId))
.Select(f => new FooDupeCheckModel
{
BarId = f.BarId,
BatId = f.BatId
});
}

How to make select query by string property name in lambda expression?

I would like to make a query by using lambda select,
Like below:
public class Foo{
public int Id {get;set;}
public string Name {get;set;}
public string Surname {get;set;}
}
var list = new List<Foo>();
var temp = list.Select(x=> x("Name"),("Surname"));
The property name needs to be sent as a string,
I dont know how to use, I have given it for being a example.
is it possible?
Edit:
Foo list :
1 A B
2 C D
3 E F
4 G H
I don't know type of generic list, I have property name such as "Name", "Surname"
I want to be like below:
Result :
A B
C D
E F
G H
The following code snippet shows 2 cases. One filtering on the list, and another creating a new list of anonymous objects, having just Name and Surname.
List<Foo> list = new List<Foo>();
var newList = list.Select(x=> new {
AnyName1 = x.Name,
AnyName2 = x.Surname
});
var filteredList = list.Select(x => x.Name == "FilteredName" && x.Surname == "FilteredSurname");
var filteredListByLinq = from cust in list
where cust.Name == "Name" && cust.Surname == "Surname"
select cust;
var filteredByUsingReflection = list.Select(c => c.GetType().GetProperty("Name").GetValue(c, null));
Interface
If you have access to the types in question, and if you always want to access the same properties, the best option is to make the types implement the same interface:
public interface INamable
{
string Name { get; }
string Surname { get; }
}
public class Foo : INamable
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
}
This will preserve type safety and enable queries like this one:
public void ExtractUsingInterface<T>(IEnumerable<T> list) where T : INamable
{
var names = list.Select(o => new { Name = o.Name, Surname = o.Surname });
foreach (var n in names)
{
Console.WriteLine(n.Name + " " + n.Surname);
}
}
If, for some reason, you can't alter the original type, here are two more options.
Reflection
The first one is reflection. This is Mez's answer, i'll just rephrase it with an anonymous type like in the previous solution (not sure what you need exactly):
public void ExtractUsingReflection<T>(IEnumerable<T> list)
{
var names = list.Select(o => new
{
Name = GetStringValue(o, "Name"),
Surname = GetStringValue(o, "Surname")
});
foreach (var n in names)
{
Console.WriteLine(n.Name + " " + n.Surname);
}
}
private static string GetStringValue<T>(T obj, string propName)
{
return obj.GetType().GetProperty(propName).GetValue(obj, null) as string;
}
Dynamic
The second uses dynamic:
public void ExtractUsingDynamic(IEnumerable list)
{
var dynamicList = list.Cast<dynamic>();
var names = dynamicList.Select(d => new
{
Name = d.Name,
Surname = d.Surname
});
foreach (var n in names)
{
Console.WriteLine(n.Name + " " + n.Surname);
}
}
With that in place, the following code:
IEnumerable<INamable> list = new List<Foo>
{
new Foo() {Id = 1, Name = "FooName1", Surname = "FooSurname1"},
new Foo() {Id = 2, Name = "FooName2", Surname = "FooSurname2"}
};
ExtractUsingInterface(list);
// IEnumerable<object> list... will be fine for both solutions below
ExtractUsingReflection(list);
ExtractUsingDynamic(list);
will produce the expected output:
FooName1 FooSurname1
FooName2 FooSurname2
FooName1 FooSurname1
FooName2 FooSurname2
FooName1 FooSurname1
FooName2 FooSurname2
I'm sure you can fiddle with that and get to what you are trying to achieve.
var temp = list.Select(x => x.Name == "Name" && x.Surname == "Surname");
var temp = list.Select(x => new {Name = x.Name, Surname = x.Surname});

Compare and merge two List<T> into new List<T>

I'm trying to figure out how best to compare and merge two List<T> with a new List<T> being generated that compares multiple properties within each object.
class Account
{
public Account() { }
public string ID { get; set; }
public string Name { get; set; }
public string Value { get; set; }
}
List<Account> Mine = new List<Account>();
List<Account> Yours = new List<Account>();
List<Account> Ours = new List<Account>();
Account m1 = new Account(){ ID = null, Name = "C_First", Value = "joe" };
Account m2 = new Account(){ ID = null, Name = "C_Last", Value = "bloggs" };
Account m3 = new Account(){ ID = null, Name = "C_Car", Value = "ford" };
Mine.Add(m1);
Mine.Add(m2);
Mine.Add(m3);
Account y1 = new Account(){ ID = "1", Name = "C_First", Value = "john" };
Account y2 = new Account(){ ID = "2", Name = "C_Last", Value = "public" };
Yours.Add(y1);
Yours.Add(y2);
The resulting List<Account> Ours would have the following List<Account> objects:
{ ID = "1", Name = "C_First", Value = "joe" };
{ ID = "2", Name = "C_Last", Value = "bloggs" };
{ ID = null, Name = "C_Car", Value = "ford" };
I need to figure out how best to compare the ID and Value properties between both List<Account> objects where the List<Account> Yours ID takes precedence over the List<Account> Mine and the List<Account> Mine Value takes precedence over List<Account> Yours along with any object that's not in List<Account> Yours being added as well.
I've tried the following:
Ours = Mine.Except(Yours).ToList();
which results in List<Ours> being empty.
I've read this post Difference between two lists in which Jon Skeet mentions using a custom IEqualityComparer<T> to do what I need but I'm stuck on how to create an IEqualityComparer<T> that compares more than 1 property value.
Not sure if it can be done in "pue" LINQ, but a bit of procedural code would do the trick:
var dict = Yours.ToDictionary(y => y.Name);
foreach (var m in Mine) {
Account y;
if (dict.TryGetValue(m.Name, out y))
Ours.Add(new Account { ID = y.ID, Name = m.Name, Value = m.Value });
else
Ours.Add(m);
}
After that, printing Ours...
foreach (var o in Ours)
Console.WriteLine("{0}\t{1}\t{2}", o.ID, o.Name, o.Value);
...gives the following result:
1 C_First joe
2 C_Last bloggs
C_Car ford
Try this:
var index = Mine.ToDictionary(x => x.Name);
foreach(var account in Yours)
{
if(index.ContainsKey(account.Name))
{
var item = index[account.Name];
if(item.ID == null)
item.ID = account.ID;
}
index.Add(account.Name, account);
}
Ours = index.Values.ToList();
try this code for IEqualityComparer:
public class D : IEqualityComparer<Account>
{
public bool Equals(Account x, Account y)
{
return x.ID == y.ID && x.Value==y.Value;
}
public int GetHashCode(Account obj)
{
return obj.ID.GetHashCode() ^ obj.Value.GetHashCode();
}
}
used like this:
Ours = Mine.Except(Yours, new D()).ToList();

Categories