Skip to content

Commit 3d181c1

Browse files
committed
Auto-generate compiled models for all contexts in the given assembly unless a single one is specified
Fixes #33558
1 parent a8128a2 commit 3d181c1

File tree

18 files changed

+280
-145
lines changed

18 files changed

+280
-145
lines changed

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

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.EntityFrameworkCore.Metadata.Internal;
1515
using Microsoft.EntityFrameworkCore.Query.Design;
1616
using Microsoft.EntityFrameworkCore.Query.Internal;
17+
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
1718

1819
namespace Microsoft.EntityFrameworkCore.Design.Internal;
1920

@@ -136,16 +137,53 @@ public virtual string ScriptDbContext(string? contextType)
136137
public virtual IReadOnlyList<string> Optimize(
137138
string? outputDir, string? modelNamespace, string? contextTypeName, string? suffix, bool scaffoldModel, bool precompileQueries)
138139
{
139-
using var context = CreateContext(contextTypeName);
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+
}
166+
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+
{
140178
var contextType = context.GetType();
141179
var services = _servicesBuilder.Build(context);
142180

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

146-
if (scaffoldModel)
183+
if (scaffoldModel
184+
&& (!optimizeAllInAssembly || contextType.Assembly == _assembly))
147185
{
148-
generatedFiles = ScaffoldCompiledModel(outputDir, modelNamespace, context, suffix, services);
186+
generatedFiles.AddRange(ScaffoldCompiledModel(outputDir, modelNamespace, context, suffix, services, generatedFileNames));
149187
if (precompileQueries)
150188
{
151189
memberAccessReplacements = ((IRuntimeModel)context.GetService<IDesignTimeModel>().Model).GetUnsafeAccessors();
@@ -154,16 +192,23 @@ public virtual IReadOnlyList<string> Optimize(
154192

155193
if (precompileQueries)
156194
{
157-
generatedFiles = generatedFiles.Concat(PrecompileQueries(
158-
outputDir, context, suffix, services, memberAccessReplacements ?? ((IRuntimeModel)context.Model).GetUnsafeAccessors()))
159-
.ToList();
195+
generatedFiles.AddRange(PrecompileQueries(
196+
outputDir,
197+
context,
198+
suffix,
199+
services,
200+
memberAccessReplacements ?? ((IRuntimeModel)context.Model).GetUnsafeAccessors(),
201+
generatedFileNames));
160202
}
161-
162-
return generatedFiles;
163203
}
164204

165205
private IReadOnlyList<string> ScaffoldCompiledModel(
166-
string? outputDir, string? modelNamespace, DbContext context, string? suffix, IServiceProvider services)
206+
string? outputDir,
207+
string? modelNamespace,
208+
DbContext context,
209+
string? suffix,
210+
IServiceProvider services,
211+
ISet<string> generatedFileNames)
167212
{
168213
var contextType = context.GetType();
169214
if (contextType.Assembly != _assembly)
@@ -199,7 +244,8 @@ private IReadOnlyList<string> ScaffoldCompiledModel(
199244
ModelNamespace = finalModelNamespace,
200245
Language = _language,
201246
UseNullableReferenceTypes = _nullable,
202-
Suffix = suffix
247+
Suffix = suffix,
248+
GeneratedFileNames = generatedFileNames
203249
});
204250

205251
var fullName = contextType.ShortDisplayName() + "Model";
@@ -219,11 +265,10 @@ private IReadOnlyList<string> ScaffoldCompiledModel(
219265
return scaffoldedFiles;
220266
}
221267

222-
private IReadOnlyList<string> PrecompileQueries(string? outputDir, DbContext context, string? suffix, IServiceProvider services, IReadOnlyDictionary<MemberInfo, QualifiedName>? memberAccessReplacements)
268+
private IReadOnlyList<string> PrecompileQueries(string? outputDir, DbContext context, string? suffix, IServiceProvider services, IReadOnlyDictionary<MemberInfo, QualifiedName>? memberAccessReplacements, ISet<string> generatedFileNames)
223269
{
224270
outputDir = Path.GetFullPath(Path.Combine(_projectDir, outputDir ?? "Generated"));
225271

226-
MSBuildLocator.RegisterDefaults();
227272
// TODO: pass through properties
228273
var workspace = MSBuildWorkspace.Create();
229274
workspace.LoadMetadataForReferencedProjects = true;
@@ -253,7 +298,14 @@ private IReadOnlyList<string> PrecompileQueries(string? outputDir, DbContext con
253298

254299
var precompilationErrors = new List<PrecompiledQueryCodeGenerator.QueryPrecompilationError>();
255300
var generatedFiles = precompiledQueryCodeGenerator.GeneratePrecompiledQueries(
256-
compilation, syntaxGenerator, context, memberAccessReplacements, precompilationErrors, assembly: _assembly, suffix);
301+
compilation,
302+
syntaxGenerator,
303+
context,
304+
memberAccessReplacements,
305+
precompilationErrors,
306+
generatedFileNames,
307+
assembly: _assembly,
308+
suffix);
257309

258310
if (precompilationErrors.Count > 0)
259311
{
@@ -267,19 +319,15 @@ private IReadOnlyList<string> PrecompileQueries(string? outputDir, DbContext con
267319
throw new InvalidOperationException(errorBuilder.ToString());
268320
}
269321

270-
Directory.CreateDirectory(outputDir);
271322
var writtenFiles = new List<string>();
272323
foreach (var generatedFile in generatedFiles)
273324
{
274-
var finalText = FormatCode(project, generatedFile).GetAwaiter().GetResult();
275-
var outputFilePath = Path.Combine(outputDir, generatedFile.Path);
276-
File.WriteAllText(outputFilePath, finalText.ToString());
277-
writtenFiles.Add(outputFilePath);
325+
generatedFile.Code = FormatCode(project, generatedFile).GetAwaiter().GetResult().ToString()!;
278326
}
279327

280-
return writtenFiles;
328+
return CompiledModelScaffolder.WriteFiles(generatedFiles, outputDir);
281329

282-
static async Task<object> FormatCode(Project project, PrecompiledQueryCodeGenerator.GeneratedInterceptorFile generatedFile)
330+
static async Task<object> FormatCode(Project project, ScaffoldedFile generatedFile)
283331
{
284332
var document = project.AddDocument("_EfGeneratedInterceptors.cs", generatedFile.Code);
285333

@@ -334,7 +382,27 @@ static async Task<object> FormatCode(Project project, PrecompiledQueryCodeGenera
334382
public virtual DbContext CreateContext(string? contextType)
335383
{
336384
EF.IsDesignTime = true;
337-
var contextPair = FindContextType(contextType);
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+
{
338406
var factory = contextPair.Value;
339407
try
340408
{

src/EFCore.Design/Query/Design/IPrecompiledQueryCodeGenerator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@ public interface IPrecompiledQueryCodeGenerator : ILanguageBasedService
2727
/// <param name="dbContext">The context.</param>
2828
/// <param name="memberAccessReplacements">The member access replacements.</param>
2929
/// <param name="precompilationErrors">A list that will contain precompilation errors.</param>
30+
/// <param name="generatedFileNames">The set of file names generated so far.</param>
3031
/// <param name="assembly">The assembly corresponding to the provided compilation.</param>
3132
/// <param name="suffix">The suffix to attach to the name of all the generated files.</param>
3233
/// <param name="cancellationToken">The cancellation token.</param>
3334
/// <returns>The files containing precompiled queries code.</returns>
34-
IReadOnlyList<GeneratedInterceptorFile> GeneratePrecompiledQueries(
35+
IReadOnlyList<ScaffoldedFile> GeneratePrecompiledQueries(
3536
Compilation compilation,
3637
SyntaxGenerator syntaxGenerator,
3738
DbContext dbContext,
3839
IReadOnlyDictionary<MemberInfo, QualifiedName>? memberAccessReplacements,
3940
List<QueryPrecompilationError> precompilationErrors,
41+
ISet<string> generatedFileNames,
4042
Assembly? assembly = null,
4143
string? suffix = null,
4244
CancellationToken cancellationToken = default);

src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.CodeAnalysis.CSharp.Syntax;
99
using Microsoft.CodeAnalysis.Editing;
1010
using Microsoft.EntityFrameworkCore.Design.Internal;
11+
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
1112

1213
namespace Microsoft.EntityFrameworkCore.Query.Internal;
1314

@@ -58,12 +59,13 @@ public PrecompiledQueryCodeGenerator()
5859
/// any release. You should only use it directly in your code with extreme caution and knowing that
5960
/// doing so can result in application failures when updating to a new Entity Framework Core release.
6061
/// </summary>
61-
public virtual IReadOnlyList<GeneratedInterceptorFile> GeneratePrecompiledQueries(
62+
public virtual IReadOnlyList<ScaffoldedFile> GeneratePrecompiledQueries(
6263
Compilation compilation,
6364
SyntaxGenerator syntaxGenerator,
6465
DbContext dbContext,
6566
IReadOnlyDictionary<MemberInfo, QualifiedName>? memberAccessReplacements,
6667
List<QueryPrecompilationError> precompilationErrors,
68+
ISet<string> generatedFileNames,
6769
Assembly? additionalAssembly = null,
6870
string? suffix = null,
6971
CancellationToken cancellationToken = default)
@@ -76,18 +78,19 @@ public virtual IReadOnlyList<GeneratedInterceptorFile> GeneratePrecompiledQuerie
7678
_liftableConstantProcessor = new LiftableConstantProcessor(null!);
7779
_queryCompiler = dbContext.GetService<IQueryCompiler>();
7880
_unsafeAccessors.Clear();
81+
var contextType = dbContext.GetType();
7982
_funcletizer = new ExpressionTreeFuncletizer(
8083
dbContext.Model,
8184
dbContext.GetService<IEvaluatableExpressionFilter>(),
82-
dbContext.GetType(),
85+
contextType,
8386
generateContextAccessors: false,
8487
dbContext.GetService<IDiagnosticsLogger<DbLoggerCategory.Query>>());
8588

8689
// This must be done after we complete generating the final compilation above
8790
_csharpToLinqTranslator.Load(compilation, dbContext, additionalAssembly);
8891

8992
// TODO: Ignore our auto-generated code! Also compiled model, generated code (comment, filename...?).
90-
var generatedFiles = new List<GeneratedInterceptorFile>();
93+
var generatedFiles = new List<ScaffoldedFile>();
9194
foreach (var syntaxTree in compilation.SyntaxTrees)
9295
{
9396
if (_queryLocator.LocateQueries(syntaxTree, precompilationErrors, cancellationToken) is not { Count: > 0 } locatedQueries)
@@ -97,7 +100,13 @@ public virtual IReadOnlyList<GeneratedInterceptorFile> GeneratePrecompiledQuerie
97100

98101
var semanticModel = compilation.GetSemanticModel(syntaxTree);
99102
var generatedFile = ProcessSyntaxTree(
100-
syntaxTree, semanticModel, locatedQueries, precompilationErrors, suffix ?? ".g", cancellationToken);
103+
syntaxTree,
104+
semanticModel,
105+
locatedQueries,
106+
precompilationErrors,
107+
"." + contextType.ShortDisplayName() + (suffix ?? ".g"),
108+
generatedFileNames,
109+
cancellationToken);
101110
if (generatedFile is not null)
102111
{
103112
generatedFiles.Add(generatedFile);
@@ -113,12 +122,13 @@ public virtual IReadOnlyList<GeneratedInterceptorFile> GeneratePrecompiledQuerie
113122
/// any release. You should only use it directly in your code with extreme caution and knowing that
114123
/// doing so can result in application failures when updating to a new Entity Framework Core release.
115124
/// </summary>
116-
protected virtual GeneratedInterceptorFile? ProcessSyntaxTree(
125+
protected virtual ScaffoldedFile? ProcessSyntaxTree(
117126
SyntaxTree syntaxTree,
118127
SemanticModel semanticModel,
119128
IReadOnlyList<InvocationExpressionSyntax> locatedQueries,
120129
List<QueryPrecompilationError> precompilationErrors,
121130
string suffix,
131+
ISet<string> generatedFileNames,
122132
CancellationToken cancellationToken)
123133
{
124134
var queriesPrecompiledInFile = 0;
@@ -296,11 +306,15 @@ namespace System.Runtime.CompilerServices
296306
public InterceptsLocationAttribute(string filePath, int line, int column) { }
297307
}
298308
}
299-
""");
300-
301-
return new(
302-
$"{Path.GetFileNameWithoutExtension(syntaxTree.FilePath)}.EFInterceptors{suffix}{Path.GetExtension(syntaxTree.FilePath)}",
303-
_code.ToString());
309+
"""
310+
);
311+
312+
var name = Uniquifier.Uniquify(
313+
Path.GetFileNameWithoutExtension(syntaxTree.FilePath),
314+
generatedFileNames,
315+
".EFInterceptors" + suffix + Path.GetExtension(syntaxTree.FilePath),
316+
CompiledModelScaffolder.MaxFileNameLength);
317+
return new(name, _code.ToString());
304318
}
305319

306320
/// <summary>
@@ -1135,11 +1149,4 @@ private INamedTypeSymbol GetTypeSymbolOrThrow(string fullyQualifiedMetadataName)
11351149
=> _compilation.GetTypeByMetadataName(fullyQualifiedMetadataName)
11361150
?? throw new InvalidOperationException("Could not find type symbol for: " + fullyQualifiedMetadataName);
11371151
}
1138-
1139-
/// <summary>
1140-
/// A generated file containing LINQ operator interceptors.
1141-
/// </summary>
1142-
/// <param name="Path">The path of the generated file.</param>
1143-
/// <param name="Code">The code of the generated file.</param>
1144-
public sealed record GeneratedInterceptorFile(string Path, string Code);
11451152
}

src/EFCore.Design/Query/Internal/QueryLocator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ bool IsDbContextType(ITypeSymbol typeSymbol)
294294
{
295295
while (true)
296296
{
297-
// TODO: Check for the user's specific DbContext type
297+
// TODO: Check for the user's specific DbContext type #33866
298298
if (typeSymbol.Equals(_symbols.DbContext, SymbolEqualityComparer.Default))
299299
{
300300
return true;

src/EFCore.Design/Scaffolding/CompiledModelCodeGenerationOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,10 @@ public class CompiledModelCodeGenerationOptions
3737
/// </summary>
3838
/// <value> The suffix to attach to the name of all the generated files. </value>
3939
public virtual string? Suffix { get; set; }
40+
41+
/// <summary>
42+
/// Gets or sets the set of file names generated so far.
43+
/// </summary>
44+
/// <value> The file names generated so far. </value>
45+
public virtual ISet<string> GeneratedFileNames { get; set; } = new HashSet<string>();
4046
}

src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,10 @@ public override ScaffoldedModel GenerateModel(
8080
var resultingFiles = new ScaffoldedModel
8181
{
8282
ContextFile = new ScaffoldedFile
83-
{
84-
Path = options.ContextDir != null
85-
? Path.Combine(options.ContextDir, dbContextFileName)
86-
: dbContextFileName,
87-
Code = generatedCode
88-
}
83+
(options.ContextDir != null
84+
? Path.Combine(options.ContextDir, dbContextFileName)
85+
: dbContextFileName,
86+
generatedCode)
8987
};
9088

9189
foreach (var entityType in model.GetEntityTypes())
@@ -106,8 +104,7 @@ public override ScaffoldedModel GenerateModel(
106104

107105
// output EntityType poco .cs file
108106
var entityTypeFileName = entityType.Name + host.Extension;
109-
resultingFiles.AdditionalFiles.Add(
110-
new ScaffoldedFile { Path = entityTypeFileName, Code = generatedCode });
107+
resultingFiles.AdditionalFiles.Add(new ScaffoldedFile(entityTypeFileName, generatedCode));
111108
}
112109

113110
return resultingFiles;

0 commit comments

Comments
 (0)