Entity Framework - Stop Lazy Loading Related Entities On Demand? - c#

I have Entity Framework set up and it works fine most of the time I need it. I have a structure like so
public partial class Topic : Entity
{
public Guid Id { get; set; }
public string Name { get; set; }
public DateTime CreateDate { get; set; }
public virtual Post LastPost { get; set; }
public virtual Category Category { get; set; }
public virtual IList<Post> Posts { get; set; }
public virtual IList<TopicTag> Tags { get; set; }
public virtual MembershipUser User { get; set; }
public virtual IList<TopicNotification> TopicNotifications { get; set; }
public virtual IList<Favourite> Favourites { get; set; }
public virtual Poll Poll { get; set; }
}
As you can see I have a number of related entities which are lists. These are mapped as standard and are lazy loaded so I can call Topic.Posts or Topic.TopicNotifications etc... (Mappings below)
HasOptional(t => t.LastPost).WithOptionalDependent().Map(m => m.MapKey("Post_Id"));
HasOptional(t => t.Poll).WithOptionalDependent().Map(m => m.MapKey("Poll_Id"));
HasRequired(t => t.Category).WithMany(t => t.Topics).Map(m => m.MapKey("Category_Id"));
HasRequired(t => t.User).WithMany(t => t.Topics).Map(m => m.MapKey("MembershipUser_Id"));
HasMany(x => x.Posts).WithRequired(x => x.Topic).Map(x => x.MapKey("Topic_Id")).WillCascadeOnDelete();
HasMany(x => x.TopicNotifications).WithRequired(x => x.Topic).Map(x => x.MapKey("Topic_Id")).WillCascadeOnDelete();
HasMany(t => t.Tags)
.WithMany(t => t.Topics)
.Map(m =>
{
m.ToTable("Topic_Tag");
m.MapLeftKey("TopicTag_Id");
m.MapRightKey("Topic_Id");
});
This is all an well. But on a couple of occasions I have the need to manually populate Topic.Posts and Topic.Favorites.
But if I try and set Topic.Posts = SomeCollection it triggers the lazy load and loads all the posts first, and then lets me set my collection so I get two sets of sql executed (The first I don't want)
Is there anyway, to switch off the lazy loading manually on demand for just when I want to set the collection manually?
Hope that makes sense... :/

You would be better to turn off lazy loading by default and instead specify when you want to load the extra data in the first place. EF is set up to allow Eager loading by using the .Include() function on your query, with lazy loading it can get messy if you start turning it on/off for various functions, you're better to just turn it off and manage what/when you want data loaded if you're feeling a need to turn it off.
See https://msdn.microsoft.com/en-nz/data/jj574232.aspx for specific examples and a breakdown of the different ways you can eager/lazy load data. The first example shows how you could pull through posts off a blog, which is similar to what you are wanting to acheive.
var topics = context.Topics
.Include(t => t.Posts)
.ToList();

I am not aware of an approach targeted to this exact scenario so I'd have to go with a temporary disable/enable of lazy loading.
using(var context = new MyContext())
{
context.Configuration.LazyLoadingEnabled = false;
// do your thing using .Include() or .Load()
context.Configuration.LazyLoadingEnabled = true;
}
Note however that this is a global configuration so there might be a concurrency problem if that happens in your scenario.

I would not recommend turning off lazing loading on a per request basis. As AllMadHare suggests, you could turn off lazy loading entirely, but that could force changes to how you load all data. I would recommend removing the virtual keyword from Posts so that your class looks like this:
public partial class Topic : Entity
{
public Guid Id { get; set; }
public string Name { get; set; }
public DateTime CreateDate { get; set; }
public virtual Post LastPost { get; set; }
public virtual Category Category { get; set; }
public IList<Post> Posts { get; set; }
public virtual IList<TopicTag> Tags { get; set; }
public virtual MembershipUser User { get; set; }
public virtual IList<TopicNotification> TopicNotifications { get; set; }
public virtual IList<Favourite> Favourites { get; set; }
public virtual Poll Poll { get; set; }
}
Per the documentation found here: https://msdn.microsoft.com/en-us/data/jj574232.aspx#lazyOffProperty this will allow you to lazy load all other navigation properties and eager load posts if you need to.

Since you are using lazy loading, you must have proxies generated for your classes and collection properties.
Replacing these proxy collection properties with your own collections seems pretty weird design to me. You loose change tracking and most probably gain a couple of other strange side effects.
I would recommend to either use proxies/lazy loading and give up the idea of replacing the collections, or stand back from using proxies and gain full control on the generated POCO classes.
Which of both approaches suits best for your needs depends on your overall usage of the entity framework.

Related

How to use Lazy Loading for DbQuery in EF Core?

I am using Lazy Loading for EF Core 2.2.3 with proxies which works well with DbSets. Now I have to load data from a SQL View and am using DbQuery for this. When trying to load related data for the entity used in the query, I'm getting a DetachedLazyLoadingWarning:
Error generated for warning
'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning:
An attempt was made to lazy-load navigation property 'ProjectStatus'
on detached entity of type 'ProjectProxy'. Lazy-loading is not
supported for detached entities or entities that are loaded with
'AsNoTracking()'.'. This exception can be suppressed or logged by
passing event ID 'CoreEventId.DetachedLazyLoadingWarning' to the
'ConfigureWarnings' method in 'DbContext.OnConfiguring' or
'AddDbContext'.
I don't use AsNoTracking() anywhere in my code.
The DbQuery is defined in OnModelCreating of the context. Excerpt:
modelBuilder.Query<ProjectView>()
.ToQuery(() => Projects
.Select(p => new ProjectView()
{
Id = p.Id,
ProjectCategory = p.ProjectCategory,
ProjectPhase = p.ProjectStatus.ProjectPhase,
}));
Projects is a DbSet of the context.
Project.ProjectCategory is a notMapped-Readonly-Property that uses the relation Project.ProjectStatus.
The Properties of the context:
public virtual DbSet<Project> Projects { get; set; }
public virtual DbSet<ProjectStatus> ProjectStatus { get; set; }
public virtual DbQuery<ProjectView> ProjectViews { get; set; }
Excerpt of the classes:
public partial class Project
{
[NotMapped]
public string ProjectCategory
{
get
{
if (this.ProjectStatus == null)
return string.Empty;
var foo = "someweiredcalculations";
return foo
}
}
public virtual ProjectStatus ProjectStatus { get; set; }
public int ProjectStatusId { get; set; }
public int Id { get; set; }
}
public class ProjectView
{
public int Id { get; set; }
public string ProjectCategory { get; set; }
public string ProjectPhase { get; set; }
}
public partial class ProjectStatus : BaseEntity, IIdEntity<int>
{
public int Id { get; set; }
public string ProjectPhase { get; set; }
public virtual ICollection<Project> Projects { get; set; } = new HashSet<Project>();
}
How can I make Lazy Loading work for this DbQuery?
Thank you very much.
This is just a bug of sorts (haven't found work around and issue is still open). Also read this. As advised and I’m quoting
Note that the warning can be configured to not throw using ConfigureWarnings in the DbContextOptionsBuilder.
This seems to be by design, regardless whether .AsNoTracking() is used or not. I did not find any documentation on this.
If you are stuck on .net core 2 the only workaround I found was to load related entities in a separate query. Effectively breaking lazy loading.
As of .net core 3+ it is possible to work around this by eager loading related properties (.Include(x => x.ProjectStatus)) when Lazy Loading is Used. This was fixed by: https://github.com/aspnet/EntityFrameworkCore/issues/12780. And the error message when using lazy loading was changed to: 'Unable to track an instance of type 'FooQuery' because it does not have a primary key. Only entity types with primary keys may be tracked.'

Entity Framework proxycreation misery

ive seen quite a few solutions on problems which are always similar to mine. But none of them satisfied my needs specifically.
I have the following setup:
public class User
{
public Int32 Id { get; set; }
public String Name { get; set; }
public virtual List<PhoneBook> Phonebooks { get; set; }
}
public class PhoneBook
{
public Int32 Id { get; set; }
public virtual Int32 UserId { get; set; }
public virtual User User { get; set; }
public String Title { get; set; }
public virtual List<PhoneNumber> Number { get; set; }
}
public class PhoneNumber
{
public Int32 Id { get; set; }
public String Num { get; set; }
public virtual Int32 PhoneBookId { get; set; }
public virtual PhoneBook PhoneBook { get; set; }
}
Now I can query perfectly fine. The problem is the circular referencing.
Let's say I would want to select every Phonebook thats for the user with the id = 1
My query would be
context.phonebook.Where(x=>x.UserId == 1)
This would load the user and every phonenumber as well.
I could turn off the proxying and I would get nothing but the Phonebook ( no numbers, no user ).
What if I wanted to get every phonenumber too but not the user?
Also I'd have to make sure that getting the phonenumber objects doesnt also mean it's getting the phonebook related to the number too.
I kind of need the proxying to only go one way. Down the ladder and not upwards.
First of all, your foreign key ids do not need to be virtual. You use virtual for navigation properties (or other entity type properties) only: your foreign key is not a navigation property, it's just an integer.
Now your question is not related to proxies really, it's related to lazy loading (proxies enable lazy loading by default, which is why you are concerned to them, but that's not necessary).
If you only want to load the specified entities, disable lazy loading and do eager loading:
context.Configuration.LazyLoadingEnabled = false;
// Load phonebook entries and users matching your id
var result = content.phonebook
.Include(x => x.Number)
.Include(x => x.User)
.Where(x => x.UserId == 1);
// Load only phonebook entries without users
var result2 = content.phonebook
.Include(x => x.Number)
.Where(x => x.UserId == 1);
That simple
PS: proxies have more advantages to them than lazy loading (like change tracking, etc.). If your problem is lazy loading, turn lazy loading off, not proxies.
In this case it's not proxy creation that's causing misery, it's lazy loading. Disable it by all means, and include the navigation properties you want or project your results specifically:
context.phonebook
.Include(x=> x.Number)
.Where(x=>x.UserId == 1);
// or
context.phonebook
.Where(x=>x.UserId == 1)
.Select(x=> new{
Phonebook = x,
Number = x.Number
});

Creating a Blog Comments and Reply section using ASP.NET MVC 4 (nested collections)

I'm building a Blog Comment and Reply section and I have these three classes mapped to my DB. The first class holds a collection of related comments to an article, the second class holds a collection of related remarks to the comments:
public class Article
{
public int ArticleID { get; set; }
public byte[] Image { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public DateTime DatePublished { get; set; }
public string Author { get; set; }
public CategoryTyp Category { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public int CommentID { get; set; }
public int ArticleID { get; set; }
public int CategoryID { get; set; }
public int UserID { get; set; }
public string Description { get; set; }
public DateTime CommentDate { get; set; }
public virtual ICollection<Remark> Remarks { get; set; }
}
public class Remark
{
public int RemarkID { get; set; }
public int CommentID { get; set; }
public int ArticleID { get; set; }
public string RemarkDetail { get; set; }
public DateTime RemarkTime { get; set; }
}
And inside my Controller:
public ActionResult GetArticle(int id)
{
var article = db.Articles.Include("Comments").Where(a => a.ArticleID == id).SingleOrDefault();
return View(article);
}
I understand the basis of eager loading but my questions are:
How do you implement it when you're pulling data from multiple related tables?
What is the best practice of populating it to the View? Once I create a View Model how do I stuff the related collections?
1) With multiple related tables you can have two scenarios:
a) Multiple top level relations: you simply add multiple Include statements (I would suggest using lambda expressions instead of strings for this, to avoid typos).
db.Articles
.Include(a=>a.Comments)
.Include(a=>a.SomethingElse)
.FirstOrDefault(a=>ArticleID==id); // Side note: I would suggest this instead of your Where plus SingleOrDefault
For these scenarios I always use a helper method like this one.
b) Multiple nested related entities:
db.Articles
.Include(a=>a.Comments.Select(c=>c.Remarks)
.FirstOrDefault(a=>ArticleID==id);
2) It's a bit up to you how you pass the data to the views. One best practice I can tell you is that you shouldn't let views lazy load any dependant entities or collections. So your use of Include is correct, but I would even suggest to remove the virtual (deactivate lazy loading) to avoid missing an Include by accident.
Regarding the ViewModels you mention, you are actually not using view models, but your data models. This is OK in most cases, unless you need to format the data somehow or add extra information. Then you would need to create a View Model and map it from the data coming from EF.
Another scenario would be if you used WebAPI or an Ajax Action. In that case, I would suggest to use a DTO (equivalent to a ViewModel) to be able to better control the data returned and its serialization.
One last comment about ViewModels is that if you have heavy entities but you only need a few properties, a good choice is to use Projections, to instruct EF to only load the required properties, instead of the full object.
db.Articles
.Include(a=>a.Comments)
.Select(a=>new ArticleDto { Id = a.ArticleID, Title = a.Title })
.ToListAsync();
This will translate to a "SELECT ArticleID, Title FROM Articles", avoiding returning the article bodies and other stuff that you might not need.
You can chain the relationships with Include. For example:
var article = db.Articles.Include("Comments.Remarks").Where(a => a.ArticleID == id).SingleOrDefault();
I'm not sure what you mean by your second question, though. By issuing this query you already have all the comments and all the remarks for those comments. Therefore, you can access them off of the article instance out of the box:
foreach (var comment in article.Comments)
{
...
foreach (var remark in comment.Remarks)
{
...
}
}
How you handle that with your view model is entirely up to you. You could map the comments/remarks to view models of their own, set them directly on the view model, etc. That's all down to what the needs of your application are, and no one but you can speak to that.

