Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider not emitting DetachedLazyLoadingWarning when entity is new rather than detached #19338

Closed
Tracked by #22954
vg2019 opened this issue Dec 17, 2019 · 12 comments
Closed
Tracked by #22954
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-enhancement
Milestone

Comments

@vg2019
Copy link

vg2019 commented Dec 17, 2019

We are using EF Core 3.0
Below is a very simplified sample of our code, to reproduce the error
On our application , when we load an Account from database , with optionsBuilder.UseLazyLoadingProxies(true) , if we write any code in the Constructor part that tries to access a navigation property ( AccountAddress for the current error case) , we receive the following error, however the object is neither detached nor the context is disposed.
It is understandable that , the Proxy is creating an instance of our Class and overriding the virtual properties, so at initalization stage , AccountAddress is null, however, before in EF 6.3 , the behaviour of proxy was to show the property ( AccountAddress ) as null, and not show an error.

Error

System.InvalidOperationException: 'Error generated for warning 'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning': An attempt was made to lazy-load navigation property 'AccountAddress' on detached entity of type 'AccountProxy'. 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'.'

Steps to reproduce

EFCore3V2Context.cs

using Microsoft.EntityFrameworkCore;

namespace EFCore3V2.BusinessLayer.Models
{
    public class EFCore3V2Context : DbContext
    {
        public EFCore3V2Context()
        {
            var connectionString = "Data Source=TestServer;" +
                "Initial Catalog=EFCore3V2DB;Integrated Security=True;MultipleActiveResultSets=True";
            DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder();
            optionsBuilder.UseSqlServer(connectionString);
            optionsBuilder.UseLazyLoadingProxies(true);
            optionsBuilder.EnableDetailedErrors(true);
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
            .UseLazyLoadingProxies()
            .UseSqlServer("Data Source=TestServer;" +
                "Initial Catalog=EFCore3V2DB;Integrated Security=True;MultipleActiveResultSets=True");
        public DbSet<Account> Accounts { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Account>().HasKey(t => t.Id);
            modelBuilder.Entity<Account>().Property(t => t.Name).HasMaxLength(100);
            modelBuilder.Entity<Account>().OwnsOne(t => t.AccountAddress);
        }
        public class Account
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public virtual Address AccountAddress { get; set; }
            private EFCore3V2Context Context { get; set; }
            public Account()
            {
                AccountAddress = new Address();

                if (AccountAddress != null)
                //Sample Call Of Navigational Property to reproduce the error
                {
                    if (AccountAddress.City.Contains("Paris"))
                    {
                        AccountAddress.Country = "France";
                    }
                }
            }
        }

        [Owned]
        public class Address
        {
            public string City { get; set; }
            public string Country { get; set; }
            public Address()
            {

            }
        }
    }
}

Program.cs

using EFCore3V2.BusinessLayer.Models;
using System;
using System.Linq;

namespace EFCore3V2.ConsoleUI
{
    class Program
    {
        static void Main(string[] args)
        {
            EFCore3V2Context context = new EFCore3V2Context();
            EFCore3V2Context.Account newAccount = new EFCore3V2Context.Account();
            newAccount.Name = "Test Name";
            newAccount.AccountAddress.City = "Paris";
            context.Accounts.Add(newAccount);
            var saveResult = context.SaveChanges();

            EFCore3V2Context contextNo2 = new EFCore3V2Context();
            var accounts = contextNo2.Accounts.ToList();
            foreach (var account in accounts)
            {
                Console.WriteLine(account.Name);
            }
            Console.ReadLine();
            return;
           
        }
    }
}

Further technical details

EF Core version: EF Core 3.0
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: NET Core 3.0
Operating system: Windows 10
IDE: Visual Studio 2019 16.3

@ajcvickers
Copy link
Member

Note for triage: we could consider trying to change this. However, I expect setting new entity instances in the constructor is going to cause other problems. We should discuss this.

@vg2019
Copy link
Author

vg2019 commented Dec 19, 2019

Note for triage: we could consider trying to change this. However, I expect setting new entity instances in the constructor is going to cause other problems. We should discuss this.

Hi,

