|
| 1 | +// EF Core compile-check for generated AppDbContext. |
| 2 | +// |
| 3 | +// Compiles the generated entity POCOs + AppDbContext TOGETHER against real |
| 4 | +// EF Core 8 assemblies (in-memory Roslyn, same pattern as EntityGeneratorTests). |
| 5 | +// This catches API mismatches that pure string-contains checks can't find — |
| 6 | +// e.g. calling .ToJson() on a PropertyBuilder<List<T>>, which EF Core 8 does not |
| 7 | +// expose (CS1061), even though the string appears valid. |
| 8 | +// |
| 9 | +// The fixture exercises the full EF surface in one model: |
| 10 | +// - object.value Address (owned type target) |
| 11 | +// - object.entity Order with: |
| 12 | +// scalar enum "status" → .HasConversion<string>() |
| 13 | +// array enum "statuses" (isArray) → .PrimitiveCollection().ElementType().HasConversion<string>() |
| 14 | +// array string "tags" (isArray) → .PrimitiveCollection() |
| 15 | +// field.object homeAddress @storage flattened → OwnsOne(...) per-property column names |
| 16 | +// field.object config (default storage) → OwnsOne(...).ToJson(...) |
| 17 | +// - object.entity ProgramSummary with source.dbView → .ToView(...) (no HasNoKey — keyed) |
| 18 | +// |
| 19 | +// The test DOES NOT include RoutesGenerator output: routes import ASP.NET Core |
| 20 | +// shared-framework types that are not available in the TRUSTED_PLATFORM_ASSEMBLIES |
| 21 | +// sandbox used by in-memory Roslyn tests. |
| 22 | + |
| 23 | +using Microsoft.CodeAnalysis; |
| 24 | +using Microsoft.CodeAnalysis.CSharp; |
| 25 | +using MetaObjects.Codegen; |
| 26 | +using MetaObjects.Codegen.Generators; |
| 27 | +using MetaObjects.Loader; |
| 28 | +using MetaObjects.Meta; |
| 29 | +using Xunit; |
| 30 | + |
| 31 | +namespace MetaObjects.Codegen.Tests; |
| 32 | + |
| 33 | +public class DbContextCompileTests |
| 34 | +{ |
| 35 | + // One model that exercises every EF-surface code-path in DbContextGenerator: |
| 36 | + // • scalar enum → .HasConversion<string>() |
| 37 | + // • array enum → .PrimitiveCollection().ElementType().HasConversion<string>() |
| 38 | + // • scalar array → .PrimitiveCollection() |
| 39 | + // • flattened object field → OwnsOne per-property column config |
| 40 | + // • json object field → OwnsOne(...).ToJson(...) |
| 41 | + // • dbView projection → .ToView(...) |
| 42 | + private const string Model = """ |
| 43 | + { "metadata.root": { "package": "acme", "children": [ |
| 44 | + { "object.value": { "name": "Address", "children": [ |
| 45 | + { "field.string": { "name": "street", "@required": true, "@maxLength": 120 } }, |
| 46 | + { "field.string": { "name": "city", "@maxLength": 80 } } |
| 47 | + ]}}, |
| 48 | + { "object.entity": { "name": "Order", "children": [ |
| 49 | + { "source.dbTable": { "@name": "orders" } }, |
| 50 | + { "field.long": { "name": "id" } }, |
| 51 | + { "field.enum": { "name": "status", "@values": ["DRAFT", "PUBLISHED", "ARCHIVED"] } }, |
| 52 | + { "field.enum": { "name": "statuses", "isArray": true, "@values": ["DRAFT", "PUBLISHED", "ARCHIVED"] } }, |
| 53 | + { "field.string": { "name": "tags", "isArray": true } }, |
| 54 | + { "field.object": { "name": "homeAddress", "@objectRef": "Address", "@storage": "flattened" } }, |
| 55 | + { "field.object": { "name": "config", "@objectRef": "Address" } }, |
| 56 | + { "identity.primary": { "@fields": "id" } } |
| 57 | + ]}}, |
| 58 | + { "object.entity": { "name": "ProgramSummary", "children": [ |
| 59 | + { "source.dbView": { "@name": "v_program_summary" } }, |
| 60 | + { "field.long": { "name": "id" } }, |
| 61 | + { "field.int": { "name": "weekCount" } }, |
| 62 | + { "identity.primary": { "@fields": "id" } } |
| 63 | + ]}} |
| 64 | + ]}} |
| 65 | + """; |
| 66 | + |
| 67 | + private static MetaRoot Load() |
| 68 | + { |
| 69 | + var r = new MetaDataLoader().Load([new InMemorySource(Model, id: "ef-compile.json")]); |
| 70 | + Assert.Empty(r.Errors); |
| 71 | + return r.Root; |
| 72 | + } |
| 73 | + |
| 74 | + private static GenContext Ctx(MetaRoot root) => new() |
| 75 | + { |
| 76 | + Entities = root.Objects(), Root = root, |
| 77 | + Config = new GenConfig { OutDir = "/tmp", Namespace = "Acme.Generated" }, |
| 78 | + }; |
| 79 | + |
| 80 | + // Build a MetadataReference list = TRUSTED_PLATFORM_ASSEMBLIES + EF Core assemblies. |
| 81 | + // TRUSTED_PLATFORM_ASSEMBLIES already includes the BCL and EF Core may or may not be |
| 82 | + // listed there depending on the test runner; using typeof(...).Assembly.Location and |
| 83 | + // deduplicating by path is the safe approach. |
| 84 | + private static List<MetadataReference> BuildReferences() |
| 85 | + { |
| 86 | + var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase); |
| 87 | + |
| 88 | + // BCL + SDK assemblies from the trusted platform. |
| 89 | + var tpa = (string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!; |
| 90 | + foreach (var p in tpa.Split(Path.PathSeparator)) |
| 91 | + if (p.Length > 0) paths.Add(p); |
| 92 | + |
| 93 | + // EF Core 8 assemblies — always add explicitly so the reference is present |
| 94 | + // even if the test runner hasn't put them in TRUSTED_PLATFORM_ASSEMBLIES. |
| 95 | + // The core package supplies DbContext / ModelBuilder / EntityTypeBuilder. |
| 96 | + // The relational package supplies the extension methods used in OnModelCreating: |
| 97 | + // .ToView(), .HasColumnName(), .ToJson(), .HasConversion(), .PrimitiveCollection(). |
| 98 | + paths.Add(typeof(Microsoft.EntityFrameworkCore.DbContext).Assembly.Location); |
| 99 | + paths.Add(typeof(Microsoft.EntityFrameworkCore.ModelBuilder).Assembly.Location); |
| 100 | + paths.Add(typeof(Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions).Assembly.Location); |
| 101 | + |
| 102 | + return paths |
| 103 | + .Where(p => File.Exists(p)) |
| 104 | + .Select(p => (MetadataReference)MetadataReference.CreateFromFile(p)) |
| 105 | + .ToList(); |
| 106 | + } |
| 107 | + |
| 108 | + [Fact] |
| 109 | + public void Generated_AppDbContext_and_entities_compile_against_EF_Core_8() |
| 110 | + { |
| 111 | + var ctx = Ctx(Load()); |
| 112 | + var refs = BuildReferences(); |
| 113 | + |
| 114 | + // Generate ALL files: entity POCOs + value-object POCOs + AppDbContext. |
| 115 | + // Routes are excluded — ASP.NET Core types are outside the sandbox. |
| 116 | + var entityFiles = new EntityGenerator().Generate(ctx).ToList(); |
| 117 | + var dbContextFiles = new DbContextGenerator().Generate(ctx).ToList(); |
| 118 | + |
| 119 | + var allSources = entityFiles.Concat(dbContextFiles).ToList(); |
| 120 | + Assert.NotEmpty(allSources); |
| 121 | + |
| 122 | + var trees = allSources |
| 123 | + .Select(f => CSharpSyntaxTree.ParseText( |
| 124 | + f.Content, |
| 125 | + new CSharpParseOptions(LanguageVersion.CSharp12))) |
| 126 | + .ToList(); |
| 127 | + |
| 128 | + var comp = CSharpCompilation.Create( |
| 129 | + "efcore_compile_" + Guid.NewGuid().ToString("N"), |
| 130 | + trees, |
| 131 | + refs, |
| 132 | + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); |
| 133 | + |
| 134 | + var errors = comp.GetDiagnostics() |
| 135 | + .Where(d => d.Severity == DiagnosticSeverity.Error) |
| 136 | + .Select(d => $"{d.Id}: {d.GetMessage()}") |
| 137 | + .ToList(); |
| 138 | + |
| 139 | + Assert.True( |
| 140 | + errors.Count == 0, |
| 141 | + "Generated entity + AppDbContext should compile against EF Core 8, but got errors:\n" |
| 142 | + + string.Join("\n", errors)); |
| 143 | + } |
| 144 | +} |
0 commit comments