From 132bd674f62dded020936e0a04a39a64f29ff0b0 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 6 Dec 2022 17:59:42 -0800 Subject: [PATCH] Experimental work towards #266 --- .../Generation/GenerationExecutor.cs | 75 ++++++++++++++++++- ...IntelliTect.Coalesce.CodeGeneration.csproj | 2 +- .../Utilities/LoggerReporter.cs | 36 +++++++++ .../TestDbContext/TestDbContext.cs | 11 +++ .../Tests/ClonerTest.cs | 47 ++++++++++++ .../IntelliTect.Coalesce.csproj | 10 ++- .../TypeUsage/DbContextTypeUsage.cs | 3 + 7 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 src/IntelliTect.Coalesce.CodeGeneration/Utilities/LoggerReporter.cs diff --git a/src/IntelliTect.Coalesce.CodeGeneration/Generation/GenerationExecutor.cs b/src/IntelliTect.Coalesce.CodeGeneration/Generation/GenerationExecutor.cs index c01465e4d..7023b67c4 100644 --- a/src/IntelliTect.Coalesce.CodeGeneration/Generation/GenerationExecutor.cs +++ b/src/IntelliTect.Coalesce.CodeGeneration/Generation/GenerationExecutor.cs @@ -7,7 +7,9 @@ using IntelliTect.Coalesce.CodeGeneration.Utilities; using IntelliTect.Coalesce.TypeDefinition; using IntelliTect.Coalesce.Validation; +using Microsoft.CodeAnalysis.Differencing; using Microsoft.DotNet.Cli.Utils; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; @@ -107,6 +109,75 @@ public async Task GenerateAsync(Type rootGenerator) types.Select(t => new SymbolTypeViewModel(rr, t)) ); +#if NET5_0_OR_GREATER + ResolveEventHandler assemblyResolver = (sender, args) => + { + var assemblyName = new AssemblyName(args.Name); + var allLoaded = AppDomain.CurrentDomain.GetAssemblies(); + var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == assemblyName.Name); + if (alreadyLoaded != null) + { + Logger.LogWarning($"Resolved {assemblyName.Name} to {alreadyLoaded.GetName().Version} ({assemblyName.Version} was requested by {genContext.DataProject.ProjectFileName})"); + return alreadyLoaded; + } + foreach (var extension in new[] { ".dll", ".exe" }) + { + var match = (genContext.DataProject as RoslynProjectContext).MsBuildProjectContext.CompilationAssemblies + .FirstOrDefault(a => a.Name == assemblyName.Name + extension); + if (match != null) + { + return Assembly.LoadFrom(match.ResolvedPath); + } + } + + return null; + }; + + AppDomain.CurrentDomain.AssemblyResolve += assemblyResolver; + try + { + foreach (var dbContext in rr.DbContexts) + { + // WARNING: This stuff is extremely fragile and implodes on the slightest version mismatch. + // It currently works if you launch a .net6 tool against a .net6 target project (e.g. coakesce-vue2.json). + // We may need to: Launch a second executable of the generator, which since it will be a fresh process, won't have any EF assemblies loaded, + // so we can load the Data project and all its dependencies into that fresh process, + // then extract and serialize the EF model (the bits we carea bout) and send it back into the generator process. + // Thought: Coalesce has roslyn in it, so create this second executable on the fly with the Data project as a dependency. + // Thought: What if the data project already has an entry point? Can we just use that? How did the old EF tooling work when it used to require an entry point? + + + // Load Microsoft.EntityFrameworkCore.Abstractions. + // If we don't load our own, local copy, + // then DB context construction will fail because we will have loaded our own version of .Relational + // and therefore the version of .Relational and .Abstractions won't match. + typeof(IndexAttribute).GetTypeInfo(); + + // See efcore\src\ef\ReflectionOperationExecutor.cs + var dataAsmPath = (genContext.DataProject as RoslynProjectContext).MsBuildProjectContext.AssemblyFullPath; + var assembly = Assembly.LoadFrom(dataAsmPath); + + var contextOperations = new Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations( + reporter: new LoggerReporter(Logger), + assembly: assembly, + startupAssembly: assembly, + projectDir: Environment.CurrentDirectory, + rootNamespace: null, + language: null, + nullable: false, + args: null); + + using var context = contextOperations.CreateContext(dbContext.ClassViewModel.FullyQualifiedName); + dbContext.Model = context.Model; + // TODO: Use the Model during code gen. + } + } + finally + { + AppDomain.CurrentDomain.AssemblyResolve -= assemblyResolver; + } +#endif + var validationResult = ValidateContext.Validate(rr); @@ -172,7 +243,9 @@ async Task TryLoadProject(ProjectConfiguration config) { try { - var projectContext = ServiceProvider.GetRequiredService().CreateContext(config, restorePackages); + var projectContext = ServiceProvider + .GetRequiredService() + .CreateContext(config, restorePackages); // Warn if the Coalesce versions referenced in the project don't // match the version of the code generation being ran. diff --git a/src/IntelliTect.Coalesce.CodeGeneration/IntelliTect.Coalesce.CodeGeneration.csproj b/src/IntelliTect.Coalesce.CodeGeneration/IntelliTect.Coalesce.CodeGeneration.csproj index b44be20fe..ad5869630 100644 --- a/src/IntelliTect.Coalesce.CodeGeneration/IntelliTect.Coalesce.CodeGeneration.csproj +++ b/src/IntelliTect.Coalesce.CodeGeneration/IntelliTect.Coalesce.CodeGeneration.csproj @@ -1,7 +1,7 @@  Core code generation library for IntelliTect.Coalesce - netstandard2.0 + netstandard2.0;netcoreapp3.1;net6.0;net7.0 AnyCPU Library diff --git a/src/IntelliTect.Coalesce.CodeGeneration/Utilities/LoggerReporter.cs b/src/IntelliTect.Coalesce.CodeGeneration/Utilities/LoggerReporter.cs new file mode 100644 index 000000000..581d0d308 --- /dev/null +++ b/src/IntelliTect.Coalesce.CodeGeneration/Utilities/LoggerReporter.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Logging; + +namespace IntelliTect.Coalesce.CodeGeneration.Utilities +{ +#if NET5_0_OR_GREATER + internal class LoggerReporter : Microsoft.EntityFrameworkCore.Design.Internal.IOperationReporter + { + public LoggerReporter(ILogger logger) + { + Logger = logger; + } + + public ILogger Logger { get; } + + public void WriteError(string message) + { + Logger.LogError(message); + } + + public void WriteInformation(string message) + { + Logger.LogInformation(message); + } + + public void WriteVerbose(string message) + { + Logger.LogTrace(message); + } + + public void WriteWarning(string message) + { + Logger.LogWarning(message); + } + } +#endif +} diff --git a/src/IntelliTect.Coalesce.Tests/TargetClasses/TestDbContext/TestDbContext.cs b/src/IntelliTect.Coalesce.Tests/TargetClasses/TestDbContext/TestDbContext.cs index da117909c..057de3622 100644 --- a/src/IntelliTect.Coalesce.Tests/TargetClasses/TestDbContext/TestDbContext.cs +++ b/src/IntelliTect.Coalesce.Tests/TargetClasses/TestDbContext/TestDbContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Diagnostics; using System; using System.Collections.Generic; @@ -47,4 +48,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasValue("impl"); } } + + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public AppDbContext CreateDbContext(string[] args) + { + var builder = new DbContextOptionsBuilder(); + builder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=CoalesceTestDb;Trusted_Connection=True;"); + return new AppDbContext(builder.Options); + } + } } diff --git a/src/IntelliTect.Coalesce.Tests/Tests/ClonerTest.cs b/src/IntelliTect.Coalesce.Tests/Tests/ClonerTest.cs index e09ffd2c0..2b3de6fb2 100644 --- a/src/IntelliTect.Coalesce.Tests/Tests/ClonerTest.cs +++ b/src/IntelliTect.Coalesce.Tests/Tests/ClonerTest.cs @@ -1,8 +1,14 @@ using IntelliTect.Coalesce.Helpers; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; +using System.Xml.Linq; using Xunit; namespace IntelliTect.Coalesce.Tests @@ -39,5 +45,46 @@ public class TestClass public string Field; } + +#if NET5_0_OR_GREATER + class NullReporter : Microsoft.EntityFrameworkCore.Design.Internal.IOperationReporter + { + public void WriteError(string message) + { + } + + public void WriteInformation(string message) + { + } + + public void WriteVerbose(string message) + { + } + + public void WriteWarning(string message) + { + } + } + + [Fact] + public void AdHoc() + { + var contextType = "AppDbContext"; + var assembly = Assembly.GetExecutingAssembly(); + var _contextOperations = new Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations( + reporter: new NullReporter(), + assembly: assembly, + startupAssembly: assembly, + projectDir: Environment.CurrentDirectory, + rootNamespace: null, + language: null, + nullable: false, + args: null); + + using var context = _contextOperations.CreateContext(contextType); + var model = context.Model; + var relationalModel = context.Model.GetRelationalModel(); + } +#endif } } diff --git a/src/IntelliTect.Coalesce/IntelliTect.Coalesce.csproj b/src/IntelliTect.Coalesce/IntelliTect.Coalesce.csproj index 27926937c..f78085509 100644 --- a/src/IntelliTect.Coalesce/IntelliTect.Coalesce.csproj +++ b/src/IntelliTect.Coalesce/IntelliTect.Coalesce.csproj @@ -29,21 +29,27 @@ - + - + + + + + + + \ No newline at end of file diff --git a/src/IntelliTect.Coalesce/TypeUsage/DbContextTypeUsage.cs b/src/IntelliTect.Coalesce/TypeUsage/DbContextTypeUsage.cs index 8706b276c..80f5674ac 100644 --- a/src/IntelliTect.Coalesce/TypeUsage/DbContextTypeUsage.cs +++ b/src/IntelliTect.Coalesce/TypeUsage/DbContextTypeUsage.cs @@ -1,5 +1,6 @@ using IntelliTect.Coalesce.TypeDefinition; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; using System; using System.Collections.Generic; using System.Linq; @@ -32,6 +33,8 @@ public DbContextTypeUsage(ClassViewModel classViewModel) public IReadOnlyList Entities { get; } + public IModel? Model { get; set; } + public override bool Equals(object? obj) => obj is DbContextTypeUsage that && that.ClassViewModel.Equals(ClassViewModel); public override int GetHashCode() => ClassViewModel.GetHashCode();