From 7d016cd0bdc448811ce7e213e72de1e3054e2eee Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 16 May 2024 11:00:22 -0700 Subject: [PATCH] Add precompiled query generation to the dbcontext optimize command Part of #33103 --- .../Design/DbContextActivator.cs | 3 +- .../DesignTimeServiceCollectionExtensions.cs | 3 + .../Design/Internal/DbContextOperations.cs | 132 +++++++++++++++++- .../Design/Internal/MigrationsOperations.cs | 1 + src/EFCore.Design/Design/OperationExecutor.cs | 23 ++- .../Properties/DesignStrings.Designer.cs | 35 +++++ .../Properties/DesignStrings.resx | 70 ++++++---- .../Design/IPrecompiledQueryCodeGenerator.cs | 43 ++++++ .../IPrecompiledQueryCodeGeneratorSelector.cs | 24 ++++ .../Query/Internal/CSharpToLinqTranslator.cs | 3 +- .../Internal/PrecompiledQueryCodeGenerator.cs | 38 +++-- .../PrecompiledQueryCodeGeneratorSelector.cs | 29 ++++ ...untimeModelLinqToCSharpSyntaxTranslator.cs | 1 - .../CSharpRuntimeModelCodeGenerator.cs | 4 +- .../RelationalRuntimeModelConvention.cs | 1 - ...icrosoft.EntityFrameworkCore.Tasks.targets | 10 +- .../Infrastructure/DbContextModelAttribute.cs | 5 +- src/EFCore/Metadata/Internal/IRuntimeModel.cs | 42 +++--- src/dotnet-ef/Project.cs | 6 +- .../Properties/Resources.Designer.cs | 20 +++ src/dotnet-ef/Properties/Resources.resx | 9 ++ src/dotnet-ef/RootCommand.cs | 5 +- src/ef/AppDomainOperationExecutor.cs | 4 +- .../DbContextOptimizeCommand.Configure.cs | 4 + src/ef/Commands/DbContextOptimizeCommand.cs | 15 +- src/ef/Commands/MigrationsBundleCommand.cs | 3 - src/ef/Commands/ProjectCommandBase.cs | 2 + src/ef/IOperationExecutor.cs | 3 +- src/ef/OperationExecutorBase.cs | 10 +- src/ef/Properties/Resources.Designer.cs | 20 +++ src/ef/Properties/Resources.resx | 9 ++ src/ef/ReflectionOperationExecutor.cs | 4 +- .../Internal/DbContextOperationsTest.cs | 6 +- .../TestUtilities/TestDbContextOperations.cs | 3 +- .../PrecompiledQueryTestHelpers.cs | 2 +- 35 files changed, 491 insertions(+), 101 deletions(-) create mode 100644 src/EFCore.Design/Query/Design/IPrecompiledQueryCodeGenerator.cs create mode 100644 src/EFCore.Design/Query/Design/IPrecompiledQueryCodeGeneratorSelector.cs create mode 100644 src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGeneratorSelector.cs diff --git a/src/EFCore.Design/Design/DbContextActivator.cs b/src/EFCore.Design/Design/DbContextActivator.cs index c7ab7df0906..df991d569d3 100644 --- a/src/EFCore.Design/Design/DbContextActivator.cs +++ b/src/EFCore.Design/Design/DbContextActivator.cs @@ -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#", diff --git a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs index ce83a43ec36..23ddac443cf 100644 --- a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs +++ b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs @@ -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; @@ -54,6 +55,8 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices( .TryAddSingleton() .TryAddSingleton() .TryAddSingleton() + .TryAddSingleton() + .TryAddSingleton() .TryAddSingleton( new DesignTimeConnectionStringResolver(applicationServiceProviderAccessor)) .TryAddSingleton() diff --git a/src/EFCore.Design/Design/Internal/DbContextOperations.cs b/src/EFCore.Design/Design/Internal/DbContextOperations.cs index 8e250ed0f2d..39fc95b8aab 100644 --- a/src/EFCore.Design/Design/Internal/DbContextOperations.cs +++ b/src/EFCore.Design/Design/Internal/DbContextOperations.cs @@ -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; @@ -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; @@ -35,6 +47,7 @@ public DbContextOperations( IOperationReporter reporter, Assembly assembly, Assembly startupAssembly, + string project, string projectDir, string? rootNamespace, string? language, @@ -44,6 +57,7 @@ public DbContextOperations( reporter, assembly, startupAssembly, + project, projectDir, rootNamespace, language, @@ -63,6 +77,7 @@ protected DbContextOperations( IOperationReporter reporter, Assembly assembly, Assembly startupAssembly, + string project, string projectDir, string? rootNamespace, string? language, @@ -73,6 +88,7 @@ protected DbContextOperations( _reporter = reporter; _assembly = assembly; _startupAssembly = startupAssembly; + _project = project; _projectDir = projectDir; _rootNamespace = rootNamespace; _language = language; @@ -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. /// - public virtual IReadOnlyList Optimize(string? outputDir, string? modelNamespace, string? contextTypeName, string? suffix) + public virtual IReadOnlyList 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(); + + IReadOnlyList generatedFiles = []; + IReadOnlyDictionary? memberAccessReplacements = null; + + if (scaffoldModel) + { + generatedFiles = ScaffoldCompiledModel(outputDir, modelNamespace, context, suffix, services); + if (precompileQueries) + { + memberAccessReplacements = ((IRuntimeModel)context.GetService().Model).GetUnsafeAccessors(); + } + } + + if (precompileQueries) + { + generatedFiles = generatedFiles.Concat(PrecompileQueries( + outputDir, context, suffix, services, memberAccessReplacements ?? ((IRuntimeModel)context.Model).GetUnsafeAccessors())) + .ToList(); + } + + return generatedFiles; + } + + private IReadOnlyList 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) { @@ -139,6 +186,8 @@ public virtual IReadOnlyList Optimize(string? outputDir, string? modelNa outputDir = Path.GetFullPath(Path.Combine(_projectDir, outputDir)); + var scaffolder = services.GetRequiredService(); + var finalModelNamespace = modelNamespace ?? GetNamespaceFromOutputPath(outputDir) ?? ""; var scaffoldedFiles = scaffolder.ScaffoldModel( @@ -170,6 +219,82 @@ public virtual IReadOnlyList Optimize(string? outputDir, string? modelNa return scaffoldedFiles; } + private IReadOnlyList PrecompileQueries(string? outputDir, DbContext context, string? suffix, IServiceProvider services, IReadOnlyDictionary? 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().Select(_language); + + var precompilationErrors = new List(); + 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(); + 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 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); @@ -208,6 +333,7 @@ public virtual IReadOnlyList Optimize(string? outputDir, string? modelNa /// public virtual DbContext CreateContext(string? contextType) { + EF.IsDesignTime = true; var contextPair = FindContextType(contextType); var factory = contextPair.Value; try diff --git a/src/EFCore.Design/Design/Internal/MigrationsOperations.cs b/src/EFCore.Design/Design/Internal/MigrationsOperations.cs index 53fa083c0d6..e6f314769e9 100644 --- a/src/EFCore.Design/Design/Internal/MigrationsOperations.cs +++ b/src/EFCore.Design/Design/Internal/MigrationsOperations.cs @@ -47,6 +47,7 @@ public MigrationsOperations( reporter, assembly, startupAssembly, + project: "", projectDir, rootNamespace, language, diff --git a/src/EFCore.Design/Design/OperationExecutor.cs b/src/EFCore.Design/Design/OperationExecutor.cs index 1f2f82bd7a6..70953d2bc88 100644 --- a/src/EFCore.Design/Design/OperationExecutor.cs +++ b/src/EFCore.Design/Design/OperationExecutor.cs @@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Design; /// public class OperationExecutor : MarshalByRefObject { + private readonly string _project; private readonly string _projectDir; private readonly string _targetAssemblyName; private readonly string _startupTargetAssemblyName; @@ -38,6 +39,7 @@ public class OperationExecutor : MarshalByRefObject /// The arguments supported by are: /// targetName--The assembly name of the target project. /// startupTargetName--The assembly name of the startup project. + /// project--The target project. /// projectDir--The target project's root directory. /// rootNamespace--The target project's root namespace. /// language--The programming language to be used to generate classes. @@ -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"]; @@ -121,6 +124,7 @@ public virtual DbContextOperations ContextOperations _reporter, Assembly, StartupAssembly, + _project, _projectDir, _rootNamespace, _language, @@ -503,7 +507,7 @@ private IEnumerable GetMigrationsImpl( } /// - /// Represents an operation to generate a compiled model from the DbContext. + /// Represents an operation to generate optimized code for a DbContext. /// public class OptimizeContext : OperationBase { @@ -513,8 +517,11 @@ public class OptimizeContext : OperationBase /// /// The arguments supported by are: /// outputDir--The directory to put files in. Paths are relative to the project directory. - /// modelNamespace--Specify to override the namespace of the generated model. - /// contextType--The to use. + /// modelNamespace--The namespace of the generated model. + /// contextType--The type to use. + /// suffix--The suffix to add to all the generated files. + /// scaffoldModel--Whether to generate a compiled model from the DbContext. + /// precompileQueries--Whether to generate code for precompiled queries. /// /// The operation executor. /// The . @@ -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 OptimizeContextImpl(string? outputDir, string? modelNamespace, string? contextType, string? suffix) - => ContextOperations.Optimize(outputDir, modelNamespace, contextType, suffix); + private IReadOnlyList OptimizeContextImpl( + string? outputDir, string? modelNamespace, string? contextType, string? suffix, bool scaffoldModel, bool precompileQueries) + => ContextOperations.Optimize(outputDir, modelNamespace, contextType, suffix, scaffoldModel, precompileQueries); /// /// Represents an operation to scaffold a and entity types for a database. diff --git a/src/EFCore.Design/Properties/DesignStrings.Designer.cs b/src/EFCore.Design/Properties/DesignStrings.Designer.cs index 1054ed2b67a..52142c48ce1 100644 --- a/src/EFCore.Design/Properties/DesignStrings.Designer.cs +++ b/src/EFCore.Design/Properties/DesignStrings.Designer.cs @@ -101,6 +101,12 @@ public static string CannotGenerateTypeQualifiedMethodCall public static string CircularBaseClassDependency => GetString("CircularBaseClassDependency"); + /// + /// Compilation failed with errors: + /// + public static string CompilationErrors + => GetString("CompilationErrors"); + /// /// A compilation must be loaded. /// @@ -169,6 +175,15 @@ public static string ConflictingContextAndMigrationName(object? name) public static string ConnectionDescription => GetString("ConnectionDescription"); + /// + /// Your target project '{assembly}' doesn't match the assembly containing '{contextType}' - '{contextAssembly}'. This is not recommended as it will cause the compiled model to not be discovered automatically. + /// Consider changing your target project to the DbContext project by using the Package Manager Console's Default project drop-down list, by executing "dotnet ef" from the directory containing the DbContext project or by supplying it with the '--project' option. + /// + public static string ContextAssemblyMismatch(object? assembly, object? contextType, object? contextAssembly) + => string.Format( + GetString("ContextAssemblyMismatch", nameof(assembly), nameof(contextType), nameof(contextAssembly)), + assembly, contextType, contextAssembly); + /// /// The context class name '{contextClassName}' is not a valid C# identifier. /// @@ -461,6 +476,12 @@ public static string MultipleContextsWithQualifiedName(object? name) GetString("MultipleContextsWithQualifiedName", nameof(name)), name); + /// + /// Could not find symbol for anonymous object creation initializer: + /// + public static string NoAnonymousSymbol + => GetString("NoAnonymousSymbol"); + /// /// Don't colorize output. /// @@ -627,6 +648,12 @@ public static string ProviderReturnedNullModel(object? providerTypeName) public static string QueryComprehensionSyntaxNotSupportedInPrecompiledQueries => GetString("QueryComprehensionSyntaxNotSupportedInPrecompiledQueries"); + /// + /// Query precompilation failed with errors: + /// + public static string QueryPrecompilationErrors + => GetString("QueryPrecompilationErrors"); + /// /// No files were generated in directory '{outputDirectoryName}'. The following file(s) already exist(s) and must be made writeable to continue: {readOnlyFiles}. /// @@ -715,6 +742,14 @@ public static string UnableToScaffoldIndexMissingProperty(object? indexName, obj GetString("UnableToScaffoldIndexMissingProperty", nameof(indexName), nameof(columnNames)), indexName, columnNames); + /// + /// The project '{project}' does not support compilation. + /// + public static string UncompilableProject(object? project) + => string.Format( + GetString("UncompilableProject", nameof(project)), + project); + /// /// Unhandled enum value '{enumValue}'. /// diff --git a/src/EFCore.Design/Properties/DesignStrings.resx b/src/EFCore.Design/Properties/DesignStrings.resx index 902e4b9807f..5029ecbe9cb 100644 --- a/src/EFCore.Design/Properties/DesignStrings.resx +++ b/src/EFCore.Design/Properties/DesignStrings.resx @@ -1,17 +1,17 @@  - @@ -150,6 +150,9 @@ You cannot add a migration with the name 'Migration'. + + Compilation failed with errors: + A compilation must be loaded. @@ -177,6 +180,10 @@ The connection string to the database. Defaults to the one specified in AddDbContext or OnConfiguring. + + Your target project '{assembly}' doesn't match the assembly containing '{contextType}' - '{contextAssembly}'. This is not recommended as it will cause the compiled model to not be discovered automatically. +Consider changing your target project to the DbContext project by using the Package Manager Console's Default project drop-down list, by executing "dotnet ef" from the directory containing the DbContext project or by supplying it with the '--project' option. + The context class name '{contextClassName}' is not a valid C# identifier. @@ -296,6 +303,9 @@ Change your target project to the migrations project by using the Package Manage More than one DbContext named '{name}' was found. Specify which one to use by providing its fully qualified name using its exact case. + + Could not find symbol for anonymous object creation initializer: + Don't colorize output. @@ -368,6 +378,9 @@ Change your target project to the migrations project by using the Package Manage LINQ query comprehension syntax is currently not supported in precompiled queries. + + Query precompilation failed with errors: + No files were generated in directory '{outputDirectoryName}'. The following file(s) already exist(s) and must be made writeable to continue: {readOnlyFiles}. @@ -405,6 +418,9 @@ Change your target project to the migrations project by using the Package Manage Unable to scaffold the index '{indexName}'. The following columns could not be scaffolded: {columnNames}. + + The project '{project}' does not support compilation. + Unhandled enum value '{enumValue}'. diff --git a/src/EFCore.Design/Query/Design/IPrecompiledQueryCodeGenerator.cs b/src/EFCore.Design/Query/Design/IPrecompiledQueryCodeGenerator.cs new file mode 100644 index 00000000000..0d3d7a3de99 --- /dev/null +++ b/src/EFCore.Design/Query/Design/IPrecompiledQueryCodeGenerator.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.EntityFrameworkCore.Design.Internal; +using static Microsoft.EntityFrameworkCore.Query.Internal.PrecompiledQueryCodeGenerator; + +namespace Microsoft.EntityFrameworkCore.Migrations.Design; + +/// +/// Used to generate code for precompiled queries. +/// +/// +/// See EF Core compiled models, and +/// EF Core design-time services for more information and examples. +/// +[Experimental(EFDiagnostics.PrecompiledQueryExperimental)] +public interface IPrecompiledQueryCodeGenerator : ILanguageBasedService +{ + /// + /// Generates the precompiled queries code. + /// + /// The compilation. + /// The syntax generator. + /// The context. + /// The member access replacements. + /// A list that will contain precompilation errors. + /// The assembly corresponding to the provided compilation. + /// The suffix to attach to the name of all the generated files. + /// The cancellation token. + /// The files containing precompiled queries code. + IReadOnlyList GeneratePrecompiledQueries( + Compilation compilation, + SyntaxGenerator syntaxGenerator, + DbContext dbContext, + IReadOnlyDictionary? memberAccessReplacements, + List precompilationErrors, + Assembly? assembly = null, + string? suffix = null, + CancellationToken cancellationToken = default); +} diff --git a/src/EFCore.Design/Query/Design/IPrecompiledQueryCodeGeneratorSelector.cs b/src/EFCore.Design/Query/Design/IPrecompiledQueryCodeGeneratorSelector.cs new file mode 100644 index 00000000000..9a6e840722d --- /dev/null +++ b/src/EFCore.Design/Query/Design/IPrecompiledQueryCodeGeneratorSelector.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Query.Design; + +/// +/// Selects an service for a given programming language. +/// +/// +/// See EF Core compiled models, and +/// EF Core design-time services for more information and examples. +/// +[Experimental(EFDiagnostics.PrecompiledQueryExperimental)] +public interface IPrecompiledQueryCodeGeneratorSelector +{ + /// + /// Selects an service for a given programming language. + /// + /// The programming language. + /// The . + IPrecompiledQueryCodeGenerator Select(string? language); +} diff --git a/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs b/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs index e029d9ad72a..57f9cceb761 100644 --- a/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs +++ b/src/EFCore.Design/Query/Internal/CSharpToLinqTranslator.cs @@ -135,8 +135,7 @@ public override Expression VisitAnonymousObjectCreationExpression(AnonymousObjec // At least for EF's purposes, it doesn't matter, so we build a placeholder. if (_semanticModel.GetSymbolInfo(anonymousObjectCreation).Symbol is not IMethodSymbol constructorSymbol) { - throw new InvalidOperationException( - "Could not find symbol for anonymous object creation initializer: " + anonymousObjectCreation); + throw new InvalidOperationException(DesignStrings.NoAnonymousSymbol + " " + anonymousObjectCreation); } var anonymousType = ResolveType(constructorSymbol.ContainingType); diff --git a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs index 91d67afa608..11857779f7b 100644 --- a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs +++ b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; +using Microsoft.EntityFrameworkCore.Design.Internal; namespace Microsoft.EntityFrameworkCore.Query.Internal; @@ -16,7 +17,7 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; /// 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. /// -public class PrecompiledQueryCodeGenerator +public class PrecompiledQueryCodeGenerator : IPrecompiledQueryCodeGenerator { private readonly QueryLocator _queryLocator; private readonly CSharpToLinqTranslator _csharpToLinqTranslator; @@ -24,17 +25,21 @@ public class PrecompiledQueryCodeGenerator private SyntaxGenerator _g = null!; private IQueryCompiler _queryCompiler = null!; private ExpressionTreeFuncletizer _funcletizer = null!; - private LinqToCSharpSyntaxTranslator _linqToCSharpTranslator = null!; + private RuntimeModelLinqToCSharpSyntaxTranslator _linqToCSharpTranslator = null!; private LiftableConstantProcessor _liftableConstantProcessor = null!; private Symbols _symbols; private readonly HashSet _namespaces = new(); + private IReadOnlyDictionary? _memberAccessReplacements; private readonly HashSet _unsafeAccessors = new(); private readonly IndentedStringBuilder _code = new(); private const string InterceptorsNamespace = "Microsoft.EntityFrameworkCore.GeneratedInterceptors"; + /// + public string? Language => "C#"; + /// /// 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 @@ -57,14 +62,17 @@ public virtual IReadOnlyList GeneratePrecompiledQuerie Compilation compilation, SyntaxGenerator syntaxGenerator, DbContext dbContext, + IReadOnlyDictionary? memberAccessReplacements, List precompilationErrors, Assembly? additionalAssembly = null, + string? suffix = null, CancellationToken cancellationToken = default) { _queryLocator.Initialize(compilation); _symbols = Symbols.Load(compilation); _g = syntaxGenerator; - _linqToCSharpTranslator = new LinqToCSharpSyntaxTranslator(_g); + _linqToCSharpTranslator = new RuntimeModelLinqToCSharpSyntaxTranslator(_g); + _memberAccessReplacements = memberAccessReplacements; _liftableConstantProcessor = new LiftableConstantProcessor(null!); _queryCompiler = dbContext.GetService(); _unsafeAccessors.Clear(); @@ -79,7 +87,7 @@ public virtual IReadOnlyList GeneratePrecompiledQuerie _csharpToLinqTranslator.Load(compilation, dbContext, additionalAssembly); // TODO: Ignore our auto-generated code! Also compiled model, generated code (comment, filename...?). - var generatedSyntaxTrees = new List(); + var generatedFiles = new List(); foreach (var syntaxTree in compilation.SyntaxTrees) { if (_queryLocator.LocateQueries(syntaxTree, precompilationErrors, cancellationToken) is not { Count: > 0 } locatedQueries) @@ -88,15 +96,15 @@ public virtual IReadOnlyList GeneratePrecompiledQuerie } var semanticModel = compilation.GetSemanticModel(syntaxTree); - var generatedSyntaxTree = ProcessSyntaxTreeAsync( - syntaxTree, semanticModel, locatedQueries, precompilationErrors, cancellationToken); - if (generatedSyntaxTree is not null) + var generatedFile = ProcessSyntaxTree( + syntaxTree, semanticModel, locatedQueries, precompilationErrors, suffix ?? ".g", cancellationToken); + if (generatedFile is not null) { - generatedSyntaxTrees.Add(generatedSyntaxTree); + generatedFiles.Add(generatedFile); } } - return generatedSyntaxTrees; + return generatedFiles; } /// @@ -105,11 +113,12 @@ public virtual IReadOnlyList GeneratePrecompiledQuerie /// 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. /// - protected virtual GeneratedInterceptorFile? ProcessSyntaxTreeAsync( + protected virtual GeneratedInterceptorFile? ProcessSyntaxTree( SyntaxTree syntaxTree, SemanticModel semanticModel, IReadOnlyList locatedQueries, List precompilationErrors, + string suffix, CancellationToken cancellationToken) { var queriesPrecompiledInFile = 0; @@ -290,7 +299,7 @@ public InterceptsLocationAttribute(string filePath, int line, int column) { } """); return new( - $"{Path.GetFileNameWithoutExtension(syntaxTree.FilePath)}.EFInterceptors.g{Path.GetExtension(syntaxTree.FilePath)}", + $"{Path.GetFileNameWithoutExtension(syntaxTree.FilePath)}.EFInterceptors{suffix}{Path.GetExtension(syntaxTree.FilePath)}", _code.ToString()); } @@ -473,7 +482,7 @@ when namedReturnType2.AllInterfaces.Prepend(namedReturnType2) // Output the interceptor method signature preceded by the [InterceptsLocation] attribute. var startPosition = operatorSyntax.SyntaxTree.GetLineSpan(memberAccessSyntax.Name.Span, cancellationToken).StartLinePosition; var interceptorName = $"Query{queryNum}_{memberAccessSyntax.Name}{operatorNum}"; - code.AppendLine($"""[InterceptsLocation("{operatorSyntax.SyntaxTree.FilePath}", {startPosition.Line + 1}, {startPosition.Character + 1})]"""); + code.AppendLine($"""[InterceptsLocation(@"{operatorSyntax.SyntaxTree.FilePath.Replace("\"","\"\"")}", {startPosition.Line + 1}, {startPosition.Character + 1})]"""); GenerateInterceptorMethodSignature(); code.AppendLine("{").IncrementIndent(); @@ -749,7 +758,7 @@ void GenerateCapturedVariableExtractors( var collectedNamespaces = new HashSet(); var unsafeAccessors = new HashSet(); var roslynPathSegment = _linqToCSharpTranslator.TranslateExpression( - linqPathSegment, constantReplacements: null, collectedNamespaces, unsafeAccessors); + linqPathSegment, constantReplacements: null, _memberAccessReplacements, collectedNamespaces, unsafeAccessors); var variableName = capturedVariablesPathTree.ExpressionType.Name; variableName = char.ToLower(variableName[0]) + variableName[1..^"Expression".Length] + ++variableCounter; @@ -869,7 +878,7 @@ private void GenerateQueryExecutor( foreach (var liftedConstant in _liftableConstantProcessor.LiftedConstants) { var variableValueSyntax = _linqToCSharpTranslator.TranslateExpression( - liftedConstant.Expression, constantReplacements: null, namespaces, unsafeAccessors); + liftedConstant.Expression, constantReplacements: null, _memberAccessReplacements, namespaces, unsafeAccessors); // code.AppendLine($"{liftedConstant.Parameter.Type.Name} {liftedConstant.Parameter.Name} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};"); code.AppendLine($"var {liftedConstant.Parameter.Name} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};"); } @@ -878,6 +887,7 @@ private void GenerateQueryExecutor( (AnonymousFunctionExpressionSyntax)_linqToCSharpTranslator.TranslateExpression( queryExecutorAfterLiftingExpression, constantReplacements: null, + _memberAccessReplacements, namespaces, unsafeAccessors); diff --git a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGeneratorSelector.cs b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGeneratorSelector.cs new file mode 100644 index 00000000000..62d4796cfc3 --- /dev/null +++ b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGeneratorSelector.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Query.Design; + +namespace Microsoft.EntityFrameworkCore.Query.Internal; + +/// +/// 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. +/// +public class PrecompiledQueryCodeGeneratorSelector : + LanguageBasedSelector, + IPrecompiledQueryCodeGeneratorSelector +{ + /// + /// 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. + /// + public PrecompiledQueryCodeGeneratorSelector(IEnumerable services) + : base(services) + { + } +} diff --git a/src/EFCore.Design/Query/Internal/RuntimeModelLinqToCSharpSyntaxTranslator.cs b/src/EFCore.Design/Query/Internal/RuntimeModelLinqToCSharpSyntaxTranslator.cs index 2ab5978e829..def66e6c68c 100644 --- a/src/EFCore.Design/Query/Internal/RuntimeModelLinqToCSharpSyntaxTranslator.cs +++ b/src/EFCore.Design/Query/Internal/RuntimeModelLinqToCSharpSyntaxTranslator.cs @@ -9,7 +9,6 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Design.Internal; -using Microsoft.EntityFrameworkCore.Internal; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Microsoft.EntityFrameworkCore.Query.Internal; diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs index 9eca5abf109..ed76fa7f3c3 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs @@ -2267,7 +2267,6 @@ private void CreateAnnotations( var runtimeType = (IRuntimeEntityType)entityType; - // TODO var unsafeAccessors = new HashSet(); var originalValuesFactory = OriginalValuesFactoryFactory.Instance.CreateExpression(runtimeType); @@ -2334,6 +2333,9 @@ private void CreateAnnotations( CreateAnnotations(entityType, _annotationCodeGenerator.Generate, parameters); + // TODO: Output any additional unsafe accessors + Check.DebugAssert(unsafeAccessors.Count == 0, "Generated unsafe accessors not handled"); + mainBuilder .AppendLine() .AppendLine("Customize(runtimeEntityType);"); diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs index 906c473e43e..519f3adbda8 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection.Emit; using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; diff --git a/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets b/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets index 922dbf4b4c7..a525e9be230 100644 --- a/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets +++ b/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets @@ -17,7 +17,7 @@ + Condition="Exists($(EFProjectsToOptimizePath)) And '$(_InEFGenerateFiles)'!='true'"> <_EFProjectsToOptimizeFiles Include="$(EFProjectsToOptimizePath)*.*" /> @@ -38,7 +38,7 @@ + Properties="Configuration=$(Configuration);Platform=$(Platform);EFStartupAssembly=$(_AssemblyFullName)" /> <_EFProjectsToOptimize Remove="$(MSBuildProjectFullPath)" /> @@ -49,7 +49,7 @@ Targets="Build" BuildInParallel="true" Condition="@(_EFProjectsToOptimize->Count()) > 0" - Properties="Configuration=$(Configuration);Platform=$(Platform);EFOptimizeContext=false" /> + Properties="Configuration=$(Configuration);Platform=$(Platform);_InEFGenerateFiles=true" /> - + @@ -115,7 +115,7 @@ This target has the same Inputs and Outputs as CoreCompile to run only if CoreCompile isn't going to be skipped --> The associated context. /// The compiled model. - public DbContextModelAttribute(Type contextType, Type modelType) + public DbContextModelAttribute( + Type contextType, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type modelType) { Check.NotNull(contextType, nameof(contextType)); diff --git a/src/EFCore/Metadata/Internal/IRuntimeModel.cs b/src/EFCore/Metadata/Internal/IRuntimeModel.cs index 960e254aecf..8c950fbf876 100644 --- a/src/EFCore/Metadata/Internal/IRuntimeModel.cs +++ b/src/EFCore/Metadata/Internal/IRuntimeModel.cs @@ -44,35 +44,33 @@ public interface IRuntimeModel : IModel /// IReadOnlyDictionary? GetUnsafeAccessors() { - var accessorsAnnotation = FindRuntimeAnnotation(CoreAnnotationNames.UnsafeAccessors); - if (accessorsAnnotation != null) - { - return accessorsAnnotation.Value as IReadOnlyDictionary; - } + return GetOrAddRuntimeAnnotationValue(CoreAnnotationNames.UnsafeAccessors, m => GetAccessors(m!), this); - var accessors = new Dictionary(); - foreach (var entityType in GetEntityTypes()) + static IReadOnlyDictionary? GetAccessors(IRuntimeModel model) { - AddPropertyAccessors(entityType, accessors); - - foreach (var property in entityType.GetDeclaredServiceProperties()) + var accessors = new Dictionary(); + foreach (var entityType in model.GetEntityTypes()) { - AddAccessors(property, accessors); - } + AddPropertyAccessors(entityType, accessors); - foreach (var navigation in entityType.GetDeclaredNavigations()) - { - AddAccessors(navigation, accessors); - } + foreach (var property in entityType.GetDeclaredServiceProperties()) + { + AddAccessors(property, accessors); + } - foreach (var navigation in entityType.GetDeclaredSkipNavigations()) - { - AddAccessors(navigation, accessors); + foreach (var navigation in entityType.GetDeclaredNavigations()) + { + AddAccessors(navigation, accessors); + } + + foreach (var navigation in entityType.GetDeclaredSkipNavigations()) + { + AddAccessors(navigation, accessors); + } } - } - SetRuntimeAnnotation(CoreAnnotationNames.UnsafeAccessors, accessors); - return accessors; + return accessors.Count > 0 ? accessors : null; + } static void AddPropertyAccessors(ITypeBase structuralType, Dictionary accessors) { diff --git a/src/dotnet-ef/Project.cs b/src/dotnet-ef/Project.cs index 4b923364899..2d2fc218d66 100644 --- a/src/dotnet-ef/Project.cs +++ b/src/dotnet-ef/Project.cs @@ -153,7 +153,7 @@ bool FileMatches() }; } - public void Build() + public void Build(IEnumerable? additionalArgs) { var args = new List { "build" }; @@ -184,6 +184,10 @@ public void Build() args.Add("/verbosity:quiet"); args.Add("/nologo"); args.Add("/p:PublishAot=false"); // Avoid NativeAOT warnings + if (additionalArgs != null) + { + args.AddRange(additionalArgs); + } var exitCode = Exe.Run("dotnet", args, handleOutput: Reporter.WriteVerbose); if (exitCode != 0) diff --git a/src/dotnet-ef/Properties/Resources.Designer.cs b/src/dotnet-ef/Properties/Resources.Designer.cs index 19262f59346..211ae4b66e9 100644 --- a/src/dotnet-ef/Properties/Resources.Designer.cs +++ b/src/dotnet-ef/Properties/Resources.Designer.cs @@ -289,6 +289,14 @@ public static string MigrationsScriptDescription public static string MigrationToDescription => GetString("MigrationToDescription"); + /// + /// Option '--{requiredOption}' must be specified if '--{conditionalOption}' is used. + /// + public static string MissingConditionalOption(object? requiredOption, object? conditionalOption) + => string.Format( + GetString("MissingConditionalOption", nameof(requiredOption), nameof(conditionalOption)), + requiredOption, conditionalOption); + /// /// More than one project was found in the current working directory. Use the --project option. /// @@ -369,6 +377,12 @@ public static string NoProjectInDirectory(object? projectDir) GetString("NoProjectInDirectory", nameof(projectDir)), projectDir); + /// + /// Don't generate a compiled model. + /// + public static string NoScaffoldDescription + => GetString("NoScaffoldDescription"); + /// /// Don't generate SQL transaction statements. /// @@ -387,6 +401,12 @@ public static string OutputDescription public static string OutputDirDescription => GetString("OutputDirDescription"); + /// + /// Generate precompiled queries. + /// + public static string PrecompileQueriesDescription + => GetString("PrecompileQueriesDescription"); + /// /// Prefix output with level. /// diff --git a/src/dotnet-ef/Properties/Resources.resx b/src/dotnet-ef/Properties/Resources.resx index f932fb8d4e2..911c16103ca 100644 --- a/src/dotnet-ef/Properties/Resources.resx +++ b/src/dotnet-ef/Properties/Resources.resx @@ -252,6 +252,9 @@ The target migration. Defaults to the last migration. + + Option '--{requiredOption}' must be specified if '--{conditionalOption}' is used. + More than one project was found in the current working directory. Use the --project option. @@ -288,6 +291,9 @@ No project was found in directory '{projectDir}'. + + Don't generate a compiled model. + Don't generate SQL transaction statements. @@ -297,6 +303,9 @@ The directory to put files in. Paths are relative to the project directory. + + Generate precompiled queries. + Prefix output with level. diff --git a/src/dotnet-ef/RootCommand.cs b/src/dotnet-ef/RootCommand.cs index 7a4de0f2e2f..aa03475a11d 100644 --- a/src/dotnet-ef/RootCommand.cs +++ b/src/dotnet-ef/RootCommand.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Reflection; using System.Runtime.Versioning; using System.Text.Json; @@ -79,7 +80,9 @@ protected override int Execute(string[] _) if (!_noBuild!.HasValue()) { Reporter.WriteInformation(Resources.BuildStarted); - startupProject.Build(); + var skipOptimization = _args!.Count > 2 + && _args[0] == "dbcontext" && _args[1] == "optimize" && !_args.Any(a => a == "--no-scaffold"); + startupProject.Build(skipOptimization ? new[] { "/p:EFOptimizeContext=false" } : null); Reporter.WriteInformation(Resources.BuildSucceeded); } diff --git a/src/ef/AppDomainOperationExecutor.cs b/src/ef/AppDomainOperationExecutor.cs index b51bffa921b..a918005b050 100644 --- a/src/ef/AppDomainOperationExecutor.cs +++ b/src/ef/AppDomainOperationExecutor.cs @@ -23,6 +23,7 @@ internal class AppDomainOperationExecutor : OperationExecutorBase public AppDomainOperationExecutor( string assembly, string? startupAssembly, + string? project, string? projectDir, string? dataDirectory, string? rootNamespace, @@ -30,7 +31,7 @@ public AppDomainOperationExecutor( bool nullable, string[] remainingArguments, IOperationReportHandler reportHandler) - : base(assembly, startupAssembly, projectDir, rootNamespace, language, nullable, remainingArguments, reportHandler) + : base(assembly, startupAssembly, project, projectDir, rootNamespace, language, nullable, remainingArguments, reportHandler) { var info = new AppDomainSetup { ApplicationBase = AppBasePath }; @@ -77,6 +78,7 @@ public AppDomainOperationExecutor( { { "targetName", AssemblyFileName }, { "startupTargetName", StartupAssemblyFileName }, + { "project", Project }, { "projectDir", ProjectDirectory }, { "rootNamespace", RootNamespace }, { "language", Language }, diff --git a/src/ef/Commands/DbContextOptimizeCommand.Configure.cs b/src/ef/Commands/DbContextOptimizeCommand.Configure.cs index a1ac859e3bd..5cc1d77a61b 100644 --- a/src/ef/Commands/DbContextOptimizeCommand.Configure.cs +++ b/src/ef/Commands/DbContextOptimizeCommand.Configure.cs @@ -11,6 +11,8 @@ internal partial class DbContextOptimizeCommand : ContextCommandBase private CommandOption? _outputDir; private CommandOption? _namespace; private CommandOption? _suffix; + private CommandOption? _noScaffold; + private CommandOption? _precompileQueries; public override void Configure(CommandLineApplication command) { @@ -19,6 +21,8 @@ public override void Configure(CommandLineApplication command) _outputDir = command.Option("-o|--output-dir ", Resources.OutputDirDescription); _namespace = command.Option("-n|--namespace ", Resources.NamespaceDescription); _suffix = command.Option("--suffix ", Resources.SuffixDescription); + _noScaffold = command.Option("--no-scaffold", Resources.NoScaffoldDescription); + _precompileQueries = command.Option("--precompile-queries", Resources.PrecompileQueriesDescription); base.Configure(command); } diff --git a/src/ef/Commands/DbContextOptimizeCommand.cs b/src/ef/Commands/DbContextOptimizeCommand.cs index c2dc6d6cdbd..e97103c855b 100644 --- a/src/ef/Commands/DbContextOptimizeCommand.cs +++ b/src/ef/Commands/DbContextOptimizeCommand.cs @@ -9,6 +9,17 @@ namespace Microsoft.EntityFrameworkCore.Tools.Commands; // ReSharper disable once ArrangeTypeModifiers internal partial class DbContextOptimizeCommand { + protected override void Validate() + { + base.Validate(); + + if (_noScaffold!.HasValue() + && !_precompileQueries!.HasValue()) + { + throw new CommandException(Resources.MissingConditionalOption(_precompileQueries.LongName, _noScaffold.LongName)); + } + } + protected override int Execute(string[] args) { if (new SemanticVersionComparer().Compare(EFCoreVersion, "6.0.0") < 0) @@ -21,7 +32,9 @@ protected override int Execute(string[] args) _outputDir!.Value(), _namespace!.Value(), Context!.Value(), - _suffix!.Value()); + _suffix!.Value() ?? "", + !_noScaffold!.HasValue(), + _precompileQueries!.HasValue()); ReportResults(result); diff --git a/src/ef/Commands/MigrationsBundleCommand.cs b/src/ef/Commands/MigrationsBundleCommand.cs index 5e8dd3b6600..4b6271f279b 100644 --- a/src/ef/Commands/MigrationsBundleCommand.cs +++ b/src/ef/Commands/MigrationsBundleCommand.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.IO; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Tools.Generators; using Microsoft.EntityFrameworkCore.Tools.Properties; diff --git a/src/ef/Commands/ProjectCommandBase.cs b/src/ef/Commands/ProjectCommandBase.cs index 14be3fb7eb0..5c8ab7c978a 100644 --- a/src/ef/Commands/ProjectCommandBase.cs +++ b/src/ef/Commands/ProjectCommandBase.cs @@ -89,6 +89,7 @@ protected IOperationExecutor CreateExecutor(string[] remainingArguments) return new AppDomainOperationExecutor( Assembly!.Value()!, StartupAssembly!.Value(), + Project!.Value(), _projectDir!.Value(), _dataDir!.Value(), _rootNamespace!.Value(), @@ -127,6 +128,7 @@ protected IOperationExecutor CreateExecutor(string[] remainingArguments) return new ReflectionOperationExecutor( Assembly!.Value()!, StartupAssembly!.Value(), + Project!.Value(), _projectDir!.Value(), _dataDir!.Value(), _rootNamespace!.Value(), diff --git a/src/ef/IOperationExecutor.cs b/src/ef/IOperationExecutor.cs index 8a5bb9e60ac..644a938ba7f 100644 --- a/src/ef/IOperationExecutor.cs +++ b/src/ef/IOperationExecutor.cs @@ -14,7 +14,8 @@ internal interface IOperationExecutor : IDisposable IDictionary GetContextInfo(string? name); void UpdateDatabase(string? migration, string? connectionString, string? contextType); IEnumerable GetContextTypes(); - IEnumerable OptimizeContext(string? outputDir, string? modelNamespace, string? contextType, string? suffix); + IEnumerable OptimizeContext( + string? outputDir, string? modelNamespace, string? contextType, string? suffix, bool scaffoldModel, bool precompileQueries); IDictionary ScaffoldContext( string provider, diff --git a/src/ef/OperationExecutorBase.cs b/src/ef/OperationExecutorBase.cs index e25f397f353..53c910449e3 100644 --- a/src/ef/OperationExecutorBase.cs +++ b/src/ef/OperationExecutorBase.cs @@ -19,6 +19,7 @@ internal abstract class OperationExecutorBase : IOperationExecutor protected string AssemblyFileName { get; set; } protected string StartupAssemblyFileName { get; set; } protected string ProjectDirectory { get; } + protected string Project { get; } protected string RootNamespace { get; } protected string? Language { get; } protected bool Nullable { get; } @@ -27,6 +28,7 @@ internal abstract class OperationExecutorBase : IOperationExecutor protected OperationExecutorBase( string assembly, string? startupAssembly, + string? project, string? projectDir, string? rootNamespace, string? language, @@ -43,6 +45,7 @@ protected OperationExecutorBase( Path.Combine(Directory.GetCurrentDirectory(), Path.GetDirectoryName(startupAssembly ?? assembly)!)); RootNamespace = rootNamespace ?? AssemblyFileName; + Project = project ?? ""; ProjectDirectory = projectDir ?? Directory.GetCurrentDirectory(); Language = language; Nullable = nullable; @@ -140,7 +143,8 @@ public void UpdateDatabase(string? migration, string? connectionString, string? public IEnumerable GetContextTypes() => InvokeOperation>("GetContextTypes"); - public IEnumerable OptimizeContext(string? outputDir, string? modelNamespace, string? contextType, string? suffix) + public IEnumerable OptimizeContext( + string? outputDir, string? modelNamespace, string? contextType, string? suffix, bool scaffoldModel, bool precompileQueries) => InvokeOperation>( "OptimizeContext", new Dictionary @@ -148,7 +152,9 @@ public IEnumerable OptimizeContext(string? outputDir, string? modelNames ["outputDir"] = outputDir, ["modelNamespace"] = modelNamespace, ["contextType"] = contextType, - ["suffix"] = suffix + ["suffix"] = suffix, + ["scaffoldModel"] = scaffoldModel, + ["precompileQueries"] = precompileQueries }); public IDictionary ScaffoldContext( diff --git a/src/ef/Properties/Resources.Designer.cs b/src/ef/Properties/Resources.Designer.cs index 8f233d91650..03e88e60f71 100644 --- a/src/ef/Properties/Resources.Designer.cs +++ b/src/ef/Properties/Resources.Designer.cs @@ -381,6 +381,14 @@ public static string MissingArgument(object? arg) GetString("MissingArgument", nameof(arg)), arg); + /// + /// Option '--{requiredOption}' must be specified if '--{conditionalOption}' is used. + /// + public static string MissingConditionalOption(object? requiredOption, object? conditionalOption) + => string.Format( + GetString("MissingConditionalOption", nameof(requiredOption), nameof(conditionalOption)), + requiredOption, conditionalOption); + /// /// Missing required option '--{option}'. /// @@ -425,6 +433,12 @@ public static string NoMigrations public static string NoPluralizeDescription => GetString("NoPluralizeDescription"); + /// + /// Don't generate a compiled model. + /// + public static string NoScaffoldDescription + => GetString("NoScaffoldDescription"); + /// /// Don't generate SQL transaction statements. /// @@ -469,6 +483,12 @@ public static string Pending public static string PendingUnknown => GetString("PendingUnknown"); + /// + /// Generate precompiled queries. + /// + public static string PrecompileQueriesDescription + => GetString("PrecompileQueriesDescription"); + /// /// Prefix output with level. /// diff --git a/src/ef/Properties/Resources.resx b/src/ef/Properties/Resources.resx index 6f5f760c8ab..b6a1ed00158 100644 --- a/src/ef/Properties/Resources.resx +++ b/src/ef/Properties/Resources.resx @@ -288,6 +288,9 @@ Missing required argument '{arg}'. + + Option '--{requiredOption}' must be specified if '--{conditionalOption}' is used. + Missing required option '--{option}'. @@ -309,6 +312,9 @@ Don't use the pluralizer. + + Don't generate a compiled model. + Don't generate SQL transaction statements. @@ -330,6 +336,9 @@ Pending status not shown. Unable to determine which migrations have been applied. This can happen when your project uses a version of Entity Framework Core lower than 5.0.0 or when an error occurs while accessing the database. + + Generate precompiled queries. + Prefix output with level. diff --git a/src/ef/ReflectionOperationExecutor.cs b/src/ef/ReflectionOperationExecutor.cs index 9e3d8f05b61..a8cd7306bcb 100644 --- a/src/ef/ReflectionOperationExecutor.cs +++ b/src/ef/ReflectionOperationExecutor.cs @@ -21,6 +21,7 @@ internal class ReflectionOperationExecutor : OperationExecutorBase public ReflectionOperationExecutor( string assembly, string? startupAssembly, + string? project, string? projectDir, string? dataDirectory, string? rootNamespace, @@ -28,7 +29,7 @@ public ReflectionOperationExecutor( bool nullable, string[] remainingArguments, IOperationReportHandler reportHandler) - : base(assembly, startupAssembly, projectDir, rootNamespace, language, nullable, remainingArguments, reportHandler) + : base(assembly, startupAssembly, project, projectDir, rootNamespace, language, nullable, remainingArguments, reportHandler) { var reporter = new OperationReporter(reportHandler); var configurationFile = (startupAssembly ?? assembly) + ".config"; @@ -63,6 +64,7 @@ public ReflectionOperationExecutor( { { "targetName", AssemblyFileName }, { "startupTargetName", StartupAssemblyFileName }, + { "project", Project }, { "projectDir", ProjectDirectory }, { "rootNamespace", RootNamespace }, { "language", Language }, diff --git a/test/EFCore.Design.Tests/Design/Internal/DbContextOperationsTest.cs b/test/EFCore.Design.Tests/Design/Internal/DbContextOperationsTest.cs index 6ecc6b10a8e..acd89795a49 100644 --- a/test/EFCore.Design.Tests/Design/Internal/DbContextOperationsTest.cs +++ b/test/EFCore.Design.Tests/Design/Internal/DbContextOperationsTest.cs @@ -30,6 +30,7 @@ public void Can_pass_null_args() new TestOperationReporter(), assembly, assembly, + project: "", projectDir: "", rootNamespace: null, language: "C#", @@ -46,6 +47,7 @@ public void CreateContext_uses_exact_factory_method() new TestOperationReporter(), assembly, assembly, + project: "", projectDir: "", rootNamespace: null, language: "C#", @@ -164,6 +166,7 @@ private static TestDbContextOperations CreateOperations(Type testProgramType) new TestOperationReporter(), assembly, assembly, + project: "", projectDir: "", rootNamespace: null, language: "C#", @@ -176,8 +179,7 @@ private static TestWebHost CreateWebHost(Func new( new ServiceCollection() .AddDbContext( - b => - configureProvider(b.EnableServiceProviderCaching(false))) + b => configureProvider(b.EnableServiceProviderCaching(false))) .BuildServiceProvider(validateScopes: true)); private class TestContext : DbContext diff --git a/test/EFCore.Design.Tests/TestUtilities/TestDbContextOperations.cs b/test/EFCore.Design.Tests/TestUtilities/TestDbContextOperations.cs index 551bd8938cc..6c089e0e627 100644 --- a/test/EFCore.Design.Tests/TestUtilities/TestDbContextOperations.cs +++ b/test/EFCore.Design.Tests/TestUtilities/TestDbContextOperations.cs @@ -9,9 +9,10 @@ public class TestDbContextOperations( IOperationReporter reporter, Assembly assembly, Assembly startupAssembly, + string project, string projectDir, string rootNamespace, string language, bool nullable, string[] args, - AppServiceProviderFactory appServicesFactory) : DbContextOperations(reporter, assembly, startupAssembly, projectDir, rootNamespace, language, nullable, args, appServicesFactory); + AppServiceProviderFactory appServicesFactory) : DbContextOperations(reporter, assembly, startupAssembly, project, projectDir, rootNamespace, language, nullable, args, appServicesFactory); diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/PrecompiledQueryTestHelpers.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/PrecompiledQueryTestHelpers.cs index 928c97253a0..aefd4903e6c 100644 --- a/test/EFCore.Relational.Specification.Tests/TestUtilities/PrecompiledQueryTestHelpers.cs +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/PrecompiledQueryTestHelpers.cs @@ -114,7 +114,7 @@ public async Task FullSourceTest( // Perform precompilation var precompilationErrors = new List(); generatedFiles = precompiledQueryCodeGenerator.GeneratePrecompiledQueries( - compilation, syntaxGenerator, dbContext, precompilationErrors, additionalAssembly: assembly); + compilation, syntaxGenerator, dbContext, memberAccessReplacements: null, precompilationErrors, additionalAssembly: assembly); if (errorAsserter is null) {