mirror of
https://github.com/zoriya/EntityFrameworkCore.Projectables.git
synced 2025-12-19 20:15:12 +00:00
Checkpoint (still hacking about)
This commit is contained in:
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.6" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ using Microsoft.Data.Sqlite;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
@@ -26,6 +28,14 @@ namespace BasicSample
|
|||||||
|
|
||||||
[Projectable]
|
[Projectable]
|
||||||
public double TotalSpent => Orders.Sum(x => x.PriceSum);
|
public double TotalSpent => Orders.Sum(x => x.PriceSum);
|
||||||
|
|
||||||
|
[Projectable]
|
||||||
|
public Order MostValuableOrder
|
||||||
|
=> Orders.OrderByDescending(x => x.PriceSum).FirstOrDefault();
|
||||||
|
|
||||||
|
[Projectable]
|
||||||
|
public IEnumerable<Product> FindOrderedProducts(string namePrefix)
|
||||||
|
=> Orders.SelectMany(x => x.Items).Select(x => x.Product).Where(x => x.Name.StartsWith(namePrefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Product
|
public class Product
|
||||||
@@ -85,7 +95,8 @@ namespace BasicSample
|
|||||||
.AddDbContext<ApplicationDbContext>(options => {
|
.AddDbContext<ApplicationDbContext>(options => {
|
||||||
options
|
options
|
||||||
.UseSqlite(dbConnection)
|
.UseSqlite(dbConnection)
|
||||||
.UseProjections();
|
.UseProjections()
|
||||||
|
.UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()));
|
||||||
})
|
})
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
|
|
||||||
@@ -119,17 +130,43 @@ namespace BasicSample
|
|||||||
dbContext.Users.Add(user);
|
dbContext.Users.Add(user);
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
|
|
||||||
var query = dbContext.Users
|
// What did our user spent in total
|
||||||
.Select(x => new {
|
{
|
||||||
Name = x.FullName,
|
var query = dbContext.Users
|
||||||
x.TotalSpent
|
.Select(x => new {
|
||||||
});
|
Name = x.FullName,
|
||||||
|
x.TotalSpent
|
||||||
|
});
|
||||||
|
|
||||||
var result = query.FirstOrDefault();
|
var result = query.FirstOrDefault();
|
||||||
|
|
||||||
|
Console.WriteLine($"Our user ({result.Name}) spent {result.TotalSpent}");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var query = dbContext.Users
|
||||||
|
.Select(x => new {
|
||||||
|
Name = x.FullName,
|
||||||
|
x.MostValuableOrder
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = query.FirstOrDefault();
|
||||||
|
|
||||||
|
Console.WriteLine($"Our users spent {result.MostValuableOrder.PriceSum} on its biggest order");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var query = dbContext.Users
|
||||||
|
.Select(x => new {
|
||||||
|
Name = x.FullName,
|
||||||
|
Ordered = x.FindOrderedProducts("Red").Select(x => x.Name)
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = query.FirstOrDefault();
|
||||||
|
|
||||||
|
Console.WriteLine($"Our users bought the following products starting with 'Red': {string.Join(", ", result.Ordered)}");
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine($"Our user {result.Name} spent {result.TotalSpent}");
|
|
||||||
Console.WriteLine($"We used the following query:");
|
|
||||||
Console.WriteLine(query.ToQueryString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generator
|
namespace EntityFrameworkCore.Projections.Generator
|
||||||
{
|
{
|
||||||
|
|
||||||
public class ExpressionSyntaxRewriter : CSharpSyntaxRewriter
|
public class ExpressionSyntaxRewriter : CSharpSyntaxRewriter
|
||||||
{
|
{
|
||||||
readonly INamedTypeSymbol _targetTypeSymbol;
|
readonly INamedTypeSymbol _targetTypeSymbol;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projections.Generator
|
||||||
|
{
|
||||||
|
public class ParameterSyntaxRewriter : CSharpSyntaxRewriter
|
||||||
|
{
|
||||||
|
readonly SemanticModel _semanticModel;
|
||||||
|
|
||||||
|
public ParameterSyntaxRewriter(SemanticModel semanticModel)
|
||||||
|
{
|
||||||
|
_semanticModel = semanticModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node)
|
||||||
|
{
|
||||||
|
// todo: Fully qualify these
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SyntaxNode? VisitParameter(ParameterSyntax node)
|
||||||
|
{
|
||||||
|
var symbol = _semanticModel.GetDeclaredSymbol(node);
|
||||||
|
|
||||||
|
if (symbol is not null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
var thisKeywordIndex = node.Modifiers.IndexOf(SyntaxKind.ThisKeyword);
|
||||||
|
if (thisKeywordIndex != -1)
|
||||||
|
{
|
||||||
|
node = node.WithModifiers(node.Modifiers.RemoveAt(thisKeywordIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,10 @@ namespace EntityFrameworkCore.Projections.Generator
|
|||||||
|
|
||||||
public IEnumerable<string> NestedInClassNames { get; set; }
|
public IEnumerable<string> NestedInClassNames { get; set; }
|
||||||
|
|
||||||
|
public string TargetClassNamespace { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<string> TargetNestedInClassNames { get; set; }
|
||||||
|
|
||||||
public string ClassName { get; set; }
|
public string ClassName { get; set; }
|
||||||
|
|
||||||
public string MemberName { get; set; }
|
public string MemberName { get; set; }
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
using Microsoft.CodeAnalysis;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generator
|
namespace EntityFrameworkCore.Projections.Generator
|
||||||
{
|
{
|
||||||
public static class ProjectableInterpreter
|
public static class ProjectableInterpreter
|
||||||
{
|
{
|
||||||
static IEnumerable<string> GetNestedInClassPath(INamedTypeSymbol namedTypeSymbol)
|
static IEnumerable<string> GetNestedInClassPath(ITypeSymbol namedTypeSymbol)
|
||||||
{
|
{
|
||||||
if (namedTypeSymbol.ContainingType is not null)
|
if (namedTypeSymbol.ContainingType is not null)
|
||||||
{
|
{
|
||||||
@@ -34,6 +31,7 @@ namespace EntityFrameworkCore.Projections.Generator
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var projectableAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("EntityFrameworkCore.Projections.ProjectableAttribute");
|
var projectableAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("EntityFrameworkCore.Projections.ProjectableAttribute");
|
||||||
|
|
||||||
var projectableAttributeClass = memberSymbol.GetAttributes()
|
var projectableAttributeClass = memberSymbol.GetAttributes()
|
||||||
@@ -46,20 +44,32 @@ namespace EntityFrameworkCore.Projections.Generator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var expressionSyntaxRewriter = new ExpressionSyntaxRewriter(memberSymbol.ContainingType, semanticModel);
|
var expressionSyntaxRewriter = new ExpressionSyntaxRewriter(memberSymbol.ContainingType, semanticModel);
|
||||||
|
var parameterSyntaxRewriter = new ParameterSyntaxRewriter(semanticModel);
|
||||||
|
|
||||||
var descriptor = new ProjectableDescriptor
|
var descriptor = new ProjectableDescriptor {
|
||||||
{
|
|
||||||
ClassName = memberSymbol.ContainingType.Name,
|
ClassName = memberSymbol.ContainingType.Name,
|
||||||
ClassNamespace = memberSymbol.ContainingType.ContainingNamespace.IsGlobalNamespace ? null : memberSymbol.ContainingType.ContainingNamespace.ToDisplayString(),
|
ClassNamespace = memberSymbol.ContainingType.ContainingNamespace.IsGlobalNamespace ? null : memberSymbol.ContainingType.ContainingNamespace.ToDisplayString(),
|
||||||
MemberName = memberSymbol.Name,
|
MemberName = memberSymbol.Name,
|
||||||
NestedInClassNames = GetNestedInClassPath(memberSymbol.ContainingType)
|
NestedInClassNames = GetNestedInClassPath(memberSymbol.ContainingType)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (memberSymbol is IMethodSymbol methodSymbol && methodSymbol.IsExtensionMethod)
|
||||||
|
{
|
||||||
|
var targetTypeSymbol = methodSymbol.Parameters.First().Type;
|
||||||
|
descriptor.TargetClassNamespace = targetTypeSymbol.ContainingNamespace.IsGlobalNamespace ? null : targetTypeSymbol.ContainingNamespace.ToDisplayString();
|
||||||
|
descriptor.TargetNestedInClassNames = GetNestedInClassPath(targetTypeSymbol);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
descriptor.TargetClassNamespace = descriptor.ClassNamespace;
|
||||||
|
descriptor.TargetNestedInClassNames = descriptor.NestedInClassNames;
|
||||||
|
}
|
||||||
|
|
||||||
if (memberDeclarationSyntax is MethodDeclarationSyntax methodDeclarationSyntax)
|
if (memberDeclarationSyntax is MethodDeclarationSyntax methodDeclarationSyntax)
|
||||||
{
|
{
|
||||||
descriptor.ReturnTypeName = methodDeclarationSyntax.ReturnType.ToString();
|
descriptor.ReturnTypeName = methodDeclarationSyntax.ReturnType.ToString();
|
||||||
descriptor.Body = expressionSyntaxRewriter.Visit(methodDeclarationSyntax.ExpressionBody.Expression);
|
descriptor.Body = expressionSyntaxRewriter.Visit(methodDeclarationSyntax.ExpressionBody.Expression);
|
||||||
descriptor.ParametersListString = methodDeclarationSyntax.ParameterList.ToString();
|
descriptor.ParametersListString = parameterSyntaxRewriter.Visit(methodDeclarationSyntax.ParameterList).ToString();
|
||||||
}
|
}
|
||||||
else if (memberDeclarationSyntax is PropertyDeclarationSyntax propertyDeclarationSyntax)
|
else if (memberDeclarationSyntax is PropertyDeclarationSyntax propertyDeclarationSyntax)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ namespace EntityFrameworkCore.Projections.Generator
|
|||||||
resultBuilder.AppendLine(usingDirective);
|
resultBuilder.AppendLine(usingDirective);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (projectable.TargetClassNamespace is not null && !projectable.UsingDirectives.Contains(projectable.TargetClassNamespace))
|
||||||
|
{
|
||||||
|
resultBuilder.AppendLine($"using {projectable.TargetClassNamespace};");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectable.ClassNamespace is not null && projectable.ClassNamespace != projectable.TargetClassNamespace && !projectable.UsingDirectives.Contains(projectable.ClassNamespace))
|
||||||
|
{
|
||||||
|
resultBuilder.AppendLine($"using {projectable.ClassNamespace};");
|
||||||
|
}
|
||||||
|
|
||||||
var generatedClassName = ProjectionExpressionClassNameGenerator.GenerateName(projectable.ClassNamespace, projectable.NestedInClassNames, projectable.MemberName);
|
var generatedClassName = ProjectionExpressionClassNameGenerator.GenerateName(projectable.ClassNamespace, projectable.NestedInClassNames, projectable.MemberName);
|
||||||
|
|
||||||
resultBuilder.Append($@"
|
resultBuilder.Append($@"
|
||||||
@@ -46,7 +56,7 @@ namespace EntityFrameworkCore.Projections.Generated
|
|||||||
{{
|
{{
|
||||||
public static class {generatedClassName}
|
public static class {generatedClassName}
|
||||||
{{
|
{{
|
||||||
public static System.Linq.Expressions.Expression<System.Func<{projectable.ClassNamespace}.{string.Join(".", projectable.NestedInClassNames)}, {projectable.ReturnTypeName}>> Expression{projectable.ParametersListString} =>
|
public static System.Linq.Expressions.Expression<System.Func<{projectable.TargetClassNamespace}.{string.Join(".", projectable.TargetNestedInClassNames)}, {projectable.ReturnTypeName}>> Expression{projectable.ParametersListString} =>
|
||||||
{ProjectionTargetParameterName} => {projectable.Body};
|
{ProjectionTargetParameterName} => {projectable.Body};
|
||||||
}}
|
}}
|
||||||
}}");
|
}}");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Query;
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query.Internal;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -18,25 +19,24 @@ namespace EntityFrameworkCore.Projections.Infrastructure.Internal
|
|||||||
|
|
||||||
public DbContextOptionsExtensionInfo Info { get; }
|
public DbContextOptionsExtensionInfo Info { get; }
|
||||||
|
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Needed")]
|
||||||
public void ApplyServices(IServiceCollection services)
|
public void ApplyServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
var existingPreprocessorFactoryRegistration = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryTranslationPreprocessorFactory));
|
var queryCompilerRegistration = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryCompiler));
|
||||||
|
if (queryCompilerRegistration?.ImplementationType is null)
|
||||||
if (existingPreprocessorFactoryRegistration?.ImplementationType is null)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Expected a QueryTranslationPreprocessor to be registered. Please make sure to register your database provider first");
|
throw new InvalidOperationException("No queryCompiler is configured yet. Please make sure to configure a database provider first"); ;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that we can still resolve this factory
|
// Ensure that we can still resolve this queryCompiler
|
||||||
services.Add(new ServiceDescriptor(existingPreprocessorFactoryRegistration.ImplementationType, existingPreprocessorFactoryRegistration.ImplementationType, existingPreprocessorFactoryRegistration.Lifetime));
|
services.Add(new ServiceDescriptor(queryCompilerRegistration.ImplementationType, queryCompilerRegistration.ImplementationType, queryCompilerRegistration.Lifetime));
|
||||||
services.Remove(existingPreprocessorFactoryRegistration);
|
services.Remove(queryCompilerRegistration);
|
||||||
|
|
||||||
services.Add(new ServiceDescriptor(
|
services.Add(new ServiceDescriptor(
|
||||||
typeof(IQueryTranslationPreprocessorFactory),
|
typeof(IQueryCompiler),
|
||||||
serviceProvider => new WrappedQueryTranslationPreprocessorFactory((IQueryTranslationPreprocessorFactory)serviceProvider.GetRequiredService(existingPreprocessorFactoryRegistration.ImplementationType), serviceProvider.GetRequiredService<QueryTranslationPreprocessorDependencies>()),
|
serviceProvider => new WrappedQueryCompiler((IQueryCompiler)serviceProvider.GetRequiredService(queryCompilerRegistration.ImplementationType)),
|
||||||
existingPreprocessorFactoryRegistration.Lifetime
|
queryCompilerRegistration.Lifetime
|
||||||
));
|
));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Validate(IDbContextOptions options)
|
public void Validate(IDbContextOptions options)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using EntityFrameworkCore.Projections.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query.Internal;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projections.Infrastructure.Internal
|
||||||
|
{
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Needed")]
|
||||||
|
public sealed class WrappedQueryCompiler : IQueryCompiler
|
||||||
|
{
|
||||||
|
readonly ProjectableExpressionReplacer _projectionExpressionReplacer = new();
|
||||||
|
readonly IQueryCompiler _queryCompiler;
|
||||||
|
|
||||||
|
public WrappedQueryCompiler(IQueryCompiler queryCompiler)
|
||||||
|
{
|
||||||
|
_queryCompiler = queryCompiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<QueryContext, TResult> CreateCompiledAsyncQuery<TResult>(Expression query)
|
||||||
|
=> _queryCompiler.CreateCompiledAsyncQuery<TResult>(PatchQuery(query));
|
||||||
|
|
||||||
|
public Func<QueryContext, TResult> CreateCompiledQuery<TResult>(Expression query)
|
||||||
|
=> _queryCompiler.CreateCompiledQuery<TResult>(PatchQuery(query));
|
||||||
|
|
||||||
|
public TResult Execute<TResult>(Expression query)
|
||||||
|
=> _queryCompiler.Execute<TResult>(PatchQuery(query));
|
||||||
|
|
||||||
|
public TResult ExecuteAsync<TResult>(Expression query, CancellationToken cancellationToken)
|
||||||
|
=> _queryCompiler.ExecuteAsync<TResult>(PatchQuery(query), cancellationToken);
|
||||||
|
|
||||||
|
Expression PatchQuery(Expression expression)
|
||||||
|
=> _projectionExpressionReplacer.Visit(expression);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using EntityFrameworkCore.Projections.Services;
|
|
||||||
using Microsoft.EntityFrameworkCore.Query;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Infrastructure.Internal
|
|
||||||
{
|
|
||||||
public class WrappedQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
|
|
||||||
{
|
|
||||||
readonly IQueryTranslationPreprocessorFactory _originalFactory;
|
|
||||||
readonly QueryTranslationPreprocessorDependencies _dependencies;
|
|
||||||
|
|
||||||
public WrappedQueryTranslationPreprocessorFactory(IQueryTranslationPreprocessorFactory originalFactory, QueryTranslationPreprocessorDependencies dependencies)
|
|
||||||
{
|
|
||||||
_originalFactory = originalFactory;
|
|
||||||
_dependencies = dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
|
|
||||||
{
|
|
||||||
var originalPreprocessor = _originalFactory.Create(queryCompilationContext);
|
|
||||||
|
|
||||||
return new WrappedQueryTranslationPreprocessor(originalPreprocessor, _dependencies, queryCompilationContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class WrappedQueryTranslationPreprocessor : QueryTranslationPreprocessor
|
|
||||||
{
|
|
||||||
readonly ProjectableExpressionReplacer _projectableExpressionReplacer;
|
|
||||||
readonly QueryTranslationPreprocessor _originalPreprocessor;
|
|
||||||
|
|
||||||
public WrappedQueryTranslationPreprocessor(QueryTranslationPreprocessor originalPreprocessor, QueryTranslationPreprocessorDependencies dependencies, QueryCompilationContext queryCompilationContext) : base(dependencies, queryCompilationContext)
|
|
||||||
{
|
|
||||||
_originalPreprocessor = originalPreprocessor;
|
|
||||||
_projectableExpressionReplacer = new ProjectableExpressionReplacer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Expression NormalizeQueryableMethod(Expression expression)
|
|
||||||
{
|
|
||||||
return _originalPreprocessor.NormalizeQueryableMethod(expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Expression Process(Expression query)
|
|
||||||
{
|
|
||||||
query = _projectableExpressionReplacer.Visit(query);
|
|
||||||
|
|
||||||
return _originalPreprocessor.Process(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projections.Services
|
||||||
|
{
|
||||||
|
public sealed class ParameterExtractor : ExpressionVisitor
|
||||||
|
{
|
||||||
|
List<ParameterExpression>? _extractedParameters = null;
|
||||||
|
|
||||||
|
protected override Expression VisitParameter(ParameterExpression node)
|
||||||
|
{
|
||||||
|
if (_extractedParameters is null)
|
||||||
|
{
|
||||||
|
_extractedParameters = new List<ParameterExpression>();
|
||||||
|
}
|
||||||
|
|
||||||
|
_extractedParameters.Add(node);
|
||||||
|
return base.VisitParameter(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ParameterExpression> ExtractedParameters
|
||||||
|
=> _extractedParameters ?? Enumerable.Empty<ParameterExpression>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,17 +43,22 @@ namespace EntityFrameworkCore.Projections.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var test1 = argumentExpressions.Cast<ParameterExpression>()!;
|
var parameterExtractor = new ParameterExtractor();
|
||||||
|
foreach (var argument in argumentExpressions)
|
||||||
|
{
|
||||||
|
parameterExtractor.Visit(argument);
|
||||||
|
}
|
||||||
|
|
||||||
var expressionFactoryConstructionMethod =
|
var lambda = Expression.Lambda<Func<LambdaExpression>>(
|
||||||
Expression.Lambda<Func<LambdaExpression>>(
|
|
||||||
Expression.Call(
|
Expression.Call(
|
||||||
expressionFactoryMethod,
|
expressionFactoryMethod,
|
||||||
argumentExpressions
|
argumentExpressions
|
||||||
)
|
),
|
||||||
).Compile();
|
parameterExtractor.ExtractedParameters
|
||||||
|
);
|
||||||
|
|
||||||
return expressionFactoryConstructionMethod.Invoke();
|
|
||||||
|
return lambda.Compile().Invoke();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace EntityFrameworkCore.Projections.FunctionalTests.ExtensionMethods
|
||||||
|
{
|
||||||
|
|
||||||
|
public partial class ExtensionMethodTests
|
||||||
|
{
|
||||||
|
public class Entity
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace EntityFrameworkCore.Projections.FunctionalTests.ExtensionMethods
|
||||||
|
{
|
||||||
|
public static class EntityExtensions
|
||||||
|
{
|
||||||
|
[Projectable]
|
||||||
|
public static int Foo(this ExtensionMethodTests.Entity entity) => entity.Id + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using EntityFrameworkCore.Projections.FunctionalTests.Helpers;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using ScenarioTests;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projections.FunctionalTests.ExtensionMethods
|
||||||
|
{
|
||||||
|
|
||||||
|
public partial class ExtensionMethodTests
|
||||||
|
{
|
||||||
|
[Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)]
|
||||||
|
public void PlayScenario(ScenarioContext scenario)
|
||||||
|
{
|
||||||
|
using var dbContext = new SampleDbContext<Entity>();
|
||||||
|
|
||||||
|
scenario.Fact("We can select on a projectable extension method", () => {
|
||||||
|
const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]";
|
||||||
|
|
||||||
|
var query = dbContext.Set<Entity>()
|
||||||
|
.Select(x => x.Foo());
|
||||||
|
|
||||||
|
Assert.Equal(expectedQueryString, query.ToQueryString());
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.SqlTypes;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using EntityFrameworkCore.Projections.FunctionalTests.Helpers;
|
||||||
|
using EntityFrameworkCore.Projections.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using ScenarioTests;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projections.FunctionalTests
|
||||||
|
{
|
||||||
|
public partial class StatefullComplexFunctionTests
|
||||||
|
{
|
||||||
|
public record Entity
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Projectable]
|
||||||
|
public int Computed(int argument) => Id + argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)]
|
||||||
|
public void PlayScenario(ScenarioContext scenario)
|
||||||
|
{
|
||||||
|
// Setup
|
||||||
|
using var dbContext = new SampleDbContext<Entity>();
|
||||||
|
|
||||||
|
scenario.Fact("We can filter on a projectable property", () => {
|
||||||
|
const string expectedQueryString =
|
||||||
|
@"DECLARE @__argument_0 int = 1;
|
||||||
|
|
||||||
|
SELECT [e].[Id]
|
||||||
|
FROM [Entity] AS [e]
|
||||||
|
WHERE ([e].[Id] + @__argument_0) = 2";
|
||||||
|
|
||||||
|
var query = dbContext.Set<Entity>().AsQueryable()
|
||||||
|
.Where(x => x.Computed(1) == 2);
|
||||||
|
|
||||||
|
Assert.Equal(expectedQueryString, query.ToQueryString());
|
||||||
|
});
|
||||||
|
|
||||||
|
scenario.Fact("We can select on a projectable property", () => {
|
||||||
|
const string expectedQueryString =
|
||||||
|
@"DECLARE @__argument_0 int = 1;
|
||||||
|
|
||||||
|
SELECT [e].[Id] + @__argument_0
|
||||||
|
FROM [Entity] AS [e]";
|
||||||
|
|
||||||
|
var query = dbContext.Set<Entity>()
|
||||||
|
.AsQueryable()
|
||||||
|
.Select(x => x.Computed(1));
|
||||||
|
|
||||||
|
Assert.Equal(expectedQueryString, query.ToQueryString());
|
||||||
|
});
|
||||||
|
|
||||||
|
scenario.Fact("We can pass in variables", () => {
|
||||||
|
const string expectedQueryString =
|
||||||
|
@"DECLARE @__argument_0 int = 1;
|
||||||
|
|
||||||
|
SELECT [e].[Id] + @__argument_0
|
||||||
|
FROM [Entity] AS [e]";
|
||||||
|
|
||||||
|
var argument = 1;
|
||||||
|
var query = dbContext.Set<Entity>()
|
||||||
|
.Select(x => x.Computed(argument));
|
||||||
|
|
||||||
|
Assert.Equal(expectedQueryString, query.ToQueryString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using EntityFrameworkCore.Projections.FunctionalTests.Helpers;
|
using EntityFrameworkCore.Projections.FunctionalTests.Helpers;
|
||||||
|
using EntityFrameworkCore.Projections.Services;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using ScenarioTests;
|
using ScenarioTests;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -28,25 +29,39 @@ namespace EntityFrameworkCore.Projections.FunctionalTests
|
|||||||
using var dbContext = new SampleDbContext<Entity>();
|
using var dbContext = new SampleDbContext<Entity>();
|
||||||
|
|
||||||
scenario.Fact("We can filter on a projectable property", () => {
|
scenario.Fact("We can filter on a projectable property", () => {
|
||||||
const string expectedQueryString = "SELECT [e].[Id]\r\nFROM [Entity] AS [e]\r\nWHERE 0 = 1";
|
const string expectedQueryString =
|
||||||
|
@"DECLARE @__p_0 bit = CAST(0 AS bit);
|
||||||
|
|
||||||
var query = dbContext.Set<Entity>()
|
SELECT [e].[Id]
|
||||||
|
FROM [Entity] AS [e]
|
||||||
|
WHERE @__p_0 = CAST(1 AS bit)";
|
||||||
|
|
||||||
|
var query = dbContext.Set<Entity>().AsQueryable()
|
||||||
.Where(x => x.Computed(0) == 1);
|
.Where(x => x.Computed(0) == 1);
|
||||||
|
|
||||||
Assert.Equal(expectedQueryString, query.ToQueryString());
|
Assert.Equal(expectedQueryString, query.ToQueryString());
|
||||||
});
|
});
|
||||||
|
|
||||||
scenario.Fact("We can select on a projectable property", () => {
|
scenario.Fact("We can select on a projectable property", () => {
|
||||||
const string expectedQueryString = "SELECT 0\r\nFROM [Entity] AS [e]";
|
const string expectedQueryString =
|
||||||
|
@"DECLARE @__argument1_0 int = 0;
|
||||||
|
|
||||||
|
SELECT @__argument1_0
|
||||||
|
FROM [Entity] AS [e]";
|
||||||
|
|
||||||
var query = dbContext.Set<Entity>()
|
var query = dbContext.Set<Entity>()
|
||||||
|
.AsQueryable()
|
||||||
.Select(x => x.Computed(0));
|
.Select(x => x.Computed(0));
|
||||||
|
|
||||||
Assert.Equal(expectedQueryString, query.ToQueryString());
|
Assert.Equal(expectedQueryString, query.ToQueryString());
|
||||||
});
|
});
|
||||||
|
|
||||||
scenario.Fact("We can pass in variables", () => {
|
scenario.Fact("We can pass in variables", () => {
|
||||||
const string expectedQueryString = "SELECT 0\r\nFROM [Entity] AS [e]";
|
const string expectedQueryString =
|
||||||
|
@"DECLARE @__argument1_0 int = 0;
|
||||||
|
|
||||||
|
SELECT @__argument1_0
|
||||||
|
FROM [Entity] AS [e]";
|
||||||
|
|
||||||
var argument = 0;
|
var argument = 0;
|
||||||
var query = dbContext.Set<Entity>()
|
var query = dbContext.Set<Entity>()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
|
#nullable disable
|
||||||
|
{
|
||||||
|
public static class Foo_C_Foo
|
||||||
|
{
|
||||||
|
public static System.Linq.Expressions.Expression<System.Func<Foo.D, int>> Expression(D d) =>
|
||||||
|
projectionTarget => 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using EntityFrameworkCore.Projections;
|
using EntityFrameworkCore.Projections;
|
||||||
|
using Foo;
|
||||||
|
|
||||||
namespace EntityFrameworkCore.Projections.Generated
|
namespace EntityFrameworkCore.Projections.Generated
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|||||||
@@ -292,6 +292,31 @@ namespace Foo {
|
|||||||
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task ProjectableExtensionMethod()
|
||||||
|
{
|
||||||
|
var compilation = CreateCompilation(@"
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using EntityFrameworkCore.Projections;
|
||||||
|
namespace Foo {
|
||||||
|
class D { }
|
||||||
|
|
||||||
|
static class C {
|
||||||
|
[Projectable]
|
||||||
|
public static int Foo(this D d) => 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
");
|
||||||
|
|
||||||
|
var result = RunGenerator(compilation);
|
||||||
|
|
||||||
|
Assert.Empty(result.Diagnostics);
|
||||||
|
Assert.Single(result.GeneratedTrees);
|
||||||
|
|
||||||
|
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user