SetFetchMode Lazy doesn't overwrite ClassMap settings

In Criteria I do SetFetchMode as Lazy but still fetching all items, how can I fix this?
public class MenuItem : BaseClass<MenuItem>
{
public virtual int MenuItemId { get; set; }
public virtual string Text { get; set; }
public virtual IList<MenuItem> Children { get; set; }
public virtual MenuItem Parent { get; set; }
public MenuItem()
{
Children = new List<MenuItem>();
}
}
class MenuItemMap : ClassMap<MenuItem>
{
public MenuItemMap()
{
Id(x => x.MenuItemId);
Map(x => x.Text);
HasMany(x => x.Children).KeyColumn("ParentId").Not.LazyLoad().Fetch.Select();
References(x => x.Parent).Not.LazyLoad().Fetch.Select();
}
}
using (var session = NHibernateHelper<T>.OpenSession())
{
var CC = session.CreateCriteria(typeof(T));
CC.SetFetchMode("Children", FetchMode.Lazy);
return CC.List<T>();
}
I have to say, that this is not possible. Our JAVA brother Hibernate (from which NHibernate was ported into .NET) seems to have that option:
Hibernate: Enabling lazy fetching in Criteria API
But NHibernate does not support that. Chek also Override lazy loading behavior 'lazy=false'.
What we can do, is to bet on Projections. This way we really assemble exactly one SQL statement and get a list of Transformed objects (semi-filled, dependent on amount of selected properties)
16.6. Projections
Here is an example how to build a deep projections (including References/many-to-one)
Here is how to transform them into originally mapped object Graph.

