Skip to content

Commit

Permalink
Precompiled query inner loop source generator
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Mar 18, 2024
1 parent 88617cc commit abc8daf
Show file tree
Hide file tree
Showing 17 changed files with 1,077 additions and 1 deletion.
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
{
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>
1 change: 1 addition & 0 deletions 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
1 change: 1 addition & 0 deletions 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
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

0 comments on commit abc8daf

Please sign in to comment.