EF Core: detached lazy-loading navigation entity. Why? - c#

I have the following model:
public partial class Device
{
public int Id { get; set; }
public virtual Tablet Tablet { get; set; }
where Tablet is the following:
public class Tablet
{
public string TabletId { get; set; }
public int DeviceId { get; set; }
public virtual Device Device { get; set; }
private ICollection<TabletTransferRequest> _tabletTransferRequests;
public virtual ICollection<TabletTransferRequest> TabletTransferRequests { get => _tabletTransferRequests ?? (_tabletTransferRequests = new List<TabletTransferRequest>()); protected set => _tabletTransferRequests = value; }
}
and the mapping class:
public class TabletMap : IEntityTypeConfiguration<Tablet>
{
public void Configure(EntityTypeBuilder<Tablet> builder)
{
builder.ToTable(nameof(Tablet));
builder.HasKey(p => p.TabletId);
builder.HasOne(p => p.Device)
.WithOne(o => o.Tablet)
.HasForeignKey<Tablet>(p => p.DeviceId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict)
;
}
}
DTO classes:
public class DeviceDisplayDto
{
public int Id { get; set; }
public TabletPartDto Tablet { get; set; }
}
public class TabletPartDto
{
public string TabletId { get; set; }
public List<TabletTransferRequestElementDto> TabletTransferRequests { get; set; }
}
public class TabletTransferRequestElementDto : DeviceRequestElementAbstractDto
{
public string TabletId { get; set; }
public int DeviceId { get; set; }
}
when I try to do the following
var query = _context.Devices.Include(d => d.Tablet).ThenInclude(d => d.TabletTransferRequests);
var devices = new PagedList<DeviceDisplayDto>(query.ProjectTo<DeviceDisplayDto>(_mapperConfig), pageIndex, pageSize);
I got the following:
Error generated for warning
'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning:
An attempt was made to lazy-load navigation property
'TabletTransferRequests' on detached entity of type 'TabletProxy'.
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'.
why it's detached?

Is the error occurring on the var devices = new PagedList(...) or later, such as when the controller method completes? I don't recognize that PagedList implementation, but from what you've typed that rendition appears to be initializing with IQueryable<T> rather than a static collection IList<T> which may be an issue if the PagedList ends up attempting to resolve paging from the IQueryable after an initial load.
Normally PagedList would utilize a ToPagedList(page, pageSize) method when working with IQueryable which would trigger a one-off load of a Page. If written as a class initialized with an IQueryable, a data retrieval query could be non-fillable if queried after the DbContext is disposed.

Related

Prevent circular references when getting result from database

If I have the following simple model:
public class Company
{
public Guid CompanyId { get; set; }
public ICollection<CompanyUser> Users { get; set; }
}
public class CompanyUser
{
public Guid CompanyId { get; set; }
public Guid UserId { get; set; }
public Company Company { get; set; }
public User User { get; set; }
}
public class User
{
public Guid UserId { get; set; }
public ICollection<CompanyUser> Companies { get; set; }
}
To get a list of companies + their users + the user object I run the following query:
return await _dataContext.Companies
.Include(m => m.Users)
.ThenInclude(m => m.User)
.OrderBy(m => m.Name)
.ToListAsync();
The results work, but I use a mapper to map the results to a view model by going recurisvely through the model.
What happens is that the Company object has a reference to a list of CompanyUser, in each of those CompanyUser objects we have a Company which has a list of CompanyUser again, which just keeps repeating until we get a stack overflow.
The mapper is a very simple one:
var results = companies.ToViewModel<Company, CompanyViewModel>();
public static IList<TModel> ToViewModel<TEntity, TModel>(this IEnumerable<TEntity> entities)
where TEntity : class
where TModel : class, IViewModel<TEntity>, new()
{
return entities?.Select(entity => entity.ToViewModel<TEntity, TModel>()).ToList();
}
public static TModel ToViewModel<TEntity, TModel>(this TEntity entity)
where TEntity : class
where TModel : class, IViewModel<TEntity>, new()
{
if (entity == null)
{
return null;
}
var model = new TModel();
model.ToViewModel(entity);
return model;
}
public interface IViewModel<in TEntity>
where TEntity : class
{
void ToViewModel(TEntity entity);
}
public class CompanyViewModel : IViewModel<Company>
{
public Guid CompanyId { get; set; }
public IList<CompanyUserViewModel> Users { get; set; }
public void ToViewModel(Company entity)
{
CompanyId = entity.CompanyId;
Users = entity.Users.ToViewModel<CompanyUser, CompanyUserViewModel>();
}
}
public class CompanyUserViewModel : IViewModel<CompanyUser>
{
public Guid CompanyId { get; set; }
public Guid UserId { get; set; }
public CompanyViewModel Company { get; set; }
public UserViewModel User { get; set; }
public void ToViewModel(CompanyUser entity)
{
CompanyId = entity.CompanyId;
UserId = entity.UserId;
Company = entity.Company.ToViewModel<Company, CompanyViewModel>();
User = entity.User.ToViewModel<User, UserViewModel>();
}
}
public class UserViewModel : IViewModel<User>
{
public Guid UserId { get; set; }
public void ToViewModel(User entity)
{
UserId = entity.Id;
}
}
Is there a way to prevent these references to be resolved?
There are multiple solutions:
1) You can use automapper instead of own mapper. It has MaxDepth property which will prevents from this problem:
Mapper.CreateMap<Source, Destination>().MaxDepth(1);
2) You can remove dependencies from your entities and use shadow properties in one direction.
Are you open to changing your data model? I think the best solution would be to remove the circular reference.
If a company contains a list of users, does the User also need both the CompanyId and the Company object he is contained in? I would remove public Company Company { get; set; } from your CompanyUser object and Companies from your User object.
Your issue is that you are mapping to CompanyViewModel which then maps to CompanyUserViewModel but this then maps back again to CompanyViewModel which creates an infinite loop.
If you expect to always start at Company (to CompanyView) then remove the recursion back from CompanyUserViewModel.
public void ToViewModel(CompanyUser entity)
{
CompanyId = entity.CompanyId;
UserId = entity.UserId;
// Company = entity.Company.ToViewModel<Company, CompanyViewModel>();
User = entity.User.ToViewModel<User, UserViewModel>();
}
Alternatively do not map the relations in your ToViewModel mapping, wire up the relations after based on the Ids.