Many-to-many relationships using EF Code First

I have two classes defined as such:
public class Questionnaire
{
public int QuestionnaireID { get; set; }
public string Title { get; set; }
public bool Active { get; set; }
public virtual ICollection<Question> Questions { get; set; }
public virtual ICollection<Vendor> Vendors { get; set; }
}
public class Vendor
{
public int VendorID { get; set; }
public string VendorName { get; set; }
public virtual ICollection<Questionnaire> OpenQuestionnaires { get; set; }
public virtual ICollection<Questionnaire> SubmittedQuestionnaires { get; set; }
public virtual ICollection<QuestionnaireUser> QuestionnaireUsers { get; set; }
}
I beleive this is the correct way to establish a many-to-many relationship between these classes, and when the project is built, I would expect three tables to be created.
However, when I attempt to to relate one Questionnaire to two different Vendors, I receive the following error when attempting to save the changes (context.SaveChanges()):
*Multiplicity constraint violated. The role 'Vendor_OpenQuestionnaires_Source' of the relationship 'QuestionnaireApp.Models.Vendor_OpenQuestionnaires' has multiplicity 1 or 0..1.*
If I assign a Questionnaire to only one Vendor, save the changes and then assign it to another and again save changes I no longer get the error; however the Questionaire is then related only to the last Vendor to which it was assigned, indicating that (at best) there is a one-to-many relationship being created.
I'm hoping that there is something wrong with the way I'm declaring the many-to-many relationship between these classes, or perhaps there is something I need to add to the context class to "encourage" the relationsip, but perhaps many-to-many relationships like this are not supported, or cannot be created using "Code First"?
Thank you for your time,
Jason
If you don't have any Fluent API code your expected mapping relies on EF Code First conventions. The convention which you expect to kick in here is the AssociationInverseDiscoveryConvention. Now if you look in Intellisense (and probably also documentation) it says about this convention:
Convention to detect navigation properties to be inverses of each
other when only one pair of navigation properties exists between the
related types.
Now, that's the problem: You don't have only "one pair" of navigation properties between Questionnaire and Vendor. You have two collections in Vendor refering to Questionnaire and one collection in Questionnaire refering to Vendor. The result is that this convention doesn't get applied and EF maps actually three one-to-many relationships with only one end exposed as navigation property in the model.
Moreover the mapping you want to achieve is not possible with your model: You cannot map the one end Questionnaire.Vendors to the two ends Vendor.OpenQuestionnaires and Vendor.SubmittedQuestionnaires.
One workaround is to change your model the following way:
public class Vendor
{
public int VendorID { get; set; }
public string VendorName { get; set; }
[NotMapped]
public IEnumerable<Questionnaire> OpenQuestionnaires
{
get { return Questionnaires.Where(q => q.IsActive); }
}
[NotMapped]
public IEnumerable<Questionnaire> SubmittedQuestionnaires
{
get { return Questionnaires.Where(q => !q.IsActive); }
}
public virtual ICollection<Questionnaire> Questionnaires { get; set; }
public virtual ICollection<QuestionnaireUser> QuestionnaireUsers { get; set; }
}
Now Vendor.Questionnaires is mapped to Questionnaire.Vendors (AssociationInverseDiscoveryConvention should detect this) and the helper properties OpenQuestionnaires and SubmittedQuestionnaires allow you to pull out the selected items. (I'm not sure if IsActive is your distinguishing flag. Otherwise you have to introduce some new flag.)
The [NotMapped] attribute is just here to make it explicite. It is probably not necessary because EF won't map IEnumerable collections and readonly properties with only a getter anyway.
Go figure, after an hour or so of searching, I go and find the exact answer 30 seconds after I post my question.
The solution was to add the following to the context class:
modelBuilder.Entity<Vendor>()
.HasMany<Questionnaire>(x => x.OpenQuestionnaires)
.WithMany(x => x.Vendors)
.Map(x =>
{
x.MapLeftKey("vID");
x.MapRightKey("qID");
x.ToTable("VendorQuestionnaires");
});
I found the answer by reading this Stack Overflow post: EF Code First Many-to-Many not working

Categories

Resources