Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/en/framework/infrastructure/data-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@ public override string GetCompiledQueryCacheKey()
}
````

The `HasTranslation` callback runs only when EF Core compiles the query, and the generated SQL is cached. That's why the example adds `IsActiveFilterEnabled` to the cache key.

The default compiled-query cache key does not include the tenant ID. ABP's built-in tenant filter uses a runtime parameter, so queries can be reused across tenants.

> If your custom `HasTranslation` callbacks read tenant-specific values at compilation time or generate different SQL by tenant, add the tenant ID to the cache key. Otherwise, another tenant can reuse SQL compiled for the first tenant.

````csharp
public override string GetCompiledQueryCacheKey()
{
return $"{base.GetCompiledQueryCacheKey()}:{IsActiveFilterEnabled}:{CurrentTenantId?.ToString() ?? "Null"}";
}
````

Include any other values read at compilation time in the cache key as well. Keep the call to `base.GetCompiledQueryCacheKey()` to preserve ABP's filter-state keys.

##### Make User-defined function mapping compatible with [DevExtreme.AspNet.Data's](https://github.com/DevExpress/DevExtreme.AspNet.Data) `IAsyncAdapter`

If you are using [DevExtreme.AspNet.Data](https://github.com/DevExpress/DevExtreme.AspNet.Data) and `User-defined function mapping`, You need to create a custom adapter to compatible with `IAsyncAdapter`.
Expand Down Expand Up @@ -466,4 +481,4 @@ public class MyMongoDbModule : AbpModule
.Replace(ServiceDescriptor.Transient(typeof(IMongoDbRepositoryFilterer<,>),typeof(MyMongoDbRepositoryFilterer<,>)));
}
}
````
````
27 changes: 27 additions & 0 deletions docs/en/release-info/migration-guides/abp-10-7.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,33 @@ Until you can upgrade, give every authorization server its own client signing ke

