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 ;
412using Microsoft . EntityFrameworkCore . Infrastructure . Internal ;
513using 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
719namespace 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 {
0 commit comments