Related
I have some data which represents parent-child relationship on the same entity. Given a node, I need to find its entire upper hierarchy (parent, grand-parent, great grand-parent, etc..)
My entity is like this:
public partial class Location{
public int LocationId { get; set; }
public int? FkParentLocationId { get; set; }
..... more properties here.......
public virtual Location FkParentLocation { get; set; }
public virtual ICollection<Location> InverseFkParentLocation { get; set; }
}
I'm referring to the Hierarchy traverse implementation suggested here, but it works when you go down the hierarchy. How would retrieve the upper hierarchy using LINQ?
Sample data:
List<Location> locations = new List<Location> {
new Location { LocationId = 5, FkParentLocationId = 3, LocationName = "Windsor", LocationDisplayName = "Windsor"},
new Location { LocationId = 15, FkParentLocationId = 3, LocationName = "Hampshire", LocationDisplayName = "Hampshire" },
new Location { LocationId = 12, FkParentLocationId = 3, LocationName = "Sussex", LocationDisplayName = "Sussex"},
new Location { LocationId = 13, FkParentLocationId = 3, LocationName = "Willowood", LocationDisplayName = "Willowood"},
new Location { LocationId = 1, FkParentLocationId = 3, LocationName = "Gerbshire", LocationDisplayName = "Gerbshire"},
new Location { LocationId = 3, FkParentLocationId = 2, LocationName = "Lincoln", LocationDisplayName = "Lincoln"},
new Location { LocationId = 2, LocationName = "Mains", LocationDisplayName = "Mains" } };
Expected output: given location Id:5, I should get a list containing the locations 3 and 2 (as they are the parents).
Here is an approach you could use, demoed with a console app. Heavily borrowing from Jon Skeet.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Locations
{
public partial class Location
{
public int LocationId { get; set; }
public int? FkParentLocationId { get; set; }
public virtual Location FkParentLocation { get; set; }
public virtual ICollection<Location> InverseFkParentLocation { get; set; }
public string LocationName { get; set; }
public string LocationDisplayName { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Location> locations = new List<Location> {
new Location { LocationId = 5, FkParentLocationId = 3, LocationName = "Windsor", LocationDisplayName = "Windsor"},
new Location { LocationId = 15, FkParentLocationId = 3, LocationName = "Hampshire", LocationDisplayName = "Hampshire" },
new Location { LocationId = 12, FkParentLocationId = 3, LocationName = "Sussex", LocationDisplayName = "Sussex"},
new Location { LocationId = 13, FkParentLocationId = 3, LocationName = "Willowood", LocationDisplayName = "Willowood"},
new Location { LocationId = 1, FkParentLocationId = 3, LocationName = "Gerbshire", LocationDisplayName = "Gerbshire"},
new Location { LocationId = 3, FkParentLocationId = 2, LocationName = "Lincoln", LocationDisplayName = "Lincoln"},
new Location { LocationId = 2, LocationName = "Mains", LocationDisplayName = "Mains" } };
var result = GetAncestorsIds(locations, 5);
foreach (var id in result)
{
System.Console.WriteLine(id);
}
}
private static IEnumerable<int> GetAncestorsIds(List<Location> locations, int id)
{
Location location = locations.SingleOrDefault(l => l.LocationId == id);
if(location != null)
{
while(location != null && location.FkParentLocationId != null)
{
location = locations.SingleOrDefault(l => l.LocationId == location.FkParentLocationId);
if(location != null)
{
yield return location.LocationId;
}
}
}
}
}
}
And this approach can be turned into your own Linq extension. Here's how it could look.
public static class MyExtensions
{
public static IEnumerable<int> GetAncestorIds<TSource>(this IEnumerable<TSource> source, Func<TSource, int> pk, Func<TSource, int?> fk, int id)
{
TSource currentObj = source.SingleOrDefault(s => pk(s) == id);
while(currentObj != null && fk(currentObj) != null)
{
currentObj = source.SingleOrDefault(s => pk(s) == fk(currentObj));
if(currentObj != null)
{
yield return pk(currentObj);
}
}
}
}
and then to call this for your scenario you would do this
var result = locations.GetAncestorIds(l => l.LocationId, l => l.FkParentLocationId, 5);
Let's say I have two tables, parent table 'P' and sub-table 'S', I usually wrote the Linq like this to get what I want:
var rows = from p in db.P
join s in db.S on p.Id equals s.ParentId into subContent
where (some condition here)
select new{
Id = p.Id,
Title = p.Title
SubContentCount = subContent.Count()
}
It's very simple, but if for some reason I have to pass a parameter into this query when there has one (let's say 'key'), I have to do this (I guess :-):
var rows = from p in db.P
join s in db.S on p.Id equals s.ParentId into subContent
where (some condition here)
select p;
if(!string.IsNullOrEmpty(key)){ // I'm using C#
rows = rows.Where(q => q.Title.Contains(key))
}
And then:
var list = rows.Select(q => new ()
{
Id = q.Id,
Title = q.Title,
subCount = ???.Count()
});
Is that passable to do Linq like this? if so, how?
Thanks for any kind help!
You could create a method that receives a Func<Table, bool>as parameter and use it to filter your dataset:
public static void Main(string[] args)
{
var rows = new List<Table>
{
new Table { Id = 1, Title = "A", SubContent = new [] { "A1" } },
new Table { Id = 2, Title = "B", SubContent = new [] { "B1", "B2" } },
new Table { Id = 3, Title = "C", SubContent = new [] { "C1", "C2", "C3" } },
};
var title = "C";
foreach (var item in Filter(rows, table =>
String.IsNullOrEmpty(title) || table.Title == title))
{
Console.WriteLine(
"Title={0}, SubContent.Length={1}",
item.Title, item.SubContent.Length);
}
}
public static List<Table> Filter(List<Table> original, Func<Table, bool> filter)
{
return original.Where(filter).ToList();
}
public class Table
{
public int Id { get; set; }
public string Title { get; set; }
public string[] SubContent { get; set; }
}
Why not include the filter in the where clause?
where string.IsNullOrEmpty(key) || p.Title.Contains(key)
Quick example in the interactive console:
public class Parent { public int Id {get; set;} public string Title {get; set;} }
public class SubTable { public int Id {get; set;} public int ParentId {get; set;} }
public class Result { public int Id {get; set;} public string Title {get; set;} public int SubContentCount {get; set;} }
var p1 = new Parent() { Id = 1, Title = "Parent_1" };
var p2 = new Parent() { Id = 2, Title = "Parent_2" };
var p3 = new Parent() { Id = 3, Title = "Parent_3" };
var s1_1 = new SubTable() { Id = 11, ParentId = 1 };
var s1_2 = new SubTable() { Id = 12, ParentId = 1 };
var s1_3 = new SubTable() { Id = 13, ParentId = 1 };
var s2_1 = new SubTable() { Id = 21, ParentId = 2 };
var s2_2 = new SubTable() { Id = 22, ParentId = 2 };
var s3_1 = new SubTable() { Id = 31, ParentId = 3 };
var db_P = new List<Parent>() { p1, p2, p3 };
var db_S = new List<SubTable>() { s1_1, s1_2, s1_3, s2_1, s2_2, s3_1 };
public IEnumerable<Result> GetResults(string key = null)
{
var rows = from p in db_P
join s in db_S on p.Id equals s.ParentId into subContent
where string.IsNullOrEmpty(key) || p.Title.Contains(key)
select new Result() {
Id = p.Id,
Title = p.Title,
SubContentCount = subContent.Count()
};
return rows;
}
And example output (formatted onto multiple lines for readability)
> GetResults().ToList()
List<Submission#0.Result>(3) {
Submission#0.Result { Id=1, SubContentCount=3, Title="Parent_1" },
Submission#0.Result { Id=2, SubContentCount=2, Title="Parent_2" },
Submission#0.Result { Id=3, SubContentCount=1, Title="Parent_3" }
}
> GetResults("1").ToList()
List<Submission#0.Result>(1) {
Submission#0.Result { Id=1, SubContentCount=3, Title="Parent_1" }
}
>
Consider tables
Inventions, list of inventions
Components, list of all components available for use in inventions, and
InventionComponents, list of utilized components, with count, in an invention
For a given invention, &inventionID, I would like to do a 'covering' left join to all of the components instead of just the utilized components.
SQL would be something like
select
I.name as inventionName
, C.name as componentName
, coalese (IC.count, 0) as componentCount
from
(select &inventionID as inventionID, ID, name from components) C -- all components applied to some &inventionID
left join
inventionComponents IC
on
C.ID = IC.ComponentID
and C.inventionID = IC.inventionID
join
inventions I
on
I.ID = C.inventionID
Sample data and Linq query in .NET fiddle at https://dotnetfiddle.net/cx4bHp results in an exception
[System.NullReferenceException: Object reference not set to an instance of an object.]
Question: How should the Linq query be modified to perform the desired covering query ?
For completeness, the C# fiddle code is repeated here
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
var components = new List<Component>{
new Component { ID=1, Name = "Florgebit" },
new Component { ID=2, Name = "Phadron" },
new Component { ID=3, Name = "Goobstem" },
new Component { ID=4, Name = "Larchwren" },
new Component { ID=5, Name = "Zangponder" },
new Component { ID=6, Name = "Spoofork" },
new Component { ID=7, Name = "Forkoon" },
new Component { ID=8, Name = "Blidget" },
new Component { ID=9, Name = "Wazzawim" },
new Component { ID=10, Name = "Klackberg" },
};
var inventions = new List<Invention>{
new Invention { ID=21, Name = "Swazzlute" },
new Invention { ID=22, Name = "Corpocran" },
new Invention { ID=23, Name = "Fillyboof" },
};
var inventionComponents = new List<InventionComponent>{
new InventionComponent { ID=100, InventionID=21, ComponentID=1, Count=1 },
new InventionComponent { ID=101, InventionID=21, ComponentID=2, Count=2 },
new InventionComponent { ID=102, InventionID=21, ComponentID=8, Count=3 },
new InventionComponent { ID=103, InventionID=23, ComponentID=5, Count=4 },
new InventionComponent { ID=104, InventionID=23, ComponentID=6, Count=5 },
new InventionComponent { ID=105, InventionID=23, ComponentID=3, Count=4 },
new InventionComponent { ID=106, InventionID=21, ComponentID=4, Count=3 },
new InventionComponent { ID=107, InventionID=22, ComponentID=5, Count=2 },
new InventionComponent { ID=108, InventionID=22, ComponentID=4, Count=1 },
new InventionComponent { ID=109, InventionID=22, ComponentID=1, Count=6 },
new InventionComponent { ID=110, InventionID=22, ComponentID=7, Count=1 },
new InventionComponent { ID=111, InventionID=21, ComponentID=9, Count=1 },
};
var details =
from A in inventions
join B in inventionComponents on A.ID equals B.InventionID
join C in components on B.ComponentID equals C.ID
orderby A.Name, C.Name
select new {
InventionName = A.Name,
ComponentName = C.Name,
ComponentCount = B.Count
};
/*
foreach(var d in details)
{
Console.WriteLine("Invention: {0}, Component: {1}, Count: {2}", d.InventionName, d.ComponentName, d.ComponentCount);
}
*/
var inventionID = 22;
var index = 1;
// want full coverage of inventionID, componentID with applied counts
// 22,1,6
// 22,2,**0**
// 22,3,**0**
// 22,4,1
// 22,5,2
// 22,6,**0**
// 22,7,1
// 22,8,**0**
// 22,9,**0**
// 22,10,**0**
var corpcheck =
from C in components select new { InventionID = inventionID, ComponentID = C.ID, ComponentName = C.Name } into allcomps
join B in inventionComponents on new { allcomps.InventionID, allcomps.ComponentID } equals new { B.InventionID, B.ComponentID } into join1
// from j1 in Join1 // inner join
from j1 in join1.DefaultIfEmpty() // causes exception
orderby allcomps.ComponentName
select new {
RowNum = index++,
InventionID = allcomps.InventionID,
ComponentName = allcomps.ComponentName,
ComponentCount = j1.Count,
};
foreach(var x in corpcheck)
{
Console.WriteLine("InventionID: {0}, RowNum: {1}, ComponentName: {2}, Count: {3}", x.InventionID, x.RowNum, x.ComponentName, x.ComponentCount);
}
}
public class Invention
{
public int ID { get; set; }
public string Name { get; set; }
}
public class InventionComponent
{
public int ID { get; set; }
public int InventionID { get; set; }
public int ComponentID { get; set; }
public int Count { get; set; }
}
public class Component
{
public int ID { get; set; }
public string Name { get; set; }
}
}
By adding DefaultIfEmpty(), j1 can be null for some components. If j1 is null, I assume you want count to be 0:
from C in components select new { InventionID = inventionID, ComponentID = C.ID, ComponentName = C.Name } into allcomps
join B in inventionComponents on new { allcomps.InventionID, allcomps.ComponentID } equals new { B.InventionID, B.ComponentID } into join1
from j1 in join1.DefaultIfEmpty()
orderby allcomps.ComponentName
select new {
RowNum = index++,
InventionID = allcomps.InventionID,
ComponentName = allcomps.ComponentName,
ComponentCount = j1 == null ? 0 : j1.Count, // add null check
};
In LINQ-to-objects you could also use ComponentCount = j1?.Count ?? 0. but I assume you're going to use this in LINQ to a SQL backend.
I'm trying to perform a nested linq query.
public class Sic
{
public int Id { get; set; }
public string Code { get; set; }
}
public class Message
{
public List<Sic> Sics { get; set; }
public int Id { get; set; }
}
List<Message> msgList = new List<Message>();
Message m1 = new Message
{
Id = 0,
Sics = new List<Sic>()
{
new Sic() {Id = 0, Code = "A2A"},
new Sic() {Id = 1, Code = "A2B"},
new Sic() {Id = 2, Code = "A2C"},
new Sic() {Id = 3, Code = "A2D"}
}
};
Message m2 = new Message
{
Id = 1,
Sics = new List<Sic>()
{
new Sic() {Id = 4, Code = "B2A"},
new Sic() {Id = 5, Code = "B2B"},
new Sic() {Id = 6, Code = "B2C"},
new Sic() {Id = 7, Code = "B2D"}
}
};
msgList.Add(m1);
msgList.Add(m2);
List<string> searchList = new List<string> {"A2A", "A2B"};
I want to find messages in msgList where searchList is contained by the Sics of each each message, i.e. m1 should be found using the above searchList.
You can use something like,
msgList.Where(msg=>msg.Sics.Any(sic=>searchList.Contains(sic.Code)));
or if you need to match all search terms,
msgList.Where(msg => searchList.All(searchTerm=>msg.Sics.Any(sic=>sic.Code==searchTerm)))
If all ids in searchlist need to be matched
msgList.Where(msg => searchList.Any() && searchList.All(s => msg.Sics.Any(sic => s==sic.Code)))
If any id in searchlist need to be matched
msgList.Where(msg=>msg.Sics.Any(sic=>searchList.Contains(sic.Code)));
Problem is with the addresses not being outputted
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LinqToObjects
{
class Program
{
static void Main(string[] args)
{
var customers = Customer.GetAllCustomers();
var addresses = Address.GetAllAddresses();
var addressRelations = AddressRelation.GetAllAddressRelations();
var results = customers
.Join(addressRelations,
c => c.CustomerID,
ar => ar.CustomerID,
(c, ar) => new
{
CustomerName = c.FirstName + " " + c.LastName,
CustomerID = c.CustomerID,
AddressRelID = ar.AddressID
});
var resultsJoined = results
.GroupJoin(addresses,
ar => ar.AddressRelID,
a => a.AddressID,
(ar, a) => new
{
CustomerName = ar.CustomerName,
AddressLine = addresses.Select(b => b.StreetAddress).FirstOrDefault()
});
foreach(var item in resultsJoined)
{
Console.WriteLine(item.CustomerName);
Console.WriteLine(item.AddressLine);
Console.WriteLine("-----------------");
}
}
}
public class AddressRelation
{
public int AddressRelationID { get; set; }
public int CustomerID { get; set; }
public int AddressID { get; set; }
public AddressRelation(int id, int customerId, int addressId)
{
AddressRelationID = id; CustomerID = customerId; AddressID = addressId;
}
public static List<AddressRelation> GetAllAddressRelations()
{
var AllAddressRelations = new List<AddressRelation>();//simulate data returned from db
var addressRelation1 = new AddressRelation(1, 1, 1);
var addressRelation2 = new AddressRelation(2, 3, 3);
var addressRelation3 = new AddressRelation(3, 2, 2);
AllAddressRelations.Add(addressRelation1);
AllAddressRelations.Add(addressRelation2);
AllAddressRelations.Add(addressRelation3);
return AllAddressRelations;
}
}
public class Address
{
public int AddressID { get; set; }
public string StreetAddress { get; set; }
public Address(int id, string streetAddress)
{
AddressID = id; StreetAddress = streetAddress;
}
public static List<Address> GetAllAddresses()
{
var AllAddresses = new List<Address>();
Address customer1Address = new Address(1, "Elm St");
Address customer2Address = new Address(2, "Willow Way");
Address customer3Address = new Address(3, "Linq Ln");
AllAddresses.Add(customer1Address);
AllAddresses.Add(customer2Address);
AllAddresses.Add(customer3Address);
return AllAddresses;
}
}
public class Customer
{
public int CustomerID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Customer(int id,string firstName, string lastName)
{
CustomerID = id; FirstName = firstName; LastName = lastName;
}
public static List<Customer> GetAllCustomers()
{
var AllCustomers = new List<Customer>();
var customer1 = new Customer(1, "James", "T");
var customer2 = new Customer(2, "Donnie", "H");
var customer3 = new Customer(3, "Sarah", "H");
AllCustomers.Add(customer1);
AllCustomers.Add(customer2);
AllCustomers.Add(customer3);
return AllCustomers;
}
}
}
The query isn't very expressive. If I was going to join three lists using LinqToObjects, I'd do this:
var query =
from c in customers
join xr in addressRelations on c.CustomerId equals xr.CustomerId
join a in addresses on xr.AddressId equals a.AddressId
select new {Customer = c, Address = a};
Looks like another mistake. I bet that AddressRelId is the key to the AddressRelation table, and not what you want use to connect to the Address table.
.GroupJoin(addresses,
ar => ar.Address**Rel**ID,
a => a.AddressID,
In response to comment:
var query = customers
.Join(addressRelations,
c => c.CustomerId,
xr => xr.CustomerId,
(c, xr) => new {c, xr})
.Join(addresses,
x => x.xr.AddressId,
a => a.AddressId,
(x, a) => new {c = x.c, xr = x.xr, a = a})
.Select(x => new {Customer = x.c, Address = x.a});
It's returning the first customer address because you've told it to:
AddressLine = addresses.Select(b => b.AddressLine1).FirstOrDefault()
Here, addresses is all addresses. I suspect you just want:
AddressLine = a.Select(b => b.AddressLine1).FirstOrDefault()