Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Precompiled query inner loop source generator #33350

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/EFCore.Analyzers/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,5 @@
### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------------------
EF1003 | USage | Warning | PrecompiledQuerySourceGenerator
4 changes: 4 additions & 0 deletions src/EFCore.Analyzers/EFCore.Analyzers.csproj
Expand Up @@ -34,6 +34,10 @@
<PackageReference Update="NETStandard.Library" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<None Include="LinqQuerySourceGenerator.props" Pack="true" PackagePath="build" />
</ItemGroup>

<Target Name="SetPackageProperties" BeforeTargets="InitializeStandardNuspecProperties" DependsOnTargets="Build">
<PropertyGroup>
<!-- Make sure we create a symbols.nupkg. -->
Expand Down
31 changes: 31 additions & 0 deletions src/EFCore.Analyzers/Helpers/FakeAnalyzerConfigOptionsProvider.cs
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.EntityFrameworkCore;

public sealed class FakeAnalyzerConfigOptionsProvider(params (string, string)[] globalOptions) : AnalyzerConfigOptionsProvider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 This should be in tests

{
public override AnalyzerConfigOptions GlobalOptions { get; } = new ConfigOptions(globalOptions);

public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
=> GlobalOptions;

public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
=> GlobalOptions;

private sealed class ConfigOptions : AnalyzerConfigOptions
{
private readonly Dictionary<string, string> _globalOptions;

public ConfigOptions((string, string)[] globalOptions)
=> _globalOptions = globalOptions.ToDictionary(t => t.Item1, t => t.Item2);

public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
=> _globalOptions.TryGetValue(key, out value);
}
}

37 changes: 37 additions & 0 deletions src/EFCore.Analyzers/Helpers/KnownTypeSymbols.cs
@@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis;

// ReSharper disable InconsistentNaming

namespace Microsoft.EntityFrameworkCore;

internal sealed class KnownTypeSymbols(Compilation compilation)
{
public INamedTypeSymbol? IEnumerableOfTType => GetOrResolveType(typeof(IEnumerable<>), ref _IEnumerableOfTType);
private Option<INamedTypeSymbol?> _IEnumerableOfTType;

private INamedTypeSymbol? GetOrResolveType(Type type, ref Option<INamedTypeSymbol?> field)
=> GetOrResolveType(type.FullName!, ref field);

private INamedTypeSymbol? GetOrResolveType(string fullyQualifiedName, ref Option<INamedTypeSymbol?> field)
{
if (field.HasValue)
{
return field.Value;
}

// TODO: What to do if the type is not found
var type = compilation.GetTypeByMetadataName(fullyQualifiedName)
?? throw new InvalidOperationException("Could not find type symbol for: " + fullyQualifiedName);
field = new(type);
return type;
}

private readonly struct Option<T>(T value)
{
public readonly bool HasValue = true;
public readonly T Value = value;
}
}
17 changes: 17 additions & 0 deletions src/EFCore.Analyzers/Helpers/NullableAttributes.cs
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Diagnostics.CodeAnalysis;

