Query Performance on Many To Many With EF6 - entity-framework

We're running into a performance issue with a query that utilizes a few M:M relationships with a single table.
Tables:
Currently, EF generates a MONSTROUS query that uses UNION ALL to the Contact table 3 times. Pastebin of SQL Once for each join. Our projection is like so
var contacts = query.Select(contact => new Contact
{
Brands = contact.Brands.Select(b => new Brand
{
Id = b.Id,
Name = b.Name,
Aliases = b.Aliases.Select(a => a.Value).ToList()
}).ToList(),
Categories = contact.Categories.Select(category => new Category
{
Id = category.Id
}).ToList(),
ContactTypes = contact.ContactTypes.Select(ct => new ContactType
{
Id = ct.Id
}).ToList(),
Disabled = contact.Disabled,
Email = contact.Email,
Fax = contact.Fax,
Id = contact.Id,
Latitude = contact.Coordinates != null ? contact.Coordinates.Latitude : 0,
Longitude = contact.Coordinates != null ? contact.Coordinates.Longitude : 0,
Distance = searchLocation != null && contact.Coordinates != null ? searchLocation.Distance(contact.Coordinates) * conversionFactor : 0,
PostalCode = contact.PostalCode,
PhoneSupport = contact.PhoneSupport,
PhoneSales = contact.PhoneSales,
PhoneAfterHoursSupport = contact.PhoneAfterHoursSupport,
Phone = contact.Phone,
Preferred = contact.Preferred,
ShopOnlineImageUrl = contact.ShopOnlineImageUrl,
ContactTranslations = contact.NameContent.TranslatedContents.Where(tc => tc.IsoLanguageCode == searchModel.IsoLanguageCode ||
tc.IsoLanguageCode == SearchContactsHelpers.ISO_LANGUAGE_CODE_ENGLISH)
.Select(tc => new ContactTranslationImpl
{
Name = tc.Value,
IsoLanguageCode = tc.IsoLanguageCode,
NameId = tc.MasterContentId
}).ToList(),
AdditionalInformationContentId = contact.AdditionalInformationContentId,
AddressLine1ContentId = contact.AddressLine1ContentId,
AddressLine2ContentId = contact.AddressLine2ContentId,
CityContentId = contact.CityContentId,
StateOrProvinceContentId = contact.StateOrProvinceContentId,
CountryContentId = contact.Country.NameContentId,
HoursOfOperationContentId = contact.HoursOfOperationContentId,
WebsiteContentId = contact.WebsiteContentId,
OnlineSellerHomePageUrlContentId = contact.OnlineSellerHomePageUrlContentId,
ContactFormUrlContentId = contact.ContactFormUrlContentId,
RequestAQuoteUrlContentId = contact.RequestAQuoteUrlContentId,
NameContentId = contact.NameContentId
});
Removing the M:M relationships from the query produces a sane single query (albeit with many subselects). As soon as a single M:M relationship is added to the projection, EF produces 2 queries with a UNION ALL for the results.
Here's our configuration as well:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
modelBuilder.Entity<Contact>()
.HasMany(c => c.Brands)
.WithMany(c => c.Contacts)
.Map(mc =>
{
mc.MapRightKey("BrandId");
mc.MapLeftKey("ContactId");
mc.ToTable("ContactBrands");
});
modelBuilder.Entity<Contact>()
.HasMany(c => c.Categories)
.WithMany(c => c.Contacts)
.Map(mc =>
{
mc.MapRightKey("CategoryId");
mc.MapLeftKey("ContactId");
mc.ToTable("ContactCategories");
});
modelBuilder.Entity<Contact>()
.HasMany(c => c.ContactTypes)
.WithMany(c => c.Contacts)
.Map(mc =>
{
mc.MapRightKey("ContactTypeId");
mc.MapLeftKey("ContactId");
mc.ToTable("ContactToContactType");
});
}
I am unsure of why EF is generating such a terrible query. Any insight on how to possibly improve the query is immensely helpful.

