Skip to content

Commit 3e7ad80

Browse files
dmealingclaude
andcommitted
fix(csharp-codegen): FR-017 — emit TPH discriminator base as abstract (EF model build)
CI surfaced a real EF Core model-build failure on the C# TPH lanes: InvalidOperationException: The entity type 'Auth' has a discriminator property, but does not have a discriminator value configured. A concrete base with HasDiscriminator + HasValue<Sub>(...) but no HasValue<Base>(...) is invalid in EF Core; since every row is a concrete subtype (no plain-base rows), the base must be abstract — then EF requires no base discriminator value. Because EF validates the whole model on first use, this single misconfig failed EVERY C# integration test (incl. non-TPH ones). EntityGenerator now emits `public abstract class <Base>` for a TPH discriminator base (TphPlanBuilder.IsTphDiscriminatorBase); subtypes/regular entities unchanged. Regenerated the committed Generated/Auth.g.cs; updated TphCodegenTests. Adds AppDbContextModelTests — a Docker-FREE guard that finalizes `AppDbContext.Model` (EF builds the model offline, no DB connection) and asserts the TPH hierarchy maps cleanly; this reproduces the exact CI error without Testcontainers. Verified locally: model test 1/1, full Codegen.Tests 162/0/1 (incl. the fixture drift gate) — no Docker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7e06ee3 commit 3e7ad80

4 files changed

Lines changed: 50 additions & 3 deletions

File tree

server/csharp/MetaObjects.Codegen.Tests/TphCodegenTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ public void Entity_base_carries_table_and_discriminator_property()
9595
var files = new EntityGenerator().Generate(Ctx(Load())).ToList();
9696
var auth = FileContent(files, "Auth.g.cs");
9797
Assert.Contains("[Table(\"auths\")]", auth);
98-
Assert.Contains("public class Auth", auth);
98+
// FR-017 TPH: the discriminator base is abstract (no plain-base rows; a
99+
// concrete base with a discriminator + no base value fails EF model build).
100+
Assert.Contains("public abstract class Auth", auth);
99101
// The discriminator field is a base property (the `type` enum -> AuthType).
100102
// Nullable because the `type` field is not @required and not in the PK; the
101103
// TS-owned `auths.type` column is likewise nullable TEXT.

server/csharp/MetaObjects.Codegen/Generators/EntityGenerator.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,13 @@ private EmittedFile EmitMappedClass(MetaObject entity, GenContext ctx)
106106
}
107107
if (!isProjection)
108108
sb.AppendLine($"[Table(\"{CSharpNaming.Table(entity)}\")]");
109-
sb.AppendLine($"public class {className}");
109+
// FR-017 TPH: the discriminator base is emitted `abstract` — every row is a
110+
// concrete subtype (no plain-base rows exist), so EF Core must not require a
111+
// base discriminator value. A concrete base with a discriminator but no
112+
// HasValue<Base>(...) fails model validation ("has a discriminator property,
113+
// but does not have a discriminator value configured").
114+
var classKeyword = TphPlanBuilder.IsTphDiscriminatorBase(entity, ctx.Root) ? "abstract class" : "class";
115+
sb.AppendLine($"public {classKeyword} {className}");
110116
sb.AppendLine("{");
111117

112118
// Nested enum declarations (before the class properties, as C# enum members
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// AppDbContextModelTests — Docker-FREE EF Core model-build gate.
2+
//
3+
// EF Core finalizes (validates) its model offline — no database connection is
4+
// opened just to build `context.Model`. So this test reproduces model-level
5+
// misconfiguration (e.g. a TPH discriminator base with no discriminator value)
6+
// WITHOUT Testcontainers. It is the fast guard for the class of failure that
7+
// otherwise only surfaces in the Docker integration suite.
8+
9+
using MetaObjects.IntegrationTests.Generated;
10+
using Microsoft.EntityFrameworkCore;
11+
using Xunit;
12+
13+
namespace MetaObjects.IntegrationTests;
14+
15+
public class AppDbContextModelTests
16+
{
17+
[Fact]
18+
public void Model_builds_without_error()
19+
{
20+
// A dummy connection string is enough — model building never connects.
21+
var options = new DbContextOptionsBuilder<AppDbContext>()
22+
.UseNpgsql("Host=localhost;Database=unused")
23+
.Options;
24+
using var db = new AppDbContext(options);
25+
26+
// Forces EF model finalization. Before the FR-017 fix this threw
27+
// InvalidOperationException: "The entity type 'Auth' has a discriminator
28+
// property, but does not have a discriminator value configured."
29+
var model = db.Model;
30+
31+
// The TPH hierarchy is mapped to one table with the discriminator + subtypes.
32+
var auth = model.FindEntityType(typeof(Auth));
33+
Assert.NotNull(auth);
34+
Assert.NotNull(model.FindEntityType(typeof(BridgeAuth)));
35+
Assert.NotNull(model.FindEntityType(typeof(CopayAuth)));
36+
Assert.NotNull(model.FindEntityType(typeof(PriorAuthAuth)));
37+
Assert.Equal("auths", auth!.GetTableName());
38+
}
39+
}

server/csharp/MetaObjects.IntegrationTests/Generated/Auth.g.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
namespace MetaObjects.IntegrationTests.Generated;
1010

1111
[Table("auths")]
12-
public class Auth
12+
public abstract class Auth
1313
{
1414
public enum AuthType { Bridge, Copay, PriorAuth }
1515
[Key]

0 commit comments

Comments
 (0)