How do I use LINQ to update objects in one list from objects in a second list? My question is very similar to LINQ In Line Property Update During Join except that, in my case, the second list is smaller than the parent list.
In other words, I want to update those members of the master collection that have corresponding updates in a second collection. Otherwise I want the master object left unaltered. The technique in the article referenced above seems to result in an inner join of the two collections.
Thanks
The answer in the other article is fine for your problem too, as what you really want is an inner join. It's important to note that the inner join is only used to perform the function, it's not modifying the list (i.e. items that don't meet the inner join are left untouched in the list).
For completeness here's the solution I would use:
List<Person> people = new List<Person>();
people.Add( new Person{ Name = "Timothy", Rating = 2 } );
people.Add( new Person{ Name = "Joe", Rating = 3 } );
people.Add( new Person{ Name = "Dave", Rating = 4 } );
List<Person> updatedPeople = new List<Person>();
updatedPeople.Add( new Person { Name = "Timothy", Rating = 1 } );
updatedPeople.Add( new Person { Name = "Dave", Rating = 2 } );
ShowPeople( "Full list (before changes)", people );
Func<Person, Person, Person> updateRating =
( personToUpdate, personWithChanges ) =>
{
personToUpdate.Rating = personWithChanges.Rating;
return personToUpdate;
};
var updates = from p in people
join up in updatedPeople
on p.Name equals up.Name
select updateRating( p, up );
var appliedChanges = updates.ToList();
ShowPeople( "Full list (after changes)", people );
ShowPeople( "People that were edited", updatedPeople );
ShowPeople( "Changes applied", appliedChanges );
Here's the output I get:
Full list (before changes)
-----
Name: Timothy, Rating: 2
Name: Joe, Rating: 3
Name: Dave, Rating: 4
Full list (after changes)
-----
Name: Timothy, Rating: 1
Name: Joe, Rating: 3
Name: Dave, Rating: 2
People that were edited
-----
Name: Timothy, Rating: 1
Name: Dave, Rating: 2
Changes applied
-----
Name: Timothy, Rating: 1
Name: Dave, Rating: 2
Related
I guess there must be an easy way, but not finding it. I would like to check whether a list of items, appear (completely or partially) in another list.
For example: Let's say I have people in a department as List 1. Then I have a list of sports with a list of participants in that sport.
Now I want to count, in how many sports does all the people of a department appear.
(I know some tables might not make sense when looking at it from a normalisation angle, but it is easier this way than to try and explain my real tables)
So I have something like this:
var peopleInDepartment = from d in Department_Members
group d by r.DepartmentID into g
select new
{
DepartmentID = g.Key,
TeamMembers = g.Select(r => d.PersonID).ToList()
};
var peopleInTeam = from s in Sports
select new
{
SportID = s.SportID,
PeopleInSport = s.Participants.Select(x => x.PersonID),
NoOfMatches = peopleInDepartment.Contains(s.Participants.Select(x => x.PersonID)).Count()
};
The error here is that peopleInDepartment does not contain a definition for 'Contains'. Think I'm just in need of a new angle to look at this.
As the end result I would like print:
Department 1 : The Department participates in 3 sports
Department 2 : The Department participates in 0 sports
etc.
Judging from the expected result, you should base the query on Department table like the first query. Maybe just include the sports count in the first query like so :
var peopleInDepartment =
from d in Department_Members
group d by r.DepartmentID into g
select new
{
DepartmentID = g.Key,
TeamMembers = g.Select(r => d.PersonID).ToList(),
NumberOfSports = Sports.Count(s => s.Participants
.Any(p => g.Select(r => r.PersonID)
.Contains(p.PersonID)
)
)
};
NumberOfSports should contains count of sports, where any of its participant is listed as member of current department (g.Select(r => r.PersonID).Contains(p.PersonID))).
Now I have to sum the results by grouping the customers.
I have made a query but It is not working at all. Can anybody help me? Here is my query:
![var query=from p in context.Delivery
join o in
(from o1 in context.OrderTables select o1)
on p.OrderID equals o.OrderID
into go from po in go.DefaultIfEmpty()
join d in (from d1 in context.Diagram select d1)
on po.DiagramID equals d.DiagramID into gd
from pd in gd.Distinct()
group pd by pd.CustomerID into groupCustomer
join cu in context.CustomerCompanyTables
on groupCustomer.Key equals cu.CustomerID
select new { cu.CompanyName, SumNoTax = p.Sum(t => t.OrderID!=0 ? p.Price:d.Price)};][2]
As I noted in the comments your query seems massively over complicated:
The use of
…
join x in (from z in context.Xx select z) on …
rather than just:
…
join x in context.Xx on …
Those checks for non-exitant data (go.DefaultIfEmpty()): the form
of join you are using is an inner join: data will be returned
only when matching objects exist for both sides of the condition.
But in the end, your problem is refering back to the original collections
in the final select clause rather than to the result of the group by.
And of course there is no SellingPrice in PdtDeliveryTable also
used in that final select clause.
My approach with queries like this is to build things up incrementally,
ensuring I understand what I am doing with each step.
So the first step is to do the join. For this purpose I've defined
a slightly simpler structure to try and keep things clear (see the
bottom of this answer for definitions) and some test data. I'm using
LINQ to Objects, but the synatax and semantics of the LINQ operators
is the same (and it saves creating a more complex project & database).
A customer has multiple orders, each with a single Sku (stock control
unit—a product). The order can optionally override the default price
of the sku (hence using a Nullable<decimal>). There is also some
test data.
The first step is to check I'm doing the join right, and check
I'm handling that price override:
var ctx = GetTestData();
var query = from c in ctx.Customers
join o in ctx.Orders on c.CustomerId equals o.CustomerId
join s in ctx.Skus on o.SkuId equals s.SkuId
select new { Customer = c, Order = o, Sku = s };
Console.WriteLine("CustId\tCust\t\tOrder\tSku\tPaid");
foreach (var v in query) {
Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}",
v.Customer.CustomerId,
v.Customer.Name,
v.Order.OrderId,
v.Sku.SkuId,
v.Order.SpecificPrice ?? v.Sku.DefaultPrice);
}
which results in
CustId Cust Order Sku Paid
1 Acme Corp 1 1 10.0
1 Acme Corp 2 2 15.0
1 Acme Corp 3 3 30.0
2 Beta Corp 4 1 9.99
Note the only data is where there are matching objects of each type, so
there is nothing from the third customer (has no orders) and only one row
from the second customer (doesn't have any orders for other SKUs): there
are no null objects to remove.
The second step is to perform the grouping. This will result in
a rather different data structure something like:
class AnonOfGroup : IEnumerable<AnonOfRow> {
KeyType Key;
}
where AnonOfRow is the type of whatever you are grouping.
Thus:
var query = from c in ctx.Customers
join o in ctx.Orders on c.CustomerId equals o.CustomerId
join s in ctx.Skus on o.SkuId equals s.SkuId
select new { Customer = c, Order = o, Sku = s } into joinRes
group joinRes by joinRes.Customer.CustomerId into g
select g;
has, using the above terms the type of Key being the type of CustomerId and
AnonOfRow being the type of joinRes which is the type of the first
select clause.
This can be shown with a double loop:
Outer over the each different group (with the same key)
Inner over each object in each group (created by the first select
clause)
so:
Console.WriteLine("Customer");
Console.WriteLine("\tOrder\tSku\tPrice");
foreach (var grp in query) {
Console.WriteLine("{0}: {1}", grp.Key, grp.First().Customer.Name);
foreach (var row in grp) {
Console.WriteLine("\t{0}\t{1}\t{2}",
row.Order.OrderId,
row.Sku.SkuId,
row.Order.SpecificPrice ?? row.Sku.DefaultPrice);
}
}
gives:
Customer
Order Sku Price
1: Acme Corp
1 1 10.0
2 2 15.0
3 3 30.0
2: Beta Corp
4 1 9.99
Note also that I'm alble to access the "inner data" from the outer loop
by performing some enumeration (in this case I know the customer is
the same in each inner object so I'll use the first). This is safe
because the underlying join is an inner join.
The Final Step is to sum over each order within the group. This can
be done for each group in the final select cluase. This will be processed
once for each group, but within that clause we can aggregate
over the rows in that group:
var query = from c in ctx.Customers
join o in ctx.Orders on c.CustomerId equals o.CustomerId
join s in ctx.Skus on o.SkuId equals s.SkuId
select new { Customer = c, Order = o, Sku = s } into joinRes
group joinRes by joinRes.Customer.CustomerId into g
select new {
CustomerId = g.Key,
CustomerName = g.First().Customer.Name,
TotalPrice = g.Sum(r => r.Order.SpecificPrice ?? r.Sku.DefaultPrice)
};
Console.WriteLine("Cust\tName\t\tTotal");
foreach (var row in query) {
Console.WriteLine("{0}\t{1}\t{2}", row.CustomerId, row.CustomerName, row.TotalPrice);
}
In this case the aggregation changes the list of lists into a (flat)
list, so only needing a single loop to see all the data. The use of
the null check within the summation means that there are no nulls:
Cust Name Total
1 Acme Corp 55.0
2 Beta Corp 9.99
which is clearly correct for the input data.
Your Solution should just be a matter of substituting your types, adding the fourth
as an extra join, and adjusting to the slightly different types.
class Customer {
public int CustomerId;
public string Name;
}
class Sku {
public int SkuId;
public decimal DefaultPrice;
}
class Order {
public int OrderId;
public int CustomerId;
public int SkuId;
public decimal? SpecificPrice;
}
class Context {
public List<Customer> Customers;
public List<Sku> Skus;
public List<Order> Orders;
}
static Context GetTestData() {
var customers = new List<Customer> {
new Customer { CustomerId = 1, Name = "Acme Corp" },
new Customer { CustomerId = 2, Name = "Beta Corp" },
new Customer { CustomerId = 3, Name = "Gamma Corp" }
};
var skus = new List<Sku> {
new Sku { SkuId = 1, DefaultPrice = 10.0m },
new Sku { SkuId = 2, DefaultPrice = 20.0m },
new Sku { SkuId = 3, DefaultPrice = 30.0m }
};
var orders = new List<Order> {
new Order { OrderId = 1, CustomerId = 1, SkuId = 1 },
new Order { OrderId = 2, CustomerId = 1, SkuId = 2, SpecificPrice = 15.0m },
new Order { OrderId = 3, CustomerId = 1, SkuId = 3 },
new Order { OrderId = 4, CustomerId = 2, SkuId = 1, SpecificPrice = 9.99m }
};
return new Context {
Customers = customers,
Skus = skus,
Orders = orders
};
}
To simplify the scenario, let's say we have a list of People with FirstName and LastName properties.
Our data looks like this:
Bob Smith
Jane Smith
Todd Grover
Larry Lewis
Jill Lewis
Frank Lewis
The first step would be to add an Integer property that gets incremented for each item:
Bob Smith 1
Jane Smith 2
Todd Grover 3
Larry Lewis 4
Jill Lewis 5
Frank Lewis 6
Ideally, I'd like to reset the counter for every new group to achieve this:
Bob Smith 1
Jane Smith 2
Todd Grover 1
Larry Lewis 1
Jill Lewis 2
Frank Lewis 3
Maybe LINQ isn't appropriate. It just seems like LINQ should be able to do this elegantly.
If you just want a counter that increments with each item:
var query = people
.Select((x, i) => new { x.FirstName, x.LastName, Index = i + 1 });
Or if you want to reset the counter for each group, grouped by LastName:
var query = people
.GroupBy(x => x.LastName)
.Select
(
x => x.Select((y, i) => new { y.FirstName, y.LastName, Index = i + 1 })
)
.SelectMany(x => x);
And to quickly display the query results:
foreach (var item in query)
{
Console.WriteLine(item.FirstName + "\t" + item.LastName + "\t" + item.Index);
}
You can use the second overload of the Select method, which incorporates an index parameter passed to the lambda expression, for example:
var people = new [] {
new { FirstName = "Bob", LastName = "Smith"},
new { FirstName = "Jane", LastName = "Smith"},
new { FirstName = "Todd", LastName = "Grover"},
new { FirstName = "Larry", LastName = "Lewis"},
new { FirstName = "Jill", LastName = "Lewis"},
new { FirstName = "Frank", LastName = "Lewis"},
}.ToList();
people.Select((p, index) => new {
FirstName = p.FirstName,
LastName = p.LastName,
Index = index
}
);
Result:
Assuming there is an integer property already on the record type, and the collection is already sorted, then you can abuse Aggregate (i.e. left-fold), something like this
collection.Aggregate( (old, next) => { if (namesDiffer(old, next)) next.Index = 1 else next.Index = old.Index +1; return next;}, aRecordWithEmptyName);
EDIT -- fixed return value; fingers had been on autopilot.
I've got the following code:
List<Person> people = new List<Person>
{
new Person{ Id = 1, Name = "Bob"},
new Person{ Id = 2, Name = "Joe"},
new Person{ Id = 3, Name = "Bob"}
};
var peopleGroupedByName = from p in people
group p by p.Name;
//get all groups where the number of people in the group is > 1
For the life of me I can't figure out how to work with the values returned by the linq query to be able to then filter all of the groups that were returned so that I only have the groups that have more than one item in them.
At the moment I'm banging my head against a wall and I can't quite think of what keywords to use in a google search in order to figure it out for myself.
I'd really appreciate any help on how to do this in Linq cause it seems like it should be very simple to do.
List<Person> people = new List<Person> {
new Person{ Id = 1, Name = "Bob"},
new Person{ Id = 2, Name = "Joe"},
new Person{ Id = 3, Name = "Bob"}
};
var peopleGroupedByName = from p in people
group p by p.Name into peopleGroup
where peopleGroup.Count() > 1
select peopleGroup;
//get all groups where the number of people in the group is > 1
Alternatively, where peopleGroup.Skip(1).Any() as Mehrdad has suggested will generally provide better performance with Linq to Objects since Count() iterates over the entire group's contents, and Skip(1).Any() merely over the first 2 elements - (see his comment for details; Count is fine for group-by clauses).
Aside: For readability, I prefer consistently using either the .GroupBy(... extension method syntax or the group ... by ... into ... query syntax but not both.
var peopleGroupedByName = people.GroupBy(p => p.Name)
.Where(g => g.Count() > 1);
var peopleGroupedByName = from p in people
group p by p.Name into g
where g.Count() > 1
select g;
This is actually quite easy.
var filtererGroups = people
.GroupBy(p => p.Name)
.Where(grp => grp.Count() > 1);
To filter by key you would do something like that.
var filtererGroups = people
.GroupBy(p => p.Name)
.Where(grp => grp.Key == "Bob");
I have a single table and I need to build a bunch of nested objects based on the single table.
Data:
PointA PointB Month Time Price
1 2 11 11:00 10.99
1 2 12 11:00 9.99
Objects are
POINTS {PointA, PointB, Details}
Details {Month, ExtraDetails}
ExtraDetails {Time, Price}
I want to avoid having loads of loops and if statements, so should be able to use linq to do this. but its beyond my linq experience.
edit: These need grouping aswell
any help would be great.
Thanks
Just tried out a solution:
var nestedObjects = from row in data
select new {row.PointA, row.PointB, Details = new {
row.Month, ExtraDetails = new {
row.Time, row.Price
}
}};
This is assuming that you have already got your data into data.
Group by
If you want to group the Points together, you need 'Group By':
var nestedObjects = from row in data
group row by new { row.PointA, row.PointB } into Points
select new {
Points = Points.Key,
Details = from details in Points
select new { row.Month, ExtraDetails = new {
row.Time, row.Price
}}
};
A little more complicated - of course you might want to group by month as well, in which case, you need to follow the same pattern as for the Points bit. Note, this will not create tables, because the group by doesn't quite do that, but it at least creates the structure for you.
Assuming you got your classes defined for the objects you mentioned, and you have a constructor or properties so you can propery create the object in one line you could have a LINQ query returning a list of a POINTS.
If would go something lik this :
var res =
from item in table.AsEnumerable()
select new Points(){PointA = item["PointA"];
PointB = item["PointB"];
Details = from item2 in table.AsEnumberable()
where item["PointA"] = item2["PointA"] and item["PointB"] = item2["PointB"]
select new Details(){
month=item2["month"],
extraDetails = from item3 in table.AsEnumerable()...
}
};
At the end res will be a IEnumerable of Points
I am sorry for the code, I am not at a computer with .NET 3.5 so I cannot write a proper testable query