Skip to content

Global query filters raise a "An item with the same key has already been added" exception #7

@dapnet2018

Description

@dapnet2018

If an entity has Global Query Filters defined (e.g. in the OnModelCreating method of the DbContext executing modelBuilder.Entity().HasQueryFilter(...)) and the DbContext is using UseSecondLevelCache, then when trying to retrieve data from the DbContext an exception is thrown "An item with the same key has already been added".

Exception message: System.ArgumentException: An item with the same key has already been added.
Stack trace:
at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at Microsoft.EntityFrameworkCore.Query.QueryContext.AddParameter(String name, Object value)
   at lambda_method(Closure , QueryContext )
   at EntityFrameworkCore.Cacheable.CustomQueryCompiler.<>c__DisplayClass21_2`1.<CompileQueryCore>b__1(QueryContext qc) in C:\Users\Paul\Source\Repos\EntityFrameworkCore.Cacheable\EntityFrameworkCore.Cacheable\CustomQueryCompiler.cs:line 196
   at EntityFrameworkCore.Cacheable.CustomQueryCompiler.Execute[TResult](Expression query) in C:\Users\Paul\Source\Repos\EntityFrameworkCore.Cacheable\EntityFrameworkCore.Cacheable\CustomQueryCompiler.cs:line 120
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at Remotion.Linq.QueryableBase`1.GetEnumerator()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at EntityFrameworkCore.CacheableTests.GlobalQueryFilterTest.GlobalQueryFiltersWithCacheableEnabledAndUsingCacheQuery() in C:\Users\Paul\Source\Repos\EntityFrameworkCore.Cacheable\EntityFrameworkCore.CacheableTests\GlobalQueryFilterTest.cs:line 190
Result Message:	
Test method EntityFrameworkCore.CacheableTests.GlobalQueryFilterTest.GlobalQueryFiltersWithCacheableEnabledAndUsingCacheQuery threw exception: 
System.ArgumentException: An item with the same key has already been added.

Steps to reproduce

The below unit test shows the issue - it will error on line 103 "Assert.IsTrue(test1Context.Blogs.Count() == 3);"

This unit test is modelling a multi-tenant scenario, where each Entity has a MultiTenantId int property. The relevant MultiTenantID is passed to the DbContext in it's constructor and the Global Query Filter automatically applies a "MultiTenantID = X" predicate.

This unit test will succeed if the "optionsBuilder.UseSecondLevelCache();" is removed from the OnConfiguring method in the GlobalQueryFilterContext class

using System.ComponentModel.DataAnnotations;
using System.Linq;
using EntityFrameworkCore.Cacheable;
using EntityFrameworkCore.CacheableTests.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace EntityFrameworkCore.CacheableTests.QuickTest
{
    [TestClass]
    public class ProofOfIssue
    {
        public class GlobalQueryFilterContext : DbContext
        {
            private readonly int _multiTenantId;

            public GlobalQueryFilterContext(int multiTenantId, DbContextOptions<GlobalQueryFilterContext> options)
                : base(options)
            {
                _multiTenantId = multiTenantId;
            }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSecondLevelCache();
            }

            #region Overrides of DbContext

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);

                // Add global query filters to all entities
                modelBuilder.Entity<GlobalQueryFilterBlog>().HasQueryFilter(e => e.MultiTenantId == _multiTenantId);
            }

            #endregion

            public DbSet<GlobalQueryFilterBlog> Blogs { get; set; }
        }

        public class GlobalQueryFilterBlog
        {
            [Key]
            public int BlogId { get; set; }
            public int MultiTenantId { get; set; }
            public string Url { get; set; }
        }


        [TestMethod]
        public void QueryingWithGlobalQueryFilters()
        {

            MemoryCacheProvider.ClearCache();

            var loggerProvider = new DebugLoggerProvider();
            var loggerFactory = new LoggerFactory(new[] { loggerProvider });

            var options = new DbContextOptionsBuilder<GlobalQueryFilterContext>()
                .UseLoggerFactory(loggerFactory)
                .UseInMemoryDatabase(databaseName: "GlobalQueryFilterTest")
                .Options;

            // Multi Tenant
            const int firstMultiTenantId = 1;
            const int secondMultiTenantId = 2;

            // create test entries - this time don't disable the cacheable extension
            using (var init1Context = new GlobalQueryFilterContext(firstMultiTenantId, options))
            {
                init1Context.Blogs.Add(new GlobalQueryFilterBlog
                { BlogId = 1, MultiTenantId = firstMultiTenantId, Url = "http://sample.com/cats" });

                init1Context.Blogs.Add(new GlobalQueryFilterBlog
                { BlogId = 2, MultiTenantId = firstMultiTenantId, Url = "http://sample.com/catfish" });

                init1Context.Blogs.Add(new GlobalQueryFilterBlog
                { BlogId = 3, MultiTenantId = firstMultiTenantId, Url = "http://sample.com/dogs" });
                init1Context.SaveChanges();
            }

            using (var init2Context = new GlobalQueryFilterContext(secondMultiTenantId, options))
            {
                init2Context.Blogs.Add(new GlobalQueryFilterBlog
                { BlogId = 4, MultiTenantId = secondMultiTenantId, Url = "http://sample.com/clowns" });

                init2Context.Blogs.Add(new GlobalQueryFilterBlog
                { BlogId = 5, MultiTenantId = secondMultiTenantId, Url = "http://sample.com/clownfish" });

                init2Context.Blogs.Add(new GlobalQueryFilterBlog
                { BlogId = 6, MultiTenantId = secondMultiTenantId, Url = "http://sample.com/circus" });

                init2Context.Blogs.Add(new GlobalQueryFilterBlog
                { BlogId = 7, MultiTenantId = secondMultiTenantId, Url = "http://sample.com/circustent" });
                init2Context.SaveChanges();
            }

            using (var test1Context = new GlobalQueryFilterContext(firstMultiTenantId, options))
            {
                Assert.IsTrue(test1Context.Blogs.Count() == 3);

                Assert.IsTrue(test1Context.Blogs.ToList().All(b => b.MultiTenantId == firstMultiTenantId));
            }

            using (var test2Context = new GlobalQueryFilterContext(secondMultiTenantId, options))
            {
                Assert.IsTrue(test2Context.Blogs.Count() == 4);

                Assert.IsTrue(test2Context.Blogs.ToList().All(b => b.MultiTenantId == secondMultiTenantId));
            }

        }
    }
}

Further technical details

EntityFrameworkCore.Cacheable version: 2.0.0 (and also latest Master branch code from GitHub)
EF Core version: 2.2.0
IDE: Visual Studio 2017 15.9.6

Metadata

Metadata

Labels

bugSomething isn't workinghelp wantedExtra attention is needed

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions