I have a list of the following class:
public class SiloRelationship
{
public int RelationshipType { get; set; }
public string MasterKey { get; set; }
public string SlaveKey { get; set; }
public int QueryId { get; set; }
}
I have a second list of the following class:
public class SiloNode
{
public string Key { get; private set; }
public string Url { get; private set; }
public List<NodeQuery> Queries { get; private set; }
}
Which has a sub-class:
public class NodeQuery
{
public string Query { get; private set; }
public int Seq { get; private set; }
}
Lists:
LandingSilo.Relationships is a list of SiloRelationship
LandingSilo.Nodes is a list of SiloNode.
Here's my query - there is a simple join, after which I need to return the Url and Query properties - the filter should result in a single QueryNode from the list.
What we have is:
SiloRelationship => 1 to 1 SiloNode => 1 to many QueryNode
A Kvp would be adequate for the purpose of the exercise but I can't see the Query property with the code I've got so far.
var query =
from r in LandingSilo.Relationships
join n in LandingSilo.Nodes on r.SlaveKey equals n.Key
where r.RelationshipType == 1 &&
n.Queries.Select(y => y.Seq).Contains(r.QueryId)
Any help appreciated.
Try this:
IEnumerable<string> queries = LandingSilo.Relationships
.Where(r => r.RelationshipType == 1)
.Join(
LandingSilo.Nodes,
r => r.SlaveKey,
n => n.Key,
(r, n) => n.Queries.SingleOrDefault(q => q.Seq == r.QueryId))
.Where(q => q != null)
.Select(q => q.Query);
Line by line: filter all Relationships with type different from 1, join on SlaveKey/Key and select the only query in the node that has the Seq equal to the Relationships QueryId. Filter out null results and select the Query property. This is going to throw an InvalidOperationException if there are multiple queries within one node matching.
This can be also done in the LINQ keyword syntax like this:
IEnumerable<string> queries =
from r in LandingSilo.Relationships
where r.RelationshipType == 1
join n in LandingSilo.Nodes on r.SlaveKey equals n.Key
from q in n.Queries.SingleOrDefault(q => q.Seq == r.QueryId)
where q != null
select q.Query;
You just need to filter the Queries. Change last statement like below
select n.Queries.FirstOrDefault(q => q.Seq == q.QueryId);
Related
So the equivalent of this query:
select * from car
left join parts ON car.Id = parts.carId
where parts.MemberId = 1
is this, in EntityFrameworkCore LINQ , using an IQueryable which has already selected car.Include(x => x.parts):
queryable = queryable.Where(x =>
x.parts.Select(y => y.MemberId).Contains(1);
But how can I convert the following SQL to LINQ, so that it includes rows from the left car table that have no respective MemberId entries in the parts table?
select * from car
left join parts ON car.Id = parts.CarId and parts.MemberId = 1
Models:
public class Car
{
public int Id { get; set; }
public virtual ICollection<Part> Parts { get; set; }
}
public class Parts
{
public int Id { get; set; }
public int CarId { get; set; }
public virtual Car { get; set; }
public int MemberId { get; set; }
}
A filtered Include does exactly what you want:
var cars = context.Cars
.Include(c => c.Parts.Where(p => p.MemberId == 1));
This doesn't generate the shorter join statement with a composite condition, but an outer join to a filtered subquery on Parts, to the same effect.
queryable = queryable
.Where(x => x.parts.Select(y => y.MemberId).Contains(1) || !x.parts.Any());
Try it like that:
queryable = queryable.Include(x => x.parts).Where(x =>
x.parts.Any(y => y.MemberId == 1).ToList();
Given the following Models
public class ApiImageModel
{
public int ID { get; set; }
...
public List<TagModel> Tags { get; set; } = new();
}
and
public class TagModel
{
public int ID { get; set; }
...
public string Name { get; set; }
public List<ApiImageModel> Images { get; set; } = new();
}
How to query a list of ApiImageModel based on a given set of TagModels using Linq?
I am struggling with this for a while now and I'm certainly missing something basic but I can't put a pin on it.
I tried this approach for EF6:
EF6 How to query where children contains all values of a list
like so, holding all TagModel-IDs in an array "tagIDs":
int[] tagIDs;
...
IQueryable<ApiImageModel> images = context.Images.Where(image => tagIDs.All(id => image.Tags.Any(tag => tag.ID == id)));
But visual studio rewards me with an "InvalidOperationException":
The LINQ expression 'DbSet<ApiImageModel>()
.Where(a => __tagIDs_0
.All(id => DbSet<Dictionary<string, object>>("ApiImageModelTagModel")
.Where(a0 => EF.Property<Nullable<int>>(a, "ID") != null && object.Equals(
objA: (object)EF.Property<Nullable<int>>(a, "ID"),
objB: (object)EF.Property<Nullable<int>>(a0, "ImagesID")))
.Join(
inner: DbSet<TagModel>(),
outerKeySelector: a0 => EF.Property<Nullable<int>>(a0, "TagsID"),
innerKeySelector: t => EF.Property<Nullable<int>>(t, "ID"),
resultSelector: (a0, t) => new TransparentIdentifier<Dictionary<string, object>, TagModel>(
Outer = a0,
Inner = t
))
.Any(ti => ti.Inner.ID == id)))' could not be translated.
I'd be glad for some help :)
Assuming that your tags tagIDs are unique, you can do the following:
int[] tagIDs;
var tagCount = tagIDs.Length;
...
var images = context.Images
.Where(image => image.Tags.Where(tag => tagIDs.Contains(tag.ID)).Count() == tagCount);
Here we use Contains to grab tags in which we are interested and if their Count() is equal to tagIDs.Length - all tags are present in image's Tags relation.
I have SQL query like this
SELECT T.*
FROM
(
SELECT ServiceRecords.DistrictId, Districts.Name as DistrictName, COUNT(Distinct(NsepServiceRecords.ClientRegNo)) AS ClientsServedCount
FROM ServiceRecords
INNER JOIN Districts ON ServiceRecords.DistrictId = Districts.ID
INNER JOIN NsepServiceRecords ON NsepServiceRecords.ServiceRecordId = ServiceRecords.Id
WHERE ServiceRecords.CreatedAtUtc >= #StartDate
AND ServiceRecords.CreatedAtUtc <= #EndDate
AND ServiceRecords.DistrictId = #DistrictId
GROUP BY ServiceRecords.DistrictId, Districts.Name
) AS T
ORDER BY T.DistrictName ASC, T.DistrictId
Query results:
DistrictId DistrictName ClientsServedCount
8d059005-1e6b-44ad-bc2c-0b3264fb4567 Bahawalpur 117
27ab6e24-50a6-4722-8115-dc31cd3127fa Gujrat 492
14b648f3-4912-450e-81f9-bf630a3dfc72 Jhelum 214
8c602b99-3308-45b5-808b-3375d61fdca0 Lodhran 23
059ffbea-7787-43e8-bd97-cab7cb77f6f6 Muzafarghar 22
580ee42b-3516-4546-841c-0bd8cef04df9 Peshawar 211
I'm struggling converting this to LINQ to entities query. I want to get same results (except District Id column) using LINQ.
I have tried like this, but not working as expected. Can somebody tell me what I'm doing wrong?
_dbContext.ServiceRecords
.Include(x => x.District)
.Include(x=>x.NsepServiceRecords)
.GroupBy(x => x.DistrictId)
.Select(x => new DistrictClientsLookUpModel
{
DistrictName = x.Select(record => record.District.Name).FirstOrDefault(),
ClientsServedCount = x.Sum(t=> t.NsepServiceRecords.Count)
});
Model classes are like this
public class BaseEntity
{
public Guid Id { get; set; }
}
public class NsepServiceRecord : BaseEntity
{
public DateTime CreatedAtUtc { get; set; }
public Guid ServiceRecordId { get; set; }
public string ClientRegNo { get; set; }
// other prop .......
public virtual ServiceRecord ServiceRecord { get; set; }
}
public class ServiceRecord : BaseEntity
{
public DateTime CreatedAtUtc { get; set; }
public string DistrictId { get; set; }
public virtual District District { get; set; }
public virtual ICollection<NsepServiceRecord> NsepServiceRecords { get; set; }
}
public class DistrictClientsLookUpModel
{
public string DistrictName { get; set; }
public int ClientsServedCount { get; set; }
}
I'm using Microsoft.EntityFrameworkCore, Version 2.2.4
EDIT
I have also tried like this
var startUniversalTime = DateTime.SpecifyKind(request.StartDate, DateTimeKind.Utc);
var endUniversalTime = DateTime.SpecifyKind(request.EndDate, DateTimeKind.Utc);
return _dbContext.NsepServiceRecords
.Join(_dbContext.ServiceRecords, s => s.ServiceRecordId,
r => r.Id, (s, r) => r)
.Include(i => i.District)
.Where(x => x.DistrictId == request.DistrictId
&& x.CreatedAtUtc.Date >= startUniversalTime
&& x.CreatedAtUtc.Date <= endUniversalTime)
.OrderBy(x => x.DistrictId)
.GroupBy(result => result.DistrictId)
.Select(r => new DistrictClientsLookUpModel
{
DistrictName = r.Select(x=>x.District.Name).FirstOrDefault(),
ClientsServedCount = r.Sum(x=>x.NsepServiceRecords.Count())
});
Another try,
from s in _dbContext.ServiceRecords
join record in _dbContext.NsepServiceRecords on s.Id equals record.ServiceRecordId
join district in _dbContext.Districts on s.DistrictId equals district.Id
group s by new
{
s.DistrictId,
s.District.Name
}
into grp
select new DistrictClientsLookUpModel
{
DistrictName = grp.Key.Name,
ClientsServedCount = grp.Sum(x => x.NsepServiceRecords.Count)
};
It takes too long, I waited for two minutes before I killed the request.
UPDATE
EF core have issues translating GroupBy queries to server side
Assuming the District has a collection navigation property to ServiceRecord as it should, e.g. something like
public virtual ICollection<ServiceRecord> ServiceRecords { get; set; }
you can avoid the GroupBy by simply starting the query from District and use simple projection Select following the navigations:
var query = _dbContext.Districts
.Select(d => new DistrictClientsLookUpModel
{
DistrictName = d.Name,
ClientsServedCount = d.ServiceRecords
.Where(s => s.CreatedAtUtc >= startUniversalTime && s.CreatedAtUtc <= endUniversalTime)
.SelectMany(s => s.NsepServiceRecords)
.Select(r => r.ClientRegNo).Distinct().Count()
});
You don't appear to be doing a join properly.
Have a look at this:
Join/Where with LINQ and Lambda
Here is a start on the linq query, I'm not sure if this will give you quite what you want, but its a good start.
Basically within the .Join method you need to first supply the entity that will be joined. Then you need to decide on what they will be joined on, in this case district=> district.Id, serviceRecord=> serviceRecord.Id.
_dbContext.ServiceRecords
.Join( _dbContext.District,district=> district.Id, serviceRecord=> serviceRecord.Id)
.Join(_dbContext.NsepServiceRecords, Nsep=> Nsep.ServiceRecord.Id,district=>district.Id)
.GroupBy(x => x.DistrictId)
.Select(x => new DistrictClientsLookUpModel
{
DistrictName = x.Select(record => record.District.Name).FirstOrDefault(),
ClientsServedCount = x.Sum(t=> t.NsepServiceRecords.Count)
});
Consider this existing linq statement:
IEnumerable<string> queries = LandingSilo.Relationships
.Where(x => x.Type == 1 && x.RootKey == root.Key)
.Join(
nodes,
r => r.NodeKey,
n => n.Key,
(r, n) => n.Queries.SingleOrDefault(q => q.Seq == r.QueryId))
.Where(q => q != null)
.Select(q => q.Query);
This query doesn't quite do what I need it to do. It only exposes variables from 'n.Queries', whereas I need to access another variable from 'n' itself, further up the tree.
Required: n.Url and q.Query - output as a KeyValuePair<string,string>.
As you can see, the query links two lists - LandingSilo.Relationships and a list represented by the variable nodes.
Nodes also has a nested list property which contains one of the keys needed to join the lists together, here are the keys:
LandingSilo.Relationships: NodeKey, QueryId
nodes: Key, Queries.Seq (Queries is the list inside nodes)
Here are the specs:
LandingSilo.Relationships: List<SiloRelationship>();
public class SiloRelationship
{
public SiloRelationship(int type, string rootKey, string nodeKey, int queryId)
{
Type = type;
RootKey = rootKey;
NodeKey = nodeKey; // Key 1
QueryId = queryId; // Key 2
}
public int Type { get; set; }
public string RootKey { get; set; }
public string NodeKey { get; set; }
public int QueryId { get; set; }
}
nodes: List<SiloNode>();
public class SiloNode
{
public string Key { get; private set; } // Key 1
public string Url { get; private set; }
public List<NodeQuery> Queries { get; private set; }
}
public class NodeQuery
{
public string Query { get; private set; }
public int Seq { get; private set; } // Key 2
}
Should be able to select the values as the result of the join as a KeyValuePair
IEnumerable<string> queries = LandingSilo.Relationships
.Where(x => x.Type == 1 && x.RootKey == root.Key)
.Join(
nodes,
r => r.NodeKey,
n => n.Key,
(r, n) => new KeyValuePair<string,string>(n.Url, n.Queries.SingleOrDefault(q => q.Seq == r.QueryId)?.Query))
.Where(q => q.Value != null);
You can use intermediate anonymous projections to provide access to the other objects from the query, but it leads to quite unreadable code.
For queries involving joins the LINQ query syntax is much more appropriate due to transparent identifier access. Rewriting your query with query syntax could be like this:
var query =
from r in relationships
where r.Type == 1 && r.RootKey == root.Key
join n in nodes on r.NodeKey equals n.Key
from q in n.Queries
// Here you have access to r,n and q
where r.QueryId == q.Seq
select new KeyValuePair<string, string>(n.Url, q.Query);
I have 3 classes and trying to use LINQ methods to perform an INNER JOIN and a LEFT JOIN. I'm able to perform each separately, but no luck together since I can't even figure out the syntax.
Ultimately, the SQL I'd write would be:
SELECT *
FROM [Group] AS [g]
INNER JOIN [Section] AS [s] ON [s].[GroupId] = [g].[Id]
LEFT OUTER JOIN [Course] AS [c] ON [c].[SectionId] = [s].[Id]
Classes
public class Group {
public int Id { get; set; }
public int Name { get; set; }
public bool IsActive { get; set; }
public ICollection<Section> Sections { get; set; }
}
public class Section {
public int Id { get; set; }
public int Name { get; set; }
public int GroupId { get; set; }
public Group Group { get; set; }
public bool IsActive { get; set; }
public ICollection<Course> Courses { get; set; }
}
public class Course {
public int Id { get; set; }
public int UserId { get; set; }
public int Name { get; set; }
public int SectionId { get; set; }
public bool IsActive { get; set; }
}
Samples
I want the result to be of type Group. I successfully performed the LEFT JOIN between Section and Course, but then I have an object of type IQueryable<a>, which is not what I want, sinceGroup`.
var result = db.Section
.GroupJoin(db.Course,
s => s.Id,
c => c.SectionId,
(s, c) => new { s, c = c.DefaultIfEmpty() })
.SelectMany(s => s.c.Select(c => new { s = s.s, c }));
I also tried this, but returns NULL because this performs an INNER JOIN on all tables, and the user has not entered any Courses.
var result = db.Groups
.Where(g => g.IsActive)
.Include(g => g.Sections)
.Include(g => g.Sections.Select(s => s.Courses))
.Where(g => g.Sections.Any(s => s.IsActive && s.Courses.Any(c => c.UserId == _userId && c.IsActive)))
.ToList();
Question
How can I perform an INNER and a LEFT JOIN with the least number of calls to the database and get a result of type Group?
Desired Result
I would like to have 1 object of type Group, but only as long as a Group has a Section. I also want to return the Courses the user has for the specific Section or return NULL.
I think what you ask for is impossible without returning a new (anonymous) object instead of Group (as demonstrated in this answer). EF will not allow you to get a filtered Course collection inside a Section because of the way relations and entity caching works, which means you can't use navigational properties for this task.
First of all, you want to have control over which related entities are loaded, so I suggest to enable lazy loading by marking the Sections and Courses collection properties as virtual in your entities (unless you've enabled lazy loading for all entities in your application) as we don't want EF to load related Sections and Courses as it would load all courses for each user anyway.
public class Group {
public int Id { get; set; }
public int Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Section> Sections { get; set; }
}
public class Section {
public int Id { get; set; }
public int Name { get; set; }
public int GroupId { get; set; }
public Group Group { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
In method syntax, the query would probably look something like this:
var results = db.Group
.Where(g => g.IsActive)
.GroupJoin(
db.Section.Where(s => s.IsActive),
g => g.Id,
s => s.GroupId,
(g, s) => new
{
Group = g,
UserSections = s
.GroupJoin(
db.Course.Where(c => c.IsActive && c.UserId == _userId).DefaultIfEmpty(),
ss => ss.Id,
cc => cc.SectionId,
(ss, cc) => new
{
Section = ss,
UserCourses = cc
}
)
})
.ToList();
And you would consume the result as:
foreach (var result in results)
{
var group = result.Group;
foreach (var userSection in result.UserSections)
{
var section = userSection.Section;
var userCourses = userSection.UserCourses;
}
}
Now, if you don't need additional filtering of the group results on database level, you can as well go for the INNER JOIN and LEFT OUTER JOIN approach by using this LINQ query and do the grouping in-memory:
var results = db.Group
.Where(g => g.IsActive)
.Join(
db.Section.Where(s => s.IsActive),
g => g.Id,
s => s.GroupId,
(g, s) => new
{
Group = g,
UserSection = new
{
Section = s,
UserCourses = db.Course.Where(c => c.IsActive && c.UserId == _userId && c.SectionId == s.Id).DefaultIfEmpty()
}
})
.ToList() // Data gets fetched from database at this point
.GroupBy(x => x.Group) // In-memory grouping
.Select(x => new
{
Group = x.Key,
UserSections = x.Select(us => new
{
Section = us.UserSection,
UserCourses = us.UserSection.UserCourses
})
});
Remember, whenever you're trying to access group.Sections or section.Courses, you will trigger the lazy loading which will fetch all child section or courses, regardless of _userId.
Use DefaultIfEmpty to perform an outer left join
from g in db.group
join s in db.section on g.Id equals s.GroupId
join c in db.course on c.SectionId equals s.Id into courseGroup
from cg in courseGroup.DefaultIfEmpty()
select new { g, s, c };
Your SQL's type is not [Group] (Type group would be: select [Group].* from ...), anyway if you want it like that, then in its simple form it would be:
var result = db.Groups.Where( g => g.Sections.Any() );
However, if you really wanted to convert your SQL, then:
var result = from g in db.Groups
from s in g.Sections
from c in s.Courses.DefaultIfEmpty()
select new {...};
Even this would do:
var result = from g in db.Groups
select new {...};
Hint: In a well designed database with relations, you very rarely need to use join keyword. Instead use navigational properties.