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>
|
||||
<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>
|
||||
|
||||
@@ -4,9 +4,11 @@ using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
@@ -26,6 +28,14 @@ namespace BasicSample
|
||||
|
||||
[Projectable]
|
||||
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
|
||||
@@ -85,7 +95,8 @@ namespace BasicSample
|
||||
.AddDbContext<ApplicationDbContext>(options => {
|
||||
options
|
||||
.UseSqlite(dbConnection)
|
||||
.UseProjections();
|
||||
.UseProjections()
|
||||
.UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()));
|
||||
})
|
||||
.BuildServiceProvider();
|
||||
|
||||
@@ -119,6 +130,8 @@ namespace BasicSample
|
||||
dbContext.Users.Add(user);
|
||||
dbContext.SaveChanges();
|
||||
|
||||
// What did our user spent in total
|
||||
{
|
||||
var query = dbContext.Users
|
||||
.Select(x => new {
|
||||
Name = x.FullName,
|
||||
@@ -127,9 +140,33 @@ namespace BasicSample
|
||||
|
||||
var result = query.FirstOrDefault();
|
||||
|
||||
Console.WriteLine($"Our user {result.Name} spent {result.TotalSpent}");
|
||||
Console.WriteLine($"We used the following query:");
|
||||
Console.WriteLine(query.ToQueryString());
|
||||
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)}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generator
|
||||
{
|
||||
|
||||
public class ExpressionSyntaxRewriter : CSharpSyntaxRewriter
|
||||
{
|
||||
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 string TargetClassNamespace { get; set; }
|
||||
|
||||
public IEnumerable<string> TargetNestedInClassNames { get; set; }
|
||||
|
||||
public string ClassName { 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.Syntax;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generator
|
||||
{
|
||||
public static class ProjectableInterpreter
|
||||
{
|
||||
static IEnumerable<string> GetNestedInClassPath(INamedTypeSymbol namedTypeSymbol)
|
||||
static IEnumerable<string> GetNestedInClassPath(ITypeSymbol namedTypeSymbol)
|
||||
{
|
||||
if (namedTypeSymbol.ContainingType is not null)
|
||||
{
|
||||
@@ -34,6 +31,7 @@ namespace EntityFrameworkCore.Projections.Generator
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
var projectableAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("EntityFrameworkCore.Projections.ProjectableAttribute");
|
||||
|
||||
var projectableAttributeClass = memberSymbol.GetAttributes()
|
||||
@@ -46,20 +44,32 @@ namespace EntityFrameworkCore.Projections.Generator
|
||||
}
|
||||
|
||||
var expressionSyntaxRewriter = new ExpressionSyntaxRewriter(memberSymbol.ContainingType, semanticModel);
|
||||
var parameterSyntaxRewriter = new ParameterSyntaxRewriter(semanticModel);
|
||||
|
||||
var descriptor = new ProjectableDescriptor
|
||||
{
|
||||
var descriptor = new ProjectableDescriptor {
|
||||
ClassName = memberSymbol.ContainingType.Name,
|
||||
ClassNamespace = memberSymbol.ContainingType.ContainingNamespace.IsGlobalNamespace ? null : memberSymbol.ContainingType.ContainingNamespace.ToDisplayString(),
|
||||
MemberName = memberSymbol.Name,
|
||||
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)
|
||||
{
|
||||
descriptor.ReturnTypeName = methodDeclarationSyntax.ReturnType.ToString();
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -38,6 +38,16 @@ namespace EntityFrameworkCore.Projections.Generator
|
||||
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);
|
||||
|
||||
resultBuilder.Append($@"
|
||||
@@ -46,7 +56,7 @@ namespace EntityFrameworkCore.Projections.Generated
|
||||
{{
|
||||
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};
|
||||
}}
|
||||
}}");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using Microsoft.EntityFrameworkCore.Query.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -18,25 +19,24 @@ namespace EntityFrameworkCore.Projections.Infrastructure.Internal
|
||||
|
||||
public DbContextOptionsExtensionInfo Info { get; }
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Needed")]
|
||||
public void ApplyServices(IServiceCollection services)
|
||||
{
|
||||
var existingPreprocessorFactoryRegistration = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryTranslationPreprocessorFactory));
|
||||
|
||||
if (existingPreprocessorFactoryRegistration?.ImplementationType is null)
|
||||
var queryCompilerRegistration = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryCompiler));
|
||||
if (queryCompilerRegistration?.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
|
||||
services.Add(new ServiceDescriptor(existingPreprocessorFactoryRegistration.ImplementationType, existingPreprocessorFactoryRegistration.ImplementationType, existingPreprocessorFactoryRegistration.Lifetime));
|
||||
services.Remove(existingPreprocessorFactoryRegistration);
|
||||
// Ensure that we can still resolve this queryCompiler
|
||||
services.Add(new ServiceDescriptor(queryCompilerRegistration.ImplementationType, queryCompilerRegistration.ImplementationType, queryCompilerRegistration.Lifetime));
|
||||
services.Remove(queryCompilerRegistration);
|
||||
|
||||
services.Add(new ServiceDescriptor(
|
||||
typeof(IQueryTranslationPreprocessorFactory),
|
||||
serviceProvider => new WrappedQueryTranslationPreprocessorFactory((IQueryTranslationPreprocessorFactory)serviceProvider.GetRequiredService(existingPreprocessorFactoryRegistration.ImplementationType), serviceProvider.GetRequiredService<QueryTranslationPreprocessorDependencies>()),
|
||||
existingPreprocessorFactoryRegistration.Lifetime
|
||||
typeof(IQueryCompiler),
|
||||
serviceProvider => new WrappedQueryCompiler((IQueryCompiler)serviceProvider.GetRequiredService(queryCompilerRegistration.ImplementationType)),
|
||||
queryCompilerRegistration.Lifetime
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
var test1 = argumentExpressions.Cast<ParameterExpression>()!;
|
||||
var parameterExtractor = new ParameterExtractor();
|
||||
foreach (var argument in argumentExpressions)
|
||||
{
|
||||
parameterExtractor.Visit(argument);
|
||||
}
|
||||
|
||||
var expressionFactoryConstructionMethod =
|
||||
Expression.Lambda<Func<LambdaExpression>>(
|
||||
var lambda = Expression.Lambda<Func<LambdaExpression>>(
|
||||
Expression.Call(
|
||||
expressionFactoryMethod,
|
||||
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.Threading.Tasks;
|
||||
using EntityFrameworkCore.Projections.FunctionalTests.Helpers;
|
||||
using EntityFrameworkCore.Projections.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ScenarioTests;
|
||||
using Xunit;
|
||||
@@ -28,25 +29,39 @@ namespace EntityFrameworkCore.Projections.FunctionalTests
|
||||
using var dbContext = new SampleDbContext<Entity>();
|
||||
|
||||
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);
|
||||
|
||||
Assert.Equal(expectedQueryString, query.ToQueryString());
|
||||
});
|
||||
|
||||
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>()
|
||||
.AsQueryable()
|
||||
.Select(x => x.Computed(0));
|
||||
|
||||
Assert.Equal(expectedQueryString, query.ToQueryString());
|
||||
});
|
||||
|
||||
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 query = dbContext.Set<Entity>()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#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.Linq;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using EntityFrameworkCore.Projections;
|
||||
using Foo;
|
||||
|
||||
namespace EntityFrameworkCore.Projections.Generated
|
||||
#nullable disable
|
||||
|
||||
@@ -292,6 +292,31 @@ namespace Foo {
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user