Entity Framework query with simple join

I am using Entity Framework 6 in a project and am having trouble creating a query.
Say my classes are defined like:
public class MyContext : DbContext
{
public MyContext(string connectionString) : base(connectionString)
{
}
public DbSet<EntityXXX> XXXSet { get; set; }
public DbSet<EntityYYY> YYYSet { get; set; }
}
public class EntityXXX
{
public string XXXName { get; set; }
public int id { get; set; }
public int YYYid { get; set; }
}
public class EntityYYY
{
public string YYYName { get; set; }
public int id { get; set; }
}
The YYYid property of EntityXXX is the 'id' of the EntityYYY instance that it relates to.
I want to be able to fill a Grid with rows where the first Column is XXXName and the second column is YYYName (from its related EntityYYY), but I can't see how to do this?
I'm sure it's really simple, but I'm new to EF.
You need to put a virtual navigation property on your EntityXXX
public virtual EntityYYY YYY { get; set; }
Then you can do a projection:
db.XXXSet
.Select(x => new { x.XXXName, YYYName = x.YYY.YYYName })
.ToList();
Which will get you the list you need.

ASP.NET Core relation is in database but is not retrieved

I am new to ASP.NET Core and I am trying to setup basic relationship with EntityFramework Core. I have a NavigationCategory class that has 1:M relationship with WebPage class. The relationship is understood by my DB (It has a foreign key) but when retrieving the WebPage from repository, it has no NavigationCategory, even though it has NavigationCategoryId set.
public class WebPage : BaseEntity
{
private string _route;
public string Title { get; set; }
public string Body { get; set; }
[ForeignKey("NavigationCategory")]
public long NavigationCategoryId { get; set; }
public NavigationCategory NavigationCategory { get; set; }
public WebPage()
{
}
}
public class NavigationCategory : BaseEntity
{
public string Title { get; set; }
public IEnumerable<WebPage> WebPages { get; set; }
public NavigationCategory()
{
}
}
This is simple BaseEntity:
public class BaseEntity
{
public long Id { get; set; }
}
This is my DB context:
public class AppDataContext : DbContext
{
public DbSet<Employee> Employees { get; set; }
public DbSet<KinderClass> KinderClasses { get; set; }
public DbSet<Feed> Feeds { get; set; }
public DbSet<NavigationCategory> NavigationCategories { get; set; }
public DbSet<WebPage> WebPages { get; set; }
public AppDataContext(DbContextOptions<AppDataContext> options) : base(options)
{
Database.EnsureCreated();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<WebPage>()
.HasOne(wp => wp.NavigationCategory)
.WithMany(c => c.WebPages)
.HasForeignKey(wp => wp.NavigationCategoryId);
}
}
You need to explicitly include any navigation property you want included when you fetch entities using EF Core. For example, update your query as follows:
var webpage = dbContext.WebPages
.Include(w => w.NavigationCategory)
.FirstOrDefault();
Note, you need these two namespaces, at minimum:
using Microsoft.EntityFrameworkCore;
using System.Linq;
Learn more about loading related data and why lazy loading shouldn't be used in web apps.

What's the proper way to get data using Entity Framework so you can navigate through objects?

