Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI commands on desktop: resolve startup assembly from external project #5286

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/Microsoft.EntityFrameworkCore.Tools.Cli/OperationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,36 @@ public OperationExecutor(
#endif
}

var startupAssemblyName = startupProjectContext.ProjectFile.GetCompilerOptions(startupFramework, startupConfiguration).OutputName;
var startupCompilerOptions = startupProjectContext.ProjectFile.GetCompilerOptions(startupFramework, startupConfiguration);
var startupAssemblyName = startupCompilerOptions.OutputName;
var assemblyName = project.GetCompilerOptions(projectFramework, projectConfiguration).OutputName;
var projectDir = project.ProjectDirectory;
var startupProjectDir = startupProjectContext.ProjectFile.ProjectDirectory;
var rootNamespace = project.Name;

var projectAssembly = Assembly.Load(new AssemblyName { Name = assemblyName });
#if NET451
// TODO use app domains
// TODO it would be better to follow the powershell approach: create new appdomain
// and remote into it to execute commands, rather than trying to bring startup assemblies into
// the current domain.
var startupAssemblyLoader = new AssemblyLoader(Assembly.Load);

AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler((sender, args) =>
{
// this is a partial fix. It may break down if startup project lifts to dependency versions higher than
// what the current project is using
var assemblyFileName = new AssemblyName(args.Name).Name;
var ext = assemblyFileName == startupAssemblyName && startupCompilerOptions.EmitEntryPoint == true
? ".exe"
: ".dll";

var assemblyPath = Path.Combine(runtimeOutputPath, new AssemblyName(args.Name).Name + ext);
if (!File.Exists(assemblyPath))
{
return null;
}
return Assembly.LoadFrom(assemblyPath);
});
#else
AssemblyLoader startupAssemblyLoader;
if (externalStartup)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@

namespace Microsoft.EntityFrameworkCore.Tools.Cli.FunctionalTests
{
public class DotNetEfFixture
public class DotNetEfFixture : IDisposable
{
private readonly string _solutionDir;
private string _suffix;
private bool _toolsBuilt;
private static readonly string LocalArtifacts = Path.Combine(AppContext.BaseDirectory, "artifacts");
private static readonly string _localArtifacts = Path.Combine(AppContext.BaseDirectory, "artifacts");
private static readonly string[] PackagesToBuild =
{
"Microsoft.EntityFrameworkCore.Tools",
Expand All @@ -27,6 +27,8 @@ public class DotNetEfFixture
"Microsoft.EntityFrameworkCore",
};

public string TestProjectRoot { get; } = Path.Combine(AppContext.BaseDirectory, "TestProjects");

public DotNetEfFixture()
{
var dir = AppContext.BaseDirectory;
Expand All @@ -35,6 +37,20 @@ public DotNetEfFixture()
dir = Path.GetDirectoryName(dir);
}
_solutionDir = dir;

foreach (var file in Directory.EnumerateFiles(TestProjectRoot, "project.json.ignore", SearchOption.AllDirectories))
{
File.Move(file, Path.Combine(Path.GetDirectoryName(file), "project.json"));
}
}

public void Dispose()
{
// cleanup to prevent later errors with restore
foreach (var file in Directory.EnumerateFiles(TestProjectRoot, "project.json", SearchOption.AllDirectories))
{
File.Move(file, Path.Combine(Path.GetDirectoryName(file), "project.json.ignore"));
}
}

private void BuildToolsPackages(ITestOutputHelper logger)
Expand All @@ -44,17 +60,17 @@ private void BuildToolsPackages(ITestOutputHelper logger)
return;
}
_toolsBuilt = true;

_suffix = "t" + DateTime.UtcNow.ToString("yyMMddHHmmss");