Related

Get all the roles after include with main entity in Entity framework Core

I have three entities which are Account, Role and AccountRole. Through repository pattern I have fetched all the accounts, roles and accountRoles like below:
var accounts = _accountRepository.Query()
.Include(x => x.AccountRoles)
.ThenInclude(x => x.Role);
Now I would like to get a single account information with all it's roles. So I have written the below query. But the problem is only first role comes out.
var userAccount = accounts.Where(x => x.Id == id)
.Select(x => new AccountDto
{
Id = x.Id,
Name = x.UserFullName,
FirstName = x.FirstName,
LastName = x.LastName,
Email = x.Email,
Mobile = x.Mobile,
UserName = x.UserName,
PhotoUrl = x.PhotoUrl,
IsActive = x.IsActive,
Roles = accounts.Where(x => x.Id == id).Select(x => new RoleDto
{
Title = x.AccountRoles
.Where(x => x.IsActive == true)
.Select(x => x.Role.Title).FirstOrDefault()
}).ToList()
}).FirstOrDefault();
Can anyone help me to write the proper query?
First, this part
Roles = accounts.Where(x => x.Id == id).Select(x => new RoleDto
{
Title = x.AccountRoles
.Where(x => x.IsActive == true)
.Select(x => x.Role.Title).FirstOrDefault()
}).ToList()
is quite strange mixture, you should simply use the navigation property (yes, it is not only for includes as some people think).
Second, don't apply FirstOrDefault() when you want to get all items rather than the first only
// here x variable type is Account (coming from the outer Select)
Roles = x.AccountRoles
.Where(ar => ar.IsActive)
.Select(ar => new RoleDto
{
Title = ar.Role.Title,
}).ToList()

Use FirstOrDefaultAsync in query when creating instance

