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

Entity not tracked by change tracker when selecting not mapped property #15989

Closed
oliverhanappi opened this issue Jun 7, 2019 · 7 comments
Closed
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@oliverhanappi
Copy link

oliverhanappi commented Jun 7, 2019

When selecting a not mapped property, the entity is not added to the change tracker, even though the entity can be accessed and modified.

public class Entity
{
    public int EntityId { get; set; }
    [NotMapped] public Entity This => this;
}

var entity = dbContext.Entities.Select(e => e.This).Single();
var entryCount = dbContext.ChangeTracker.Entries().Count();

// fails with "Expected entry count to be 1, but was 0"
Debug.Assert(entryCount == 1, $"Expected entry count to be 1, but was {entryCount}");

Steps to reproduce

public class Entity
{
    public int EntityId { get; set; }
    [NotMapped] public Entity This => this;
}

public class EntityDbContext : DbContext
{
    public DbSet<Entity> Entities { get; set; }

    public EntityDbContext(DbContextOptions options)
        : base(options)
    {
    }
}

public static class Program
{
    public static void Main()
    {
        var connection = new SqliteConnection("Data Source=:memory:");
        connection.Open();

        var options = new DbContextOptionsBuilder().UseSqlite(connection).Options;

        using (var dbContext = new EntityDbContext(options))
        {
            dbContext.Database.EnsureCreated();
            
            dbContext.Entities.Add(new Entity());
            dbContext.SaveChanges();
        }

        using (var dbContext = new EntityDbContext(options))
        {
            var entity = dbContext.Entities.Select(e => e.This).Single();
            var entryCount = dbContext.ChangeTracker.Entries().Count();
            Debug.Assert(entryCount == 1, $"Expected entry count to be 1, but was {entryCount}");
        }
    }
}

Further technical details

EF Core version: 2.1.11 and 2.2.4
Database Provider: Microsoft.EntityFrameworkCore.Sqlite

@ajcvickers
Copy link
Member

Note for triage: this is a very unusual case. It's a projection of an un-mapped property, so I would not expect it to be tracked. However, it's actually the same type as the query root, and what's more it's even the same instance as returned by the query. But also imagine a slight variation of this where the projected object is the same type, but is a different instance than returned from the query--for example:

public class Entity
{
    public int EntityId { get; set; }
    [NotMapped] public Entity This => this.Clone();
}

/cc @divega @smitpatel @maumar @roji

@oliverhanappi
Copy link
Author

@ajcvickers I agree with you that this is a very unusual case. Please note however, that this is just a minimal reproduction example.

I was surprised that such a projection is possible at all because in my real project I turned off client evaluation.

The thing that worries me most is the fact that EF Core created and exposed an entity without change tracking, even though change tracking was not explicitly disabled. According to the documentation entities should be included in change tracking even when returned as a result of a projection.

@smitpatel
Copy link
Member

In 3.0, unmapped property causes client eval. It will happen because it is in projection. The entity we materialize from database will be tracked. (We avoided tracking objects before client eval in 2.x but we are going away from that behavior). If your unmapped property returns object of a mapped type, it won't be tracked because tracking happens on the objects we materialize from database regardless of what comes out as result.

@divega
Copy link
Contributor

divega commented Jun 7, 2019

The thing that worries me most is the fact that EF Core created and exposed an entity without change tracking, even though change tracking was not explicitly disabled. According to the documentation entities should be included in change tracking even when returned as a result of a projection.

All the explanations in our documentation are trying to hit the sweet spot between simplicity and accuracy to enable the user to build a mental model of how things work without reading half of the EF Core codebase. Unfortunately, there are many things you can express in a LINQ query in which it isn't clear what the outcome is going to be. In the past, it has been hard to distill simple and consistent principles to guide what the behavior should be in those cases.

Projecting an un-mapped property is one example of such scenario in which tracking could go either way. Another example is explicitly creating an entity type in the projection, by either invoking the constructor or invoking a client method that returns an instance.

As @smitpatel mentioned, in the new LINQ implementation in EF Core 3.0 we are trying to align to a new set of principles that can help us make these decisions. Those principles are our best effort based on many years of experience from EF and LINQ to SQL.

One of the principles is that the projection is split into what can be pushed down to the server and what needs to be evaluated on the client. EF Core will materialize any entities returned by the server query and track them, and then it will invoke the client-side part of the projection before returning the results. Some of this design is explained at #12795 (comment).

An un-mapped property like this is completely opaque to EF Core and has to be evaluated on the client. Tracking is completely oblivious to anything that happens in the client-side projection. Consistent with this, in 3.0 we have also decided to not track an entity that is constructed explicitly or returned by a client method in the projection.

As @smitpatel already explained, for this particular query, in 3.0 the entity will be tracked because we will materialize it before we project the un-mapped property on the client, but not because it is an entity type reachable in the final results.

We can definitively update the documentation to reflect the 3.0 design, but unfortunately the explanation would be much more complicated if we also want to also describe the 2.x behavior.

@oliverhanappi
Copy link
Author

It sounds very reasonable that each entity which gets materialized by EF Core will also be tracked for changes in 3.0.

I assumed that 2.x already behaves like that. Therefore I created this issue. I am glad to hear that the issue will be resolved in 3.0.

Just to be sure: In 3.0 property A will be tracked, whereas property B will be not. Is this correct?

dbContext.Entities.Select(e => { A = e.This, B = new Entity() })

@divega
Copy link
Contributor

divega commented Jun 8, 2019

@oliverhanappi in 3.0 for that query, B should not be tracked.

@ajcvickers
Copy link
Member

Discussed in triage and we are happy with the 3.0 behavior.

@ajcvickers ajcvickers added the closed-no-further-action The issue is closed and no further action is planned. label Jun 10, 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
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

4 participants