foreach (var pkg in PackagesToBuild)
{
logger?.WriteLine($"Building {pkg} version {_suffix}");
// TODO can remove when https://github.com/NuGet/Home/issues/2469 is available
AssertCommand.Passes(
new PackCommand(
Path.Combine(_solutionDir, "src", pkg, Constants.ProjectFileName),
LocalArtifacts,
_localArtifacts,
logger)
.Execute($"--version-suffix {_suffix}"));
}
Expand All @@ -66,10 +82,10 @@ public void InstallTool(string projectPath, ITestOutputHelper logger, params str

var json = File.ReadAllText(projectPath);
File.WriteAllText(projectPath, json.Replace("$toolVersion$", _suffix));
var fallbacks = new [] { LocalArtifacts }
var fallbacks = new[] { _localArtifacts }
.Concat(fallbackRestoreLocations ?? Enumerable.Empty<string>())
.Select(s => "--fallbacksource " + s);

AssertCommand.Passes(
new RestoreCommand(projectPath, logger)
.Execute(" --verbosity error " + string.Join(" ", fallbacks))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ namespace Microsoft.EntityFrameworkCore.Tools.Cli.FunctionalTests
{
public class EndToEndTests : IClassFixture<DotNetEfFixture>
{
private static readonly string _testProjectRoot = Path.Combine(
AppContext.BaseDirectory,
"TestProjects");

private readonly ITestOutputHelper _output;
private readonly DotNetEfFixture _fixture;

Expand All @@ -26,7 +22,7 @@ public EndToEndTests(DotNetEfFixture fixture, ITestOutputHelper output)
}

[Fact(Skip = "Unreliable on CI")]
public void RunsMigrationCommandsOnDesktop()
public void MigrationsOnDesktop()
{
// TODO use xunit helpers from SpecTests. Currently this causes re-compilation of the test graph
// because of the pre-compile script on this project
Expand All @@ -37,89 +33,90 @@ public void RunsMigrationCommandsOnDesktop()
}

[Fact(Skip = "Unreliable on CI")]
public void RunsMigrationsForAspNetApp()
public void MigrationsOnDesktopWithContextInLibrary()
{
var ignoredJson = Path.Combine(_testProjectRoot, "LibraryUsingSqlite", "project.json.ignore");
var libraryProject = Path.Combine(_testProjectRoot, "LibraryUsingSqlite", "project.json");
File.Move(ignoredJson, libraryProject);

try
if (!PlatformServices.Default.Runtime.OperatingSystem.Equals("Windows", StringComparison.OrdinalIgnoreCase))
{
AssertCommand.Passes(new RestoreCommand(libraryProject, _output)
.Execute(" --verbosity error "));

AddAndApplyMigrationImpl("AspNetHostingPortableApp", "LibraryContext", "initialLibrary");
AddAndApplyMigrationImpl("AspNetHostingPortableApp", "TestContext", "initialTest");
}
finally
{
File.Move(libraryProject, ignoredJson);
return;
}

var startupProject = Path.Combine(_fixture.TestProjectRoot, "DesktopStartupProject/project.json");
var targetProject = Path.Combine(_fixture.TestProjectRoot, "DesktopClassLibrary/project.json");

_fixture.InstallTool(targetProject, _output, _fixture.TestProjectRoot);

AssertCommand.Passes(new RestoreCommand(startupProject, _output)
.Execute(" --verbosity error "));

AddAndApplyMigrationImpl(
project: "DesktopClassLibrary",
contextName: "DesktopContext",
migrationName: "Initial",
startupProject: "../DesktopStartupProject");
}

[Fact(Skip = "Unreliable on CI")]
public void AddMigrationToDifferentFolder()
public void MigrationsForAspNetApp()
{
var ignoredJson = Path.Combine(_testProjectRoot, "PortableAppWithTools", "project.json.ignore");
var libraryProject = Path.Combine(_testProjectRoot, "PortableAppWithTools", "project.json");
File.Move(ignoredJson, libraryProject);
var libraryProject = Path.Combine(_fixture.TestProjectRoot, "LibraryUsingSqlite", "project.json");
AssertCommand.Passes(new RestoreCommand(libraryProject, _output)
.Execute(" --verbosity error "));

try
{
Assert.False(Directory.Exists(Path.Combine(_testProjectRoot, "SomeOtherDir")));
AddAndApplyMigrationImpl("AspNetHostingPortableApp", "LibraryContext", "initialLibrary");
AddAndApplyMigrationImpl("AspNetHostingPortableApp", "TestContext", "initialTest");
}

[Fact(Skip = "Unreliable on CI")]
public void AddMigrationToDifferentFolder()
{
var libraryProject = Path.Combine(_fixture.TestProjectRoot, "PortableAppWithTools", "project.json");
Assert.False(Directory.Exists(Path.Combine(_fixture.TestProjectRoot, "SomeOtherDir")));

_fixture.InstallTool(libraryProject, _output, _testProjectRoot);
_fixture.InstallTool(libraryProject, _output, _fixture.TestProjectRoot);

AssertCommand.Passes(new MigrationAddCommand(libraryProject, "OtherFolderMigration", _output)
.Execute($" --context TestContext --output-dir ../SomeOtherDir"));
AssertCommand.Passes(new MigrationAddCommand(libraryProject, "OtherFolderMigration", _output)
.Execute($" --context TestContext --output-dir ../SomeOtherDir"));

Assert.True(Directory.Exists(Path.Combine(_testProjectRoot, "SomeOtherDir")));
Assert.True(Directory.EnumerateFiles(Path.Combine(_testProjectRoot, "SomeOtherDir"), "*.cs").Any());
}
finally
{
File.Move(libraryProject, ignoredJson);
}
Assert.True(Directory.Exists(Path.Combine(_fixture.TestProjectRoot, "SomeOtherDir")));
Assert.True(Directory.EnumerateFiles(Path.Combine(_fixture.TestProjectRoot, "SomeOtherDir"), "*.cs").Any());
}

[Theory(Skip = "Unreliable on CI")]
[InlineData("PortableAppWithTools")]
[InlineData("StandaloneAppWithTools")]
public void RunsMigrationCommandsForNetCoreApps(string project)
public void MigrationCommandsForNetCoreApps(string project)
=> AddAndApplyMigrationImpl(project, "TestContext", "Initial");

private void AddAndApplyMigrationImpl(string project, string contextName, string migrationName)
private void AddAndApplyMigrationImpl(
string project,
string contextName,
string migrationName,
string startupProject = null)
{
var ignoredJson = Path.Combine(_testProjectRoot, project, "project.json.ignore");
var testProject = Path.Combine(_testProjectRoot, project, "project.json");
File.Move(ignoredJson, testProject);

var migrationDir = Path.Combine(Path.GetDirectoryName(testProject), "Migrations");
var targetProject = Path.Combine(_fixture.TestProjectRoot, project, "project.json");
var migrationDir = Path.Combine(Path.GetDirectoryName(targetProject), "Migrations");
var snapshotFile = contextName + "ModelSnapshot.cs";

try
if (Directory.Exists(migrationDir))
{
if (Directory.Exists(migrationDir))
{
Assert.False(Directory.EnumerateFiles(migrationDir, snapshotFile, SearchOption.AllDirectories).Any());
}

_fixture.InstallTool(testProject, _output, _testProjectRoot);

AssertCommand.Passes(new MigrationAddCommand(testProject, migrationName, _output)
.Execute($" --context {contextName}"));
Assert.False(Directory.EnumerateFiles(migrationDir, snapshotFile, SearchOption.AllDirectories).Any());
}

Assert.True(Directory.EnumerateFiles(migrationDir, snapshotFile, SearchOption.AllDirectories).Any());
_fixture.InstallTool(targetProject, _output, _fixture.TestProjectRoot);

AssertCommand.Passes(new DatabaseUpdateCommand(testProject, _output)
.Execute($" --context {contextName}"));
}
finally
var args = $" --context {contextName}";
if (startupProject != null)
{
// project.json, even nested in bin/, will fail dotnet-restore on the solution
File.Move(testProject, ignoredJson);
args += $" --startup-project {startupProject}";
}
}

AssertCommand.Passes(new MigrationAddCommand(targetProject, migrationName, _output)
.Execute(args));

Assert.True(Directory.EnumerateFiles(migrationDir, snapshotFile, SearchOption.AllDirectories).Any());

AssertCommand.Passes(new DatabaseUpdateCommand(targetProject, _output)
.Execute(args));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.EntityFrameworkCore;

namespace DesktopClassLibrary
{
public class DesktopContext : DbContext
{
public DesktopContext(DbContextOptions<DesktopContext> options) : base (options) { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace DesktopClassLibrary
{
// TODO remove this when https://github.com/dotnet/cli/issues/2919 is resolved
public class Program
{
public static void Main() { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"version": "1.0.0-*",
"buildOptions": {
"emitEntryPoint": true
},
"dependencies": {
"Microsoft.EntityFrameworkCore.Tools": {
"version": "1.0.0-$toolVersion$",
"type": "build",
"target": "package"
},
"Microsoft.EntityFrameworkCore.Sqlite": {
"target": "project"
}
},
"frameworks": {
"net451": { }
},
"tools": {
"Microsoft.EntityFrameworkCore.Tools": {
"version": "1.0.0-$toolVersion$",
"imports": [
"portable-net451+win8"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace DesktopStartupProject
{
public class Program
{
public static void Main() { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using DesktopClassLibrary;

namespace DesktopStartupProject
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DesktopContext>(o => o.UseSqlite("Filename=./desktop.db"));

// Exercises assembly dependency conflict resolution
JsonConvert.SerializeObject(new object());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "1.0.0-*",
"buildOptions": {
"emitEntryPoint": true
},
"dependencies": {
"DesktopClassLibrary": {
"target": "project"
},
"Newtonsoft.Json": "8.0.3"
},
"frameworks": {
"net451": { }
}
}