I have the following Entity Framework query:
IQueryable<Unit> units = context.Units;
Product product = new Product {
Conversions = model.Conversions.Select(y => new Conversion {
UnitId = units
.Where(z => z.Name == y.Unit)
.Select(z => z.Unit.Id)
.FirstOrDefault(),
Value = y.Value
}).ToList(),
Name = model.Name
}
I tried to use await before units.Where( ... as:
Product product = new Product {
Conversions = model.Conversions.Select(y => new Conversion {
UnitId = await units
.Where(z => z.Name == y.Unit)
.Select(z => z.Unit.Id)
.FirstOrDefaultAsync(),
Value = y.Value
}).ToList(),
Name = model.Name
}
This is not allowed as it is inside new Conversion ...
Shouldn't I use await? How can I use it in this query?
You can use this approach :
unitId = await Task.Factory.Start<int>(()=>{
return units.Where(z => z.Name == y.Unit)
.Select(z => z.Unit.Id)
.FirstOrDefault();
})
change int to your return type.

Prevent sort result of union in entity framework

In SQL server union, result is sorted based on primary key column. I want to prevent this behavior in entity framework.
In this post, #praveen has explained how to do this in pure sql. But I want to do this in entity framework.
My code:
public virtual ActionResult Search(string keyword)
{
var products = _db.Products
.Where(x => x.IsActive)
.AsQueryable();
var productExactlyTitle = products.Where(x => x.Title == keyword);
var productStartTitle = products.Where(x => x.Title.StartsWith(keyword));
var productContainsTitle = products.Where(x => x.Title.Contains(keyword)
|| x.Title.Contains(keyword)
|| x.SubTitle.Contains(keyword)
|| x.OtherName.Contains(keyword));
var productList = productExactlyTitle.Union(productStartTitle)
.Union(productContainsTitle)
.Take(10)
.AsEnumerable()
.Select(x => new ProductItemViewModel()
{
Id = x.Id,
Title = x.Title,
Price = x.Price.ToPrice(),
Image = x.Images.FirstOrDefault(y => y.IsCoverPhoto)?.ImageUrl
});
// some code ...
}
I want to show records with below order:
First: records of productExactlyTitle
Second: records of productStartTitle
Third: records of productContainsTitle
But result is sorted with Id column! and I don't want this.
Is there a way for do this?
In SQL all queries without an order by explicitly set is considered unordered. (and EF queries a translated into SQL). So if you want a specific order after your union just specify it.
var result = q1.Union(q2).OrderBy(x => x.?);
For your specific case:
var p1 = productExactlyTitle.Select(x => new { Item = x, Order = 1 });
var p2 = productStartTitle.Select(x => new { Item = x, Order = 2 });
var p3 = productContainsTitle.Select(x => new { Item = x, Order = 3 });
var productList = p1.Union(p2)
.Union(p3)
.OrderBy(x => x.Order)
.Select(x => x.Item)
.Take(10);

Single database call pulling data from multiple tables in EF Core

The following code currently opens a connection three times to my database, to pull out each object.
Is there a better way to craft the query so the database is only hit once and pulls back all the objects I'm looking for?
var metadataResult = new MetadataViewModel
{
Milestones = goalsContext.Milestones.Select(m => new MilestoneViewModel
{
Id = m.Id,
Name = m.Name,
Year = m.Year,
Date = m.Date
}),
Aggregates = goalsContext.Aggregates.Select(a => new AggregateViewModel
{
Id = a.Id,
Name = a.Name
}),
Metrics = goalsContext.Metrics.Select(m => new MetricViewModel
{
Id = m.Id,
Name = m.Name,
Description = m.Description
})
};
If your view models are a fairly similar shape then you should be able to use Union to get everything in one query and then transform the rows into appropriate ViewModel instances afterwards. Something like the following -
var combinedResults =
context.Products.Select(p => new
{
Type = "Product",
ID = p.ProductID,
Name = p.ProductName,
SupplierName = p.Supplier.CompanyName
})
.Union(
context.Categories.Select(c => new
{
Type = "Category",
ID = c.CategoryID,
Name = c.CategoryName,
SupplierName = (string)null
})
)
.ToList();
var viewModel = new ViewModel
{
Products = combinedResults
.Where(x => x.Type == "Product")
.Select(x => new ProductViewModel
{
ID = x.ID,
Name = x.Name,
SupplierName = x.SupplierName
}),
Categories = combinedResults
.Where(x => x.Type == "Category")
.Select(x => new CategoryViewModel
{
ID = x.ID,
Name = x.Name
})
};

EntityFramework. SelectMany with Anonymous Type and Projection

I have a Banner with multiple Packs. Each pack has multiple files.
I have the following query:
List<BannerModel> models = context.Banners
.Select(x => x.Packs
.SelectMany(p => p.Files, (p, f) => new {
Id = p.Id,
Flag = p.Flag,
File = new { Id = f.Id, Flag = f.Flag, Key = f.Key, Mime = f.Mime }
})
.Where(a => a.File.Flag == "Img_200")
.Select(a => new BannerModel { PackId = a.Id, ImageKey = a.File.Key })
).ToList();
1) I get the error on "ToList()".
Cannot implicitly convert type 'System.Collections.Generic.List>' to 'System.Collections.Generic.List'
2) Then I removed the ToList and added "var models = ..."
I know there are 10 records where 5 of them satisfy the criteria:
.Where(a => a.File.Flag == "Img_200")
What is strange is that I get 10 items, 5 with data and 5 with no data.
Where I should only get a list of 5 items. The one that satisfy the criteria.
Could someone help me solving this problem?
Thank you,
Miguel
Should this be:
List<BannerModel> models = context.Banners
.SelectMany(x => x.Packs
.SelectMany(p => p.Files, (p, f) => new {
Id = p.Id,
Flag = p.Flag,
File = new { Id = f.Id, Flag = f.Flag, Key = f.Key, Mime = f.Mime }
})
.Where(a => a.File.Flag == "Img_200")
.Select(a => new BannerModel { PackId = a.Id, ImageKey = a.File.Key })
).ToList();