From 61a454f6e5a7dfd4ea9b3d81d117e3f1fb7a0e9f Mon Sep 17 00:00:00 2001 From: Koen Date: Sun, 26 Jul 2026 01:00:47 +0000 Subject: [PATCH] Enhance query filter expansion safety by adding validation for non-lambda transformers --- .../ExpressiveExpandQueryFiltersConvention.cs | 27 +++++++--- .../Sqlite/QueryFilterExpansionSafetyTests.cs | 53 +++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Tests/Sqlite/QueryFilterExpansionSafetyTests.cs diff --git a/src/ExpressiveSharp.EntityFrameworkCore/Infrastructure/Internal/ExpressiveExpandQueryFiltersConvention.cs b/src/ExpressiveSharp.EntityFrameworkCore/Infrastructure/Internal/ExpressiveExpandQueryFiltersConvention.cs index c70fa047..0002f82c 100644 --- a/src/ExpressiveSharp.EntityFrameworkCore/Infrastructure/Internal/ExpressiveExpandQueryFiltersConvention.cs +++ b/src/ExpressiveSharp.EntityFrameworkCore/Infrastructure/Internal/ExpressiveExpandQueryFiltersConvention.cs @@ -1,6 +1,8 @@ using System.Linq.Expressions; using ExpressiveSharp.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions; @@ -33,21 +35,34 @@ public void ProcessModelFinalizing( if (filter.Expression is null) continue; - var expanded = filter.Expression.ExpandExpressives(_options) as LambdaExpression; - if (filter.Key is not null) - entityType.SetQueryFilter(filter.Key, expanded); + entityType.SetQueryFilter(filter.Key, Expand(filter.Expression, entityType)); else - entityType.SetQueryFilter(expanded); + entityType.SetQueryFilter(Expand(filter.Expression, entityType)); } #else var queryFilter = entityType.GetQueryFilter(); if (queryFilter is not null) { - entityType.SetQueryFilter( - queryFilter.ExpandExpressives(_options) as LambdaExpression); + entityType.SetQueryFilter(Expand(queryFilter, entityType)); } #endif } } + + // SetQueryFilter(null) REMOVES a filter, so a non-lambda expansion result must never be + // cast-and-forwarded: a silently dropped soft-delete/tenant filter widens data access. + private LambdaExpression Expand(LambdaExpression filter, IConventionEntityType entityType) + { + var expanded = filter.ExpandExpressives(_options); + if (expanded is not LambdaExpression lambda) + { + throw new InvalidOperationException( + $"Expanding the query filter for entity type '{entityType.DisplayName()}' produced a " + + $"{expanded.NodeType} expression instead of a lambda, which would silently remove the filter. " + + "A registered IExpressionTreeTransformer must return a LambdaExpression when given one."); + } + + return lambda; + } } diff --git a/tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Tests/Sqlite/QueryFilterExpansionSafetyTests.cs b/tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Tests/Sqlite/QueryFilterExpansionSafetyTests.cs new file mode 100644 index 00000000..782d4c06 --- /dev/null +++ b/tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Tests/Sqlite/QueryFilterExpansionSafetyTests.cs @@ -0,0 +1,53 @@ +using System.Linq.Expressions; +using ExpressiveSharp.EntityFrameworkCore; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ExpressiveSharp.EntityFrameworkCore.IntegrationTests.Tests.Sqlite; + +[TestClass] +public class QueryFilterExpansionSafetyTests +{ + private sealed class RootUnwrappingTransformer : IExpressionTreeTransformer + { + public Expression Transform(Expression expression) + => expression is LambdaExpression lambda ? lambda.Body : expression; + } + + private sealed class RootUnwrappingPlugin : IExpressivePlugin + { + public void ApplyServices(IServiceCollection services) + { + } + + public IExpressionTreeTransformer[] GetTransformers() => [new RootUnwrappingTransformer()]; + } + + [TestMethod] + public async Task QueryFilter_TransformerReturningNonLambda_FailsModelBuildingLoudly() + { + var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + try + { + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .UseExpressives(o => o.AddPlugin(new RootUnwrappingPlugin())) + .Options; + + await using var context = new QueryFilterTestDbContext(options); + + var ex = await Assert.ThrowsExactlyAsync( + () => context.Database.EnsureCreatedAsync()); + + StringAssert.Contains(ex.Message, "query filter"); + StringAssert.Contains(ex.Message, "IExpressionTreeTransformer"); + } + finally + { + await connection.DisposeAsync(); + } + } +}