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

Eager loading include with using UseLazyLoadingProxies #15170

Closed
Bitz opened this issue Mar 27, 2019 · 5 comments
Closed

Eager loading include with using UseLazyLoadingProxies #15170

Bitz opened this issue Mar 27, 2019 · 5 comments

Comments

@Bitz
Copy link

Bitz commented Mar 27, 2019

I am creating the db connection like so:

protected override void OnConfiguring(DbContextOptionsBuilder optionbuilder)
{
	optionbuilder.UseLazyLoadingProxies().UseSqlite(@"Data Source=Data.db");
}

And I am trying to access an object like so:

public static User GetProfile(int uid)
{
	using (Db db = new Db())
	{
		return db.Users.Include(x => x.Settings).FirstOrDefault(x => x.UserId == uid);
	}
}

The user object is as follows:

public class User
{
	[Key]
	[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
	public int UserId { get; set; }

	public string Name { get; set; }
	public DateTime? LastUsed{ get; set; }

	public virtual Setting Settings { get; set; }
}

but upon accessing Users.Settings, it throws the error listed below.

'Error generated for warning 'Microsoft.EntityFrameworkCore.Infrastructure.LazyLoadOnDisposedContextWarning: An attempt was made to lazy-load navigation property 'Settings' on entity type 'UserProxy' after the associated DbContext was disposed.'. This exception can be suppressed or logged by passing event ID 'CoreEventId.LazyLoadOnDisposedContextWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.'

I understand what this means but it goes against my understanding of includes and how it causes eager loading.

My understanding was that when using an include and accessing the object explicitly by calling FirstOrDefault eager load the related objects to be populated immediately without the need for the db connection to remain open; but apparently, this is not the case. Am I missing something here about the usage or is this a bug?

Further information

db.Users.FirstOrDefault(x => x.UserId == uid); works fine.

db.Users.Include(x => x.Settings).FirstOrDefault(x => x.UserId == uid); while not invoking UseLazyLoadingProxies() works fine.

I posted a question on SO assuming I was misusing it, but a user there directed me here informing me that it may be a bug. (https://stackoverflow.com/questions/55369146)

Steps to reproduce

Create object and connector as shown, try to include a related object and load data.

Upon accessing the include like so:

var user = User.Identity.GetProfile();
if (user.Settings != null && string.IsNullOrEmpty(user.Settings.GoogleMapsApiKey))
{
	 //Do things
}

The error will be thrown.

Further technical details

EF Core version: 2.2.3
Database Provider: Microsoft.EntityFrameworkCore.Sqlite
Operating system: W10
IDE: Visual Studio 2017 15.9.9

@Bitz
Copy link
Author

Bitz commented Mar 27, 2019

Sounds like this bug may be related to and is addressed in 3.0?

https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#lazy-loading-proxies-no-longer-assume-navigation-properties-are-fully-loaded

Old behavior

Before EF Core 3.0, once a DbContext was disposed there was no way of knowing if a given navigation property on an entity obtained from that context was fully loaded or not. Proxies would instead assume that a reference navigation is loaded if it has a non-null value, and that a collection navigation is loaded if it isn't empty. In these cases, attempting to lazy-load would be a no-op.

New behavior

Starting with EF Core 3.0, proxies keep track of whether or not a navigation property is loaded. This means attempting to access a navigation property that is loaded after the context has been disposed will always be a no-op, even when the loaded navigation is empty or null. Conversely, attempting to access a navigation property that isn't loaded will throw an exception if the context is disposed even if the navigation property is a non-empty collection. If this situation arises, it means the application code is attempting to use lazy-loading at an invalid time, and the application should be changed to not do this.

Why

This change was made to make the behavior consistent and correct when attempting to lazy-load on a disposed DbContext instance.

This seems to be exactly what I am attempting to do.

#12780

@ajcvickers
Copy link
Member

@Bitz My first impression was that this is the issue that resulted in the breaking change you mention. However, that doesn't jive with, "db.Users.FirstOrDefault(x => x.UserId == uid); works fine" since it should also hit this issue on attempting to lazy load.

@Bitz
Copy link
Author

Bitz commented Mar 27, 2019

Hey @ajcvickers I feel like I am misunderstanding how FirstOrDefault works in this case with relation to lazy loading. Why would a direct (non lazy-loaded) object throw an exception when loaded directly as an instance of itself?

Here is proof that what I am saying happens:

image

(API keys are random guids, nothing sensitive is being shown)
The first instance refers to user.ApiKey which is a property of the User object.
The second refers to user.Settings which is an include derived virtual object.

@ajcvickers
Copy link
Member

@Bitz I was able to reproduce most of the behavior you are seeing and it does seem to be a duplicate of #12780. However, this:
db.Users.FirstOrDefault(x => x.UserId == uid); works fine.
I was not able to reproduce. I see the same exception, as expected. My code is below if you want to try to dig further into this, otherwise I'll close as a dupe.

public class User
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int UserId { get; set; }

    public string Name { get; set; }
    public DateTime? LastUsed { get; set; }

    public virtual Setting Settings { get; set; }
}

public class Setting
{
    public int Id { get; set; }
}


public class BloggingContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseLazyLoadingProxies()
            .UseSqlite("DataSource=testdb");
    }

    public DbSet<User> Users { get; set; }
}

public class Program
{
    public static void Main()
    {
        using (var context = new BloggingContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            context.Add(new User {Settings = new Setting()});
            context.SaveChanges();
        }

        User user;

        using (var context = new BloggingContext())
        {
            var uid = 1;
            user = context.Users.FirstOrDefault(x => x.UserId == uid);
            //user = context.Users.Include(x => x.Settings).FirstOrDefault(x => x.UserId == uid);
        }

        var _ = user.Settings;
    }
}

@Bitz
Copy link
Author

Bitz commented Mar 28, 2019

I think it can be closed as a dupe since the main issue is solved in 3RC4, if I do encounter the FirstOrDefault issue, I will create another ticket referencing this one.

Thanks!

@Bitz Bitz closed this as completed Mar 28, 2019
@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants