diff --git a/Directory.Build.props b/Directory.Build.props index 922eddb183..c5555f06dd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,6 +10,7 @@ ( $(MSBuildProjectName.EndsWith('.Tests')) OR $(MSBuildProjectName.EndsWith('.FunctionalTests'))) ">true false + $(MSBuildThisFileDirectory) diff --git a/Directory.Build.targets b/Directory.Build.targets index bca11ece4c..1ecc794c90 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -8,7 +8,7 @@ - + diff --git a/EFCore.sln b/EFCore.sln index d45bf85cdf..9af5fa087f 100644 --- a/EFCore.sln +++ b/EFCore.sln @@ -138,6 +138,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.SqlServer.Abstractio EndProject Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "EFCore.VisualBasic.FunctionalTests", "test\EFCore.VisualBasic.FunctionalTests\EFCore.VisualBasic.FunctionalTests.vbproj", "{2AC6A8AC-5C0A-422A-B21A-CDC8D75F20A3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.Tasks", "src\EFCore.Tasks\EFCore.Tasks.csproj", "{711EE8F3-F92D-4470-8B0B-25D8B13EF282}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -152,6 +154,10 @@ Global {4F7C93F3-A30F-4061-804C-32293DC256A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F7C93F3-A30F-4061-804C-32293DC256A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F7C93F3-A30F-4061-804C-32293DC256A1}.Release|Any CPU.Build.0 = Release|Any CPU + {711EE8F3-F92D-4470-8B0B-25D8B13EF282}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {711EE8F3-F92D-4470-8B0B-25D8B13EF282}.Debug|Any CPU.Build.0 = Debug|Any CPU + {711EE8F3-F92D-4470-8B0B-25D8B13EF282}.Release|Any CPU.ActiveCfg = Release|Any CPU + {711EE8F3-F92D-4470-8B0B-25D8B13EF282}.Release|Any CPU.Build.0 = Release|Any CPU {715C38E9-B2F5-4DB2-8025-0C6492DEBDD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {715C38E9-B2F5-4DB2-8025-0C6492DEBDD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {715C38E9-B2F5-4DB2-8025-0C6492DEBDD4}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -371,6 +377,7 @@ Global GlobalSection(NestedProjects) = preSolution {2D66A1DA-D102-4DD9-960B-7D863BBB53DE} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} {4F7C93F3-A30F-4061-804C-32293DC256A1} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} + {711EE8F3-F92D-4470-8B0B-25D8B13EF282} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} {715C38E9-B2F5-4DB2-8025-0C6492DEBDD4} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} {11B51A41-47CB-4EDB-9D8A-17095A65034A} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} {D3D0A8E8-EC2F-4E01-8650-8554E186A66F} = {CE6B50B2-34AE-44C9-940A-4E48C3E1B3BC} diff --git a/eng/Versions.props b/eng/Versions.props index 1067ab3366..f38f48d693 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -31,10 +31,11 @@ 9.0.0-beta.24114.1 + 17.0.0 + 17.0.0 + 17.0.0 4.8.0 1.1.2-beta1.23578.3 - 2.6.1 - 2.5.3 diff --git a/eng/testing/linker/trimmingTests.props b/eng/testing/linker/trimmingTests.props index f1928b5afd..3945aa10e8 100644 --- a/eng/testing/linker/trimmingTests.props +++ b/eng/testing/linker/trimmingTests.props @@ -1,7 +1,6 @@ - $([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'trimmingTests')) - $([MSBuild]::NormalizeDirectory('$(TrimmingTestDir)', 'projects')) + $([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)')) $(MSBuildThisFileDirectory)project.csproj.template enable diff --git a/eng/testing/linker/trimmingTests.targets b/eng/testing/linker/trimmingTests.targets index 2174c5ab2a..b8b04229e0 100644 --- a/eng/testing/linker/trimmingTests.targets +++ b/eng/testing/linker/trimmingTests.targets @@ -64,7 +64,7 @@ <_additionalProjectReferenceTemp Include="$(AdditionalProjectReferences)" /> - <_additionalProjectReference Include="<ProjectReference Include="$(LibrariesProjectRoot)%(_additionalProjectReferenceTemp.Identity)\src\%(_additionalProjectReferenceTemp.Identity).csproj" SkipUseReferenceAssembly="true" />" /> + <_additionalProjectReference Include="<ProjectReference Include="$(SolutionRoot)%(_additionalProjectReferenceTemp.Identity)\src\%(_additionalProjectReferenceTemp.Identity).csproj" SkipUseReferenceAssembly="true" />" /> diff --git a/src/EFCore.Design/EFCore.Design.csproj b/src/EFCore.Design/EFCore.Design.csproj index c91aa89bdd..5dd5be8a41 100644 --- a/src/EFCore.Design/EFCore.Design.csproj +++ b/src/EFCore.Design/EFCore.Design.csproj @@ -47,10 +47,7 @@ - - True - build - + diff --git a/src/EFCore.Tasks/EFCore.Tasks.csproj b/src/EFCore.Tasks/EFCore.Tasks.csproj new file mode 100644 index 0000000000..6f52fa837c --- /dev/null +++ b/src/EFCore.Tasks/EFCore.Tasks.csproj @@ -0,0 +1,88 @@ + + + + MSBuild tasks for Entity Framework Core projects. + $(DefaultNetCoreTargetFramework);net472 + Microsoft.EntityFrameworkCore.Tasks + Microsoft.EntityFrameworkCore + false + true + true + true + NU5100;NU5128 + true + $(MSBuildThisFileDirectory)..\..\rulesets\EFCore.noxmldocs.ruleset + + + + + + + + + + + + + + TextTemplatingFileGenerator + Resources.Designer.cs + Microsoft.EntityFrameworkCore.Tools.Properties + + + + + + True + True + Resources.Designer.tt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + $(MSBuildThisFileDirectory)$(MSBuildProjectName).nuspec + + + + + + + + + + + + + + + + diff --git a/src/EFCore.Tasks/EFCore.Tasks.nuspec b/src/EFCore.Tasks/EFCore.Tasks.nuspec new file mode 100644 index 0000000000..12df70caf8 --- /dev/null +++ b/src/EFCore.Tasks/EFCore.Tasks.nuspec @@ -0,0 +1,23 @@ + + + + + $CommonMetadataElements$ + + + + + + docs\PACKAGE.md + + + $CommonFileElements$ + + + + + + + + + diff --git a/src/EFCore.Tasks/PACKAGE.md b/src/EFCore.Tasks/PACKAGE.md new file mode 100644 index 0000000000..d073d54d1f --- /dev/null +++ b/src/EFCore.Tasks/PACKAGE.md @@ -0,0 +1,17 @@ +The Entity Framework Core MSBuild tasks integrate EF design-time tools into the build process. They're primarily used to generate the compiled model. + +This package should be referenced by the project containing the derived `DbContext`. + +## Usage + +Install the package into your project, set `true` and then run build normally. + +If the startup project is different from the current project it needs to be specified: `..\Startup\Startup.csproj` + +## Getting started with EF Core + +See [Getting started with EF Core](https://learn.microsoft.com/ef/core/get-started/overview/install) for more information about EF NuGet packages, including which to install when getting started. + +## Feedback + +If you encounter a bug or issues with this package,you can [open an Github issue](https://github.com/dotnet/efcore/issues/new/choose). For more details, see [getting support](https://github.com/dotnet/efcore/blob/main/.github/SUPPORT.md). diff --git a/src/EFCore.Tasks/Properties/Resources.Designer.cs b/src/EFCore.Tasks/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..137060329e --- /dev/null +++ b/src/EFCore.Tasks/Properties/Resources.Designer.cs @@ -0,0 +1,58 @@ +// + +using System; +using System.Reflection; +using System.Resources; + +#nullable enable + +namespace Microsoft.EntityFrameworkCore.Tools.Properties +{ + /// + /// 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. + /// + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.EntityFrameworkCore.Properties.Resources", typeof(Resources).Assembly); + + /// + /// Startup project '{startupProject}' targets framework '.NETCoreApp' version '{targetFrameworkVersion}'. This version of the Entity Framework Core .NET Command-line Tools only supports version 2.0 or higher. For information on using older versions of the tools, see https://go.microsoft.com/fwlink/?linkid=871254 + /// + public static string NETCoreApp1StartupProject(object? startupProject, object? targetFrameworkVersion) + => string.Format( + GetString("NETCoreApp1StartupProject", nameof(startupProject), nameof(targetFrameworkVersion)), + startupProject, targetFrameworkVersion); + + /// + /// Startup project '{startupProject}' targets framework '.NETStandard'. There is no runtime associated with this framework, and projects targeting it cannot be executed directly. To use the Entity Framework Core .NET Command-line Tools with this project, add an executable project targeting .NET Core or .NET Framework that references this project, and set it as the startup project using --startup-project; or, update this project to cross-target .NET Core or .NET Framework. For more information on using the Entity Framework Tools with .NET Standard projects, see https://go.microsoft.com/fwlink/?linkid=2034781 + /// + public static string NETStandardStartupProject(object? startupProject) + => string.Format( + GetString("NETStandardStartupProject", nameof(startupProject)), + startupProject); + + /// + /// Startup project '{startupProject}' targets framework '{targetFramework}'. The Entity Framework Core .NET Command-line Tools don't support this framework. See https://aka.ms/efcore-docs-cli-tfms for more information. + /// + public static string UnsupportedFramework(object? startupProject, object? targetFramework) + => string.Format( + GetString("UnsupportedFramework", nameof(startupProject), nameof(targetFramework)), + startupProject, targetFramework); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name)!; + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + + return value; + } + } +} + diff --git a/src/EFCore.Tasks/Properties/Resources.Designer.tt b/src/EFCore.Tasks/Properties/Resources.Designer.tt new file mode 100644 index 0000000000..9e9ef701f7 --- /dev/null +++ b/src/EFCore.Tasks/Properties/Resources.Designer.tt @@ -0,0 +1,7 @@ +<# + Session["ResourceFile"] = "Resources.resx"; + Session["ResourceNamespace"] = "Microsoft.EntityFrameworkCore.Properties"; + Session["AccessModifier"] = "internal"; + Session["NoDiagnostics"] = true; +#> +<#@ include file="..\..\..\tools\Resources.tt" #> \ No newline at end of file diff --git a/src/EFCore.Tasks/Properties/Resources.resx b/src/EFCore.Tasks/Properties/Resources.resx new file mode 100644 index 0000000000..5d2fa92187 --- /dev/null +++ b/src/EFCore.Tasks/Properties/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Startup project '{startupProject}' targets framework '.NETCoreApp' version '{targetFrameworkVersion}'. This version of the Entity Framework Core .NET Command-line Tools only supports version 2.0 or higher. For information on using older versions of the tools, see https://go.microsoft.com/fwlink/?linkid=871254 + + + Startup project '{startupProject}' targets framework '.NETStandard'. There is no runtime associated with this framework, and projects targeting it cannot be executed directly. To use the Entity Framework Core .NET Command-line Tools with this project, add an executable project targeting .NET Core or .NET Framework that references this project, and set it as the startup project using --startup-project; or, update this project to cross-target .NET Core or .NET Framework. For more information on using the Entity Framework Tools with .NET Standard projects, see https://go.microsoft.com/fwlink/?linkid=2034781 + + + Startup project '{startupProject}' targets framework '{targetFramework}'. The Entity Framework Core .NET Command-line Tools don't support this framework. See https://aka.ms/efcore-docs-cli-tfms for more information. + + \ No newline at end of file diff --git a/src/EFCore.Tasks/Tasks/Internal/MsBuildUtilities.cs b/src/EFCore.Tasks/Tasks/Internal/MsBuildUtilities.cs new file mode 100644 index 0000000000..5fbaa229ef --- /dev/null +++ b/src/EFCore.Tasks/Tasks/Internal/MsBuildUtilities.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET472 +using System.Configuration; +#endif + +namespace Microsoft.EntityFrameworkCore.Tasks.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. +/// +internal class MsBuildUtilities +{ + public static string[] Split(string s) + => !string.IsNullOrEmpty(s) + ? s.Split(';') + .Select(entry => entry.Trim()) + .Where(entry => entry.Length != 0) + .ToArray() + : []; + + public static string? TrimAndGetNullForEmpty(string? s) + { + if (s == null) + { + return null; + } + + s = s.Trim(); + + return s.Length == 0 ? null : s; + } + + public static string[] TrimAndExcludeNullOrEmpty(string?[]? strings) + => strings == null + ? [] + : strings + .Select(TrimAndGetNullForEmpty) + .Where(s => s != null) + .Cast() + .ToArray(); + + public static bool IsTrue(string? value) => bool.TrueString.Equals(TrimAndGetNullForEmpty(value), StringComparison.OrdinalIgnoreCase); + + public static bool IsTrueOrEmpty(string? value) => TrimAndGetNullForEmpty(value) == null || IsTrue(value); + + public static bool? GetBooleanOrNull(string? value) => bool.TryParse(value, out var result) ? result : null; + + public static string? ToMsBuild(string? value) => value?.Replace(',', ';'); +} diff --git a/src/EFCore.Tasks/Tasks/Internal/OperationTaskBase.cs b/src/EFCore.Tasks/Tasks/Internal/OperationTaskBase.cs new file mode 100644 index 0000000000..9d82495a61 --- /dev/null +++ b/src/EFCore.Tasks/Tasks/Internal/OperationTaskBase.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Versioning; +using System.Text; +using System.Text.Json; +using Microsoft.Build.Framework; +using Microsoft.EntityFrameworkCore.Tools; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tasks.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 abstract class OperationTaskBase : Build.Utilities.Task +{ + /// + /// The assembly to use. + /// + [Required] + public ITaskItem Assembly { get; set; } = null!; + + /// + /// The startup assembly to use. + /// + [Required] + public ITaskItem StartupAssembly { get; set; } = null!; + + /// + /// The target framework moniker. + /// + [Required] + public string TargetFrameworkMoniker { get; set; } = null!; + + /// + /// The target runtime framework version. + /// + public string? RuntimeFrameworkVersion { get; set; } + + /// + /// The project assets file. + /// + public string? ProjectAssetsFile { get; set; } + + /// + /// The directory containing the database files. + /// + public ITaskItem? DataDir { get; set; } + + /// + /// The project directory. + /// + public ITaskItem? ProjectDir { get; set; } + + /// + /// The root namespace to use. + /// + public string? RootNamespace { get; set; } + + /// + /// The language to use. Defaults to C#. + /// + public string? Language { get; set; } + + /// + /// A flag indicating whether nullable reference types are enabled. + /// + public bool Nullable { get; set; } + + protected virtual bool Execute(IEnumerable additionalArguments, out string? result) + { + var args = new List(); + + var startupAssemblyName = Path.GetFileNameWithoutExtension(StartupAssembly.ItemSpec); + var targetDir = Path.GetDirectoryName(Path.GetFullPath(StartupAssembly.ItemSpec))!; + var depsFile = Path.Combine( + targetDir, + startupAssemblyName + ".deps.json"); + var runtimeConfig = Path.Combine( + targetDir, + startupAssemblyName + ".runtimeconfig.json"); + var projectAssetsFile = MsBuildUtilities.TrimAndGetNullForEmpty(ProjectAssetsFile); + + string executable; + var targetFramework = new FrameworkName(TargetFrameworkMoniker); + if (targetFramework.Identifier == ".NETCoreApp") + { + if (targetFramework.Version < new Version(2, 0)) + { + throw new InvalidOperationException( + Resources.NETCoreApp1StartupProject(startupAssemblyName, targetFramework.Version)); + } + + executable = "dotnet"; + args.Add("exec"); + + if (File.Exists(depsFile)) + { + args.Add("--depsfile"); + args.Add(depsFile); + } + + if (projectAssetsFile != null + && File.Exists(projectAssetsFile)) + { + using var file = File.OpenRead(projectAssetsFile); + using var reader = JsonDocument.Parse(file); + var projectAssets = reader.RootElement; + var packageFolders = projectAssets.GetProperty("packageFolders").EnumerateObject().Select(p => p.Name); + + foreach (var packageFolder in packageFolders) + { + args.Add("--additionalprobingpath"); + args.Add(packageFolder.TrimEnd(Path.DirectorySeparatorChar)); + } + } + + var runtimeFrameworkVersion = MsBuildUtilities.TrimAndGetNullForEmpty(RuntimeFrameworkVersion); + if (File.Exists(runtimeConfig)) + { + args.Add("--runtimeconfig"); + args.Add(runtimeConfig); + } + else if (runtimeFrameworkVersion != null) + { + args.Add("--fx-version"); + args.Add(runtimeFrameworkVersion); + } + + args.Add(Path.Combine( + Path.GetDirectoryName(typeof(OperationTaskBase).Assembly.Location)!, + "..", + "..", + "tools", + "netcoreapp2.0", + "ef.dll")); + } + else if (targetFramework.Identifier == ".NETStandard") + { + throw new InvalidOperationException(Resources.NETStandardStartupProject(startupAssemblyName)); + } + else + { + throw new InvalidOperationException( + Resources.UnsupportedFramework(startupAssemblyName, targetFramework.Identifier)); + } + + args.AddRange(additionalArguments); + args.Add("--assembly"); + args.Add(Assembly.ItemSpec); + + if (StartupAssembly != null) + { + args.Add("--startup-assembly"); + args.Add(StartupAssembly.ItemSpec); + } + + if (ProjectDir != null) + { + args.Add("--project-dir"); + args.Add(ProjectDir.ItemSpec); + } + + if (DataDir != null) { + args.Add("--data-dir"); + args.Add(DataDir.ItemSpec); + } + + var rootNamespace = MsBuildUtilities.TrimAndGetNullForEmpty(RootNamespace); + if (rootNamespace != null) { + args.Add("--root-namespace"); + args.Add(rootNamespace); + } + + var language = MsBuildUtilities.TrimAndGetNullForEmpty(Language); + if (language != null) { + args.Add("--language"); + args.Add(language); + } + + if (Nullable) + { + args.Add("--nullable"); + } + + args.Add("--working-dir"); + args.Add(Directory.GetCurrentDirectory()); + + args.Add("--verbose"); + args.Add("--no-color"); + args.Add("--prefix-output"); + + var resultBuilder = new StringBuilder(); + var exitCode = Exe.Run(executable, args, ProjectDir?.ItemSpec, HandleOutput, processCommandLine: Log.LogCommandLine); + result = resultBuilder.Length > 0 ? resultBuilder.ToString() : null; + + return exitCode == 0; + + void HandleOutput(string? output) + { + if (output == null) + { + return; + } + + if (output.StartsWith(Reporter.ErrorPrefix, StringComparison.InvariantCulture)) + { + Log.LogError(output.Substring(Reporter.ErrorPrefix.Length)); + } + else if (output.StartsWith(Reporter.WarningPrefix, StringComparison.InvariantCulture)) + { + Log.LogWarning(output.Substring(Reporter.WarningPrefix.Length)); + } + else if (output.StartsWith(Reporter.InfoPrefix, StringComparison.InvariantCulture)) + { + Log.LogMessage(output.Substring(Reporter.InfoPrefix.Length)); + } + else if (output.StartsWith(Reporter.VerbosePrefix, StringComparison.InvariantCulture)) + { + Log.LogMessage(MessageImportance.Low, output.Substring(Reporter.VerbosePrefix.Length)); + } + else if (output.StartsWith(Reporter.DataPrefix, StringComparison.InvariantCulture)) + { + resultBuilder.AppendLine(output.Substring(Reporter.DataPrefix.Length)); + } + else if(output.StartsWith("fail: ", StringComparison.InvariantCulture)) + { + Log.LogError(output.Substring(6)); + } + else if (output.StartsWith("warn: ", StringComparison.InvariantCulture)) + { + Log.LogWarning(output.Substring(6)); + } + else if (output.StartsWith("info: ", StringComparison.InvariantCulture)) + { + Log.LogMessage(output.Substring(6)); + } + else if (output.StartsWith("dbug: ", StringComparison.InvariantCulture) + || output.StartsWith("trce: ", StringComparison.InvariantCulture)) + { + Log.LogMessage(MessageImportance.Low, output.Substring(6)); + } + else + { + Log.LogError(output); + } + } + } +} diff --git a/src/EFCore.Tasks/Tasks/OptimizeContext.cs b/src/EFCore.Tasks/Tasks/OptimizeContext.cs new file mode 100644 index 0000000000..27524d13e2 --- /dev/null +++ b/src/EFCore.Tasks/Tasks/OptimizeContext.cs @@ -0,0 +1,84 @@ +// 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.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.EntityFrameworkCore.Tasks.Internal; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tasks; + +/// +/// Generates files that contain tailored code for some runtime services. +/// +public class OptimizeContext : OperationTaskBase +{ + /// + /// The name of the target DbContext. + /// + public string? DbContextName { get; set; } + + /// + /// The namespace to use for the generated classes. + /// + public string? TargetNamespace { get; set; } + + /// + /// The output directory. Usually, relative to the project directory. + /// + public ITaskItem? OutputDir { get; set; } + + /// + /// Generated files that should be include in the build. + /// + [Output] + public ITaskItem[] GeneratedFiles { get; private set; } = null!; + + /// + public override bool Execute() + { + try + { + Log.LogMessage(MessageImportance.High, "Optimizing DbContext..."); + + var additionalArguments = new List { "dbcontext", "optimize" }; + if (OutputDir != null) + { + additionalArguments.Add("--output-dir"); + additionalArguments.Add(OutputDir.ItemSpec); + } + + var targetNamespace = MsBuildUtilities.TrimAndGetNullForEmpty(TargetNamespace); + if (targetNamespace != null) + { + additionalArguments.Add("--namespace"); + additionalArguments.Add(targetNamespace); + } + + var dbContextName = MsBuildUtilities.TrimAndGetNullForEmpty(DbContextName); + if(dbContextName != null) + { + additionalArguments.Add("--context"); + additionalArguments.Add(dbContextName); + } + + var success = Execute(additionalArguments, out var result); + if (!success + || result == null) + { + return false; + } + + GeneratedFiles = result.Split(new string[] { Environment.NewLine }, StringSplitOptions.None) + .Select(f => new TaskItem(f)).ToArray(); + } + catch (Exception e) + { + Log.LogErrorFromException(e); + } + + return !Log.HasLoggedErrors; + } +} diff --git a/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.props b/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.props new file mode 100644 index 0000000000..0046e1a412 --- /dev/null +++ b/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.props @@ -0,0 +1,20 @@ + + + + <_TaskTargetFramework Condition="'$(MSBuildRuntimeType)' == 'core'">net8.0 + <_TaskTargetFramework Condition="'$(MSBuildRuntimeType)' != 'core'">net472 + <_EFCustomTasksAssembly>$([MSBuild]::NormalizePath($(MSBuildThisFileDirectory), '..\tasks\$(_TaskTargetFramework)\$(MSBuildThisFileName).dll')) + + + + + + false + + + + C# + VB + F# + + \ No newline at end of file diff --git a/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets b/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets new file mode 100644 index 0000000000..ebd85bdabf --- /dev/null +++ b/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets @@ -0,0 +1,116 @@ + + + + + $([MSBuild]::NormalizePath($(MSBuildProjectDirectory), '$(IntermediateOutputPath)$(AssemblyName).EFGeneratedFiles.txt')) + + + + + + $(MSBuildProjectFullPath) + $(RootNamespace) + $(AssemblyName) + $(EFRootNamespace) + <_FullOutputPath>$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), '$(OutputPath)')) + <_FullIntermediateOutputPath>$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), '$(IntermediateOutputPath)')) + false + + + + true + + + + <_AssemblyFullName>$(_FullOutputPath)$(AssemblyName).dll + + + <_AssemblyFullName>$(_FullOutputPath)$(AssemblyName).exe + + + <_AssemblyFullName>$(_FullOutputPath)$(AssemblyName).exe + + + + + + + + + + + + <_FullOutputPath>$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), '$(OutputPath)')) + + + + <_StartupAssemblyFullName>$(_FullOutputPath)$(AssemblyName).dll + + + <_StartupAssemblyFullName>$(_FullOutputPath)$(AssemblyName).exe + + + <_StartupAssemblyFullName>$(_FullOutputPath)$(AssemblyName).exe + + + + + + + + + + + + + + + + + + + + <_GeneratedFiles Include="@(_ReadGeneratedFiles)" /> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/EFCore.Tools/EFCore.Tools.csproj b/src/EFCore.Tools/EFCore.Tools.csproj index 91e375fae5..779f77df29 100644 --- a/src/EFCore.Tools/EFCore.Tools.csproj +++ b/src/EFCore.Tools/EFCore.Tools.csproj @@ -1,4 +1,4 @@ - + @@ -7,9 +7,12 @@ Microsoft.EntityFrameworkCore.Tools $(MSBuildThisFileDirectory)$(MSBuildProjectName).nuspec true + true true + true false false + true Entity Framework Core Tools for the NuGet Package Manager Console in Visual Studio. Enables these commonly used commands: @@ -25,58 +28,25 @@ Script-Migration Update-Database False - true - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + - - true - - - true - - - diff --git a/src/EFCore.Tools/EFCore.Tools.nuspec b/src/EFCore.Tools/EFCore.Tools.nuspec index 49db60b954..fa956e5db9 100644 --- a/src/EFCore.Tools/EFCore.Tools.nuspec +++ b/src/EFCore.Tools/EFCore.Tools.nuspec @@ -1,12 +1,11 @@ - + $CommonMetadataElements$ - 3.6 - + docs\PACKAGE.md @@ -14,7 +13,6 @@ $CommonFileElements$ - diff --git a/src/EFCore.Tools/lib/net8.0/_._ b/src/EFCore.Tools/lib/net8.0/_._ deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/dotnet-ef/Exe.cs b/src/dotnet-ef/Exe.cs index bcac989a39..1501de4660 100644 --- a/src/dotnet-ef/Exe.cs +++ b/src/dotnet-ef/Exe.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.Collections.Generic; using System.Diagnostics; using System.Text; @@ -13,33 +12,53 @@ internal static class Exe string executable, IReadOnlyList args, string? workingDirectory = null, - bool interceptOutput = false) + Action? handleOutput = null, + Action? handleError = null, + Action? processCommandLine = null) { var arguments = ToArguments(args); - Reporter.WriteVerbose(executable + " " + arguments); + processCommandLine ??= Reporter.WriteVerbose; + processCommandLine(executable + " " + arguments); var startInfo = new ProcessStartInfo { FileName = executable, Arguments = arguments, UseShellExecute = false, - RedirectStandardOutput = interceptOutput + RedirectStandardOutput = handleOutput != null, + RedirectStandardError = handleError != null }; if (workingDirectory != null) { startInfo.WorkingDirectory = workingDirectory; } - var process = Process.Start(startInfo)!; - - if (interceptOutput) + var process = new Process { - string? line; - while ((line = process.StandardOutput.ReadLine()) != null) - { - Reporter.WriteVerbose(line); - } + StartInfo = startInfo + }; + + if (handleOutput != null) + { + process.OutputDataReceived += (sender, args) => handleOutput(args.Data); + } + + if (handleError != null) + { + process.ErrorDataReceived += (sender, args) => handleError(args.Data); + } + + process.Start(); + + if (handleOutput != null) + { + process.BeginOutputReadLine(); + } + + if (handleError != null) + { + process.BeginErrorReadLine(); } process.WaitForExit(); diff --git a/src/dotnet-ef/Project.cs b/src/dotnet-ef/Project.cs index a37a869dae..1296a2d1c4 100644 --- a/src/dotnet-ef/Project.cs +++ b/src/dotnet-ef/Project.cs @@ -185,7 +185,7 @@ internal class Project args.Add("/nologo"); args.Add("/p:PublishAot=false"); // Avoid NativeAOT warnings - var exitCode = Exe.Run("dotnet", args, interceptOutput: true); + var exitCode = Exe.Run("dotnet", args, handleOutput: Reporter.WriteVerbose); if (exitCode != 0) { throw new CommandException(Resources.BuildFailed); diff --git a/src/ef/AnsiTextWriter.cs b/src/ef/AnsiTextWriter.cs index 689d3ccb74..41239c4c1f 100644 --- a/src/ef/AnsiTextWriter.cs +++ b/src/ef/AnsiTextWriter.cs @@ -1,10 +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; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Text.RegularExpressions; namespace Microsoft.EntityFrameworkCore.Tools; @@ -30,7 +27,7 @@ internal class AnsiTextWriter private void Interpret(string value) { - var matches = Regex.Matches(value, "\x1b\\[([0-9]+)?m"); + var matches = Regex.Matches(value, "\x1b\\[([0-9]+)?m", RegexOptions.None, TimeSpan.FromSeconds(10)); var start = 0; foreach (var match in matches.Cast()) diff --git a/src/ef/Commands/DbContextOptimizeCommand.cs b/src/ef/Commands/DbContextOptimizeCommand.cs index 38f6602ff4..9b9bcc4afc 100644 --- a/src/ef/Commands/DbContextOptimizeCommand.cs +++ b/src/ef/Commands/DbContextOptimizeCommand.cs @@ -17,11 +17,21 @@ internal partial class DbContextOptimizeCommand } using var executor = CreateExecutor(args); - executor.OptimizeContext( + var result = executor.OptimizeContext( _outputDir!.Value(), _namespace!.Value(), Context!.Value()); + ReportResults(result); + return base.Execute(args); } + + private static void ReportResults(IEnumerable generatedFiles) + { + foreach (var file in generatedFiles) + { + Reporter.WriteData(file); + } + } } diff --git a/src/ef/Commands/MigrationsBundleCommand.cs b/src/ef/Commands/MigrationsBundleCommand.cs index 845b68bc4a..5e8dd3b660 100644 --- a/src/ef/Commands/MigrationsBundleCommand.cs +++ b/src/ef/Commands/MigrationsBundleCommand.cs @@ -177,7 +177,7 @@ internal partial class MigrationsBundleCommand publishArgs.Add("--disable-build-servers"); - var exitCode = Exe.Run("dotnet", publishArgs, directory, interceptOutput: true); + var exitCode = Exe.Run("dotnet", publishArgs, directory, handleOutput: Reporter.WriteVerbose); if (exitCode != 0) { throw new CommandException(Resources.BuildBundleFailed); diff --git a/src/ef/Reporter.cs b/src/ef/Reporter.cs index 11469e4b6b..446fe1fb6c 100644 --- a/src/ef/Reporter.cs +++ b/src/ef/Reporter.cs @@ -8,6 +8,12 @@ namespace Microsoft.EntityFrameworkCore.Tools; internal static class Reporter { + public const string ErrorPrefix = "error: "; + public const string WarningPrefix = "warn: "; + public const string InfoPrefix = "info: "; + public const string DataPrefix = "data: "; + public const string VerbosePrefix = "verbose: "; + public static bool IsVerbose { get; set; } public static bool NoColor { get; set; } public static bool PrefixOutput { get; set; } @@ -17,22 +23,22 @@ internal static class Reporter => NoColor ? value : colorizeFunc(value); public static void WriteError(string? message) - => WriteLine(Prefix("error: ", Colorize(message, x => Bold + Red + x + Reset))); + => WriteLine(Prefix(ErrorPrefix, Colorize(message, x => Bold + Red + x + Reset))); public static void WriteWarning(string? message) - => WriteLine(Prefix("warn: ", Colorize(message, x => Bold + Yellow + x + Reset))); + => WriteLine(Prefix(WarningPrefix, Colorize(message, x => Bold + Yellow + x + Reset))); public static void WriteInformation(string? message) - => WriteLine(Prefix("info: ", message)); + => WriteLine(Prefix(InfoPrefix, message)); public static void WriteData(string? message) - => WriteLine(Prefix("data: ", Colorize(message, x => Bold + Gray + x + Reset))); + => WriteLine(Prefix(DataPrefix, Colorize(message, x => Bold + Gray + x + Reset))); public static void WriteVerbose(string? message) { if (IsVerbose) { - WriteLine(Prefix("verbose: ", Colorize(message, x => Bold + Black + x + Reset))); + WriteLine(Prefix(VerbosePrefix, Colorize(message, x => Bold + Black + x + Reset))); } } diff --git a/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj b/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj index 37019a1aa2..865de57807 100644 --- a/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj +++ b/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj @@ -48,7 +48,6 @@ - diff --git a/test/EFCore.Specification.Tests/TestUtilities/ModelAsserter.cs b/test/EFCore.Specification.Tests/TestUtilities/ModelAsserter.cs index e7493ce3c4..e6b2b0c1ab 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/ModelAsserter.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/ModelAsserter.cs @@ -72,6 +72,10 @@ public class ModelAsserter expectedEntityTypes = expectedEntityTypes.OrderBy(p => p.Name); actualEntityTypes = actualEntityTypes.OrderBy(p => p.Name); } + else + { + expectedEntityTypes = expectedEntityTypes.Select(x => x); + } Assert.Equal(expectedEntityTypes, actualEntityTypes, (expected, actual) => @@ -214,6 +218,10 @@ public class ModelAsserter expectedProperties = expectedProperties.OrderBy(p => p.Name); actualProperties = actualProperties.OrderBy(p => p.Name); } + else + { + expectedProperties = expectedProperties.Select(x => x); + } Assert.Equal(expectedProperties, actualProperties, (expected, actual) => @@ -280,6 +288,10 @@ public class ModelAsserter expectedProperties = expectedProperties.OrderBy(p => p.Name); actualProperties = actualProperties.OrderBy(p => p.Name); } + else + { + expectedProperties = expectedProperties.Select(x => x); + } Assert.Equal(expectedProperties, actualProperties, (expected, actual) => @@ -407,6 +419,10 @@ public class ModelAsserter expectedProperties = expectedProperties.OrderBy(p => p.Name); actualProperties = actualProperties.OrderBy(p => p.Name); } + else + { + expectedProperties = expectedProperties.Select(x => x); + } Assert.Equal(expectedProperties, actualProperties, (expected, actual) => @@ -465,6 +481,10 @@ public class ModelAsserter expectedNavigations = expectedNavigations.OrderBy(p => p.Name); actualNavigations = actualNavigations.OrderBy(p => p.Name); } + else + { + expectedNavigations = expectedNavigations.Select(x => x); + } Assert.Equal(expectedNavigations, actualNavigations, (expected, actual) => @@ -540,6 +560,10 @@ public class ModelAsserter expectedNavigations = expectedNavigations.OrderBy(p => p.Name); actualNavigations = actualNavigations.OrderBy(p => p.Name); } + else + { + expectedNavigations = expectedNavigations.Select(x => x); + } Assert.Equal(expectedNavigations, actualNavigations, (expected, actual) => @@ -615,6 +639,10 @@ public class ModelAsserter expectedKeys = expectedKeys.Order(KeyComparer.Instance); actualKeys = actualKeys.Order(KeyComparer.Instance); } + else + { + expectedKeys = expectedKeys.Select(x => x); + } Assert.Equal(expectedKeys, actualKeys, (expected, actual) => @@ -689,6 +717,10 @@ public class ModelAsserter expectedForeignKey = expectedForeignKey.Order(ForeignKeyComparer.Instance); actualForeignKey = actualForeignKey.Order(ForeignKeyComparer.Instance); } + else + { + expectedForeignKey = expectedForeignKey.Select(x => x); + } Assert.Equal(expectedForeignKey, actualForeignKey, (expected, actual) => @@ -777,6 +809,10 @@ public class ModelAsserter expectedIndex = expectedIndex.Order(IndexComparer.Instance); actualIndex = actualIndex.Order(IndexComparer.Instance); } + else + { + expectedIndex = expectedIndex.Select(x => x); + } Assert.Equal(expectedIndex, actualIndex, (expected, actual) =>