Skip to content

Commit

Permalink
Add precompiled query generation to the dbcontext optimize command
Browse files Browse the repository at this point in the history
Part of #33103
  • Loading branch information
AndriySvyryd committed Jun 6, 2024
1 parent 27ea548 commit 7d016cd
Show file tree
Hide file tree
Showing 35 changed files with 491 additions and 101 deletions.
3 changes: 1 addition & 2 deletions src/EFCore.Design/Design/DbContextActivator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ public static DbContext CreateInstance(
{
Check.NotNull(contextType, nameof(contextType));

EF.IsDesignTime = true;

return new DbContextOperations(
new OperationReporter(reportHandler),
contextType.Assembly,
startupAssembly ?? contextType.Assembly,
project: "",
projectDir: "",
rootNamespace: null,
language: "C#",
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.Design;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;

Expand Down Expand Up @@ -54,6 +55,8 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices(
.TryAddSingleton<ICompiledModelCodeGenerator, CSharpRuntimeModelCodeGenerator>()
.TryAddSingleton<ICompiledModelCodeGeneratorSelector, CompiledModelCodeGeneratorSelector>()
.TryAddSingleton<ICompiledModelScaffolder, CompiledModelScaffolder>()
.TryAddSingleton<IPrecompiledQueryCodeGenerator, PrecompiledQueryCodeGenerator>()
.TryAddSingleton<IPrecompiledQueryCodeGeneratorSelector, PrecompiledQueryCodeGeneratorSelector>()
.TryAddSingleton<IDesignTimeConnectionStringResolver>(
new DesignTimeConnectionStringResolver(applicationServiceProviderAccessor))
.TryAddSingleton<IPluralizer, HumanizerPluralizer>()
Expand Down
132 changes: 129 additions & 3 deletions src/EFCore.Design/Design/Internal/DbContextOperations.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using System.Text;
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Design;
using Microsoft.EntityFrameworkCore.Query.Internal;

namespace Microsoft.EntityFrameworkCore.Design.Internal;

Expand All @@ -17,6 +28,7 @@ public class DbContextOperations
private readonly IOperationReporter _reporter;
private readonly Assembly _assembly;
private readonly Assembly _startupAssembly;
private readonly string _project;
private readonly string _projectDir;
private readonly string? _rootNamespace;
private readonly string? _language;
Expand All @@ -35,6 +47,7 @@ public DbContextOperations(
IOperationReporter reporter,
Assembly assembly,
Assembly startupAssembly,
string project,
string projectDir,
string? rootNamespace,
string? language,
Expand All @@ -44,6 +57,7 @@ public DbContextOperations(
reporter,
assembly,
startupAssembly,
project,
projectDir,
rootNamespace,
language,
Expand All @@ -63,6 +77,7 @@ protected DbContextOperations(
IOperationReporter reporter,
Assembly assembly,
Assembly startupAssembly,
string project,
string projectDir,
string? rootNamespace,
string? language,
Expand All @@ -73,6 +88,7 @@ protected DbContextOperations(
_reporter = reporter;
_assembly = assembly;
_startupAssembly = startupAssembly;
_project = project;
_projectDir = projectDir;
_rootNamespace = rootNamespace;
_language = language;
Expand Down Expand Up @@ -117,13 +133,44 @@ public virtual string ScriptDbContext(string? contextType)
/// 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 IReadOnlyList<string> Optimize(string? outputDir, string? modelNamespace, string? contextTypeName, string? suffix)
public virtual IReadOnlyList<string> Optimize(
string? outputDir, string? modelNamespace, string? contextTypeName, string? suffix, bool scaffoldModel, bool precompileQueries)
{
using var context = CreateContext(contextTypeName);
var contextType = context.GetType();

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

IReadOnlyList<string> generatedFiles = [];
IReadOnlyDictionary<MemberInfo, QualifiedName>? memberAccessReplacements = null;

if (scaffoldModel)
{
generatedFiles = ScaffoldCompiledModel(outputDir, modelNamespace, context, suffix, services);
if (precompileQueries)
{
memberAccessReplacements = ((IRuntimeModel)context.GetService<IDesignTimeModel>().Model).GetUnsafeAccessors();
}
}

if (precompileQueries)
{
generatedFiles = generatedFiles.Concat(PrecompileQueries(
outputDir, context, suffix, services, memberAccessReplacements ?? ((IRuntimeModel)context.Model).GetUnsafeAccessors()))
.ToList();
}

return generatedFiles;
}

private IReadOnlyList<string> ScaffoldCompiledModel(
string? outputDir, string? modelNamespace, DbContext context, string? suffix, IServiceProvider services)
{
var contextType = context.GetType();
if (contextType.Assembly != _assembly)
{
_reporter.WriteWarning(DesignStrings.ContextAssemblyMismatch(
_assembly.GetName().Name, contextType.ShortDisplayName(), contextType.Assembly.GetName().Name));
}

if (outputDir == null)
{
Expand All @@ -139,6 +186,8 @@ public virtual IReadOnlyList<string> Optimize(string? outputDir, string? modelNa

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

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

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

var scaffoldedFiles = scaffolder.ScaffoldModel(
Expand Down Expand Up @@ -170,6 +219,82 @@ public virtual IReadOnlyList<string> Optimize(string? outputDir, string? modelNa
return scaffoldedFiles;
}

private IReadOnlyList<string> PrecompileQueries(string? outputDir, DbContext context, string? suffix, IServiceProvider services, IReadOnlyDictionary<MemberInfo, QualifiedName>? memberAccessReplacements)
{
outputDir = Path.GetFullPath(Path.Combine(_projectDir, outputDir ?? "Generated"));

MSBuildLocator.RegisterDefaults();
// TODO: pass through properties
var workspace = MSBuildWorkspace.Create();
workspace.LoadMetadataForReferencedProjects = true;
var project = workspace.OpenProjectAsync(_project).GetAwaiter().GetResult();
if (!project.SupportsCompilation)
{
throw new NotSupportedException(DesignStrings.UncompilableProject(_project));
}
var compilation = project.GetCompilationAsync().GetAwaiter().GetResult()!;
var errorDiagnostics = compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
if (errorDiagnostics.Any())
{
var errorBuilder = new StringBuilder();
errorBuilder.AppendLine(DesignStrings.CompilationErrors);
foreach (var diagnostic in errorDiagnostics)
{
errorBuilder.AppendLine(diagnostic.ToString());
}

throw new InvalidOperationException(errorBuilder.ToString());
}

var syntaxGenerator = SyntaxGenerator.GetGenerator(
workspace, _language == "VB" ? LanguageNames.VisualBasic : _language ?? LanguageNames.CSharp);

var precompiledQueryCodeGenerator = services.GetRequiredService<IPrecompiledQueryCodeGeneratorSelector>().Select(_language);

var precompilationErrors = new List<PrecompiledQueryCodeGenerator.QueryPrecompilationError>();
var generatedFiles = precompiledQueryCodeGenerator.GeneratePrecompiledQueries(
compilation, syntaxGenerator, context, memberAccessReplacements, precompilationErrors, assembly: _assembly, suffix);

if (precompilationErrors.Count > 0)
{
var errorBuilder = new StringBuilder();
errorBuilder.AppendLine(DesignStrings.QueryPrecompilationErrors);
foreach (var error in precompilationErrors)
{
errorBuilder.AppendLine(error.ToString());
}

throw new InvalidOperationException(errorBuilder.ToString());
}

Directory.CreateDirectory(outputDir);
var writtenFiles = new List<string>();
foreach (var generatedFile in generatedFiles)
{
var finalText = FormatCode(project, generatedFile).GetAwaiter().GetResult();
var outputFilePath = Path.Combine(outputDir, generatedFile.Path);
File.WriteAllText(outputFilePath, finalText.ToString());
writtenFiles.Add(outputFilePath);
}

return writtenFiles;

static async Task<object> FormatCode(Project project, PrecompiledQueryCodeGenerator.GeneratedInterceptorFile generatedFile)
{
var document = project.AddDocument("_EfGeneratedInterceptors.cs", generatedFile.Code);

// Run the simplifier to e.g. get rid of unneeded parentheses
var syntaxRootFoo = (await document.GetSyntaxRootAsync().ConfigureAwait(false))!;
var annotatedDocument = document.WithSyntaxRoot(syntaxRootFoo.WithAdditionalAnnotations(Simplifier.Annotation));
document = await Simplifier.ReduceAsync(annotatedDocument, optionSet: null).ConfigureAwait(false);
document = await Formatter.FormatAsync(document, options: null).ConfigureAwait(false);

var finalSyntaxTree = (await document.GetSyntaxTreeAsync().ConfigureAwait(false))!;
var finalText = await finalSyntaxTree.GetTextAsync().ConfigureAwait(false);
return finalText;
}
}

private string? GetNamespaceFromOutputPath(string directoryPath)
{
var subNamespace = SubnamespaceFromOutputPath(_projectDir, directoryPath);
Expand Down Expand Up @@ -208,6 +333,7 @@ public virtual IReadOnlyList<string> Optimize(string? outputDir, string? modelNa
/// </summary>
public virtual DbContext CreateContext(string? contextType)
{
EF.IsDesignTime = true;
var contextPair = FindContextType(contextType);
var factory = contextPair.Value;
try
Expand Down
1 change: 1 addition & 0 deletions src/EFCore.Design/Design/Internal/MigrationsOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public MigrationsOperations(
reporter,
assembly,
startupAssembly,
project: "",
projectDir,
rootNamespace,
language,
Expand Down
23 changes: 16 additions & 7 deletions src/EFCore.Design/Design/OperationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Design;
/// </remarks>
public class OperationExecutor : MarshalByRefObject
{
private readonly string _project;
private readonly string _projectDir;
private readonly string _targetAssemblyName;
private readonly string _startupTargetAssemblyName;
Expand All @@ -38,6 +39,7 @@ public class OperationExecutor : MarshalByRefObject
/// <para>The arguments supported by <paramref name="args" /> are:</para>
/// <para><c>targetName</c>--The assembly name of the target project.</para>
/// <para><c>startupTargetName</c>--The assembly name of the startup project.</para>
/// <para><c>project</c>--The target project.</para>
/// <para><c>projectDir</c>--The target project's root directory.</para>
/// <para><c>rootNamespace</c>--The target project's root namespace.</para>
/// <para><c>language</c>--The programming language to be used to generate classes.</para>
Expand All @@ -54,6 +56,7 @@ public OperationExecutor(IOperationReportHandler reportHandler, IDictionary args
_reporter = new OperationReporter(reportHandler);
_targetAssemblyName = (string)args["targetName"]!;
_startupTargetAssemblyName = (string)args["startupTargetName"]!;
_project = (string)args["project"]!;
_projectDir = (string)args["projectDir"]!;
_rootNamespace = (string?)args["rootNamespace"];
_language = (string?)args["language"];
Expand Down Expand Up @@ -121,6 +124,7 @@ public virtual DbContextOperations ContextOperations
_reporter,
Assembly,
StartupAssembly,
_project,
_projectDir,
_rootNamespace,
_language,
Expand Down Expand Up @@ -503,7 +507,7 @@ private IEnumerable<IDictionary> GetMigrationsImpl(
}

/// <summary>
/// Represents an operation to generate a compiled model from the DbContext.
/// Represents an operation to generate optimized code for a DbContext.
/// </summary>
public class OptimizeContext : OperationBase
{
Expand All @@ -513,8 +517,11 @@ public class OptimizeContext : OperationBase
/// <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>
/// <para><c>modelNamespace</c>--The namespace of the generated model.</para>
/// <para><c>contextType</c>--The <see cref="DbContext" /> type to use.</para>
/// <para><c>suffix</c>--The suffix to add to all the generated files.</para>
/// <para><c>scaffoldModel</c>--Whether to generate a compiled model from the DbContext.</para>
/// <para><c>precompileQueries</c>--Whether to generate code for precompiled queries.</para>
/// </remarks>
/// <param name="executor">The operation executor.</param>
/// <param name="resultHandler">The <see cref="IOperationResultHandler" />.</param>
Expand All @@ -532,13 +539,15 @@ public OptimizeContext(
var modelNamespace = (string?)args["modelNamespace"];
var contextType = (string?)args["contextType"];
var suffix = (string?)args["suffix"];
var scaffoldModel = (bool)(args["scaffoldModel"] ?? true);
var precompileQueries = (bool)(args["precompileQueries"] ?? false);

Execute(() => executor.OptimizeContextImpl(outputDir, modelNamespace, contextType, suffix));
Execute(() => executor.OptimizeContextImpl(outputDir, modelNamespace, contextType, suffix, scaffoldModel, precompileQueries));
}
}

private IReadOnlyList<string> OptimizeContextImpl(string? outputDir, string? modelNamespace, string? contextType, string? suffix)
=> ContextOperations.Optimize(outputDir, modelNamespace, contextType, suffix);
private IReadOnlyList<string> OptimizeContextImpl(
string? outputDir, string? modelNamespace, string? contextType, string? suffix, bool scaffoldModel, bool precompileQueries)
=> ContextOperations.Optimize(outputDir, modelNamespace, contextType, suffix, scaffoldModel, precompileQueries);

/// <summary>
/// Represents an operation to scaffold a <see cref="DbContext" /> and entity types for a database.
Expand Down
35 changes: 35 additions & 0 deletions src/EFCore.Design/Properties/DesignStrings.Designer.cs

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

Loading

0 comments on commit 7d016cd

Please sign in to comment.