Many-to-many relationships in Entity Framework - entity-framework

I'm getting data from 2 tables, Regions and Subregions. A Region can have many Subregions, linked by RegionID.
I want to return all the Regions from the database and their related Subregions.
I've done this so far:
var RegionDet = (from s in context.Region
join b in context.SubRegion on s.RegionID equals b.RegionID
group b by s.RegionName into g
select new RegionsMV
{
RegionName = g.Key,
SubRegions = g.ToList()
}).ToList();
This works fine, apart from it will only return Regions that have Subregions associated. If a Region has no Subregion, it won't be returned. I've tried different ways, but can't get it working as it should

You have different options for this. Off the cuff, I can think of these:
a. Extend the manual join from the approach in the OP
from s in context.Region
join b in context.SubRegion on s.RegionID equals b.RegionID into bg
from b in bg.DefaultIfEmpty()
group b by s.RegionName into g
select new RegionsMV
{
RegionName = g.Key,
SubRegions = g.ToList()
}
b. Leverage the Entity mapping
public class Region
{
public int RegionID { get; set; }
public string RegionName { get; set; }
public ICollection<SubRegion> SubRegions { get; set; }
}
Then your LINQ query would be:
from s in context.Region.Include(r => r.SubRegions)
select new RegionsMV
{
RegionName = s.RegionName,
SubRegions = s.SubRegions
}
Or this is equivalent:
context.Region.Include(r => r.SubRegions)
.Select(s => new RegionsMV()
{
RegionName = s.RegionName,
SubRegions = s.SubRegions
});

Here is how to fetch all the Regions and the SubRegions associated to those Regions.
List<Region> regions = context.Region.Include(r => r.SubRegions).ToList();
regions will contain all Regions. Each Region in the list will contain its SubRegions.

Related

The right way to apply GroupBy extension method with aggregate function

I have this simple model.
public class Room
{
public Guid Id { get; set; }
public Guid? postSubjectId { get; set; }
[ForeignKey("postSubjectId")]
public PostSubject postSubject { get; set; }
public string MemberId { get; set; }
[ForeignKey("MemberId")]
public AppUser Member { get; set; }
}
Basically I need to get Grouped postSubjectId along with MemberId.Count() , I know it's easy .. but it never comes with the expected result.
I made this simple GroupBy query
var mmbrs = _context.Rooms
.Select(g => new { id = g.postSubjectId, mmbrscount = g.MemberId })
.AsEnumerable()
.GroupBy(g => new { id = g.id , mmbrscount = g.mmbrscount.Count() }).ToList();
but it gives me unexpected result
However I did the same using ordinary sql query
select [postSubjectId] as postId, count([MemberId]) as mmbrsCount from [dbo].[Rooms] group by [postSubjectId]
and It gives me result as expected
I need to apply that expected result using LINQ GruoupBy extention method
The grouping key new { id = g.postSubjectId, mmbrscount = g.MemberId }) is like typing group by [postSubjectId], count([MemberId]) in SQL.
The correct statement is:
_context.Rooms
.GroupBy(r => r.postSubjectId)
.Select(g => new
{
id = g.Key,
mmbrscount = g.Count()
})
So every Room has exactly one property PostSubjectId, and one string property MemberId.
I need to get Grouped postSubjectId along with MemberId.Count()
Apparently you want to make groups of Rooms that have the same value for property PostSubjectId AND have the same value for MemberId.Count().
var result = dbContext.Rooms.GroupBy(room => new
{
PostSubjectId = room.PostSucjectId,
MemberIdLength = room.MemberId.Count(),
});
The result is a sequence of groups of Rooms. Every group has a key, which is a combination of [PostSubjectId, MemberIdLength]. The group is a sequence of Rooms. All rooms in one group have the same combination of [PostSubjectId, MemberIdLength].
If you don't want a sequence of groups of Rooms, you can use the overload of GroupBy that has a parameter resultSelector
var result = dbContext.Rooms.GroupBy(
// parameter keySelector
room => new
{ PostSubjectId = room.PostSucjectId,
MemberIdLength = room.MemberId.Count(),
},
// parameter resultSelector:
// from every combination of [PostSubjectId, MemberIdLength] (= the key) and
// all rooms that have this combination, make one new object:
(key, roomsWithThisKey) => new
{
// select the properties that you actually plan to use, for example
PostSubjectId = key.PostSubjectId,
MemberIdLength = key.MemberIdLength,
RoomInformations = roomsWithThisKey.Select(roomWithThisKey => new
{
Id = roomWithThisKey.Id,
Member = roomWithThisKey.Member,
...
})
.ToList(),
});

