Merge pull request #44 from koenbeuk/default-fullcompat

Default to full compatibility mode
This commit is contained in:
Koen
2022-10-13 15:50:55 +01:00
committed by GitHub
18 changed files with 170 additions and 67 deletions

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
</ItemGroup>

View File

@@ -92,11 +92,11 @@ namespace BasicSample
dbConnection.Open();
using var serviceProvider = new ServiceCollection()
.AddDbContext<ApplicationDbContext>(options => {
.AddDbContext<ApplicationDbContext>((provider, options) => {
options
.UseSqlite(dbConnection)
.UseProjectables()
.UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()));
.UseInternalServiceProvider(provider);
})
.BuildServiceProvider();

View File

@@ -138,7 +138,7 @@ namespace EntityFrameworkCore.Projectables.Generated
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public static class {generatedClassName}
{{
public static System.Linq.Expressions.Expression<System.Func<{lambdaTypeArguments.Arguments}, {projectable.ReturnTypeName}>> Expression{(projectable.TypeParameterList?.Parameters.Any() == true ? projectable.TypeParameterList.ToString() : string.Empty)}()");
public static System.Linq.Expressions.Expression<System.Func<{(lambdaTypeArguments.Arguments.Any() ? $"{lambdaTypeArguments.Arguments}, " : "")}{projectable.ReturnTypeName}>> Expression{(projectable.TypeParameterList?.Parameters.Any() == true ? projectable.TypeParameterList.ToString() : string.Empty)}()");
if (projectable.ConstraintClauses is not null)
{

View File

@@ -10,8 +10,6 @@ namespace EntityFrameworkCore.Projectables.Extensions
{
public static class ExpressionExtensions
{
static ProjectableExpressionReplacer _projectableExpressionReplacer = new ProjectableExpressionReplacer(new ProjectionExpressionResolver());
[Obsolete("Use ExpandProjectables instead")]
public static Expression ExpandQuaryables(this Expression expression)
=> ExpandProjectables(expression);
@@ -20,6 +18,6 @@ namespace EntityFrameworkCore.Projectables.Extensions
/// Replaces all calls to properties and methods that are marked with the <C>Projectable</C> attribute with their respective expression tree
/// </summary>
public static Expression ExpandProjectables(this Expression expression)
=> _projectableExpressionReplacer.Visit(expression);
=> new ProjectableExpressionReplacer(new ProjectionExpressionResolver()).Visit(expression);
}
}

View File

@@ -10,12 +10,11 @@ namespace EntityFrameworkCore.Projectables.Infrastructure
{
/// <summary>
/// Projectables are expanded on each individual query invocation.
/// This mode can be used when you wan't to pass scoped services to your Projectable methods
/// </summary>
Full,
/// <summary>
/// Projectables are expanded in the query preprocessor and afterwards cached.
/// This is the default compatibility mode.
/// This yields some performance benefits over native EF with the downside of being incompatible with dynamic parameters.
/// </summary>
Limited
}

View File

@@ -13,12 +13,12 @@ using Microsoft.EntityFrameworkCore.Query.Internal;
namespace EntityFrameworkCore.Projectables.Infrastructure.Internal
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Needed")]
public sealed class CustomQueryProvider : IQueryCompiler
public sealed class CustomQueryCompiler : IQueryCompiler
{
readonly IQueryCompiler _decoratedQueryCompiler;
readonly ProjectableExpressionReplacer _projectableExpressionReplacer;
public CustomQueryProvider(IQueryCompiler decoratedQueryCompiler)
public CustomQueryCompiler(IQueryCompiler decoratedQueryCompiler)
{
_decoratedQueryCompiler = decoratedQueryCompiler;
_projectableExpressionReplacer = new ProjectableExpressionReplacer(new ProjectionExpressionResolver());

View File

@@ -0,0 +1,19 @@
using System.Linq.Expressions;
using EntityFrameworkCore.Projectables.Extensions;
using Microsoft.EntityFrameworkCore.Query;
namespace EntityFrameworkCore.Projectables.Infrastructure.Internal
{
public class CustomQueryTranslationPreprocessor : QueryTranslationPreprocessor
{
readonly QueryTranslationPreprocessor _decoratedPreprocessor;
public CustomQueryTranslationPreprocessor(QueryTranslationPreprocessor decoratedPreprocessor, QueryTranslationPreprocessorDependencies dependencies, QueryCompilationContext queryCompilationContext) : base(dependencies, queryCompilationContext)
{
_decoratedPreprocessor = decoratedPreprocessor;
}
public override Expression Process(Expression query)
=> _decoratedPreprocessor.Process(query.ExpandProjectables());
}
}

View File

@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using EntityFrameworkCore.Projectables.Extensions;
using EntityFrameworkCore.Projectables.Services;
using Microsoft.EntityFrameworkCore.Query;
@@ -24,17 +22,4 @@ namespace EntityFrameworkCore.Projectables.Infrastructure.Internal
public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
=> new CustomQueryTranslationPreprocessor(_decoratedFactory.Create(queryCompilationContext), _queryTranslationPreprocessorDependencies, queryCompilationContext);
}
public class CustomQueryTranslationPreprocessor : QueryTranslationPreprocessor
{
readonly QueryTranslationPreprocessor _decoratedPreprocessor;
public CustomQueryTranslationPreprocessor(QueryTranslationPreprocessor decoratedPreprocessor, QueryTranslationPreprocessorDependencies dependencies, QueryCompilationContext queryCompilationContext) : base(dependencies, queryCompilationContext)
{
_decoratedPreprocessor = decoratedPreprocessor;
}
public override Expression Process(Expression query)
=> _decoratedPreprocessor.Process(query.ExpandProjectables());
}
}

View File

@@ -16,7 +16,7 @@ namespace EntityFrameworkCore.Projectables.Infrastructure.Internal
{
public class ProjectionOptionsExtension : IDbContextOptionsExtension
{
CompatibilityMode _compatibilityMode = CompatibilityMode.Limited;
CompatibilityMode _compatibilityMode = CompatibilityMode.Full;
public ProjectionOptionsExtension()
{
@@ -57,7 +57,7 @@ namespace EntityFrameworkCore.Projectables.Infrastructure.Internal
throw new InvalidOperationException("No QueryProvider is configured yet. Please make sure to configure a database provider first"); ;
}
var decoratorObjectFactory = ActivatorUtilities.CreateFactory(typeof(CustomQueryProvider), new[] { targetDescriptor.ServiceType });
var decoratorObjectFactory = ActivatorUtilities.CreateFactory(typeof(CustomQueryCompiler), new[] { targetDescriptor.ServiceType });
services.Replace(ServiceDescriptor.Describe(
targetDescriptor.ServiceType,

View File

@@ -9,21 +9,11 @@ namespace EntityFrameworkCore.Projectables.Services
{
public sealed class ExpressionArgumentReplacer : ExpressionVisitor
{
readonly IEnumerable<(ParameterExpression parameter, Expression argument)>? _parameterArgumentMapping;
public ExpressionArgumentReplacer(IEnumerable<(ParameterExpression, Expression)>? parameterArgumentMapping = null)
{
_parameterArgumentMapping = parameterArgumentMapping;
}
public Dictionary<ParameterExpression, Expression> ParameterArgumentMapping { get; } = new();
protected override Expression VisitParameter(ParameterExpression node)
{
var mappedArgument = _parameterArgumentMapping?
.Where(x => x.parameter == node)
.Select(x => x.argument)
.FirstOrDefault();
if (mappedArgument is not null)
if (ParameterArgumentMapping.TryGetValue(node, out var mappedArgument))
{
return mappedArgument;
}

View File

@@ -1,43 +1,66 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace EntityFrameworkCore.Projectables.Services
{
public sealed class ProjectableExpressionReplacer : ExpressionVisitor
{
readonly IProjectionExpressionResolver _resolver;
readonly ExpressionArgumentReplacer _expressionArgumentReplacer = new();
readonly Dictionary<MemberInfo, LambdaExpression?> _projectableMemberCache = new();
public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver)
{
_resolver = projectionExpressionResolver;
}
bool TryGetReflectedExpression(MemberInfo memberInfo, [NotNullWhen(true)] out LambdaExpression? reflectedExpression)
{
if (!_projectableMemberCache.TryGetValue(memberInfo, out reflectedExpression))
{
var projectableAttribute = memberInfo.GetCustomAttribute<ProjectableAttribute>(false);
reflectedExpression = projectableAttribute is not null
? _resolver.FindGeneratedExpression(memberInfo)
: (LambdaExpression?)null;
_projectableMemberCache.Add(memberInfo, reflectedExpression);
}
return reflectedExpression is not null;
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.GetCustomAttributes(false).OfType<ProjectableAttribute>().Any())
if (TryGetReflectedExpression(node.Method, out var reflectedExpression))
{
var reflectedExpression = _resolver.FindGeneratedExpression(node.Method);
var parameterArgumentMapping = node.Object is not null
? Enumerable.Repeat((reflectedExpression.Parameters[0], node.Object), 1)
: Enumerable.Empty<(ParameterExpression, Expression)>();
if (reflectedExpression.Parameters.Count > 0)
for (var parameterIndex = 0; parameterIndex < reflectedExpression.Parameters.Count; parameterIndex++)
{
parameterArgumentMapping = parameterArgumentMapping.Concat(
node.Object is not null
? reflectedExpression.Parameters.Skip(1).Zip(node.Arguments, (parameter, argument) => (parameter, argument))
: reflectedExpression.Parameters.Zip(node.Arguments, (parameter, argument) => (parameter, argument))
);
}
var parameterExpession = reflectedExpression.Parameters[parameterIndex];
var mappedArgumentExpression = (parameterIndex, node.Object) switch {
(0, not null) => node.Object,
(_, not null) => node.Arguments[parameterIndex - 1],
(_, null) => node.Arguments.Count > parameterIndex ? node.Arguments[parameterIndex] : null
};
if (mappedArgumentExpression is not null)
{
_expressionArgumentReplacer.ParameterArgumentMapping.Add(parameterExpession, mappedArgumentExpression);
}
}
var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body);
_expressionArgumentReplacer.ParameterArgumentMapping.Clear();
var expressionArgumentReplacer = new ExpressionArgumentReplacer(parameterArgumentMapping);
return Visit(
expressionArgumentReplacer.Visit(reflectedExpression.Body)
updatedBody
);
}
@@ -46,17 +69,16 @@ namespace EntityFrameworkCore.Projectables.Services
protected override Expression VisitMember(MemberExpression node)
{
if (node.Member.GetCustomAttributes(false).OfType<ProjectableAttribute>().Any())
if (TryGetReflectedExpression(node.Member, out var reflectedExpression))
{
var reflectedExpression = _resolver.FindGeneratedExpression(node.Member);
if (node.Expression is not null)
{
var expressionArgumentReplacer = new ExpressionArgumentReplacer(
Enumerable.Repeat((reflectedExpression.Parameters[0], node.Expression), 1)
);
_expressionArgumentReplacer.ParameterArgumentMapping.Add(reflectedExpression.Parameters[0], node.Expression);
var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body);
_expressionArgumentReplacer.ParameterArgumentMapping.Clear();
return Visit(
expressionArgumentReplacer.Visit(reflectedExpression.Body)
updatedBody
);
}
else

View File

@@ -28,7 +28,6 @@ namespace EntityFrameworkCore.Projectables.FunctionalTests.Generics
return Verifier.Verify(query.ToQueryString());
}
[Fact]
public void MultipleInvocations()
{

View File

@@ -22,7 +22,7 @@ namespace EntityFrameworkCore.Projectables.FunctionalTests.Helpers
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=(localdb)\v11.0;Integrated Security=true"); // Fake connection string as we're actually never connecting
optionsBuilder.UseSqlServer("Server=(localdb)\\v11.0;Integrated Security=true"); // Fake connection string as we're actually never connecting
optionsBuilder.UseProjectables(options => {
options.CompatibilityMode(_compatibilityMode); // Needed by our ComplexModelTests
});

View File

@@ -0,0 +1,19 @@
// <auto-generated/>
using System;
using System.Linq;
using System.Collections.Generic;
using EntityFrameworkCore.Projectables;
namespace EntityFrameworkCore.Projectables.Generated
#nullable disable
{
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public static class _Foo_Zero
{
public static System.Linq.Expressions.Expression<System.Func<int>> Expression()
{
return () =>
0;
}
}
}

View File

@@ -0,0 +1,19 @@
// <auto-generated/>
using System;
using System.Linq;
using System.Collections.Generic;
using EntityFrameworkCore.Projectables;
namespace EntityFrameworkCore.Projectables.Generated
#nullable disable
{
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public static class _Foo_Zero
{
public static System.Linq.Expressions.Expression<System.Func<int, int>> Expression()
{
return (int x) =>
0;
}
}
}

View File

@@ -1074,6 +1074,53 @@ namespace Foo {
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
[Fact]
public Task StaticMethodWithNoParameters()
{
var compilation = CreateCompilation(@"
using System;
using System.Linq;
using System.Collections.Generic;
using EntityFrameworkCore.Projectables;
public static class Foo {
[Projectable]
public static int Zero() => 0;
}
");
var result = RunGenerator(compilation);
Assert.Empty(result.Diagnostics);
Assert.Single(result.GeneratedTrees);
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
[Fact]
public Task StaticMethodWithParameters()
{
var compilation = CreateCompilation(@"
using System;
using System.Linq;
using System.Collections.Generic;
using EntityFrameworkCore.Projectables;
public static class Foo {
[Projectable]
public static int Zero(int x) => 0;
}
");
var result = RunGenerator(compilation);
Assert.Empty(result.Diagnostics);
Assert.Single(result.GeneratedTrees);
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
[Fact]
public Task ConstMember()
{

View File

@@ -3,7 +3,9 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />

View File

@@ -16,7 +16,11 @@ namespace EntityFrameworkCore.Projectables.Tests.Services
{
var parameter = Expression.Parameter(typeof(int));
var argument = Expression.Constant(1);
var subject = new ExpressionArgumentReplacer(new[] { (parameter, (Expression)argument) });
var subject = new ExpressionArgumentReplacer() {
ParameterArgumentMapping = {
{ parameter, argument }
}
};
var result = subject.Visit(parameter);