I have the following structure: Training has many Module has many Phase has many Question.
I use the following query to get the above
Context.Trainings
.Include(x => x.Modules)
.ThenInclude(x => x.Phases)
.ThenInclude(y => y.Questions)
Question also has many Comment but that relationship is not defined as navigation property because Comment can have different type of patents. So Comment just has a ParentId that is sometimes Question and sometimes other things.
My question is how do I modify the above query to, for every Question, count the child Comment from the Context.Comments and assign it to Question.CommentCount? Kind of like a manual Include
In my head it's something like this
Context.Trainings
.Include(x => x.Modules)
.ThenInclude(x => x.Phases)
.ThenInclude(y => y.Questions.Select(x=> new Question.Question {
Name = x.Name,
Description = x.Description,
CommentCount = Context.Comments.Where(y=>y.ParentId == x.Id)
}));
But it seems you can't put projections in Include and I don't know how to think about this in another way.
With the entities set up such as ...
public class Training
{
public int Id { get; set; }
public ICollection<Module> Modules { get; set; }
}
public class Module
{
public int Id { get; set; }
public ICollection<Phase> Phases { get; set; }
}
public class Phase
{
public int Id { get; set; }
public ICollection<Question> Questions { get; set; }
}
public class Question
{
public int Id { get; set; }
[NotMapped]
public int CommentCount { get; set; }
}
public class Comment
{
public int Id { get; set; }
public int ParentId { get; set; }
}
// DbContext
public DbSet<Training> Trainings { get; set; }
public DbSet<Module> Modules { get; set; }
public DbSet<Phase> Phases { get; set; }
public DbSet<Question> Questions { get; set; }
public DbSet<Comment> Comments { get; set; }
... it can be done in a single query, but it's quite messy.
// query all nested navigations using projections with extra data
var projected = await context.Trainings
.Select(t =>
new
{
Training = t,
Modules = t.Modules.Select(m =>
new
{
Module = m,
Phases = m.Phases.Select(p =>
new
{
Phase = p,
Questions = p.Questions.Select(q =>
new
{
Question = q,
CommentCount = context.Comments.Count(c => c.ParentId == q.Id)
}
)
}
)
}
)
}
)
.ToListAsync();
// fixup by setting comment count from dto projection to "real" tracked entity
foreach (var q in projected.SelectMany(t => t.Modules).SelectMany(m => m.Phases).SelectMany(m => m.Questions))
{
q.Question.CommentCount = q.CommentCount;
}
// thanks to ef core entity tracker this will still work
var trainings = projected.Select(p => p.Training);
var totalCommentCount = trainings.SelectMany(t => t.Modules).SelectMany(m => m.Phases).SelectMany(p => p.Questions).Sum(q => q.CommentCount);
final query
SELECT [t].[Id], [t0].[Id], [t0].[TrainingId], [t0].[Id0], [t0].[ModuleId], [t0].[Id00], [t0].[PhaseId], [t0].[c]
FROM [Trainings] AS [t]
LEFT JOIN (
SELECT [m].[Id], [m].[TrainingId], [t1].[Id] AS [Id0], [t1].[ModuleId], [t1].[Id0] AS [Id00], [t1].[PhaseId], [t1].[c]
FROM [Modules] AS [m]
LEFT JOIN (
SELECT [p].[Id], [p].[ModuleId], [q].[Id] AS [Id0], [q].[PhaseId], (
SELECT COUNT(*)
FROM [Comments] AS [c]
WHERE [c].[ParentId] = [q].[Id]) AS [c]
FROM [Phases] AS [p]
LEFT JOIN [Questions] AS [q] ON [p].[Id] = [q].[PhaseId]
) AS [t1] ON [m].[Id] = [t1].[ModuleId]
) AS [t0] ON [t].[Id] = [t0].[TrainingId]
ORDER BY [t].[Id], [t0].[Id], [t0].[Id0]
As pointed out in comments, you could benefit from using TPH with real navigation collection back to comments from questions, and you should also probably use split query or multiple queries instead of joining it all up like this. But depending on use case, perhaps a single query might perform better for you.
Related
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)
});
After reading extensively in this topic, I wanted to implement a multiple many-many relationship of my own. Here is my used case:
FYI: Although, there is no FK relationship between Program and Provider, Program.ProvID and Provider.ProvID act as the relationship columns
In my DAL, I have the following Models:
public class Patient
{
public Patient()
{
Programs = new HashSet<Program>();
}
public virtual ICollection<Program> Programs { get; set; }
}
public class Program
{
public Program()
{
Patients = new HashSet<Patient>();
Organization = new HashSet<Organization>();
}
public int ProgramId { get; set; }
public string Name { get; set; }
public string SiteName { get; set; }
public int ProviderId { get; set; }
public virtual ICollection<Patient> Patients { get; set; }
public virtual ICollection<Organization> Organization { get; set; }
}
public class Organization
{
public Organization()
{
Programs = new HashSet<Program>();
}
public int OrgID { get; set; }
public string Name { get; set; }
public virtual ICollection<Program> Programs { get; set; }
}
In my AppContext I mapped these models as following:
modelBuilder.Entity<Organization>().ToTable("Organization").HasKey(x => x.OrgID);
modelBuilder.Entity<Patient>().ToTable("Patient").HasKey(x => x.PatientID);
modelBuilder.Entity<Program>().ToTable("Program").HasKey(p => p.ProgramId);
modelBuilder.Entity<Program>().ToTable("Program").Property(p => p.ProviderId).HasColumnName("ProvID");
modelBuilder.Entity<Program>()
.HasMany(p => p.Patients)
.WithMany(p => p.Programs)
.Map(pp =>
{
pp.MapLeftKey("ProgID");
pp.MapRightKey("PatientID");
pp.ToTable("PatProg");
});
modelBuilder.Entity<Organization>()
.HasMany(o => o.Programs)
.WithMany(p => p.Organization)
.Map(prov =>
{
prov.MapLeftKey("OrgID");
prov.MapRightKey("ProvID");
prov.ToTable("Provider");
});
Now, I want to select ALL of the Patients that are in an Organization.
return Context.Set<Organization>().AsNoTracking().Where(o => o.OrgID == organizationId)
.SelectMany(o => o.Programs)
.SelectMany(p => p.Patients)
However, this yields 0 return. Furthermore, when I run the profiler during execution, the output query IS NOT anything close to what I've mapped.
SELECT
[Join1].[PatientID1] AS [PatientID],
[Join1].[FirstName] AS [FirstName],
[Join1].[LastName] AS [LastName],
[Join1].[SSN#] AS [SSN#],
[Join1].[Suffix] AS [Suffix]
FROM
[dbo].[Provider] AS [Extent1]
INNER JOIN (
SELECT
[Extent2].[ProgID] AS [ProgID],
[Extent3].[PatientID] AS [PatientID1],
[Extent3].[FirstName] AS [FirstName],
[Extent3].[LastName] AS [LastName],
[Extent3].[SSN#] AS [SSN#],
[Extent3].[Suffix] AS [Suffix]
FROM
[dbo].[PatProg] AS [Extent2]
INNER JOIN [dbo].[Patient] AS [Extent3] ON [Extent2].[PatientID] = [Extent3].[PatientID] ) AS [Join1] ON [Extent1].[ProvID] = [Join1].[ProgID]
WHERE [Extent1].[OrgID] = 111
I'm really not sure what I'm doing wrong here.
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.
I have referenced numerous questions on this site related to calculated fields and ViewModels, but I can't seem to extrapolate from examples given. I hope that laying out a specific scenario would allow someone to pin point what I can't see. I am new to WebApp design in general. Please take that into consideration. Also, if I've left off any relevant information, please let me know and I will update the question.
Here is the scenario:
I have a complex query that is spanning multiple tables to return data used in calculations. Specifically, I store units for a recipe converted to a base unit and then convert the quantity to the units specified by the user.
I am using AutoMapper to map from entities to ViewModels and vice versa, but I am not sure how to handle the calculated values. Especially with the nested ViewModel Collection thrown into the mix.
Option 1
Do I return an autonomous set of data? Like the following... and then somehow use AutoMapper to do the mapping? Perhaps I would need to do the mapping manually, which I haven't found a solid example which includes nested ViewModels. At this point, I'm not even sure if the following code handles the nested collection correctly for the autonomous data.
var userId = User.Identity.GetUserId();
var recipes = from u in db.Users.Where(u => u.Id == userId)
from c in db.Categories
from r in db.Recipes
join ur in db.UserRecipes.Where(u => u.UserId == userId) on r.Id equals ur.RecipeId
join mus in db.MeasUnitSystems on ur.RecipeYieldUnitSysId equals mus.Id
join muc in db.MeasUnitConvs on mus.Id equals muc.UnitSysId
join mu in db.MeasUnits on mus.UnitId equals mu.Id
join msy in db.MeasUnitSymbols on mu.Id equals msy.UnitId
select new
{
Id = c.Id,
ParentId = c.ParentId,
Name = c.Name,
Descr = c.Descr,
Category1 = c.Category1,
Category2 = c.Category2,
Recipes = new
{
Id = r.Id,
Title = r.Title,
Descr = r.Descr,
Yield = String.Format("{0} {1}", ((r.Yield * muc.UnitBaseConvDiv / muc.UnitBaseConvMult) - muc.UnitBaseConvOffset), msy.Symbol)
}
};
Option 2
Another option that crossed my mind was to return the entities and use AutoMapper as I normally would. Then iterate through the collections and perform the calculations there. I feel like I could make this work, but it seems inefficient to me because it would result in many queries back to the database.
Option 3
???? I can't think of any other method to do this. But, please, if you have suggestions, I am more than willing to hear them.
Relevant Data
Here is the query returning the data I want in SQL Server (more or less).
declare #uid as nvarchar(128) = 'da5435ae-5198-4690-b502-ea3723a9b217'
SELECT c.[Name] as [Category]
,r.Title
,r.Descr
,(r.Yield*rmuc.UnitBaseConvDiv/rmuc.UnitBaseConvMult)-rmuc.UnitBaseConvOffset as [Yield]
,rmsy.Symbol
FROM Category as c
inner join RecipeCat as rc on c.Id = rc.CategoryId
inner join Recipe as r on rc.RecipeId = r.Id
inner join UserRecipe as ur on r.Id = ur.RecipeId and ur.UserId = #uid
inner join MeasUnitSystem as rmus on ur.RecipeYieldUnitSysId = rmus.Id
inner join MeasUnitConv as rmuc on rmus.Id = rmuc.UnitSysId
inner join MeasUnit as rmu on rmus.UnitId = rmu.Id
inner join MeasUnitSymbol as rmsy on rmu.Id = rmsy.UnitId
inner join UserUnitSymbol as ruus on rmsy.UnitId = ruus.UnitId and rmsy.SymIndex = ruus.UnitSymIndex and ruus.UserId = #uid
ViewModels
public class CategoryRecipeIndexViewModel
{
public int Id { get; set; }
public int ParentId { get; set; }
[Display(Name = "Category")]
public string Name { get; set; }
[Display(Name = "Description")]
public string Descr { get; set; }
public ICollection<CategoryRecipeIndexViewModel> Category1 { get; set; }
public CategoryRecipeIndexViewModel Category2 { get; set; }
public ICollection<RecipeIndexViewModel> Recipes { get; set; }
}
public class RecipeIndexViewModel
{
public int Id { get; set; }
[Display(Name = "Recipe")]
public string Title { get; set; }
[Display(Name = "Description")]
public string Descr { get; set; }
[Display(Name = "YieldUnit")]
public string Yield { get; set; }
}
UPDATE 2/10/2018
I found an answer here that does a very good job of explaining exactly what I'm looking at. Particularly under the A Better solution ? section. Mapping queries directly to my ViewModels looks like it would allow me to get my calculated values as well. Problem is, the example given is once again too simplistic.
He gives the following DTO's
public class UserDto
{
public int Id {get;set;}
public string Name {get;set;}
public UserTypeDto UserType { set; get; }
}
public class UserTypeDto
{
public int Id { set; get; }
public string Name { set; get; }
}
And does the following for mapping:
var users = dbContext.Users.Select(s => new UserDto
{
Id = s.Id,
Name = s.Name,
UserType = new UserTypeDto
{
Id = s.UserType.Id,
Name = s.UserType.Name
}
});
Now what if the UserDTO looked like this:
public class UserDto
{
public int Id {get;set;}
public string Name {get;set;}
public ICollection<UserTypeDto> UserTypes { set; get; }
}
How would the mapping be done if the UserTypes were a collection?
Update 2/13/2018
I feel I am making progress, but am currently headed in the wrong direction. I found this and came up with the following (which currently errors because of the method call in the linq query):
*Note: I removed Category2 from the ViewModel as I found it was not needed and only complicated this further.
query inside index controller method
IEnumerable<CategoryRecipeIndexViewModel> recipesVM = db.Categories
.Where(x => x.ParentId == null)
.Select(x => new CategoryRecipeIndexViewModel()
{
Id = x.Id,
ParentId = x.ParentId,
Name = x.Name,
Descr = x.Descr,
Category1 = MapCategoryRecipeIndexViewModelChildren(x.Category1),
Recipes = x.Recipes.Select(y => new RecipeIndexViewModel()
{
Id = y.Id,
Title = y.Title,
Descr = y.Descr
})
});
Recursive Method
private static IEnumerable<CategoryRecipeIndexViewModel> MapCategoryRecipeIndexViewModelChildren(ICollection<Category> categories)
{
return categories
.Select(c => new CategoryRecipeIndexViewModel
{
Id = c.Id,
ParentId = c.ParentId,
Name = c.Name,
Descr = c.Descr,
Category1 = MapCategoryRecipeIndexViewModelChildren(c.Category1),
Recipes = c.Recipes.Select(r => new RecipeIndexViewModel()
{
Id = r.Id,
Title = r.Title,
Descr = r.Descr
})
});
}
At this point, I don't even have the calculations I require, but that doesn't matter until I get this working (small steps). I quickly discovered you can't really call a method inside a Linq Query. Then a thought occurs to me, if I need to force the Linq Query to execute and then perform all the mapping on the in memory data, then I would essentially be doing the same thing as Option 2 (above), but I could perform the calculations within the ViewModel. This is the solution I will pursue and will keep everyone posted.
You have to iterate over UserType Collection and map the value to UserType dto's collection.
Use this code.
var users = dbContext.Users.Select(s => new UserDto
Id = s.Id,
Name = s.FullName,
UserType = s.UserType.Select(t => new UserTypeDto
{
Id = t.Id,
Name = t.Name
}).ToList()
Hope this will help.
I got it working! ...I think. ...Maybe. If anything, I'm querying the data, mapping it to my ViewModels and I have the calculations too. I do have additional questions, but they are a lot more specific. I will layout the solution I followed and where I think it requires work below.
I basically implemented my Option 2 from above, but instead of iterating through the collections, I just performed the calculations within the ViewModels.
Controller Method
public ActionResult Index()
{
var userId = User.Identity.GetUserId();
var recipes = db.Categories.Where(u => u.Users.Any(x => x.Id == userId))
.Include(c => c.Category1)
.Include(r => r.Recipes
.Select(u => u.UserRecipes
.Select(s => s.MeasUnitSystem.MeasUnitConv)))
.Include(r => r.Recipes
.Select(u => u.UserRecipes
.Select(s => s.MeasUnitSystem.MeasUnit.MeasUnitSymbols)));
IEnumerable<CategoryRecipeIndexViewModel> recipesVM = Mapper.Map<IEnumerable<Category>, IEnumerable<CategoryRecipeIndexViewModel>>(recipes.ToList());
return View(recipesVM);
}
View Models
public class CategoryRecipeIndexViewModel
{
public int Id { get; set; }
public int ParentId { get; set; }
[Display(Name = "Category")]
public string Name { get; set; }
[Display(Name = "Description")]
public string Descr { get; set; }
public ICollection<CategoryRecipeIndexViewModel> Category1 { get; set; }
public ICollection<RecipeIndexViewModel> Recipes { get; set; }
}
public class RecipeIndexViewModel
{
public int Id { get; set; }
[Display(Name = "Recipe")]
public string Title { get; set; }
[Display(Name = "Description")]
public string Descr { get; set; }
public double Yield { get; set; }
public ICollection<UserRecipeIndexViewModel> UserRecipes { get; set; }
[Display(Name = "Yield")]
public string UserYieldUnit
{
get
{
return System.String.Format("{0} {1}", ((Yield *
UserRecipes.FirstOrDefault().MeasUnitSystem.MeasUnitConv.UnitBaseConvDiv /
UserRecipes.FirstOrDefault().MeasUnitSystem.MeasUnitConv.UnitBaseConvMult) -
UserRecipes.FirstOrDefault().MeasUnitSystem.MeasUnitConv.UnitBaseConvOffset).ToString("n1"),
UserRecipes.FirstOrDefault().MeasUnitSystem.MeasUnit.MeasUnitSymbols.FirstOrDefault().Symbol);
}
}
}
public class UserRecipeIndexViewModel
{
public MeasUnitSystemIndexViewModel MeasUnitSystem { get; set; }
}
public class MeasUnitSystemIndexViewModel
{
public MeasUnitIndexViewModel MeasUnit { get; set; }
public MeasUnitConvIndexViewModel MeasUnitConv { get; set; }
}
public class MeasUnitIndexViewModel
{
public ICollection<MeasUnitSymbolIndexViewModel> MeasUnitSymbols { get; set; }
}
public class MeasUnitConvIndexViewModel
{
public double UnitBaseConvMult { get; set; }
public double UnitBaseConvDiv { get; set; }
public double UnitBaseConvOffset { get; set; }
}
public class MeasUnitSymbolIndexViewModel
{
public string Symbol { get; set; }
}
This appears to be working, but I know it needs some work.
For instance, the relation shown between the Recipe and UserRecipe shows one to many. In reality, if the UserRecipe were filtered by the current user, the relationship would be one to one. Also, the same goes for the MeasUnit and the MeasUnitSymbol entities. Currently, I'm relying on the FirstOrDefault of those collections to actually perform the calculations.
Also, I have seen numerous posts that state that calculations should not be done in the View Models. Except for some who say it's okay if it is only a requirement of the View.
Last I will say that paying attention to variable names within the ViewModels would have saved me some headaches. And I thought I knew how to utilize Linq Queries, but had issues with the data returned. It was easier to rely on the eager loading provided by Entity Framework to bring back the hierarchical data structure needed, versus the flat table structures I'm used to working with.
I'm still new to a lot of this and wrapping my head around some of the quirks of MVC and Entity Framework leaves me brain dead after a few hours, but I will continue to optimize and adopt better programming methods as I go.
This question already has answers here:
How to manually load related entities in a N:N relationship?
(2 answers)
Closed 6 years ago.
I am trying to filter the results from an explicit load in EntityFramework.
The explicit loading works when I do not apply any filter but it does not load results once the filter is applied.
Classes
public partial class Student
{
public int StudentId { get; set; }
public int CourseId { get; set; }
public string Name { get; set; }
public string Status { get; set; }
public virtual ICollection<Grade> Grades { get; set; }
}
public partial class Grade
{
public int GradeId { get; set; }
public string Value { get; set; }
public string Status { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
Fluent API Mapping
modelBuilder.Entity<Grade>()
.HasMany(e => e.Students)
.WithMany(x => x.Grades)
.Map(m => m.ToTable("StudentGrades").MapLeftKey("GradeId").MapRightKey("StudentId"));
Usage
This works and populates the student.Grades property.
using (var context = new Model1())
{
context.Configuration.LazyLoadingEnabled = false;
var student = context.Students.Single(x => x.StudentId == 1);
context.Entry(student).Collection(x => x.Grades).Load();
}
The SQL that is generated looks like this:
SELECT
[Extent2].[GradeId] AS [GradeId],
[Extent2].[Value] AS [Value],
[Extent2].[Status] AS [Status]
FROM [dbo].[StudentGrades] AS [Extent1]
INNER JOIN [dbo].[Grades] AS [Extent2] ON [Extent1].[GradeId] = [Extent2].[GradeId]
WHERE [Extent1].[StudentId] = 1 // this is parameterized in the actual hit.
When I run this query I get the full results.
However, when I apply filtering and use the following line, it does not populate student.Grades.
context.Entry(student).Collection(x => x.Grades).Query().Where(x => x.Status == "A").Load();
This line generates this query:
SELECT
[Extent2].[GradeId] AS [GradeId],
[Extent2].[Value] AS [Value],
[Extent2].[Status] AS [Status]
FROM [dbo].[StudentGrades] AS [Extent1]
INNER JOIN [dbo].[Grades] AS [Extent2] ON [Extent1].[GradeId] = [Extent2].[GradeId]
WHERE ([Extent1].[StudentId] = 1) AND ('A' = [Extent2].[Status])
//the "1" is parameterized in the actual hit.
When I run this manually against the DB I get the correctly filtered results within SQL Server. The problem is that this doesn't populate student.Grades in the C# object.
This technique is mentioned in the MSDN Article - Applying filters when explicitly loading related entities section, so it's supposed to be supported and working. Strangely enough, it's working for one-to-many relationship, many-to-many with explicit link table and 2 one-to-many associations, but not for many-to-many with implicit link table.
I have no explanation why is that (didn't find related documentation). I also have no explanation why, but combining it with a request for eagerly loading the other collection does the trick:
context.Entry(student).Collection(s => s.Grades)
.Query().Where(g => g.Status == "A")
.Include(g => g.Students)
.Load();
The drawback of this (as mentioned in the comments) is that it would also load a lot of students that belong to the loaded grades.
So the better way would be to use the explicit link table and relationships like this:
Model:
public partial class Student
{
public int StudentId { get; set; }
public int CourseId { get; set; }
public string Name { get; set; }
public string Status { get; set; }
public virtual ICollection<StudentGrade> StudentGrades { get; set; }
}
public partial class Grade
{
public int GradeId { get; set; }
public string Value { get; set; }
public string Status { get; set; }
public virtual ICollection<StudentGrade> StudentGrades { get; set; }
}
public class StudentGrade
{
public int StudentId { get; set; }
public int GradeId { get; set; }
public virtual Student Student { get; set; }
public virtual Grade Grade { get; set; }
}
Configuration:
modelBuilder.Entity<StudentGrade>()
.ToTable("StudentGrades")
.HasKey(e => new { e.GradeId, e.StudentId });
modelBuilder.Entity<StudentGrade>()
.HasRequired(e => e.Grade)
.WithMany(x => x.StudentGrades)
.HasForeignKey(e => e.GradeId)
.WillCascadeOnDelete();
modelBuilder.Entity<StudentGrade>()
.HasRequired(e => e.Student)
.WithMany(x => x.StudentGrades)
.HasForeignKey(e => e.StudentId)
.WillCascadeOnDelete();
Now the explicit loading does not require tricks and will load the filtered related StudentGrade entities with only GradeId and StudentId fields populated, thus avoiding loading the additional Grade and Student objects:
context.Entry(student).Collection(s => s.StudentGrades)
.Query().Where(sg => sg.Grade.Status == "A")
.Load();
That's expected
The first Load() is DbCollectionEntry.Load()
The second is IQueryable.Load()
Basically you call Load on an empty IQueryable
Try
var grades = context.Students.Where(s => s.StudentId == 1).SelectMany(s => s.Grades).Where(g => g.Status == "A").ToList();
SELECT
[Extent2].[GradeId] AS [GradeId],
[Extent2].[Value] AS [Value],
[Extent2].[Status] AS [Status]
FROM [dbo].[StudentGrades] AS [Extent1]
INNER JOIN [dbo].[Grades] AS [Extent2] ON [Extent1].[GradeId] = [Extent2].[GradeId]
WHERE (N'A' = [Extent2].[Status]) AND (1 = [Extent1].[StudentId])