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

Working implementation of precompiled queries #29949

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions All.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ The .NET Foundation licenses this file to you under the MIT license.
<s:Boolean x:Key="/Default/UserDictionary/Words/=funcletization/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Includable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=initializers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Inliner/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=keyless/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=materializer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=materializers/@EntryIndexedValue">True</s:Boolean>
Expand All @@ -307,6 +308,7 @@ The .NET Foundation licenses this file to you under the MIT license.&#xD;
<s:Boolean x:Key="/Default/UserDictionary/Words/=niladic/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pluralizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Poolable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=precompilation/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pushdown/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=remapper/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=requiredness/@EntryIndexedValue">True</s:Boolean>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.EntityFrameworkCore.Design.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;

namespace Microsoft.EntityFrameworkCore.Design;
Expand Down Expand Up @@ -60,6 +61,13 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices(
.TryAddSingleton<IScaffoldingTypeMapper, ScaffoldingTypeMapper>()
.TryAddSingleton<MigrationsCodeGeneratorDependencies, MigrationsCodeGeneratorDependencies>()
.TryAddSingleton<ModelCodeGeneratorDependencies, ModelCodeGeneratorDependencies>()

// Query precompilation
.TryAddSingleton<IPrecompiledQueryCodeGenerator, PrecompiledQueryCodeGenerator>()
.TryAddSingleton<IQueryLocator, QueryLocator>()
.TryAddSingleton<ICSharpToLinqTranslator, CSharpToLinqTranslator>()
.TryAddSingleton<ISqlTreeQuoter, SqlTreeQuoter>()

.TryAddScoped<IReverseEngineerScaffolder, ReverseEngineerScaffolder>()
.TryAddScoped<MigrationsScaffolderDependencies, MigrationsScaffolderDependencies>()
.TryAddScoped<IMigrationsScaffolder, MigrationsScaffolder>()
Expand Down
119 changes: 73 additions & 46 deletions src/EFCore.Design/Design/Internal/DbContextOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;

namespace Microsoft.EntityFrameworkCore.Design.Internal;

Expand Down Expand Up @@ -119,52 +120,55 @@ public virtual string ScriptDbContext(string? contextType)
/// </summary>
public virtual void Optimize(string? outputDir, string? modelNamespace, string? contextTypeName)
{
using var context = CreateContext(contextTypeName);
var contextType = context.GetType();

var services = _servicesBuilder.Build(context);
var scaffolder = services.GetRequiredService<ICompiledModelScaffolder>();

if (outputDir == null)
{
var contextSubNamespace = contextType.Namespace ?? "";
if (!string.IsNullOrEmpty(_rootNamespace)
&& contextSubNamespace.StartsWith(_rootNamespace, StringComparison.Ordinal))
{
contextSubNamespace = contextSubNamespace[_rootNamespace.Length..];
}

outputDir = Path.Combine(contextSubNamespace.Replace('.', Path.DirectorySeparatorChar), "CompiledModels");
}

outputDir = Path.GetFullPath(Path.Combine(_projectDir, outputDir));

var finalModelNamespace = modelNamespace ?? GetNamespaceFromOutputPath(outputDir) ?? "";

scaffolder.ScaffoldModel(
context.GetService<IDesignTimeModel>().Model,
outputDir,
new CompiledModelCodeGenerationOptions
{
ContextType = contextType,
ModelNamespace = finalModelNamespace,
Language = _language,
UseNullableReferenceTypes = _nullable
});

var fullName = contextType.ShortDisplayName() + "Model";
if (!string.IsNullOrEmpty(modelNamespace))
{
fullName = modelNamespace + "." + fullName;
}

_reporter.WriteInformation(DesignStrings.CompiledModelGenerated($"options.UseModel({fullName}.Instance)"));

var cacheKeyFactory = context.GetService<IModelCacheKeyFactory>();
if (!(cacheKeyFactory is ModelCacheKeyFactory))
{
_reporter.WriteWarning(DesignStrings.CompiledModelCustomCacheKeyFactory(cacheKeyFactory.GetType().ShortDisplayName()));
}
// TODO: Temporary hack mode, replace precompiled model with precompiled queries
PrecompileQueries(outputDir, contextTypeName);

// using var context = CreateContext(contextTypeName);
// var contextType = context.GetType();
//
// var services = _servicesBuilder.Build(context);
// var scaffolder = services.GetRequiredService<ICompiledModelScaffolder>();
//
// if (outputDir == null)
// {
// var contextSubNamespace = contextType.Namespace ?? "";
// if (!string.IsNullOrEmpty(_rootNamespace)
// && contextSubNamespace.StartsWith(_rootNamespace, StringComparison.Ordinal))
// {
// contextSubNamespace = contextSubNamespace[_rootNamespace.Length..];
// }
//
// outputDir = Path.Combine(contextSubNamespace.Replace('.', Path.DirectorySeparatorChar), "CompiledModels");
// }
//
// outputDir = Path.GetFullPath(Path.Combine(_projectDir, outputDir));
//
// var finalModelNamespace = modelNamespace ?? GetNamespaceFromOutputPath(outputDir) ?? "";
//
// scaffolder.ScaffoldModel(
// context.GetService<IDesignTimeModel>().Model,
// outputDir,
// new CompiledModelCodeGenerationOptions
// {
// ContextType = contextType,
// ModelNamespace = finalModelNamespace,
// Language = _language,
// UseNullableReferenceTypes = _nullable
// });
//
// var fullName = contextType.ShortDisplayName() + "Model";
// if (!string.IsNullOrEmpty(modelNamespace))
// {
// fullName = modelNamespace + "." + fullName;
// }
//
// _reporter.WriteInformation(DesignStrings.CompiledModelGenerated($"options.UseModel({fullName}.Instance)"));
//
// var cacheKeyFactory = context.GetService<IModelCacheKeyFactory>();
// if (!(cacheKeyFactory is ModelCacheKeyFactory))
// {
// _reporter.WriteWarning(DesignStrings.CompiledModelCustomCacheKeyFactory(cacheKeyFactory.GetType().ShortDisplayName()));
// }
}

private string? GetNamespaceFromOutputPath(string directoryPath)
Expand Down Expand Up @@ -197,6 +201,29 @@ public virtual void Optimize(string? outputDir, string? modelNamespace, string?
: null;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void PrecompileQueries(string? outputDir, string? contextTypeName)
{
using var context = CreateContext(contextTypeName);

var services = _servicesBuilder.Build(context);
var generator = services.GetRequiredService<IPrecompiledQueryCodeGenerator>();

outputDir = Path.GetFullPath(Path.Combine(_projectDir, outputDir ?? ""));

// TODO: Need the project csproj. For now hacking an assumption that the csproj is the same as the assembly name.
Copy link
Member Author

Choose a reason for hiding this comment

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

FYI @bricelam, can we get the csproj here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, you'll need to flow it (MSBuildProjectFile) in from dotnet-ef the same way we do projectDir

var csprojName = _assembly.GetName().Name;
var csprojPath = Path.Combine(_projectDir, csprojName + ".csproj");

// TODO: Async, cancellation token?
generator.GeneratePrecompiledQueries(csprojPath, context, outputDir).GetAwaiter().GetResult();
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
36 changes: 36 additions & 0 deletions src/EFCore.Design/Design/OperationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,42 @@ public OptimizeContext(
private void OptimizeContextImpl(string? outputDir, string? modelNamespace, string? contextType)
=> ContextOperations.Optimize(outputDir, modelNamespace, contextType);

/// <summary>
/// Represents an operation to precompile LINQ queries.
/// </summary>
public class PrecompileQueries : OperationBase
{
/// <summary>
/// Initializes a new instance of the <see cref="PrecompileQueries" /> class.
/// </summary>
/// <remarks>
/// <para>The arguments supported by <paramref name="args" /> are:</para>
/// <para><c>outputDir</c>--The directory to put files in. Paths are relative to the project directory.</para>
/// <para><c>modelNamespace</c>--Specify to override the namespace of the generated model.</para>
/// <para><c>contextType</c>--The <see cref="DbContext" /> to use.</para>
/// </remarks>
/// <param name="executor">The operation executor.</param>
/// <param name="resultHandler">The <see cref="IOperationResultHandler" />.</param>
/// <param name="args">The operation arguments.</param>
public PrecompileQueries(
OperationExecutor executor,
IOperationResultHandler resultHandler,
IDictionary args)
: base(resultHandler)
{
Check.NotNull(executor, nameof(executor));
Check.NotNull(args, nameof(args));

var outputDir = (string?)args["outputDir"];
var contextType = (string?)args["contextType"];

Execute(() => executor.PrecompileQueriesImpl(outputDir, contextType));
}
}

private void PrecompileQueriesImpl(string? outputDir, string? contextType)
=> ContextOperations.PrecompileQueries(outputDir, contextType);

/// <summary>
/// Represents an operation to scaffold a <see cref="DbContext" /> and entity types for a database.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/EFCore.Design/EFCore.Design.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelVersion)" />
<PackageReference Include="Microsoft.Extensions.HostFactoryResolver.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsHostFactoryResolverSourcesVersion)" />
<PackageReference Include="Mono.TextTemplating" Version="2.2.1" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.5.5" />
</ItemGroup>

<ItemGroup>
Expand Down
104 changes: 104 additions & 0 deletions src/EFCore.Design/Extensions/Internal/TypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Microsoft.EntityFrameworkCore.Query.Internal;

internal static class TypeExtensions
{
internal static TypeSyntax GetTypeSyntax(this Type type)
{
// TODO: Qualification...
if (type.IsGenericType)
{
return GenericName(
Identifier(type.Name.Substring(0, type.Name.IndexOf('`'))),
TypeArgumentList(SeparatedList(type.GenericTypeArguments.Select(GetTypeSyntax))));
}

if (type == typeof(string))
{
return PredefinedType(Token(SyntaxKind.StringKeyword));
}

if (type == typeof(bool))
{
return PredefinedType(Token(SyntaxKind.BoolKeyword));
}

if (type == typeof(byte))
{
return PredefinedType(Token(SyntaxKind.ByteKeyword));
}

if (type == typeof(sbyte))
{
return PredefinedType(Token(SyntaxKind.SByteKeyword));
}

if (type == typeof(int))
{
return PredefinedType(Token(SyntaxKind.IntKeyword));
}

if (type == typeof(uint))
{
return PredefinedType(Token(SyntaxKind.UIntKeyword));
}

if (type == typeof(short))
{
return PredefinedType(Token(SyntaxKind.ShortKeyword));
}

if (type == typeof(ushort))
{
return PredefinedType(Token(SyntaxKind.UShortKeyword));
}

if (type == typeof(long))
{
return PredefinedType(Token(SyntaxKind.LongKeyword));
}

if (type == typeof(ulong))
{
return PredefinedType(Token(SyntaxKind.ULongKeyword));
}

if (type == typeof(float))
{
return PredefinedType(Token(SyntaxKind.FloatKeyword));
}

if (type == typeof(double))
{
return PredefinedType(Token(SyntaxKind.DoubleKeyword));
}

if (type == typeof(decimal))
{
return PredefinedType(Token(SyntaxKind.DecimalKeyword));
}

if (type == typeof(char))
{
return PredefinedType(Token(SyntaxKind.CharKeyword));
}

if (type == typeof(object))
{
return PredefinedType(Token(SyntaxKind.ObjectKeyword));
}

if (type == typeof(void))
{
return PredefinedType(Token(SyntaxKind.VoidKeyword));
}

return IdentifierName(type.Name);
}
}
Loading