EF Core Filter results based on child data

Given tables CatalogItem and Option
Where CatalogItem has 0..many Option
In SQL I can do this...
Select * from
CatalogItem C
inner join [Option] O on O.CatalogItemId = C.Id
Where
O.Quantity > 0
So simple. In EF I have entities
public class CatalogItem
{
public int Id {get; set;}
public virtual ICollection<Option> Options { get; set; }
...
}
public class Option
{
public int Id {get; set;}
public int CatalogItem Id { get; set; }
public int Quantity { get; set; }
...
}
In EF I've tried
var q = context.CatelogItems
.Include(i => i.Options)
.Where(i => i.Options.Any(o => o.Quantity > 0)).ToList();
And various other things, but so far I am not able to get the equivalent results as I am from the above SQL. Namely, all catalog items that have at least one option that had a quantity > 0
I've seen some other posts looking for the same with a join, with a projection that includes both sets of objects as an anonymous type, etc. but I'm looking for a List that has the Options collection populated IF there are options with a quantity... all in one DB query.
Any help?
var q=from c in context.catalogItem
join o in context.options on c.id equal o.CatalogItemId
where o.quantity>0
But I think you need to use Group by as below:
var cQuery=from c in context.catalogItem select c;
var oQuery=from o in context.options
group o by new {o.CatalogItemId}into groups
select new {CatalogItemId=groups.Key.Id,TQuantity=groups.Sum(t => t.Quantity)};
var q=from c in cQuery
join o in oQuery on c.id=o.CatalogItemId
where o.TQuantity>0
You can use third party package to use IncludeFilter
Query IncludeFilter
var q = context.CatelogItems
.IncludeFilter(i => i.Options.Where(o => o.Quantity > 0))
.ToList();

Include with additional param on join

