Skip to content

Commit

Permalink
Use AssemblyLoadContext to load EFC.Design
Browse files Browse the repository at this point in the history
Fixes #35265
  • Loading branch information
AndriySvyryd committed Jan 25, 2025
1 parent 3d9cdf9 commit 19535a7
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 53 deletions.
17 changes: 16 additions & 1 deletion src/EFCore.Tasks/Tasks/Internal/OperationTaskBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public abstract class OperationTaskBase : ToolTask
/// </summary>
public ITaskItem? StartupAssembly { get; set; }

/// <summary>
/// The location of Microsoft.EntityFrameworkCore.Design.dll
/// </summary>
public ITaskItem? DesignAssembly { get; set; }

/// <summary>
/// The target framework moniker.
/// </summary>
Expand Down Expand Up @@ -188,13 +193,17 @@ protected override string GenerateCommandLineCommands()
args.Add(runtimeFrameworkVersion);
}

#if !NET10_0
#elif NET472
#error Target framework needs to be updated here
#endif
args.Add(
Path.Combine(
Path.GetDirectoryName(typeof(OperationTaskBase).Assembly.Location)!,
"..",
"..",
"tools",
"netcoreapp2.0",
"net10.0",
"ef.dll"));

args.AddRange(AdditionalArguments);
Expand All @@ -207,6 +216,12 @@ protected override string GenerateCommandLineCommands()
args.Add(Path.ChangeExtension(StartupAssembly.ItemSpec, ".dll"));
}

if (DesignAssembly != null)
{
args.Add("--design-assembly");
args.Add(DesignAssembly.ItemSpec);
}

