Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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.");
}
Comment on lines +57 to +64

return lambda;
}
}
Original file line number Diff line number Diff line change
@@ -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<QueryFilterTestDbContext>()
.UseSqlite(connection)
.UseExpressives(o => o.AddPlugin(new RootUnwrappingPlugin()))
.Options;

await using var context = new QueryFilterTestDbContext(options);

var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(
() => context.Database.EnsureCreatedAsync());

StringAssert.Contains(ex.Message, "query filter");
StringAssert.Contains(ex.Message, "IExpressionTreeTransformer");
}
finally
{
await connection.DisposeAsync();
}
}
}
Loading