> See [#26148](https://github.com/abpframework/abp/pull/26148) for details.

### Compiled Query Cache Key of Global Filters

**Who is affected**

- Applications that map a custom global filter with user-defined function mapping and read the current tenant, or another tenant specific value, inside its `HasTranslation` callback.

**What changed**

- `AbpDbContext.GetCompiledQueryCacheKey()` no longer includes the current tenant ID. It contains only the soft delete and multi-tenant filter states, so EF Core compiles a query once and reuses it for all tenants. Previously every tenant compiled and cached its own copy of every query, so applications with many tenants kept compiling the same queries again.
- ABP's built-in tenant filter passes the current tenant to the SQL as a parameter, so it is not affected. Applications that set `AbpEfCoreGlobalFilterOptions.UseDbFunction` to `false` are not affected either, because this key is only used with user-defined function mapping.
- A `HasTranslation` callback runs only when EF Core compiles the query. If it reads the current tenant and writes it into the SQL, for example as a `SqlConstantExpression`, other tenants now reuse that SQL and get the data of the tenant that compiled it first.

**What to do**

No action is required if your `HasTranslation` callbacks do not read tenant specific values. Otherwise, add the tenant ID to the cache key of that `DbContext` and keep the base key:

```csharp
public override string GetCompiledQueryCacheKey()
{
return $"{base.GetCompiledQueryCacheKey()}:{CurrentTenantId?.ToString() ?? "Null"}";
}
```

Add any other value the callback reads at compilation time to the key the same way.

> See the [Data Filtering](../../framework/infrastructure/data-filtering.md) document and [#26282](https://github.com/abpframework/abp/pull/26282) for details.

### Dependency Updates

**Who is affected**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,6 @@ public virtual bool UseDbFunction()

public virtual string GetCompiledQueryCacheKey()
{
return $"{CurrentTenantId?.ToString() ?? "Null"}:{IsSoftDeleteFilterEnabled}:{IsMultiTenantFilterEnabled}";
return $"{IsSoftDeleteFilterEnabled}:{IsMultiTenantFilterEnabled}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore.GlobalFilters;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.TestApp.Testing;
using Volo.Abp.Uow;
using Xunit;

namespace Volo.Abp.EntityFrameworkCore.DataFiltering.CompiledQueryCache;

public abstract class CompiledQueryCacheTestBase<TStartupModule> : TestAppTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly string _queryTag = Guid.NewGuid().ToString();

protected abstract bool UseDbFunction { get; }

[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public async Task Should_Reuse_Compiled_Query_Without_Reusing_Tenant_Or_Filter_State(bool asyncQuery, bool hostFirst)
{
var counter = GetRequiredService<QueryCompilationCounter>();
var before = counter.Count;
IModel sharedModel = null;
var cacheKeys = new Dictionary<(bool SoftDelete, bool MultiTenant), object>();
var tenants = hostFirst
? new Guid?[]
{
null,
CompiledQueryCacheTestModuleBase.TenantA,
CompiledQueryCacheTestModuleBase.TenantB,
null,
CompiledQueryCacheTestModuleBase.TenantA
}
: new Guid?[]
{
CompiledQueryCacheTestModuleBase.TenantA,
CompiledQueryCacheTestModuleBase.TenantB,
null,
CompiledQueryCacheTestModuleBase.TenantA
};

foreach (var tenantId in tenants)
{
foreach (var softDeleteEnabled in new[] { true, false, true })
{
foreach (var multiTenantEnabled in new[] { true, false, true })
{
using var scope = ServiceProvider.CreateScope();
using var tenant = scope.ServiceProvider.GetRequiredService<ICurrentTenant>().Change(tenantId);
var dataFilter = scope.ServiceProvider.GetRequiredService<IDataFilter>();
using var softDelete = softDeleteEnabled
? dataFilter.Enable<ISoftDelete>()
: dataFilter.Disable<ISoftDelete>();
using var multiTenant = multiTenantEnabled
? dataFilter.Enable<IMultiTenant>()
: dataFilter.Disable<IMultiTenant>();
using var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>().Begin();
var context = scope.ServiceProvider.GetRequiredService<CompiledQueryCacheTestDbContext>();
context.UseDbFunction().ShouldBe(UseDbFunction);

sharedModel ??= context.Model;
context.Model.ShouldBeSameAs(sharedModel);

var query = context.Rows
.AsNoTracking()
.TagWith(_queryTag)
.OrderBy(x => x.Name)
.Select(x => x.Name);
var cacheKeyGenerator = context.GetService<ICompiledQueryCacheKeyGenerator>();
if (UseDbFunction)
{
cacheKeyGenerator.ShouldBeOfType<AbpCompiledQueryCacheKeyGenerator>();
}
else
{
cacheKeyGenerator.ShouldNotBeOfType<AbpCompiledQueryCacheKeyGenerator>();
}

var cacheKey = cacheKeyGenerator.GenerateCacheKey(query.Expression, asyncQuery);
var filterState = (softDeleteEnabled, multiTenantEnabled);
if (cacheKeys.TryGetValue(filterState, out var previousKey))
{
cacheKey.ShouldBe(previousKey);
}
else
{
cacheKeys.Add(filterState, cacheKey);
}

var names = asyncQuery ? await query.ToListAsync() : query.ToList();
names.ShouldBe(CompiledQueryCacheTestModuleBase.Rows
.Where(row => !softDeleteEnabled || !row.Deleted)
.Where(row => !multiTenantEnabled || row.TenantId == tenantId)
.Select(row => row.Name)
.OrderBy(name => name)
.ToArray());
await uow.CompleteAsync();
}
}
}

var expectedCompilationCount = UseDbFunction ? 4 : 1;
cacheKeys.Values.Distinct().Count().ShouldBe(expectedCompilationCount);
(counter.Count - before).ShouldBe(expectedCompilationCount);
}

[Fact]
public async Task Should_Read_Current_Tenant_When_Reusing_Query_In_Same_DbContext()
{
using var uow = GetRequiredService<IUnitOfWorkManager>().Begin();
var context = GetRequiredService<CompiledQueryCacheTestDbContext>();
context.UseDbFunction().ShouldBe(UseDbFunction);
var currentTenant = GetRequiredService<ICurrentTenant>();
var counter = GetRequiredService<QueryCompilationCounter>();
var before = counter.Count;
var query = context.Rows
.AsNoTracking()
.TagWith(_queryTag)
.OrderBy(x => x.Name)
.Select(x => x.Name);
var tenants = new Guid?[]
{
CompiledQueryCacheTestModuleBase.TenantA,
CompiledQueryCacheTestModuleBase.TenantB,
null,
CompiledQueryCacheTestModuleBase.TenantA
};

foreach (var tenantId in tenants)
{
using var tenant = currentTenant.Change(tenantId);
var names = await query.ToListAsync();
names.ShouldBe(CompiledQueryCacheTestModuleBase.Rows
.Where(row => !row.Deleted && row.TenantId == tenantId)
.Select(row => row.Name)
.OrderBy(name => name)
.ToArray());
}

(counter.Count - before).ShouldBe(1);
await uow.CompleteAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;

namespace Volo.Abp.EntityFrameworkCore.DataFiltering.CompiledQueryCache;

public class CompiledQueryCacheTestDbContext : AbpDbContext<CompiledQueryCacheTestDbContext>
{
public DbSet<CompiledQueryCacheTestRow> Rows { get; set; }

public CompiledQueryCacheTestDbContext(DbContextOptions<CompiledQueryCacheTestDbContext> options)
: base(options)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Volo.Abp.Autofac;
using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.Modularity;

namespace Volo.Abp.EntityFrameworkCore.DataFiltering.CompiledQueryCache;

[DependsOn(typeof(AbpEntityFrameworkCoreSqliteModule), typeof(AbpAutofacModule))]
public class CompiledQueryCacheTestModule : CompiledQueryCacheTestModuleBase
{
protected override bool UseDbFunction => true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore.GlobalFilters;
using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.Modularity;
using Volo.Abp.Uow;

namespace Volo.Abp.EntityFrameworkCore.DataFiltering.CompiledQueryCache;

public abstract class CompiledQueryCacheTestModuleBase : AbpModule
{
public static readonly Guid TenantA = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
public static readonly Guid TenantB = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
public static readonly (string Name, Guid? TenantId, bool Deleted)[] Rows =
{
("A-live", TenantA, false),
("A-deleted", TenantA, true),
("B-live", TenantB, false),
("B-deleted", TenantB, true),
("Host-live", null, false),
("Host-deleted", null, true)
};

private readonly AbpUnitTestSqliteDatabase _database = new();
private ServiceProvider _efServiceProvider;

protected abstract bool UseDbFunction { get; }

public override void ConfigureServices(ServiceConfigurationContext context)
{
var counter = new QueryCompilationCounter();

// Avoid growing EF's global service-provider cache across test applications.
var efServices = new ServiceCollection();
efServices.AddEntityFrameworkSqlite();
if (UseDbFunction)
{
new AbpDbContextOptionsExtension().ApplyServices(efServices);
}

_efServiceProvider = efServices.BuildServiceProvider();
context.Services.AddSingleton(counter);
context.Services.AddAbpDbContext<CompiledQueryCacheTestDbContext>();
Configure<AbpEfCoreGlobalFilterOptions>(options => options.UseDbFunction = UseDbFunction);
Configure<AbpDbConnectionOptions>(options => options.ConnectionStrings.Default = _database.ConnectionString);
Configure<AbpDbContextOptions>(options => options.Configure(configuration =>
{
configuration.UseSqlite();
configuration.DbContextOptions.UseLoggerFactory(null);
configuration.DbContextOptions.UseInternalServiceProvider(_efServiceProvider);
configuration.DbContextOptions.LogTo(
_ => counter.Increment(),
new[] { CoreEventId.QueryCompilationStarting });
}));
}

public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
using var scope = context.ServiceProvider.CreateScope();
using var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>().Begin();
var dbContext = scope.ServiceProvider.GetRequiredService<CompiledQueryCacheTestDbContext>();
dbContext.Database.EnsureCreated();
foreach (var row in Rows)
{
dbContext.Database.ExecuteSqlInterpolated(
$@"INSERT INTO Rows (Id, TenantId, IsDeleted, Name)
VALUES ({Guid.NewGuid()}, {row.TenantId}, {row.Deleted}, {row.Name})");
}
}

public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
_database.Dispose();
_efServiceProvider.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Volo.Abp.Autofac;
using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.Modularity;

namespace Volo.Abp.EntityFrameworkCore.DataFiltering.CompiledQueryCache;

[DependsOn(typeof(AbpEntityFrameworkCoreSqliteModule), typeof(AbpAutofacModule))]
public class CompiledQueryCacheTestModuleWithoutDbFunction : CompiledQueryCacheTestModuleBase
{
protected override bool UseDbFunction => false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.MultiTenancy;

namespace Volo.Abp.EntityFrameworkCore.DataFiltering.CompiledQueryCache;

public class CompiledQueryCacheTestRow : Entity<Guid>, IMultiTenant, ISoftDelete
{
public Guid? TenantId { get; set; }
public bool IsDeleted { get; set; }
public string Name { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Volo.Abp.EntityFrameworkCore.DataFiltering.CompiledQueryCache;

public class CompiledQueryCache_Tests : CompiledQueryCacheTestBase<CompiledQueryCacheTestModule>
{
protected override bool UseDbFunction => true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Volo.Abp.EntityFrameworkCore.DataFiltering.CompiledQueryCache;

public class CompiledQueryCache_WithoutDbFunction_Tests : CompiledQueryCacheTestBase<CompiledQueryCacheTestModuleWithoutDbFunction>
{
protected override bool UseDbFunction => false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Threading;

namespace Volo.Abp.EntityFrameworkCore.DataFiltering.CompiledQueryCache;

public class QueryCompilationCounter
{
private int _count;
public int Count => Volatile.Read(ref _count);

public void Increment()
{
Interlocked.Increment(ref _count);
}
}
Loading
Loading