dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.48k stars 3.13k forks source link

Is it possible to manually track new entity objects in a query expression? #33884

Closed xyh20180101 closed 2 hours ago

xyh20180101 commented 3 weeks ago

(Please note that the following content is generated by a translator and may contain inaccuracies.)

Background

I am developing a multi-tenant data permission framework. By inheriting from QueryCompiler and overriding the Execute/ExecuteAsync methods, I insert Where and Select methods into the expression (for filtering data rows and columns). For example:

Expression written by the user:

context.Blogs.ToList();

Expression after being processed by the framework:

context.Blogs.Where(blog => ... ).Select(blog =>new Blog{ ... }).ToList();

Problem

The framework works well in query scenarios, but there are issues in modification scenarios. Because I inserted the Select method, the returned entities will lose tracking. It looks like this:

var blog = context.Blogs.Single(b => b.Url == "http://example.com");
//actually context.Blogs.Where(b => ... ).Select(b =>new Blog{ ... }).Single(b => b.Url == "http://example.com")

blog.Url = "http://example.com/blog";
context.SaveChanges(); //not work

Currently, I bypass the framework through extension methods (like context.Blogs.AsNoDataPermission()), but this requires users to write additional code. I hope to provide a non-intrusive framework as much as possible. Is there any way to manually add tracking for new entity objects in an expression? Any suggestions would be greatly appreciated.

ajcvickers commented 3 weeks ago

@xyh20180101 You should be able to manually track the entities that are returned from the query. For example, on your DbContext:

    public T TrackEntity<T>(T entity)
    {
        Entry(entity!).State = EntityState.Unchanged;
        return entity;
    }

Then the query would be:

    var blogs = await context.Blogs
        .Where(blog => blog.Title != null)
        .Select(blog => context.TrackEntity(new Blog { Id = blog.Id, Title = blog.Title }))
        .ToListAsync();

Full POC:

using (var context = new AppDbContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.AddRange(
        new Blog { Title = "One" },
        new Blog { Title = "Two" });

    await context.SaveChangesAsync();
}

using (var context = new AppDbContext())
{
    var blogs = await context.Blogs
        .Where(blog => blog.Title != null)
        .Select(blog => context.TrackEntity(new Blog { Id = blog.Id, Title = blog.Title }))
        .ToListAsync();

    Console.WriteLine(context.ChangeTracker.DebugView.LongView);
}

public class AppDbContext : DbContext
{
    public DbSet<Blog> Blogs => Set<Blog>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer("Data Source=localhost;Database=BuildBlogs;Integrated Security=True;Trust Server Certificate=True;ConnectRetryCount=0")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    public T TrackEntity<T>(T entity)
    {
        Entry(entity!).State = EntityState.Unchanged;
        return entity;
    }
}

public class Blog
{
    public int Id { get; set; }
    public string? Title { get; set; }
}
xyh20180101 commented 3 weeks ago

@ajcvickers Thank you very much, your code runs fine, but there's still an issue. When replacing ToListAsync() with SingleAsync(p=>p.Title == "One"), it throws an exception:

System.InvalidOperationException:“The LINQ expression 'DbSet<Blog>()
    .Where(b => b.Title != null)
    .Sum(b => __context_0.TrackEntity<Blog>(new Blog{ 
        Id = b.Id, 
        Title = b.Title 
    }
    ).Id)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.”

After testing, not only the Single method, but also Where, OrderBy, Sum, and so on, all lead to exceptions. It seems to be because of calling custom methods in the expression, and I'm not sure how to proceed.

ajcvickers commented 2 weeks ago

@xyh20180101 You can only track after the entity instance has been created and returned. Do, for Single, etc. that would be after Single() has returned. Also, make sure that entities really should be tracked--for example, performing a Sum on the database does not usually return entities to track.