Why does Automapper and NHibernate result in N+1? - c#

I created this query with NHibernate:
public IList<Category> GetCategoriesByUsername(string username)
{
Category cAlias = null;
User uAlias = null;
Keyword kAlias = null;
var categories = SessionFactory.GetCurrentSession().QueryOver<Category>(() => cAlias)
.JoinAlias(() => cAlias.User, () => uAlias)
.Where(() => uAlias.Username == username)
.Future<Category>();
var keywords = SessionFactory.GetCurrentSession().QueryOver<Keyword>(() => kAlias)
.JoinAlias(c => c.Category, () => cAlias)
.Where(() => cAlias.Id == kAlias.Category.Id)
.Future<Keyword>();
IList<Category> list = (List<Category>)categories.ToList();
return list;
}
This works fine and gives me a list of categories where each category has its own keywords. In my service layer I try to convert it to a ViewModel with Automapper which works but not as expected. For every keyword in the category list it creates a new query (N+1). It doesn't use the already populated keywords in each category in the list.
These are my Models and ViewModels:
public class Category
{
public virtual Id { get; set; }
public virtual string Name { get; set; }
public virtual ICollection<Keyword> Keywords { get; set; }
public virtual User User { get; set; }
}
public class CategoryView
{
public int Id { get; set; }
public string Name { get; set; }
public IList<KeywordSummaryView> Keywords { get; set; }
}
public class Keyword
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
public virtual Category Category { get; set; }
}
public class KeywordSummaryView
{
public int Id { get; set; }
public string Name { get; set; }
}
My mapping:
public class AutoMapperBootStrapper
{
public static void ConfigureAutoMapper()
{
Mapper.CreateMap<Category, CategoryView>();
Mapper.CreateMap<Keyword, KeywordSummaryView>();
}
}
public static class CategoryMap
{
public static IList<CategoryView> ConvertToCategoryView(this IList<Category> category)
{
return Mapper.Map<IList<Category>, IList<CategoryView>>(category);
}
}
From Model to ViewModel:
IList<Category> categories = _categoryRepository.GetCategoriesByUsername(request.Username);
response.Categories = categories.ConvertToCategoryView();
It doesn't use the already populated keywords in each category in the list but instead creates a new query for each keyword (N+1). Is there something I'm doing wrong?

I guess both of these should prevent N+1 select
public IList<Category> GetCategoriesByUsername(string username)
{
User uAlias = null;
var categories = SessionFactory.GetCurrentSession().QueryOver<Category>(() => cAlias)
.Fetch(x => x.Keywords ).Eager
.JoinAlias(() => cAlias.User, () => uAlias)
.Where(() => uAlias.Username == username);
.TransformUsing(Transformers.DistinctRootEntity)
.List<Category>();
return categories ;
}
public IList<Category> GetCategoriesByUsername(string username)
{
User uAlias = null;
Keyword kAlias = null;
var categories = SessionFactory.GetCurrentSession().QueryOver<Category>(() => cAlias)
.JoinAlias(() => cAlias.User, () => uAlias)
.JoinAlias(x => x.Keywords , () => kAlias, JoinType.LeftOuterJoin)
.Where(() => uAlias.Username == username);
.TransformUsing(Transformers.DistinctRootEntity)
.List<Category>();
return categories;
}
Hope this will help

Related

EF Core not loading related entities

My original code:
public static User GetUser(int userID)
{
_context.Users.Where(x => x.UserId == userID)
.FirstOrDefault();
}
Here user.usergovernments is null.
New code:
public static User GetUser(int userID)
{
using (var _context = new SafetyContext())
{
// find lga for this user
var user_govs = from o in _context.Users
join i in _context.UserGovernments
on o.UserId equals i.UserId
where o.UserId == userID
select new { o, i };
var user = _context.Users
.Where(x => x.UserId == userID)
.FirstOrDefault();
foreach (var lga in user_govs)
{
user.UserGovernments.Add(new UserGovernment { UserId = userID, UserGovernmentId = lga.i.UserGovernmentId, LocalGovId = lga.i.LocalGovId, StateId = lga.i.StateId });
}
return user;
}
}
This time I get duplicate usergovernment records! One is loaded and the other is the one I added!
Model classes:
public class User
{
public int UserId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public UserAccess AccessLevel { get; set; }
public ICollection<UserGovernment> UserGovernments { get; set; }
}
public class UserGovernment
{
public int UserGovernmentId { get; set; }
public int UserId { get; set; }
public User User { get; set; }
public int StateId { get; set; }
public State State { get; set; }
public int LocalGovId { get; set; }
public LocalGov LocalGov { get; set; }
}
public class LocalGov
{
public int LocalGovId { get; set; }
public string Name { get; set; }
public string LgaPid { get; set; }
public ICollection<UserGovernment> UserGovernments { get; set; }
}
Context:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserGovernment>()
.HasKey(bc => new {bc.UserGovernmentId});
modelBuilder.Entity<UserGovernment>()
.HasOne(bc => bc.User)
.WithMany(b => b.UserGovernments)
.HasForeignKey(bc => bc.UserId);
modelBuilder.Entity<UserGovernment>()
.HasOne(bc => bc.LocalGov)
.WithMany(c => c.UserGovernments)
.HasForeignKey(bc => bc.LocalGovId);
}
What am I doing wrong?
LocalGov and User are individual entities while UserGovernments is the many-to-many joining table/entity
Write Query as like bellow.
Include method fill your UserGovermnet property automaticaly according to it's matched userId
_context.Users.Where(x => x.UserId == userID)
.Include(u=>u.UserGoverments)
.FirstOrDefault();

