Skip to content

Commit 4b80cb9

Browse files
authored
Add precompiled query generation to the dbcontext optimize command (#33747)
Auto-generate compiled models for all contexts in the given assembly unless a single one is specified Part of #33103 Fixes #33558
1 parent 27ea548 commit 4b80cb9

File tree

48 files changed

+928
-257
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+928
-257
lines changed

src/EFCore.Design/Design/DbContextActivator.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,11 @@ public static DbContext CreateInstance(
4343
{
4444
Check.NotNull(contextType, nameof(contextType));
4545

46-
EF.IsDesignTime = true;
47-
4846
return new DbContextOperations(
4947
new OperationReporter(reportHandler),
5048
contextType.Assembly,
5149
startupAssembly ?? contextType.Assembly,
50+
project: "",
5251
projectDir: "",
5352
rootNamespace: null,
5453
language: "C#",

src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.EntityFrameworkCore.Design.Internal;
55
using Microsoft.EntityFrameworkCore.Migrations.Internal;
6+
using Microsoft.EntityFrameworkCore.Query.Design;
67
using Microsoft.EntityFrameworkCore.Query.Internal;
78
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
89

@@ -54,6 +55,8 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices(
5455
.TryAddSingleton<ICompiledModelCodeGenerator, CSharpRuntimeModelCodeGenerator>()
5556
.TryAddSingleton<ICompiledModelCodeGeneratorSelector, CompiledModelCodeGeneratorSelector>()
5657
.TryAddSingleton<ICompiledModelScaffolder, CompiledModelScaffolder>()
58+
.TryAddSingleton<IPrecompiledQueryCodeGenerator, PrecompiledQueryCodeGenerator>()
59+
.TryAddSingleton<IPrecompiledQueryCodeGeneratorSelector, PrecompiledQueryCodeGeneratorSelector>()
5760
.TryAddSingleton<IDesignTimeConnectionStringResolver>(
5861
new DesignTimeConnectionStringResolver(applicationServiceProviderAccessor))
5962
.TryAddSingleton<IPluralizer, HumanizerPluralizer>()

src/EFCore.Design/Design/Internal/DbContextOperations.cs

Lines changed: 200 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.IO;
5+
using System.Text;
6+
using Microsoft.Build.Locator;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.Editing;
9+
using Microsoft.CodeAnalysis.Formatting;
10+
using Microsoft.CodeAnalysis.MSBuild;
11+
using Microsoft.CodeAnalysis.Simplification;
412
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
513
using Microsoft.EntityFrameworkCore.Internal;
14+
using Microsoft.EntityFrameworkCore.Metadata.Internal;
15+
using Microsoft.EntityFrameworkCore.Query.Design;
16+
using Microsoft.EntityFrameworkCore.Query.Internal;
17+
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
618

719
namespace Microsoft.EntityFrameworkCore.Design.Internal;
820

@@ -17,6 +29,7 @@ public class DbContextOperations
1729
private readonly IOperationReporter _reporter;
1830
private readonly Assembly _assembly;
1931
private readonly Assembly _startupAssembly;
32+
private readonly string _project;
2033
private readonly string _projectDir;
2134
private readonly string? _rootNamespace;
2235
private readonly string? _language;
@@ -35,6 +48,7 @@ public DbContextOperations(
3548
IOperationReporter reporter,
3649
Assembly assembly,
3750
Assembly startupAssembly,
51+
string project,
3852
string projectDir,
3953
string? rootNamespace,
4054
string? language,
@@ -44,6 +58,7 @@ public DbContextOperations(
4458
reporter,
4559
assembly,
4660
startupAssembly,
61+
project,
4762
projectDir,
4863
rootNamespace,
4964
language,
@@ -63,6 +78,7 @@ protected DbContextOperations(
6378
IOperationReporter reporter,
6479
Assembly assembly,
6580
Assembly startupAssembly,
81+
string project,
6682
string projectDir,
6783
string? rootNamespace,
6884
string? language,
@@ -73,6 +89,7 @@ protected DbContextOperations(
7389
_reporter = reporter;
7490
_assembly = assembly;
7591
_startupAssembly = startupAssembly;
92+
_project = project;
7693
_projectDir = projectDir;
7794
_rootNamespace = rootNamespace;
7895
_language = language;
@@ -117,13 +134,88 @@ public virtual string ScriptDbContext(string? contextType)
117134
/// any release. You should only use it directly in your code with extreme caution and knowing that
118135
/// doing so can result in application failures when updating to a new Entity Framework Core release.
119136
/// </summary>
120-
public virtual IReadOnlyList<string> Optimize(string? outputDir, string? modelNamespace, string? contextTypeName, string? suffix)
137+
public virtual IReadOnlyList<string> Optimize(
138+
string? outputDir, string? modelNamespace, string? contextTypeName, string? suffix, bool scaffoldModel, bool precompileQueries)
121139
{
122-
using var context = CreateContext(contextTypeName);
123-
var contextType = context.GetType();
140+
var optimizeAllInAssembly = contextTypeName == "*";
141+
var contexts = optimizeAllInAssembly ? CreateAllContexts() : [CreateContext(contextTypeName)];
142+
143+
MSBuildLocator.RegisterDefaults();
144+
145+
List<string> generatedFiles = [];
146+
HashSet<string> generatedFileNames = [];
147+
foreach (var context in contexts)
148+
{
149+
using (context)
150+
{
151+
Optimize(
152+
outputDir,
153+
modelNamespace,
154+
suffix,
155+
scaffoldModel,
156+
precompileQueries,
157+
context,
158+
optimizeAllInAssembly,
159+
generatedFiles,
160+
generatedFileNames);
161+
}
162+
}
163+
164+
return generatedFiles;
165+
}
124166

167+
private void Optimize(
168+
string? outputDir,
169+
string? modelNamespace,
170+
string? suffix,
171+
bool scaffoldModel,
172+
bool precompileQueries,
173+
DbContext context,
174+
bool optimizeAllInAssembly,
175+
List<string> generatedFiles,
176+
HashSet<string> generatedFileNames)
177+
{
178+
var contextType = context.GetType();
125179
var services = _servicesBuilder.Build(context);
126-
var scaffolder = services.GetRequiredService<ICompiledModelScaffolder>();
180+
181+
IReadOnlyDictionary<MemberInfo, QualifiedName>? memberAccessReplacements = null;
182+
183+
if (scaffoldModel
184+
&& (!optimizeAllInAssembly || contextType.Assembly == _assembly))
185+
{
186+
generatedFiles.AddRange(ScaffoldCompiledModel(outputDir, modelNamespace, context, suffix, services, generatedFileNames));
187+
if (precompileQueries)
188+
{
189+
memberAccessReplacements = ((IRuntimeModel)context.GetService<IDesignTimeModel>().Model).GetUnsafeAccessors();
190+
}
191+
}
192+
193+
if (precompileQueries)
194+
{
195+
generatedFiles.AddRange(PrecompileQueries(
196+
outputDir,
197+
context,
198+
suffix,
199+
services,
200+
memberAccessReplacements ?? ((IRuntimeModel)context.Model).GetUnsafeAccessors(),
201+
generatedFileNames));
202+
}
203+
}
204+
205+
private IReadOnlyList<string> ScaffoldCompiledModel(
206+
string? outputDir,
207+
string? modelNamespace,
208+
DbContext context,
209+
string? suffix,
210+
IServiceProvider services,
211+
ISet<string> generatedFileNames)
212+
{
213+
var contextType = context.GetType();
214+
if (contextType.Assembly != _assembly)
215+
{
216+
_reporter.WriteWarning(DesignStrings.ContextAssemblyMismatch(
217+
_assembly.GetName().Name, contextType.ShortDisplayName(), contextType.Assembly.GetName().Name));
218+
}
127219

128220
if (outputDir == null)
129221
{
@@ -139,6 +231,8 @@ public virtual IReadOnlyList<string> Optimize(string? outputDir, string? modelNa
139231

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

234+
var scaffolder = services.GetRequiredService<ICompiledModelScaffolder>();
235+
142236
var finalModelNamespace = modelNamespace ?? GetNamespaceFromOutputPath(outputDir) ?? "";
143237

144238
var scaffoldedFiles = scaffolder.ScaffoldModel(
@@ -150,7 +244,8 @@ public virtual IReadOnlyList<string> Optimize(string? outputDir, string? modelNa
150244
ModelNamespace = finalModelNamespace,
151245
Language = _language,
152246
UseNullableReferenceTypes = _nullable,
153-
Suffix = suffix
247+
Suffix = suffix,
248+
GeneratedFileNames = generatedFileNames
154249
});
155250

156251
var fullName = contextType.ShortDisplayName() + "Model";
@@ -170,6 +265,84 @@ public virtual IReadOnlyList<string> Optimize(string? outputDir, string? modelNa
170265
return scaffoldedFiles;
171266
}
172267

268+
private IReadOnlyList<string> PrecompileQueries(string? outputDir, DbContext context, string? suffix, IServiceProvider services, IReadOnlyDictionary<MemberInfo, QualifiedName> memberAccessReplacements, ISet<string> generatedFileNames)
269+
{
270+
outputDir = Path.GetFullPath(Path.Combine(_projectDir, outputDir ?? "Generated"));
271+
272+
// TODO: pass through properties
273+
var workspace = MSBuildWorkspace.Create();
274+
workspace.LoadMetadataForReferencedProjects = true;
275+
var project = workspace.OpenProjectAsync(_project).GetAwaiter().GetResult();
276+
if (!project.SupportsCompilation)
277+
{
278+
throw new NotSupportedException(DesignStrings.UncompilableProject(_project));
279+
}
280+
var compilation = project.GetCompilationAsync().GetAwaiter().GetResult()!;
281+
var errorDiagnostics = compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToArray();
282+
if (errorDiagnostics.Any())
283+
{
284+
var errorBuilder = new StringBuilder();
285+
errorBuilder.AppendLine(DesignStrings.CompilationErrors);
286+
foreach (var diagnostic in errorDiagnostics)
287+
{
288+
errorBuilder.AppendLine(diagnostic.ToString());
289+
}
290+
291+
throw new InvalidOperationException(errorBuilder.ToString());
292+
}
293+
294+
var syntaxGenerator = SyntaxGenerator.GetGenerator(
295+
workspace, _language == "VB" ? LanguageNames.VisualBasic : _language ?? LanguageNames.CSharp);
296+
297+
var precompiledQueryCodeGenerator = services.GetRequiredService<IPrecompiledQueryCodeGeneratorSelector>().Select(_language);
298+
299+
var precompilationErrors = new List<PrecompiledQueryCodeGenerator.QueryPrecompilationError>();
300+
var generatedFiles = precompiledQueryCodeGenerator.GeneratePrecompiledQueries(
301+
compilation,
302+
syntaxGenerator,
303+
context,
304+
memberAccessReplacements,
305+
precompilationErrors,
306+
generatedFileNames,
307+
assembly: _assembly,
308+
suffix);
309+
310+
if (precompilationErrors.Count > 0)
311+
{
312+
var errorBuilder = new StringBuilder();
313+
errorBuilder.AppendLine(DesignStrings.QueryPrecompilationErrors);
314+
foreach (var error in precompilationErrors)
315+
{
316+
errorBuilder.AppendLine(error.ToString());
317+
}
318+
319+
throw new InvalidOperationException(errorBuilder.ToString());
320+
}
321+
322+
var writtenFiles = new List<string>();
323+
foreach (var generatedFile in generatedFiles)
324+
{
325+
generatedFile.Code = FormatCode(project, generatedFile).GetAwaiter().GetResult().ToString()!;
326+
}
327+
328+
return CompiledModelScaffolder.WriteFiles(generatedFiles, outputDir);
329+
330+
static async Task<object> FormatCode(Project project, ScaffoldedFile generatedFile)
331+
{
332+
var document = project.AddDocument("_EfGeneratedInterceptors.cs", generatedFile.Code);
333+
334+
// Run the simplifier to e.g. get rid of unneeded parentheses
335+
var syntaxRoot = (await document.GetSyntaxRootAsync().ConfigureAwait(false))!;
336+
var annotatedDocument = document.WithSyntaxRoot(syntaxRoot.WithAdditionalAnnotations(Simplifier.Annotation));
337+
document = await Simplifier.ReduceAsync(annotatedDocument, optionSet: null).ConfigureAwait(false);
338+
document = await Formatter.FormatAsync(document, options: null).ConfigureAwait(false);
339+
340+
var finalSyntaxTree = (await document.GetSyntaxTreeAsync().ConfigureAwait(false))!;
341+
var finalText = await finalSyntaxTree.GetTextAsync().ConfigureAwait(false);
342+
return finalText;
343+
}
344+
}
345+
173346
private string? GetNamespaceFromOutputPath(string directoryPath)
174347
{
175348
var subNamespace = SubnamespaceFromOutputPath(_projectDir, directoryPath);
@@ -208,7 +381,28 @@ public virtual IReadOnlyList<string> Optimize(string? outputDir, string? modelNa
208381
/// </summary>
209382
public virtual DbContext CreateContext(string? contextType)
210383
{
211-
var contextPair = FindContextType(contextType);
384+
EF.IsDesignTime = true;
385+
return CreateContext(contextType, FindContextType(contextType));
386+
}
387+
388+
/// <summary>
389+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
390+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
391+
/// any release. You should only use it directly in your code with extreme caution and knowing that
392+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
393+
/// </summary>
394+
public virtual IEnumerable<DbContext> CreateAllContexts()
395+
{
396+
EF.IsDesignTime = true;
397+
var types = FindContextTypes();
398+
foreach (var contextPair in types)
399+
{
400+
yield return CreateContext(null, contextPair);
401+
}
402+
}
403+
404+
private DbContext CreateContext(string? contextType, KeyValuePair<Type, Func<DbContext>> contextPair)
405+
{
212406
var factory = contextPair.Value;
213407
try
214408
{

src/EFCore.Design/Design/Internal/MigrationsOperations.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public MigrationsOperations(
4747
reporter,
4848
assembly,
4949
startupAssembly,
50+
project: "",
5051
projectDir,
5152
rootNamespace,
5253
language,

0 commit comments

Comments
 (0)