mirror of
https://github.com/zoriya/EntityFrameworkCore.Projectables.git
synced 2025-12-06 05:56:10 +00:00
Merge pull request #44 from koenbeuk/default-fullcompat
Default to full compatibility mode
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,7 +28,6 @@ namespace EntityFrameworkCore.Projectables.FunctionalTests.Generics
|
||||
return Verifier.Verify(query.ToQueryString());
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void MultipleInvocations()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user