Actually it was way too simplifed , so i put a more realistic code of the project.
Actually in our scenario , we have Invoice and InvoiceDetails(BindingList), we are trying to subscribe to ListChanged event ( Later on we have further event for tracking the change in InvoiceDetail.Price, however it's same logic so i didnt include here in the sample code), to we can update the Total at The Invoice.
And the same error happens, as we try to access InvoiceDetails (Navigation property)
Do you have any insights

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;
using System.Linq;

namespace EFCore3V2.BusinessLayer.Models
{
    public class EFCore3V2Context : DbContext
    {
        public EFCore3V2Context()
        {
            var connectionString = "Data Source=TEST;" +
                "Initial Catalog=EFCore3V2DB;Integrated Security=True;MultipleActiveResultSets=True";
            DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder();
            optionsBuilder.UseSqlServer(connectionString);
            optionsBuilder.UseLazyLoadingProxies(true);
            optionsBuilder.EnableDetailedErrors(true);
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
            .UseLazyLoadingProxies()
            .UseSqlServer("Data Source=TEST;Initial Catalog=EFCore3V2DB;Integrated Security=True;MultipleActiveResultSets=True");
        public DbSet<Invoice> Invoices { get; set; }
        public DbSet<InvoiceDetail> InvoiceDetails { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Invoice>().HasKey(t => t.Id);

            modelBuilder.Entity<InvoiceDetail>().HasKey(t => t.Id);
            modelBuilder.Entity<InvoiceDetail>().HasOne(t => t.Invoice).WithMany(t => t.InvoiceDetails).HasForeignKey(t => t.InvoiceId).OnDelete(DeleteBehavior.Restrict);

        }
        public class Invoice
        {
            public int Id { get; set; }
            public double Total { get; set; }
            private EFCore3V2Context Context { get; set; }
            public virtual BindingList<InvoiceDetail> InvoiceDetails { get; set; }
            public Invoice()
            {
                InvoiceDetails = new BindingList<InvoiceDetail>();
                InvoiceDetails.ListChanged += InvoiceDetails_ListChanged;
            }

            private void InvoiceDetails_ListChanged(object sender, ListChangedEventArgs e)
            {
                this.Total = InvoiceDetails.Sum(t => t.Price);
            }
        }
        public class InvoiceDetail
        {
            public int Id { get; set; }
            public int InvoiceId { get; set; }
            public virtual Invoice Invoice { get; set; }
            public string ProductCode { get; set; }
            public double Price { get; set; }
        }
    }
}
using EFCore3V2.BusinessLayer.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;

namespace EFCore3V2.ConsoleUI
{
    class Program
    {
        static void Main(string[] args)
        {
            EFCore3V2Context context = new EFCore3V2Context();
            EFCore3V2Context.Invoice newInvoice = new EFCore3V2Context.Invoice();
            EFCore3V2Context.InvoiceDetail newInvoiceDetail = new EFCore3V2Context.InvoiceDetail();
            newInvoiceDetail.ProductCode = "AAA";
            newInvoiceDetail.Price = 23;
            newInvoice.InvoiceDetails.Add(newInvoiceDetail);
            context.Invoices.Add(newInvoice);
            context.SaveChanges();

            EFCore3V2Context context2 = new EFCore3V2Context();

// Error Happens here when the object is loaded with Proxy
            var data = context2.Invoices.ToList(); 
// Error Happens here when the object is loaded with Proxy
            Console.ReadLine();
            return;
           
        }
    }
}

@ajcvickers
Copy link
Member

@vg2019 Just checking that you are aware that you can suppress this warning with something like:

optionsBuilder.ConfigureWarnings(e => e.Ignore(CoreEventId.DetachedLazyLoadingWarning))

so this should not be blocking.

@vg2019
Copy link
Author

vg2019 commented Dec 20, 2019

Hi, @ajcvickers , Thank you for your comment.
That is true we can suppress warning with this code. I even tried to see if any error is generated by

public Invoice()
           {
               InvoiceDetails = new BindingList<InvoiceDetail>();
               try
               {
                   InvoiceDetails.ListChanged += InvoiceDetails_ListChanged;
               }
               catch (System.Exception exc)
               {

                 
               }
             
           }

This code + e.Ignore(CoreEventId.DetachedLazyLoadingWarning) , doesnt SHOW ** any errors( but in fact it cause an error but it is not caught or shown) , however once we try to access an Invoice , virtual properties( InvoiceDetails) can not be loaded anymore.
It totally collapses the LazyLoading mechanism for the object (Invoice - InvoiceDetails) which was loaded from the database.Actually this blocks the lazyloading mechanism for any other virtual property if available.

@vg2019 Just checking that you are aware that you can suppress this warning with something like:

optionsBuilder.ConfigureWarnings(e => e.Ignore(CoreEventId.DetachedLazyLoadingWarning))

so this should not be blocking.

@vg2019
Copy link
Author

vg2019 commented Dec 23, 2019

@ajcvickers hi , did you have the possibility to check again based on my last comment ?

@ajcvickers
Copy link
Member

@vg2019 I am not able to reproduce this behavior with the code you have posted. Please post a small, runnable project or complete code listing that shows how lazy-loading is blocked.

@ajcvickers
Copy link
Member

EF Team Triage: Closing this issue as the requested additional details have not been provided and we have been unable to reproduce it.

BTW this is a canned response and may have info or details that do not directly apply to this particular issue. While we'd like to spend the time to uniquely address every incoming issue, we get a lot traffic on the EF projects and that is not practical. To ensure we maximize the time we have to work on fixing bugs, implementing new features, etc. we use canned responses for common triage decisions.

@vg2019
Copy link
Author

vg2019 commented Jan 18, 2020

Dear @ajcvickers ,

Please forgive me for very late reply,
Below please find a project with which you can reproduce the behavior

Program.cs

using EFCore3V2.BusinessLayer.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;

namespace EFCore3V2.ConsoleUI
{
    class Program
    {
        static void Main(string[] args)
        {
            EFCore3V2Context context = new EFCore3V2Context();

            if (context.Invoices.Count() <2) // lets have at least 2 invoices
            {
                EFCore3V2Context.Invoice newInvoice = new EFCore3V2Context.Invoice();
                EFCore3V2Context.InvoiceDetail newInvoiceDetail = new EFCore3V2Context.InvoiceDetail();
                newInvoiceDetail.ProductCode = "AAA";
                newInvoiceDetail.Price = 23;
                newInvoice.InvoiceDetails.Add(newInvoiceDetail);
                context.Invoices.Add(newInvoice);
                context.SaveChanges();

            }

            // Access the data from a new context
            EFCore3V2Context context2 = new EFCore3V2Context();
            var allInvoices = context2.Invoices.OrderByDescending(t => t.Id).ToList();

            // At this point if you try to access any of the Invoices
            // while we have the code
            // InvoiceDetails.ListChanged += InvoiceDetails_ListChanged;
            // at the constructor of the Invoice
            // you will see that you can not access InvoiceDetails
            // because the proxy mechanism is broken
            // when we tried to access InvoiceDetails at the consturctor of the Invoice
            // however if we go to the Invoice constructor and remove 
            // InvoiceDetails.ListChanged += InvoiceDetails_ListChanged;
            // then re-run the project all works fine

            var testForAccessingInvoiceDetailsCount = allInvoices.LastOrDefault().InvoiceDetails.Count();
            Console.ReadLine();
            return;
           
        }
    }
}

EFCore3V2Context.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.ComponentModel;
using System.Linq;

namespace EFCore3V2.BusinessLayer.Models
{
    public class EFCore3V2Context : DbContext
    {
        public EFCore3V2Context()
        {
            var connectionString = "Data Source=YourServerName;Initial Catalog=EFCore3V2DB;Integrated Security=True;MultipleActiveResultSets=True";
            DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder();
            optionsBuilder.UseSqlServer(connectionString);
            optionsBuilder.UseLazyLoadingProxies(true);
            optionsBuilder.EnableDetailedErrors(true);
            //optionsBuilder.ConfigureWarnings(e => e.Ignore(CoreEventId.DetachedLazyLoadingWarning)); // PLEASE ALSO UNCOMMENT THIS LINE, THIS ALSO HAS NO EFFECT ON ERROR
           }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
            .UseLazyLoadingProxies()
            .ConfigureWarnings(e => e.Ignore(CoreEventId.DetachedLazyLoadingWarning))
            .UseSqlServer("Data Source=TgcSQL2014;Initial Catalog=EFCore3V2DB;Integrated Security=True;MultipleActiveResultSets=True");
        public DbSet<Invoice> Invoices { get; set; }
        public DbSet<InvoiceDetail> InvoiceDetails { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Invoice>().HasKey(t => t.Id);

            modelBuilder.Entity<InvoiceDetail>().HasKey(t => t.Id);
            modelBuilder.Entity<InvoiceDetail>().HasOne(t => t.Invoice).WithMany(t => t.InvoiceDetails).HasForeignKey(t => t.InvoiceId).OnDelete(DeleteBehavior.Restrict);

        }
        public class Invoice
        {
            public int Id { get; set; }
            public double Total { get; set; }
            private EFCore3V2Context Context { get; set; }
            public virtual BindingList<InvoiceDetail> InvoiceDetails { get; set; }
            public Invoice()
            {
                InvoiceDetails = new BindingList<InvoiceDetail>();
                try
                {
                    InvoiceDetails.ListChanged += InvoiceDetails_ListChanged;
                }
                catch (System.Exception exc)
                {

                  
                }
              
            }

            public void InvoiceDetails_ListChanged(object sender, ListChangedEventArgs e)
            {
                switch (e.ListChangedType)
                {
                    case ListChangedType.Reset:
                        break;
                    case ListChangedType.ItemAdded:
                        this.Total = InvoiceDetails.Sum(t => t.Price);
                        break;
                    case ListChangedType.ItemDeleted:
                        break;
                    case ListChangedType.ItemMoved:
                        break;
                    case ListChangedType.ItemChanged:
                        this.Total = InvoiceDetails.Sum(t => t.Price);
                        break;
                    case ListChangedType.PropertyDescriptorAdded:
                        break;
                    case ListChangedType.PropertyDescriptorDeleted:
                        break;
                    case ListChangedType.PropertyDescriptorChanged:
                        break;
                    default:
                        break;
                }
               
            }
        }
        public class InvoiceDetail
        {
            public int Id { get; set; }
            public int InvoiceId { get; set; }
            public virtual Invoice Invoice { get; set; }
            public string ProductCode { get; set; }
            public double Price { get; set; }
        }
    }
}

I am not adding the Migration And ModelSnapshot as they are obvious

Attached I am adding full project as .zip file.
EFCore3V2.zip

Looking forward hearing your comments.

Thanks

@ajcvickers
Copy link
Member

@vg2019 I downloaded and ran the attached project, but nothing fails for me. Is there something I need to do to make this fail?

@vg2019
Copy link
Author

vg2019 commented Jan 22, 2020

Dear @ajcvickers

Hi,

I upload a little bit modified ( removed irrelevant lines) version of the project here.
EFCore3V2.zip
Also please check this youtube video to see the behviour

https://www.youtube.com/watch?v=sLIRE3cUgQY&feature=youtu.be

@ajcvickers ajcvickers reopened this Jan 23, 2020
@ajcvickers ajcvickers changed the title Accessing virtual property on Constructor via Proxy causes DetachedLazyLoadingWarning Error Consider not emitting DetachedLazyLoadingWarning when entity is new rather than detached Jan 23, 2020
@ajcvickers
Copy link
Member

@vg2019 Thanks; I was now able to reproduce this. Two things:

  • We will consider not generating this warning in the future for entities that have been newly constructed but are not yet tracked.
  • The main issue here is access to virtual properties in the entity constructor. This results in a call to a virtual method before the object is fully constructed--calling virtual methods in constructors is generally a bad practice.

To fix this second issue, don't call the virtual property from the constructor. For example:

 public virtual BindingList<InvoiceDetail> InvoiceDetails { get; }
 public Invoice()
 {
     var bindingList = new BindingList<InvoiceDetail>();
     bindingList.ListChanged += InvoiceDetails_ListChanged;
     InvoiceDetails = bindingList;
 }

@ajcvickers
Copy link
Member

This no longer repros on 7.0.

@ajcvickers ajcvickers removed this from the Backlog milestone Dec 26, 2022
@ajcvickers ajcvickers added this to the 7.0.0 milestone Dec 26, 2022
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Dec 26, 2022
@ajcvickers ajcvickers removed their assignment Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-enhancement
Projects
None yet
Development

No branches or pull requests

2 participants