I'm using Automapper version 10.0 with EF Core 5 and .NET 5.
It would seem that automapper causes a query that selects every property separately:
SELECT FALSE, a11."Avatar", ((
SELECT a0."Id"
FROM "AspNetUserRoles" AS a
INNER JOIN "AspNetRoles" AS a0 ON a."RoleId" = a0."Id"
WHERE a11."Id" = a."UserId"
ORDER BY a0."Order" DESC
LIMIT 1) IS NULL), (
SELECT a2."Color"
FROM "AspNetUserRoles" AS a1
INNER JOIN "AspNetRoles" AS a2 ON a1."RoleId" = a2."Id"
WHERE a11."Id" = a1."UserId"
ORDER BY a2."Order" DESC
LIMIT 1), (
SELECT a4."Id"
FROM "AspNetUserRoles" AS a3
INNER JOIN "AspNetRoles" AS a4 ON a3."RoleId" = a4."Id"
WHERE a11."Id" = a3."UserId"
ORDER BY a4."Order" DESC
LIMIT 1), (
SELECT a6."IsStaff"
FROM "AspNetUserRoles" AS a5
INNER JOIN "AspNetRoles" AS a6 ON a5."RoleId" = a6."Id"
WHERE a11."Id" = a5."UserId"
ORDER BY a6."Order" DESC
LIMIT 1), (
SELECT a8."Name"
FROM "AspNetUserRoles" AS a7
INNER JOIN "AspNetRoles" AS a8 ON a7."RoleId" = a8."Id"
WHERE a11."Id" = a7."UserId"
ORDER BY a8."Order" DESC
LIMIT 1), COALESCE((
SELECT a10."Order"
FROM "AspNetUserRoles" AS a9
INNER JOIN "AspNetRoles" AS a10 ON a9."RoleId" = a10."Id"
WHERE a11."Id" = a9."UserId"
ORDER BY a10."Order" DESC
LIMIT 1), 0), a11."Title", a11."UserName", t."Body", t."CommentsThreadId", t."DateTime", t."Id"
FROM (
SELECT c."Id", c."AuthorId", c."Body", c."CommentsThreadId", c."DateTime"
FROM "Comments" AS c
WHERE c."CommentsThreadId" = #__threadId_0
ORDER BY c."DateTime" DESC
LIMIT #__p_1 OFFSET 0
) AS t
INNER JOIN "AspNetUsers" AS a11 ON t."AuthorId" = a11."Id"
ORDER BY t."DateTime" DESC
As you can see, pretty much every single property of RoleDto is being SELECTed separately, instead of being selected just once and having its columns mapped using the AS keyword.
Mappings are done to the following DTOs, property names being true to source entities:
public class UserSimpleDto
{
public string UserName { get; set; }
public string Avatar { get; set; }
public string Title { get; set; }
public RoleDto Role { get; set; }
}
It seems the above and the below are the main culprits. OgmaUser, the source entity for the mapping of the above, contains a list of user roles, of which only one should be present in the target DTO.
OgmaUser.Roles is mapped using EF Core 5's many-to-many setup, but I did try to use an explicit join entity and the result was the exact same.
public class RoleDto
{
public long Id { get; set; }
public string Name { get; set; }
public string? Color { get; set; }
public bool IsStaff { get; set; }
public int Order { get; set; }
}
As you can see, each property of the above is being SELECT ... AS ... separately.
public class CommentDto
{
public long Id { get; set; }
public long CommentsThreadId { get; set; }
public UserSimpleDto Author { get; set; }
public DateTime DateTime { get; set; }
public string Body { get; set; }
}
I doubt this particular DTO has something to do with the result, since the issue occurs with other DTOs that contain UserSimpleDto, but I thought I should include it to present the fullest possible picture.
And the mappings are as follows:
CreateMap<OgmaUser, UserSimpleDto>()
.ForMember(
usd => usd.Role,
opts => opts.MapFrom(u => u.Roles.OrderByDescending(r => r.Order).FirstOrDefault())
);
CreateMap<OgmaRole, RoleDto>();
CreateMap<Comment, CommentDto>()
.ForMember(
cd => cd.Body,
opts => opts.MapFrom(c => Markdown.ToHtml(c.Body, null))
);
The query is generated from the following method:
public async Task<IEnumerable<CommentDto>> GetPaginated(long threadId, int page)
{
return await _context.Comments
.Where(c => c.CommentsThreadId == threadId)
.OrderByDescending(c => c.DateTime)
.ProjectTo<CommentDto>(_mapper.ConfigurationProvider)
.Skip(Math.Max(0, page - 1) * _config.CommentsPerPage)
.Take(_config.CommentsPerPage);
.AsNoTracking()
.ToListAsync();
}
Stack complains that my question is mostly code, and I suppose I do understand that. I've seen myself many people just posting their code, expecting to have it fixed, or even posting their homework and asking for it to be solved.
In this case, however, I believe that the code speaks louder than whatever explanation I could provide, so, true, non-code parts of this question are few and far between. That's the reason why I'm even writing those last paragraph, in fact, as SO will not let me submit my question otherwise.
I believe the best answer is to change UserSimpleDto to
public class UserSimpleDto
{
public string UserName { get; set; }
public string Avatar { get; set; }
public string Title { get; set; }
public IEnumerable<RoleDto> Roles { get; set; }
}
to load all user roles – there aren't all that many – and simply getting only the first one when it's just the first one that's needed. A negligible overhead in the application layer, so to speak, but the resulting query is much simpler which should negate that overhead.
Project after ToListAsync(). Don't pass IQueryable to ProjectTo method.
public async Task<IEnumerable<CommentDto>> GetPaginated(long threadId, int page)
{
return (await _context.Comments
.Where(c => c.CommentsThreadId == threadId)
.OrderByDescending(c => c.DateTime)
.Skip(Math.Max(0, page - 1) * _config.CommentsPerPage)
.Take(_config.CommentsPerPage);
.AsNoTracking()
.ToListAsync())
.ProjectTo<CommentDto>(_mapper.ConfigurationProvider);
}
Related
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.
Using EF core 5 and ASP.NET Core 3.1, I am trying to get a filtered collection based on a condition on its grandchildren collection.
I have the following entities:
public class Organisation
{
public int Id { get; set; }
public int? OrganisationId { get; set; }
public IEnumerable<Customer> Customers { get; set; }
}
public partial class Customer
{
[Key]
public uint Id { get; set; }
public int? EmployerId { get; set; }
public int? OrganisationId { get; set; }
public List<TimecardProperties> TimecardsProperties { get; set; }
}
public partial class TimecardProperties
{
[Key]
public int Id { get; set; }
public int? EmployerId { get; set; }
public int? Week { get; set; }
public short? Year { get; set; }
}
The goal is to get all Organisations that have at least one customer and the customer has at least 1 timecard property that is in week=34 and year=2021.
So far I have tried the following:
////necessary join to get Organisations for user id
IQueryable<Organisation> ouQuery = (from cou in _dbContext.Organisations
join uou in _dbContext.table2 on cou.OrganisationId equals uou.OrganisationId
where uou.UsersId == int.Parse(userId)
select cou)
.Where(cou => cou.Customers.Where(c => c.TimecardsProperties.Count > 0).Any())
.Include(cou => cou.Customers.Where(c => c.TimecardsProperties.Count > 0))
.ThenInclude(c => c.TimecardsProperties.Where(tc => tc.tWeek == 34 && tc.Year > 2020))
;
This returns a organisation list that each have a customers list but some customers have a count of timecards 0. I don't want to have organisation in the returned list that does not have at least one item in the timecards collection.
Also, it is too slow, and if I try to filter the produced list its even
slower (over 15 seconds)
I have also tried a raw sql query on the organisation db context but it is again very slow:
select distinct count(id) from organisation a where organisation_id in (
select organisation_id from customers where employer_id in (select distinct employer_id from timecards a
inner join timecard_components b on a.id=b.timecards_id
where week IN(
34) and year in (2021,2021) and invoice !=0 and type = 'time'
group by employer_id, week)
);
In general, I want to know the the total
count of the returned organisation collection for pagination (so I don't need to include all attributes of each entity)
as well as return only a part of the correct results, which satisfy the conditions,
an organisation list that has at least 1 timecards in
their customers by executing the query in the end like so:
ouQuery.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
I have also tried the EntityFramework.Plus and projection with no results.
How could I write this to achieve getting the total count of the organisation list and a part of these results (first 10) to display to the user?
Use navigation properties. This is the query you want:
var orgsQuery = dbContext.Organizations
.Where( o => o.Customers.Any( c =>
c.TimecardProperties.Any( tp =>
tp.Year = 2021
&& tp.Week = 34 ) ) );
Add includes and other predicates as needed
I'm playing around EntityFrameworkCore with WebAPI while building voting app exercise.
I want to make the code in async way where is possible.
So should I have to use the nested query in async way somehow(// Problem 1, // Problem 2)?
/* The target of the question - the query*/
var pollResults =
await _context.Polls
.Select(poll => new PollDto
{
Id = poll.Id,
Question = poll.Question,
CreatedAt = poll.CreatedAt,
Options = poll.Options
.Select(option => new OptionDto
{
Id = option.Id,
Value = option.Value,
VotesCount = option.Votes.Count() // Problem 1
})
.ToList(), // Problem 2
LastVotedAt = _context.PollVotes.Where(vote=>vote.PollId == poll.Id).Select(vote => vote.VoteDate).SingleOrDefault()
})
.ToListAsync();
/* Domain classes */
public class Poll
{
public int Id { get; set; }
public ICollection<PollOption> Options { get; set; } = new List<PollOption>();
public ICollection<PollVote> Votes { get; set; } = new List<PollVote>();
}
public class PollOption
{
public int Id { get; set; }
public string Value { get; set; }
public int PollId { get; set; }
public Poll Poll { get; set; }
public ICollection<PollVote> Votes { get; set; } = new List<PollVote>();
}
public class PollVote
{
public int Id { get; set; }
public int PollId { get; set; }
public Poll Poll { get; set; }
public int OptionId { get; set; }
public PollOption Option { get; set; }
public DateTime VoteDate { get; set; }
}
/* Dto classes */
public class PollDto
{
public int Id { get; set; }
public string Question { get; set; }
public ICollection<OptionDto> Options { get; set; } = new List<OptionDto>();
public DateTime LastVotedAt { get; set; }
}
public class OptionDto
{
public int Id { get; set; }
public string Value { get; set; }
public int VotesCount { get; set; }
}
So in not nested queries Count and SingleOrDefault would make request to the database and it should be executed in async way. But in my case the whole query is a single request.
Should I have to modify something to done the methods Count and SingleOrDefault in async way ? Or calling ToListAsync at end is enough?
I believe the answer is that 1 request to the database goes in 1 async call. But I didn't find any solution in the internet.
ToListAsync() at the end is enough. Expressions inside the query are used by EF to compose the query. They are not "executed" as SQL like they would have been as stand-alone statements against the DbSets.
For instance when I run something similar:
var parents = await context.Parents
.Select(x => new
{
x.ParentId,
x.Name,
Children = x.Children.Select(c => new { c.ChildId, c.Name }).ToList(),
ChildCount = x.Children.Count()
}).ToListAsync();
in a test and set a breakpoint with a profiler running. The statement produces a single SQL statement:
SELECT
[Project2].[ParentId] AS [ParentId],
[Project2].[Name] AS [Name],
[Project2].[C2] AS [C1],
[Project2].[C1] AS [C2],
[Project2].[ChildId] AS [ChildId],
[Project2].[Name1] AS [Name1]
FROM ( SELECT
[Project1].[ParentId] AS [ParentId],
[Project1].[Name] AS [Name],
[Extent3].[ChildId] AS [ChildId],
[Extent3].[Name] AS [Name1],
CASE WHEN ([Extent3].[ChildId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1],
[Project1].[C1] AS [C2]
FROM (SELECT
[Extent1].[ParentId] AS [ParentId],
[Extent1].[Name] AS [Name],
(SELECT
COUNT(1) AS [A1]
FROM [dbo].[Children] AS [Extent2]
WHERE [Extent1].[ParentId] = [Extent2].[ParentId]) AS [C1]
FROM [dbo].[Parents] AS [Extent1] ) AS [Project1]
LEFT OUTER JOIN [dbo].[Children] AS [Extent3] ON [Project1].[ParentId] = [Extent3].[ParentId]
) AS [Project2]
ORDER BY [Project2].[ParentId] ASC, [Project2].[C1] ASC
go
Not 3 queries that you might be concerned would block. This was when looking at the navigation properties for related records.
The bigger question I saw when looking at your example to double-check was this line:
LastVotedAt = _context.PollVotes.Where(vote=>vote.PollId == poll.Id).Select(vote => vote.VoteDate).SingleOrDefault()
As this would go back directly to the Context rather than access votes through a collection on the Poll. But I tried that as well and it too still resulted in a single query.
Children = x.Children.Select(c => new { c.ChildId, c.Name }).ToList(),
ChildCount = x.Children.Count(),
YoungestChild = context.Children.OrderBy(c=>c.BirthDate).Where(c=>c.ParentId == x.ParentId).FirstOrDefault()
In my test example I go back to the context to retrieve the Youngest child for the parent record rather than the Children navigation property. In this case it still executes as 1 query.
For questions like this I definitely recommend creating an EF experimentation sandbox project with a local database, then leverage and SQL profiler tool to watch the SQL statements being produced and when they are executed. Async is useful for queries that are expected to take a while to run, but should be used sparingly as they can lower the overall performance of the queries being run when used on every trivial query.
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.
I am using EntityFramework 6 and running into some major speed issues -- this query is taking over two seconds to run. I have spent the better part of the day using LinqPad in order to speed up the query but I could only get it down from 4 to two seconds. I have tried grouping, joins, etc. but the generated SQL looks overly complicated to me. I am guessing that I am just taking the wrong approach to writing the LINQ.
Here is what I am attempting to do
Find all A where Valid is null and AccountId isn't the current user
Make sure the Collection of B does not contain any B where AccountId is the current user
Order the resulting A by the number of B in its collection in descending order
Any A that doesn't have any B should be at the end of the returned results.
I have to models which look like this:
public class A
{
public int Id { get; set; }
public bool? Valid { get; set; }
public string AccountId { get; set; }
public virtual ICollection<B> Collection { get; set; }
}
public class B
{
public int Id { get; set; }
public bool Valid { get; set; }
public string AccountId { get; set; }
public DateTime CreatedDate { get; set; }
public virtual A Property { get; set; }
}
The table for A has about one million rows and B will eventually have around ten million. Right now B is sitting at 50,000.
Here is what the query currently looks like. It gives me the expected results but I have to run an orderby multiple times and do other unnecessary steps:
var filterA = this.context.A.Where(gt => gt.Valid == null && !gt.AccountId.Contains(account.Id));
var joinedQuery = from b in this.context.B.Where(gv => !gv.AccountId.Contains(account.Id))
join a in filterA on gv.A equals a
where !a.Collection.Any(v => v.AccountId.Contains(account.Id))
let count = gt.Collection.Count()
orderby count descending
select new { A = gt, Count = count };
IQueryable<GifTag> output = joinedQuery
.Where(t => t.A != null)
.Select(t => t.A)
.Distinct()
.Take(20)
.OrderBy(t => t.Collection.Count);
Thanks
Well you could always try to remove these two lines from the joinQuery
where !a.Collection.Any(v => v.AccountId.Contains(account.Id))
and
orderby count descending
the first line have already been filtered in the first Query
and the orderline, well do do the ordering on the last Query so there is no point in doing it twice