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
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".
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
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