I have a .Net 4.5 MVC 5 database first project that I'm playing around with. There's a data access layer (Entity Framework 6), a business logic layer and the MVC layer.
If I have an object with relationships in the data layer:
namespace DataAccess
{
public class Course
{
public int CourseID { get; set; }
public string Title { get; set; }
public ICollection<Lecture> Lectures { get; set; }
public ICollection<Tutor> Tutors { get; set; }
}
public class Lecture
{
public int LectureID { get; set; }
public string Title { get; set; }
public DateTime Date { get; set; }
public ICollection<Student> Students { get; set; }
}
public class Tutor
{
public int TutorID { get; set; }
public string Name { get; set; }
}
public class Student
{
public int StudentID { get; set; }
public string Name { get; set; }
}
}
And in my business logic layer I have a method that gets courses:
namespace BusinessLogic
{
public static IEnumerable<Course> GetCourses()
{
using (var db = new MyEntities())
{
return db.Courses.Include("Lectures").Include("Lectures.Students").Include("Tutors").ToList();
}
}
}
And I get the data using my controller like this:
public class HomeController : Controller
{
public ActionResult Index()
{
var courses = BusinessLogic.GetCourses();
return View(courses);
}
}
Why is it, when I query my data in the Razor view like this:
var numLectures = courses.Lectures.Count;
var numStudents = courses.Lectures.Students.Count;
var tutorName = courses.Tutors.LastOrDefault().Name;
I get the application error System.ObjectDisposedException: The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.
I know the connection is disposed after the using statement has finished and that .ToList() will let me navigate the courses object, but how do I navigate the objects inside each course (i.e. lectures, students, tutors etc.)?
Your navigation properties need to be declared as virtual:
namespace DataAccess
{
public class Course
{
public int CourseID { get; set; }
public string Title { get; set; }
public virtual ICollection<Lecture> Lectures { get; set; }
public virtual ICollection<Tutor> Tutors { get; set; }
}
public class Lecture
{
public int LectureID { get; set; }
public string Title { get; set; }
public DateTime Date { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
...
}
When these lazy loadable properties are not marked as virtual, the EF dynamic proxies cannot override them and you will never be able to navigate from one entity to a (set of) another.
Another bit of advice: use the strongly-typed .Include when eager loading:
namespace BusinessLogic
{
public static IEnumerable<Course> GetCourses()
{
using (var db = new MyEntities())
{
return db.Courses
.Include(x => x.Lectures.Select(y => y.Students))
.Include(x => x.Tutors)
.ToList();
}
}
}
I think the problem is because one (or more than one) property that you are calling in your View is (are) not included in your query. Make sure you are including all the navigation properties you need in the view. Try with this query:
using (var db = new MyEntities())
{
return db.Courses.db.Courses.Include(c=>c.Lectures.Select(l=>l.Students)).Include(c=>c.Tutors‌​).ToList()
}
If you need to add another relative property that you use in your View, then add another Include call for that property.
Another thing, when you need to eager load two levels (like Lectures.Students), you don't need to add a Include call for each level, with the call that you do for the second level is enough to include both. Could be this way:
.Include("Lectures.Students") // as you did it
Or:
.Include(c=>c.Lectures.Select(l=>l.Students))

Why is The navigation property '' declared on type '' has been configured with conflicting multiplicities. error show up?

I have an Asp_Users table and a Resource tables. In the Entity Framework world, I have a Asp_Users POCO and a Resource POCO. Moreover, the Resource POCO is abstract and is part of a Table-per-Hierarchy model. The Table-per-Hierarchy model has the abstract Resource POCO and several Concrete POCOs like ILCResource POCO and SectionResource POCO. There is a one-to-many (1 to 0…*) relationship from Asp_Users POCO (one-side) to Resource POCO (many-side).
Here's the relevant part of my aspnet_Users POCO:
public partial class aspnet_Users
{
public aspnet_Users() { }
public virtual System.Guid ApplicationId { get; set; }
public virtual System.Guid UserId { get; set; }
public virtual string UserName { get; set; }
public virtual string LoweredUserName { get; set; }
public virtual string MobileAlias { get; set; }
public virtual bool IsAnonymous { get; set; }
public virtual System.DateTime LastActivityDate { get; set; }
public virtual ICollection<Resource> associatedResources { get; set; }
}
Here is my mapping configuration for Asp_Users
public class Aspnet_UsersMap : EntityTypeConfiguration<PerlsData.Domain.aspnet_Users>
{
public Aspnet_UsersMap()
{
this.ToTable("aspnet_Users", schemaName: "dbo");
this.HasKey(u => u.UserId);
this.Property(u => u.UserId)
.HasColumnName("UserId")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.HasOptional(u => u.associatedResources);
}
}
Here's the relevant part of my abstract Resource POCO class:
public abstract class Resource
{
public Resource(){
// associatedPerspectives = new HashSet<Perspective>();
}
public virtual System.Guid ResourceDatabaseID { get; set; }
public virtual string ResourceName { get; set; }
public virtual string DescriptionOfResource { get; set; }
public virtual System.Guid UserId { get; set; }
public virtual Nullable<System.Guid> DepartmentDatabaseID { get; set; }
public virtual string ResourceStatus { get; set; }
public virtual Nullable<short> isRemoved { get; set; }
public virtual Department Department { get; set; }
public virtual System.Guid UserId { get; set; }
public virtual aspnet_Users aspnet_Users { get; set; }
public virtual ICollection<ResourceOverToILCResourcesBridge> associatedResourceOverToILCResourcesBridgeEntry { get; set; }
}
Here is my mapping configuration for Resource:
public class ResourceMap : EntityTypeConfiguration<Resource>
{
public ResourceMap()
{
this.ToTable("Resources", schemaName: "dbo");
this.Property(r => r.ResourceDatabaseID)
.HasColumnName("ResourceDatabaseID");
this.HasKey(r => r.ResourceDatabaseID);
this.Property(x => x.ResourceDatabaseID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
// .StoreGeneratedPattern = StoreGeneratedPattern.Identity;
this.Property(r => r.ResourceName)
.HasColumnName("ResourceName");
this.Map<PerlsData.Domain.OtherItem>(m => m.Requires("discriminator").HasValue("otheritems"))
.Map<PerlsData.Domain.Audioitem>(m => m.Requires("discriminator").HasValue("audioitems"))
.Map<PerlsData.Domain.Imageitem>(m => m.Requires("discriminator").HasValue("imageitems"))
.Map<PerlsData.Domain.Videoitem>(m => m.Requires("discriminator").HasValue("videoitems"))
.Map<PerlsData.Domain.UriItem>(m => m.Requires("discriminator").HasValue("uriitems"))
.Map<PerlsData.Domain.Documentitem>(m => m.Requires("discriminator").HasValue("documentitems"))
.Map<PerlsData.Domain.DatabaseFileItem>(m => m.Requires("discriminator").HasValue("databasefileitems"));
this.HasOptional(res => res.associatedResourceOverToILCResourcesBridgeEntry);
this.HasRequired(res => res.aspnet_Users)
.WithMany(u => u.associatedResources)
.HasForeignKey(res => res.UserId);
}
}
Could you please tell me why I am getting the following error?
The navigation property 'associatedResources' declared on type
'PerlsData.Domain.aspnet_Users' has been configured with conflicting
multiplicities.
Please Explain Why it's still NULL after I created the mapping in the POCO class.
this.HasOptional(u => u.associatedResources);
That's wrong. HasOptional means 0..1.
You want HasMany().
In fact, you can get rid of that line entirely; EF can figure it out from the property.
I need to study more about the IQueryable and/or IEnumerable collections along with the lambda expression queries that are used to retrieve data, and place them in the aforementioned IQueryable and/or IEnumerable.
My problem was that I was trying to retrieve the associations of a particular POCO within a loop that iterates over the IQueryable and/or IEnumerable collections.
Here is the Faulty code (we are accessing association while we iterate over IQueryable):
resourcestempIQueryable = context.Resources;
foreach (PerlsData.Domain.Resource tempRes in resourcestempIQueryable)
{
string[] resourceRow = { tempRes.ResourceName,
tempRes.DescriptionOfResource,
tempRes.UploadDate.ToString(),
tempRes.Iconpath,
tempRes.aspnet_Users.UserName, // Error
tempRes.ResourceDatabaseID.ToString() };
tempDataTableForResources.Rows.Add(resourceRow);
}
Here is the proper code that should resolve the issue:
using (PerlsData.Context context = new PerlsData.Context())
{
context.Configuration.LazyLoadingEnabled = true;
context.Configuration.ProxyCreationEnabled = true;
resourcesIEnumerable = context.Resources.ToList<PerlsData.Domain.Resource>();
var entityValuesWithFieldsToShowInAllResourcesPage = context.Resources.Select(res => new { res.ResourceName, res.DescriptionOfResource, res.UploadDate, res.Iconpath, res.aspnet_Users.UserName, res.ResourceDatabaseID });
foreach (var varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage in entityValuesWithFieldsToShowInAllResourcesPage)
{
string[] resourceRow = {
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.ResourceName,
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.DescriptionOfResource,
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.UploadDate.ToString(),
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.Iconpath,
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.UserName,
varForIteratingOverEntityValuesWithFieldsToShowInAllResourcesPage.ResourceDatabaseID.ToString()
};
tempDataTableForResources.Rows.Add(resourceRow);
}
}

Categories