I have many Nations and for each Nation i have a lot of Teams.
The GetTeam endpoint in team controller retrieve a single Team and its related Nation. Via LINQ the query is this:
Context.Teams.Include(t => t.Nation).First(t => t.Id.Equals(__id))
The resulting JSON is what I want:
{"team":{"name":"Team1","nation":{"id":1,"name":"Nation1"}}
Let's say now that the property "name", both in Team and Nation model is dropped and a new model relation is created, with Translation.
What I want now is to retrieve the same JSON, but with a different query based on culture.
Gettin crazy understand how I can achieve it with include.
How can I compose this query in LINQ ?
select *
from Teams inner join
Translations TeamTr on Teams.id = TeamTr .id and TeamTr .culture = "IT" inner join
Nations on Teams.nation_id = Nations.id inner join
Translations NationTr on Nations .id = NationTr .id and NationTr .culture = "IT"
And compose the resulting data as JSON above?
for example:
(from team in Context.Teams
join teamTr in Context.Translations on team.id equals teamTr.id
join nation in Context.Nations on team.nation_id equals nations.id
join nationTr in Context.Translations on nation.id equals nationTr.id
where teamTr.culture == "IT" && nationTr.culture == "IT"
select new
{
teamName = team.name,
nationName = nation.name
}).ToList();
Nice catch tdayi.
First of all I've created a new class, that will be the container of the linq result:
public class TeamDetailLinqDto
{
public Team Team { get; set; }
public Translation TeamTranslation { get; set; }
public Nation Nation { get; set; }
public Translation NationTranslation { get; set; }
}
and this is the linq query:
public IQueryable<TeamDetailLinqDto> GetTeams()
{
var result = from team in Context.Teams
join teamTranslation in Context.Translations on
new { Id = team.Id, Locale = "IT" }
equals new { Id = teamTranslation.EntityId, Locale = teamTranslation.Locale }
join nation in Context.Nations on team.NationId equals nation.Id
join nationTranslation in Context.Translations on
new { Id = nation.Id, Locale = "IT" }
equals new { Id = nationTranslation.EntityId, Locale = nationTranslation.Locale }
select new TeamDetailLinqDto
{
Team = team,
TeamTranslation = teamTranslation,
Nation = nation,
NationTranslation = nationTranslation
};
return result;
}

How to perform a left outer join using Entity Framework using navigation properties

I'm trying to find out how I would define the code first navigation properties on these two classes to perform something similiar to this query:
SELECT USERID, FIRSTNAME, LASTNAME, COURSEID, NAME
FROM User
LEFT OUTER JOIN Course ON User.USERID = Course.USERID
WHERE COURSEID = 1
So I'm trying to find a list of users together with if they have attended a certain course.
public class User
{
public int UserId {get;set; }
public string FirstName {get;set;}
public string LastName {get;set;}
public virtual ICollection<Course> Courses { get; set; }
}
public class Course
{
public int CourseId { get;set; }
public int UserId { get;set; }
public string Name { get;set; }
public virtual User User {get;set;}
}
If I was to write a query to achieve this
var u = Users.Where(x => x.Courses.Any(x => x.CourseId = 1));
This does a subquery, which is not what I wanted (as people who didnt attend the course would not show).
How would the navigation property be defined?
HasMany(t => t.Courses).WithOptional(t => t.User).HasForeignKey(t => t.UserId);
Check this link:
http://msdn.microsoft.com/en-us/library/vstudio/bb397895.aspx
Left outer joins in LINQ are done via DefaultIfEmpty method.
var u = Users.Select ( x => new {
User = x,
AttendedCourse = x.Courses.Any()
} );
For specific course id,
var u = Users.Select ( x => new {
User = x,
AttendedCourse = x.Courses.Any( c => c.CourseID == 1 )
} );
Sub query is the only way to write related queries, However, EF will choose the best suitable join type and will give you correct results. And EF can manage mostly all kinds of queries without doing joins.

Creating dynamic queries with Entity Framework across multiple tables?

I'm trying to create search functionality across a couple of tables, following the pattern in Creating dynamic queries with entity framework
I have 3 tables:
People:
pk ID
varchar FirstName
varchar LastName
fk AddressMap_ID
AddressMap:
pk ID
Address:
pk ID
varchar StreetName
varchar StreeNumber
fk AddressMap_ID
Multiple people can live at one address. I pass in a Search model, and populate the results property:
public class Search
{
public string streetname { get; set; }
public string streetnumber { get; set; }
public string fname { get; set; }
public string lname { get; set; }
public IEnumerable<Results> results { get; set; }
}
public class Results
{
public int AddressID { get; set; }
public string StreetNumber { get; set; }
public string StreetName { get; set; }
public IEnumerable<PeopleResults> people { get; set; }
}
public class PeopleResults
{
public int personID { get; set; }
public string First { get; set; }
public string Last { get; set; }
}
This works if I filter on an address, or name + address:
public void GetResults(Search model)
{
Entities _context;
_context = new Entities();
var addr = from a in _context.Addresses
select a;
addr = addr.Where(filter => filter.StreetNumber == model.streetnumber);
addr = addr.Where(filter => filter.StreetName == model.streetname);
addr = from a in addr
group a by a.AddressMap_ID into addrs
select addrs.FirstOrDefault();
var ppl = from p in addr.SelectMany(p => p.AddressMap.People) select p;
ppl = ppl.Where(filter => filter.FirstName.StartsWith(model.fname));
ppl = ppl.Where(filter => filter.LastName.StartsWith(model.lname));
model.results = from a in addr
select new Results
{
AddressID = a.ID,
StreetName = a.StreetName,
StreetNumber = a.StreetNumber,
people = from p in ppl
select new PeopleResults
{
First = p.FirstName,
Last = p.LastName
}
};
}
But if I just try to filter on a name, it returns a cartesian join - every single address with all of the people that matched.
There are 3 ways to search: filtering on address only, filter on address + name, or filter on name only.
So if someone search for "123 Main", the results should be
123 Main St SticksVille Joe Smith
Jane Smith
Mary Smith
123 Main St Bedrock Fred Flintstone
Wilma Flintstone
A search for "J Smith 123 Main" should return just:
123 Main St SticksVille Joe Smith
Jane Smith
And a search for just "J Smith" should return:
123 Main St SticksVille Joe Smith
Jane Smith
456 Another St Sometown Jerry Smith
Your query looks "symmetric" to me with respect to people and addresses, it only gets "asymmetric" in the final projected result. So, my idea is to express this symmetry in the query as far as possible:
Get a set (IQueryable<Address>, not executed at once) of addresses filtered by street name and street number
Get a set (IQueryable<Person>, not executed at once) of people filtered by the beginning of first name and last name
Join the two sets by AddressMap_ID. The resulting set of people and addresses contains only those pairs that fulfill the filter criteria for addresses and people. If one of the filter criteria for person or address is not supplied (the first and the third of your examples at the bottom of the question) the join happens on the unfiltered set of all people/addresses, i.e. the joined pairs contain always all people of the filtered address (or all addresses of the filtered people)
Group the joined pairs of people and addresses by Address.ID
Project the groups into your Results collection. The group key is the AddressID. StreetName and StreetNumber can be fetched from the first address in each group and the people are projected from the people in each group.
Execute the query
The following code doesn't cover the case specifically that none of the four filter criteria is supplied. It works in that case but would just load all addresses with all people of those addresses. Maybe you want to throw an exception in that case. Or return nothing (model.Results = null or so), then just jump out of the method.
public void GetResults(Search model)
{
using (var _context = new Entities())
{
// "All" addresses
IQueryable<Address> addresses = _context.Addresses;
// "All" people
IQueryable<Person> people = _context.People;
// Build a Queryable with filtered Addresses
if (!string.IsNullOrEmpty(model.streetname))
addresses = addresses.Where(a => a.StreetName
.StartsWith(model.streetname));
if (!string.IsNullOrEmpty(model.streetnumber))
addresses = addresses.Where(a => a.StreetNumber
.StartsWith(model.streetnumber));
// Build a Queryable with filtered People
if (!string.IsNullOrEmpty(model.fname))
people = people.Where(p => p.FirstName == model.fname);
if (!string.IsNullOrEmpty(model.lname))
people = people.Where(p => p.LastName == model.lname);
// Join the two Queryables with AddressMap_ID
// and build group by Address.ID containing pairs of address and person
// and project the groups into the Results collection
var resultQuery = from a in addresses
join p in people
on a.AddressMap_ID equals p.AddressMap_ID
group new { a, p } by a.ID into g
select new Results
{
AddressID = g.Key,
StreetName = g.Select(ap => ap.a.StreetName)
.FirstOrDefault(),
StreetNumber = g.Select(ap => ap.a.StreetNumber)
.FirstOrDefault(),
people = g.Select(ap => new PeopleResults
{
First = ap.p.FirstName,
Last = ap.p.LastName
})
};
// Execute query (the whole code performs one single query)
model.results = resultQuery.ToList();
}
}
I am unsure if I interpret the AddressMap table correctly as a kind of join table for a many-to-many relationship (Address can have many people, Person can have many addresses), but the code above yields the three results of the three queries in your example as expected if the tables are filled like so:
The AddressMap table isn't actually used in the query because Addresses and People table are joined directly via the AddressMap_ID columns.
It seems like an approach like this would probably work:
IQueryable<Person> ppl = _context.People;
ppl = addr.Where(filter=>filter.First.StartsWith(model.fname));
ppl = addr.Where(filter=>filter.Last.StartsWith(model.lname));
var pplIds = ppl.Select(p => p.PersonId);
model.results = from a in addr
where a.AddressMap.People.Any(p => pplIds.Contains(p.PersonId))
select new Results {
AddressID = a.ID,
StreetName = a.StreetName,
StreetNumber = a.StreetNumber,
people = from p in a.People
select new PeopleResults {
First = p.FirstName,
Last = p.LastName
}
};
Rather than basing the people property on the matching people, you want to base the entire address set on the matching people.