if (Project != null)
{
args.Add("--project");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ For Publish:
Properties="Configuration=$(Configuration);Platform=$(Platform);_EFGenerationStage=$(_EFGenerationStage)" />
</Target>

<Target Name="OptimizeDbContext">
<Target Name="OptimizeDbContext" DependsOnTargets="ResolvePackageAssets">
<PropertyGroup>
<EFRootNamespace Condition="'$(EFRootNamespace)'==''">$(RootNamespace)</EFRootNamespace>
<EFRootNamespace Condition="'$(EFRootNamespace)'==''">$(AssemblyName)</EFRootNamespace>
Expand All @@ -90,9 +90,14 @@ For Publish:
<EFNullable Condition="'$(EFNullable)'==''">false</EFNullable>
</PropertyGroup>

<ItemGroup>
<DesignAssembly Include="@(RuntimeCopyLocalItems)" Condition="$([System.String]::Copy('%(FullPath)').EndsWith('Microsoft.EntityFrameworkCore.Design.dll'))" />
</ItemGroup>

<OptimizeDbContext Assembly="$(_AssemblyFullName)"
StartupAssembly="$(EFStartupAssembly)"
ProjectAssetsFile="$(ProjectAssetsFile)"
DesignAssembly="@(DesignAssembly->'%(FullPath)')"
RuntimeFrameworkVersion="$(RuntimeFrameworkVersion)"
TargetFrameworkMoniker="$(TargetFrameworkMoniker)"
DbContextType="$(DbContextType)"
Expand Down
9 changes: 9 additions & 0 deletions src/EFCore.Tools/tools/EntityFrameworkCore.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,15 @@ function EF($project, $startupProject, $params, $applicationArgs, [switch] $skip
$params += '--nullable'
}

# NB: -join is here to support ConvertFrom-Json on PowerShell 3.0
$references = (dotnet build $startupProject.FullName /t:ResolvePackageAssets /getItem:RuntimeCopyLocalItems) -join "`n" | ConvertFrom-Json

$designReference = $references.Items.RuntimeCopyLocalItems | ? { $_.FullPath.EndsWith('Microsoft.EntityFrameworkCore.Design.dll') }
if ($designReference -ne $null)
{
$params += '--design-assembly', $designReference.FullPath
}

$arguments = ToArguments $params
if ($applicationArgs)
{
Expand Down
101 changes: 54 additions & 47 deletions src/dotnet-ef/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public Project(string file, string? framework, string? configuration, string? ru
public string ProjectName { get; }

public string? AssemblyName { get; set; }
public string? DesignAssembly { get; set; }
public string? Language { get; set; }
public string? OutputPath { get; set; }
public string? PlatformTarget { get; set; }
Expand All @@ -50,78 +51,84 @@ public static Project FromFile(
{
Debug.Assert(!string.IsNullOrEmpty(file), "file is null or empty.");

IDictionary<string, string> metadata;
var metadataFile = Path.GetTempFileName();
try
{
var args = new List<string>
var args = new List<string>
{
"msbuild",
};

if (framework != null)
{
args.Add($"/property:TargetFramework={framework}");
}
if (framework != null)
{
args.Add($"/property:TargetFramework={framework}");
}

if (configuration != null)
{
args.Add($"/property:Configuration={configuration}");
}
if (configuration != null)
{
args.Add($"/property:Configuration={configuration}");
}

if (runtime != null)
{
args.Add($"/property:RuntimeIdentifier={runtime}");
}
if (runtime != null)
{
args.Add($"/property:RuntimeIdentifier={runtime}");
}

foreach (var property in typeof(Project).GetProperties())
{
args.Add($"/getProperty:{property.Name}");
}
foreach (var property in typeof(Project).GetProperties())
{
args.Add($"/getProperty:{property.Name}");
}

args.Add("/getProperty:Platform");
args.Add("/getProperty:Platform");

args.Add(file);
args.Add("/t:ResolvePackageAssets");
args.Add("/getItem:RuntimeCopyLocalItems");

var output = new StringBuilder();
args.Add(file);

var exitCode = Exe.Run("dotnet", args, handleOutput: line => output.AppendLine(line));
if (exitCode != 0)
{
throw new CommandException(Resources.GetMetadataFailed);
}
var output = new StringBuilder();

metadata = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(output.ToString())!["Properties"];
}
finally
var exitCode = Exe.Run("dotnet", args, handleOutput: line => output.AppendLine(line));
if (exitCode != 0)
{
File.Delete(metadataFile);
throw new CommandException(Resources.GetMetadataFailed);
}

var platformTarget = metadata[nameof(PlatformTarget)];
var metadata = JsonSerializer.Deserialize<ProjectMetadata>(output.ToString())!;

var designAssembly = metadata.Items["RuntimeCopyLocalItems"]
.Select(i => i["FullPath"])
.FirstOrDefault(i => i.Contains("Microsoft.EntityFrameworkCore.Design", StringComparison.InvariantCulture));
var properties = metadata.Properties;

var platformTarget = properties[nameof(PlatformTarget)];
if (platformTarget.Length == 0)
{
platformTarget = metadata["Platform"];
platformTarget = properties["Platform"];
}

return new Project(file, framework, configuration, runtime)
{
AssemblyName = metadata[nameof(AssemblyName)],
Language = metadata[nameof(Language)],
OutputPath = metadata[nameof(OutputPath)],
AssemblyName = properties[nameof(AssemblyName)],
DesignAssembly = designAssembly,
Language = properties[nameof(Language)],
OutputPath = properties[nameof(OutputPath)],
PlatformTarget = platformTarget,
ProjectAssetsFile = metadata[nameof(ProjectAssetsFile)],
ProjectDir = metadata[nameof(ProjectDir)],
RootNamespace = metadata[nameof(RootNamespace)],
RuntimeFrameworkVersion = metadata[nameof(RuntimeFrameworkVersion)],
TargetFileName = metadata[nameof(TargetFileName)],
TargetFrameworkMoniker = metadata[nameof(TargetFrameworkMoniker)],
Nullable = metadata[nameof(Nullable)],
TargetFramework = metadata[nameof(TargetFramework)],
TargetPlatformIdentifier = metadata[nameof(TargetPlatformIdentifier)]
ProjectAssetsFile = properties[nameof(ProjectAssetsFile)],
ProjectDir = properties[nameof(ProjectDir)],
RootNamespace = properties[nameof(RootNamespace)],
RuntimeFrameworkVersion = properties[nameof(RuntimeFrameworkVersion)],
TargetFileName = properties[nameof(TargetFileName)],
TargetFrameworkMoniker = properties[nameof(TargetFrameworkMoniker)],
Nullable = properties[nameof(Nullable)],
TargetFramework = properties[nameof(TargetFramework)],
TargetPlatformIdentifier = properties[nameof(TargetPlatformIdentifier)]
};
}

private record class ProjectMetadata
{
public Dictionary<string, string> Properties { get; set; } = null!;
public Dictionary<string, Dictionary<string, string>[]> Items { get; set; } = null!;
}

public void Build(IEnumerable<string>? additionalArgs)
{
var args = new List<string> { "build" };
Expand Down
7 changes: 7 additions & 0 deletions src/dotnet-ef/RootCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ protected override int Execute(string[] _)
args.Add("--framework");
args.Add(startupProject.TargetFramework!);

var designAssembly = startupProject.DesignAssembly;
if (!string.IsNullOrEmpty(designAssembly))
{
args.Add("--design-assembly");
args.Add(designAssembly);
}

if (_configuration.HasValue())
{
args.Add("--configuration");
Expand Down
62 changes: 58 additions & 4 deletions src/ef/Commands/ProjectCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Tools.Properties;
#if NET472
using System;
using System.Configuration;
#else
using System.Runtime.Loader;
#endif

namespace Microsoft.EntityFrameworkCore.Tools.Commands
Expand All @@ -20,6 +21,7 @@ internal abstract class ProjectCommandBase : EFCommandBase
private CommandOption? _language;
private CommandOption? _nullable;
private string? _efcoreVersion;
private CommandOption? _designAssembly;

protected CommandOption? Assembly { get; private set; }
protected CommandOption? Project { get; private set; }
Expand All @@ -29,10 +31,61 @@ internal abstract class ProjectCommandBase : EFCommandBase
protected CommandOption? Framework { get; private set; }
protected CommandOption? Configuration { get; private set; }

#if !NET472
private AssemblyLoadContext? _assemblyLoadContext;
protected AssemblyLoadContext AssemblyLoadContext
{
get
{
if (_assemblyLoadContext != null)
{
return _assemblyLoadContext;
}

if (_designAssembly!.Value() != null)
{
AssemblyLoadContext.Default.Resolving += (context, name) =>
{
var assemblyPath = Path.GetDirectoryName(_designAssembly!.Value())!;
assemblyPath = Path.Combine(assemblyPath, name.Name + ".dll");
return File.Exists(assemblyPath) ? context.LoadFromAssemblyPath(assemblyPath) : null;
};
_assemblyLoadContext = AssemblyLoadContext.Default;
}

return AssemblyLoadContext.Default;
}
}
#endif

protected string? EFCoreVersion
=> _efcoreVersion ??= System.Reflection.Assembly.Load("Microsoft.EntityFrameworkCore.Design")
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;
{
get
{
if (_efcoreVersion != null)
{
return _efcoreVersion;
}

Assembly? assembly = null;
#if !NET472
assembly = AssemblyLoadContext.LoadFromAssemblyName(new AssemblyName("Microsoft.EntityFrameworkCore.Design"));
#else
if (_designAssembly!.Value() != null)
{
var assemblyPath = Path.GetDirectoryName(_designAssembly!.Value());
assemblyPath = Path.Combine(assemblyPath, "Microsoft.EntityFrameworkCore.Design.dll");
assembly = System.Reflection.Assembly.LoadFrom(assemblyPath);
} else
{
assembly = System.Reflection.Assembly.Load("Microsoft.EntityFrameworkCore.Design");
}
#endif
_efcoreVersion = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;
return _efcoreVersion;
}
}

public override void Configure(CommandLineApplication command)
{
Expand All @@ -50,6 +103,7 @@ public override void Configure(CommandLineApplication command)
WorkingDir = command.Option("--working-dir <PATH>", Resources.WorkingDirDescription);
Framework = command.Option("--framework <FRAMEWORK>", Resources.FrameworkDescription);
Configuration = command.Option("--configuration <CONFIGURATION>", Resources.ConfigurationDescription);
_designAssembly = command.Option("--design-assembly <PATH>", Resources.DesignAssemblyDescription);

base.Configure(command);
}
Expand Down
6 changes: 6 additions & 0 deletions src/ef/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/ef/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@
<data name="DbContextType" xml:space="preserve">
<value>Type: {type}</value>
</data>
<data name="DesignAssemblyDescription" xml:space="preserve">
<value>The location of the referenced Microsoft.EntityFrameworkCore.Design assembly.</value>
</data>
<data name="DesignNotFound" xml:space="preserve">
<value>Your startup project '{startupProject}' doesn't reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project is correct, install the package, and try again.</value>
</data>
Expand Down

0 comments on commit 19535a7

Please sign in to comment.