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

Query Filters caching predicates causes issues #10363

Closed
Mardoxx opened this issue Nov 21, 2017 · 4 comments
Closed

Query Filters caching predicates causes issues #10363

Mardoxx opened this issue Nov 21, 2017 · 4 comments

Comments

@Mardoxx
Copy link
Contributor

Mardoxx commented Nov 21, 2017

Using your example code.

public class BloggingContext : DbContext
{
    //...
    public bool ViewDeleted { get; set; } = false;
    //...
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //...
            modelBuilder.Entity<Post>().HasQueryFilter(p => ViewDeleted || !p.IsDeleted);
var db = new BloggingContext();
var blogs = db.Blogs.AsNoTracking().Include(b => b.Posts); // filters out deleted entries
db.ViewDeleted = true;
var blogs = db.Blogs.AsNoTracking().Include(b => b.Posts); // still filtering out deleted entries

Changing the predicate to something more complex

modelBuilder.Entity<Post>().HasQueryFilter(p => ViewDeleted ? true : !p.IsDeleted);
// or
modelBuilder.Entity<Post>().HasQueryFilter(p => ViewDeleted | !p.IsDeleted);

and it works fine.

@Mardoxx
Copy link
Contributor Author

Mardoxx commented Nov 21, 2017

If I had to guess... The first time the function body is entered of the query filter, you see if it can be reduced and store this value. Unfortunately this results in the predicate becoming just !p.IsDeleted (ignoring LHS branch, because it is being assumed static).

Upon further testing, if we flip the bool, modelBuilder.Entity<Post>().HasQueryFilter(p => ViewDeleted | !p.IsDeleted); experiences the same issue too. With ViewDeleted as true, the predicate is reduced to true and hence ignored.

Even if we set ViewDeleted via constructor and create a new context between queries, results are the same

@Mardoxx
Copy link
Contributor Author

Mardoxx commented Nov 21, 2017

Uncomment SetupDatabase(), run once, then comment it out again and run.
Need to do this because predicates aren't re-created per context creation ---- it would seem.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

#pragma warning disable 169

namespace SoftDeleteTest
{
    public class Program
    {
        private static void Main()
        {
            //SetupDatabase();

            Console.WriteLine("=== Should see deleted === ");

            using (var db = new BloggingContext("diego", true))
            {
                var blogs = db.Blogs
                    .AsNoTracking()
                    .Include(b => b.Posts)
                    .ToList();

                foreach (var blog in blogs)
                {
                    Console.WriteLine(
                        $"{blog.Url.PadRight(33)} [Tenant: {db.Entry(blog).Property("TenantId").CurrentValue}]");

                    foreach (var post in blog.Posts)
                    {
                        Console.WriteLine($" - {post.Title.PadRight(30)} [IsDeleted: {post.IsDeleted}]");
                    }

                    Console.WriteLine();
                    Console.WriteLine();
                }
            }

            Console.WriteLine("=== Should NOT see deleted === ");

            using (var db = new BloggingContext("diego", false))
            {
                var blogs = db.Blogs
                    .AsNoTracking()
                    .Include(b => b.Posts)
                    .ToList();

                foreach (var blog in blogs)
                {
                    Console.WriteLine(
                        $"{blog.Url.PadRight(33)} [Tenant: {db.Entry(blog).Property("TenantId").CurrentValue}]");

                    foreach (var post in blog.Posts)
                    {
                        Console.WriteLine($" - {post.Title.PadRight(30)} [IsDeleted: {post.IsDeleted}]");
                    }

                    Console.WriteLine();
                }
            }

            Console.ReadLine();
        }

        private static void SetupDatabase()
        {
            using (var db = new BloggingContext("diego", true))
            {
                if (db.Database.EnsureCreated())
                {
                    db.Blogs.Add(
                        new Blog
                        {
                            Url = "http://sample.com/blogs/fish",
                            Posts = new List<Post>
                            {
                                new Post { Title = "Fish care 101" },
                                new Post { Title = "Caring for tropical fish" },
                                new Post { Title = "Types of ornamental fish" }
                            }
                        });

                    db.Blogs.Add(
                        new Blog
                        {
                            Url = "http://sample.com/blogs/cats",
                            Posts = new List<Post>
                            {
                                new Post { Title = "Cat care 101" },
                                new Post { Title = "Caring for tropical cats" },
                                new Post { Title = "Types of ornamental cats" }
                            }
                        });

                    db.SaveChanges();

                    using (var andrewDb = new BloggingContext("andrew", true))
                    {
                        andrewDb.Blogs.Add(
                            new Blog
                            {
                                Url = "http://sample.com/blogs/catfish",
                                Posts = new List<Post>
                                {
                                    new Post { Title = "Catfish care 101" },
                                    new Post { Title = "History of the catfish name" }
                                }
                            });

                        andrewDb.SaveChanges();
                    }

                    db.Posts
                        .Where(
                            p => p.Title == "Caring for tropical fish"
                                 || p.Title == "Cat care 101")
                        .ToList()
                        .ForEach(p => db.Posts.Remove(p));

                    db.SaveChanges();
                }
            }
        }
    }

    public class BloggingContext : DbContext
    {
        public BloggingContext(string tenant, bool viewDeleted)
        {
            Tenant = tenant;
            ViewDeleted = viewDeleted;
        }

        public string Tenant { get; set; }
        public bool ViewDeleted { get; set; } = false;

        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseSqlServer(
                    @"Server=(localdb)\mssqllocaldb;Database=Demo.QueryFilters;Trusted_Connection=True;ConnectRetryCount=0;");
        }

        #region Configuration
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("_tenantId");


            // Configure entity filters
            modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == Tenant);
            modelBuilder.Entity<Post>().HasQueryFilter(p => ViewDeleted || !p.IsDeleted);
        }
        #endregion

        public override int SaveChanges()
        {
            ChangeTracker.DetectChanges();

            foreach (var item in ChangeTracker.Entries().Where(
                e =>
                    e.State == EntityState.Added && e.Metadata.GetProperties().Any(p => p.Name == "TenantId")))
            {
                item.CurrentValues["TenantId"] = Tenant;
            }

            foreach (var item in ChangeTracker.Entries<Post>().Where(e => e.State == EntityState.Deleted))
            {
                item.State = EntityState.Modified;
                item.CurrentValues["IsDeleted"] = true;
            }

            return base.SaveChanges();
        }
    }

    #region Entities
    public class Blog
    {
        private string _tenantId;

        public int BlogId { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }

        public List<Post> Posts { get; set; }
    }

    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public bool IsDeleted { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
    #endregion
}

@ajcvickers
Copy link
Contributor

Duplicate of #9825

@ajcvickers ajcvickers marked this as a duplicate of #9825 Nov 21, 2017
@Mardoxx
Copy link
Contributor Author

Mardoxx commented Nov 21, 2017

@ajcvickers nice, thanks! Will remember to check closed issues next time 😉

@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