Entity Framework get SUM from child property

I have the following model where I'd like to get the sum of all OrderTotalItems for all Orders of a Customer where the OrderTotalType (Enumeration) is "total" or 99:
public class Customer
{
...
public ICollection<Order> Orders { get; set; } = new Collection<Order>();
}
public class Order
{
...
public ICollection<OrderTotalItem> OrderTotalItems { get; set; } = new Collection<OrderTotalItem>();
}
public class OrderTotalItem
{
[Required]
public int Id { get; set; }
[Required]
[Column(TypeName = "decimal(10, 4)")]
public decimal Value { get; set; }
[Required]
public OrderTotalType Type { get; set; }
}
I am building a CustomerAdminDTO to include all relevant data of a customer for the admin client:
public class CustomerAdminDto
{
public int Id { get; set; }
public string UserId { get; set; }
public Gender Gender { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string VATId { get; set; } = "";
public bool VATIdValid { get; set; } = false;
public DateTime Added { get; set; }
public DateTime LastModified { get; set; }
public decimal OrdersTotal { get; set; }
public CustomerStatusShortDto CustomerStatus { get; set; }
public CustomerAddressDto CustomerAddress { get; set; }
public CustomerAddressDto BillingAddress { get; set; }
public ICollection<OrderListShortDto> Orders { get; set; }
}
In my data service I fill the DTO like that
var customerAdmin = await _context.Customers
.Include(x => x.Addresses)
.Include(x => x.CustomerStatus)
.Include(x => x.Orders)
.ThenInclude(x => x.OrderTotalItems)
.Where(x => x.UserId == userid)
.Select(customer => new CustomerAdminDto
{
Id = customer.Id,
UserId = customer.UserId,
Gender = customer.Gender,
FirstName = customer.FirstName,
LastName = customer.LastName,
VATId = customer.VATId,
VATIdValid = customer.VATIdValid,
Added = customer.Added,
LastModified = customer.LastModified,
OrdersTotal = customer.Orders.Sum(x => x.OrderTotalItems
.Where(x => x.Type == Enums.OrderTotalType.Total)
.Sum(x => x.Value)),
CustomerStatus = new CustomerStatusShortDto
{
Id = customer.CustomerStatus.Id,
Name = customer.CustomerStatus.Name,
},
...
}
.FirstOrDefaultAsync();
where everything works, except the OrdersTotal.
API compiles fine but throws the following error at runtime:
Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
Thanks for your hints!
Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
This error in SQL server means that you tried to call aggregation function (customer.Orders.Sum() in your case) on other expression that contains aggregation function (.Sum(x => x.Value) in your case). In order to avoid this you can simplify your LINQ expression for OrdersTotal:
OrdersTotal = customer.Orders.SelectMany(o => o.OrderTotalItems).Where(x => x.Type == Enums.OrderTotalType.Total).Sum(x => x.Value)
There is only one aggregation here so it should work fine

Populate multiple collections on same type

I need to populate a Product object which contains two collections.
The current code works fine and populates the Product.GraphicItems collection, but I also need to populate the Product.InfoItems collection, but I can't figure out the syntax for multiple collections.
Current select:
var result = await this.Context.ShopCategorys
.Include(cat => cat.InfoItems)
.Include(cat => cat.Products)
.ThenInclude(prd => prd.GraphicItems)
.ThenInclude(itm => itm.Graphic)
.ThenInclude(gfx => gfx.Items)
.SingleAsync(cat => cat.Id.Equals(id));
Product.cs:
[Table("ShopProduct")]
public class Product : BaseShop
{
public bool Active { get; set; } = true;
public int CategoryId { get; set; }
public int CultureId { get; set; } = -1;
public List<ProductInfo> InfoItems { get; set; } = new List<ProductInfo>();
public List<ProductGraphic> GraphicItems { get; set; } = new List<ProductGraphic>();
}
ProductInfo.cs:
[Table("ShopProductInfo")]
public class ProductInfo : BaseShop, IInfoItem
{
public int? ProductId { get; set; }
public int CultureId { get; set; }
public Culture Culture { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
Solution:
var result = await this.Context.ShopCategorys
.Include(cat => cat.InfoItems)
.Include(cat => cat.Products)
.ThenInclude(prd => prd.InfoItems)
.Include(cat => cat.Products)
.ThenInclude(prd => prd.GraphicItems)
.ThenInclude(itm => itm.Graphic)
.ThenInclude(gfx => gfx.Items)
.SingleAsync(cat => cat.Id.Equals(id));

Nhibernate queryover matching two IEnumerable

i have this domain objects:
public class Societa : EquatableObject<Societa>
{
public virtual int IdSocieta { get; set; }
public virtual string NomeSocieta { get; set; }
}
public class Attivita {
public virtual int IdAttivita { get; set; }
public virtual IEnumerable<ProcessoEsaminato> Processi
}
public class ProcessoEsaminato {
public virtual ProcessoSocieta ProcessoCoperto { get; set; }
public virtual int Anno { get; set; }
}
public class ProcessoSocieta {
public override int Id { get; set; }
public virtual Societa SocietaDiretta { get; set; }
public virtual Societa SocietaService { get; set; }
}
public class Processo {
public virtual int Id { get; set; }
public virtual string NomeProcesso { get; set; }
public virtual IEnumerable<ProcessoSocieta> SocietaAttivate
}
i nedd to extract from db with QueryOver or LinqToNHibernate every Process of an Attivita with NomeProcesso, SocietaDiretta.NomeSocieta and SocietaService.NomeSocieta
So i think:
i have to start from Processo and get those that in their SocietaAttivate has one that is in Processi collection of Attivita, looking at ProcessoCoperto property of every element of this collection
i try this:
public IEnumerable<object> ProcessiPerAttivita (Attivita att) {
ProcessoSocieta ps = null;
var elencoPS = att.Processi.Select(p => p.ProcessoCoperto).ToList<ProcessoSocieta>();
return _session.QueryOver<Processo>()
.JoinAlias(processo => processo.SocietaAttivate, () => ps)
.Where(x => x.SocietaAttivate.IsIn(elencoPS))
.List();
}
but Where(x => x.SocietaAttivate.IsIn(elencoPS)) is not what i nedd, since it wants only a list of id. so first question is how can i do this?
Second question is how can i select only fields i need from different object, coming from different level of aggregation?
EDIT:
now i try
_session.QueryOver<Processo>()
.JoinAlias(processo => processo.SocietaAttivate, () => ps)
.Where(x => x.SocietaAttivate.Any(p => elencoPS.Contains(p)) != null)
.List();
but i get variable 'x' of type 'ProcessoSocieta' referenced from scope '', but it is not defined
Try this:
public IEnumerable<Processo> ProcessiPerAttivita (Attivita att) {
ProcessoSocieta ps = null;
var elencoPS = att.Processi.Select(p => p.ProcessoCoperto).ToList<ProcessoSocieta>();
return _session.QueryOver<Processo>()
.JoinAlias(processo => processo.SocietaAttivate, () => ps)
.WhereRestrictionOn(processo => ps.Id).IsIn(elencoPS.Select(el => el.Id).ToList())
.List<Processo>();
}
You must use the 'ps' alias!
Edit: you can use
.List<Processo>(); and return an IEnumerable<Processo>

EF can't get the reference entity of navigation property of an entity

This is my model classes
public class Serve
{
public int Id { get; set; }
public Table Table { get; set; }
public bool IsFinished { get; set; }
public decimal TotalMoney { get; set; }
public virtual ICollection<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public Item Item { get; set; }
public int Numbers { get; set; }
public bool IsCheifReceived { get; set; }
public bool IsCheifCooked { get; set; }
public Serve Serve { get; set; }
public Order()
{
IsCheifCooked = IsCheifReceived = false;
}
}
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public string Unit { get; set; }
public decimal Price { get; set; }
public Category Category { get; set; }
public bool IsNeedToNotifyChief { get; set; }
}
I want to sum the Number and total money of Orders grouped by same Item (This is the Menu Item of the restaurant).
This is the LINQ query.
var serving = db.Serves
.Where(m => m.Table.Id == table.Id)
.Where(m => m.IsFinished == false)
.OrderByDescending(m => m.Id)
.FirstOrDefault();
var groupedOrder = serving.Orders
.OrderByDescending(m => m.Id)
.GroupBy(m => m.Item)
.Select(g => new
{
Item = g.Key,
Numbers = g.Sum(ri => ri.Numbers)
}).ToList();
But in this case, in .GroupBy(m => m.Item) the m.Item is null, and it grouped all the Orders to one group. (I know the problem is the m.Item is not loaded in this Query)
Here, I don't know how to make the m.Item is loaded in this LINQ Query.
Please help me to do this.
P.S.: I use Entity Framework 6
I think the variable serving is list of some objects, not a query. If I'm right, you should to include items when you getting serving from DB. It will be look like code below:
var serving = db.Serves
.Include(x => x.Orders)
.Include(x => x.Orders.Select(x => x.Item)) //here you'll get your items
.Where(m => m.Table.Id == ta && m.IsFinished == false)
.OrderByDescending(m => m.Id)
.FirstOfDefault();
//and after that your code should work correctly
if(serving != null) //don't forget check it, if you use FirstOfDefault()
{
var groupedOrder = serving.Orders
.OrderByDescending(m => m.Id)
.GroupBy(m => m.Item)
.Select(g => new
{
Item = g.Key,
Numbers = g.Sum(ri => ri.Numbers)
}).ToList();
}

Categories

Resources