[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;

/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
528 changes: 528 additions & 0 deletions src/EFCore.Analyzers/LinqQuerySourceGenerator.cs

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/EFCore.Analyzers/LinqQuerySourceGenerator.props
@@ -0,0 +1,5 @@
<Project>
<ItemGroup>
<CompilerVisibleProperty Include="EFNukeDynamic" />
</ItemGroup>
</Project>
18 changes: 18 additions & 0 deletions src/EFCore.Analyzers/Properties/AnalyzerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/EFCore.Analyzers/Properties/AnalyzerStrings.resx
Expand Up @@ -36,4 +36,10 @@
<data name="InterpolatedStringUsageInRawQueriesMessageFormat" xml:space="preserve">
<value>Method '{0}' inserts interpolated strings directly into the SQL, without any protection against SQL injection. Consider using '{1}' instead, which protects against SQL injection, or make sure that the value is sanitized and suppress the warning.</value>
</data>
<data name="DynamicQueryTitle" xml:space="preserve">
<value>Unsupported dynamic EF Core query.</value>
</data>
<data name="DynamicQueryMessageFormat" xml:space="preserve">
<value>This call to '{0}' represents a dynamic queryable LINQ query which cannot be precompiled by EF.</value>
</data>
</root>
2 changes: 1 addition & 1 deletion src/EFCore.Cosmos/EFCore.Cosmos.csproj
Expand Up @@ -41,7 +41,7 @@

<ItemGroup>
<ProjectReference Include="..\EFCore\EFCore.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.InMemory/EFCore.InMemory.csproj
Expand Up @@ -40,7 +40,7 @@

<ItemGroup>
<ProjectReference Include="..\EFCore\EFCore.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.Proxies/EFCore.Proxies.csproj
Expand Up @@ -44,7 +44,7 @@

<ItemGroup>
<ProjectReference Include="..\EFCore\EFCore.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion src/EFCore.Relational/EFCore.Relational.csproj
Expand Up @@ -9,6 +9,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ImplicitUsings>true</ImplicitUsings>
<NoWarn>$(NoWarn);EF1003</NoWarn> <!-- Precomiled query is experimental -->
<NoWarn>$(NoWarn);CS1591</NoWarn> <!-- ignore missing docs -->
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -44,7 +45,7 @@

<ItemGroup>
<ProjectReference Include="..\EFCore\EFCore.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
Expand Up @@ -43,7 +43,7 @@
<ItemGroup>
<ProjectReference Include="..\EFCore.SqlServer\EFCore.SqlServer.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Include="..\EFCore.SqlServer.Abstractions\EFCore.SqlServer.Abstractions.csproj" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.SqlServer.NTS/EFCore.SqlServer.NTS.csproj
Expand Up @@ -52,7 +52,7 @@

<ItemGroup>
<ProjectReference Include="..\EFCore.SqlServer\EFCore.SqlServer.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.SqlServer/EFCore.SqlServer.csproj
Expand Up @@ -45,7 +45,7 @@

<ItemGroup>
<ProjectReference Include="..\EFCore.Relational\EFCore.Relational.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.Sqlite.Core/EFCore.Sqlite.Core.csproj
Expand Up @@ -47,7 +47,7 @@
<ItemGroup>
<ProjectReference Include="..\EFCore.Relational\EFCore.Relational.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Include="..\Microsoft.Data.Sqlite.Core\Microsoft.Data.Sqlite.Core.csproj" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.Sqlite.NTS/EFCore.Sqlite.NTS.csproj
Expand Up @@ -53,7 +53,7 @@

<ItemGroup>
<ProjectReference Include="..\EFCore.Sqlite.Core\EFCore.Sqlite.Core.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.Sqlite/EFCore.Sqlite.csproj
Expand Up @@ -43,7 +43,7 @@

<ItemGroup>
<ProjectReference Include="..\EFCore.Sqlite.Core\EFCore.Sqlite.Core.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" ReferenceOutputAssembly="False" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion src/EFCore/EFCore.csproj
Expand Up @@ -13,6 +13,7 @@ Microsoft.EntityFrameworkCore.DbSet
<RootNamespace>Microsoft.EntityFrameworkCore</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ImplicitUsings>true</ImplicitUsings>
<NoWarn>$(NoWarn);CS1591</NoWarn> <!-- ignore missing docs -->
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -49,7 +50,7 @@ Microsoft.EntityFrameworkCore.DbSet

<ItemGroup>
<ProjectReference Include="..\EFCore.Abstractions\EFCore.Abstractions.csproj" />
<ProjectReference Condition="'$(BuildingByReSharper)' != 'true'" Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" PrivateAssets="contentfiles;build" />
<ProjectReference Include="..\EFCore.Analyzers\EFCore.Analyzers.csproj" PrivateAssets="contentfiles;build" />
</ItemGroup>

<ItemGroup>
Expand Down
7 changes: 7 additions & 0 deletions src/EFCore/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore/Properties/CoreStrings.resx
Expand Up @@ -1480,6 +1480,9 @@
<data name="RuntimeParameterMissingParameter" xml:space="preserve">
<value>While registering a runtime parameter, the lambda expression must have only one parameter which must be same as 'QueryCompilationContext.QueryContextParameter' expression.</value>
</data>
<data name="RuntimeQueryCompilationDisabled" xml:space="preserve">
<value>This LINQ query was not precompiled, likely because it is dynamic, and runtime query compilation has been disabled.</value>
</data>
<data name="SameParameterInstanceUsedInMultipleLambdas" xml:space="preserve">
<value>The same parameter instance with name '{parameterName}' was used in multiple lambdas in the query tree. Each lambda must have its own parameter instances.</value>
</data>
Expand Down
17 changes: 17 additions & 0 deletions src/EFCore/Query/Internal/PrecompiledQuerySafeMarker.cs
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Query.Internal;

public class PrecompiledQuerySafeMarker : Expression
{
internal static readonly MethodInfo ComposeMethodInfo
= typeof(PrecompiledQuerySafeMarker).GetTypeInfo().GetDeclaredMethod(nameof(Compose))!;

public static IQueryable<TSource> Compose<TSource>(IQueryable<TSource> source)
=> source.Provider.CreateQuery<TSource>(
Call(
instance: null,
method: new Func<IQueryable<TSource>, IQueryable<TSource>>(Compose).Method,
arguments: [source.Expression]));
}
13 changes: 13 additions & 0 deletions src/EFCore/Query/QueryTranslationPreprocessor.cs
Expand Up @@ -50,6 +50,7 @@ public class QueryTranslationPreprocessor
/// <returns>A query expression after transformations.</returns>
public virtual Expression Process(Expression query)
{
query = CheckPrecompiledQuerySafeExpression(query);
query = new InvocationExpressionRemovingExpressionVisitor().Visit(query);
query = NormalizeQueryableMethod(query);
query = new CallForwardingExpressionVisitor().Visit(query);
Expand All @@ -67,6 +68,18 @@ public virtual Expression Process(Expression query)
return query;
}

private Expression CheckPrecompiledQuerySafeExpression(Expression query)
{
if (query is MethodCallExpression { Method.IsGenericMethod: true } methodCall
&& methodCall.Method.GetGenericMethodDefinition() == PrecompiledQuerySafeMarker.ComposeMethodInfo)
{
return methodCall.Arguments[0];
}

// TODO: Check feature switch for whether we should allow only safe queries
throw new InvalidOperationException(CoreStrings.RuntimeQueryCompilationDisabled);
}

/// <summary>
/// Normalizes queryable methods in the query.
/// </summary>
Expand Down
4 changes: 3 additions & 1 deletion test/EFCore.Analyzers.Tests/EFCore.Analyzers.Tests.csproj
Expand Up @@ -38,8 +38,10 @@
<ItemGroup>
<ProjectReference Include="..\..\src\EFCore.Analyzers\EFCore.Analyzers.csproj" />
<ProjectReference Include="..\..\src\EFCore.Relational\EFCore.Relational.csproj" />
<ProjectReference Include="..\..\src\EFCore.InMemory\EFCore.InMemory.csproj" />
<ProjectReference Include="..\EFCore.Tests\EFCore.Tests.csproj" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelVersion)" />
</ItemGroup>

Expand Down