Repository navigation
After twin moves from Marten to EF Core; Marten twin becomes an experiment - #18
Merged
Merged
Conversation
…iment Both twins now use EF Core 10 + Npgsql against one PostgreSQL database, so the only things that differ between them are the architecture and the mediator. The after twin runs EF Core through Wolverine's EF Core integration; the Marten version moves intact to experiments/after-marten/ and keeps running the same scenarios there. Shape is unchanged: same folders, same file names, same endpoint methods, same guard order, same pure Decide with the same rounding, same (PlacedOrder, NotifyBaker) return, no SaveChanges in any endpoint. The HTTP contract is byte-identical and no scenario changed. The DbContext has no DbSet properties and no per-entity mapping. Each table configures itself in its own feature file via IEntityTypeConfiguration and ApplyConfigurationsFromAssembly, so adding a feature adds files and edits nothing. Order.Lines stays one jsonb column via OwnsMany(...).ToJson(). No migrations folder: Wolverine-managed migrations plus resource setup on startup build the schema, because EnsureCreated no-ops when the database already exists and the twins share one. NotifyBaker returns an AddBakerTask side effect from a pure [Transactional] handler. Storage.Store<T> is an upsert on Marten but generates a no-op against EF Core, and a handler returning a side effect is invisible to AutoApplyTransactions; both fail silently. The side effect checks for redelivery, which identity-keyed upsert made free. Suite: 54/54 contract scenarios and 15/15 unit facts, 0 warnings, 16.9s warm and 22.1s cold. Marten experiment 27/27 and 15/15. Template experiment unchanged at 6/10. Counts: before twin 2,140 raw / 1,775 non-blank, unchanged. After twin 844 / 707, up from 703 / 581. Per-slice file counts unchanged path for path. Detail in docs/file-inventory.md and docs/build-log.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The core after twin moves from Marten documents to EF Core through Wolverine's EF Core integration. The Marten twin moves intact to
experiments/after-marten/and keeps running the same scenarios there.Why. The talk is about architecture (A-Frame, Railway Oriented Programming, pure functions, distance), not about the Critter Stack. With a document store on one side, the honest answer to "what changed between the twins?" included the persistence library, and a room that notices that stops listening to the argument. Both twins now use EF Core 10 + Npgsql against one PostgreSQL database, so what moves between them is the architecture and the mediator. What stays confounded, and the talk says so, is the web framework.
Baseline before the change is tagged
kcdc-marten-baselineat70a2b13.What did not change
The HTTP contract is byte-identical and no scenario changed. Same folders, same file names, same endpoint methods, same guard order, the same pure
Decidewith the sameMidpointRounding.ToEvenarithmetic,PlaceOrder.Poststill returning(PlacedOrder, NotifyBaker),[Entity(Required = true, OnMissing = OnMissing.ProblemDetailsWith404)]still loadingGetCakeandGetOrder, noSaveChangesin any endpoint.PublishCake.cs,GetCake.cs,GetOrder.csandCouponValidation.csare byte-identical to the Marten twin. Messaging is unchanged: same queue, samePublishMessage<NotifyBaker>().ToRabbitQueue(...).UseDurableOutbox(), sameListenToRabbitQueue, sameAutoProvision(), and the envelope still commits in the same transaction as the order (verified in the generated code: one command batch,INSERT INTO after.ordersandINSERT INTO after.wolverine_outgoing_envelopes).The DbContext is never edited per feature
LayerCakeDbContexthas noDbSetproperties and no per-entity mapping:HasDefaultSchema("after")plusApplyConfigurationsFromAssembly. Each table'sIEntityTypeConfiguration<T>lives in its own feature file beside the type it maps (Cakes/Cake.cscarriesCakeandCakeTable), and endpoints reach fordb.Set<T>(). That is what keeps the scorecard line "on the order slice the after twin edited zero existing files" true after the move, and it is deliberate. The before twin's DbContext is edited by three of the four slices.Order.Linesstays one jsonb column viaOwnsMany(o => o.Lines, lines => lines.ToJson()): no child table, no foreign key, identical wire shape.Schema, without a migrations folder
opts.UseEntityFrameworkCoreWolverineManagedMigrations()plusbuilder.Services.AddResourceSetupOnStartup(), so Weasel builds the tables this DbContext describes and Wolverine's envelope tables when the host starts.EnsureCreatedAsyncwas rejected on purpose: it does nothing at all when the database already exists, and the twins share one, so it would have been a silent no-op under Testcontainers and live. Seeding therefore moved into aSeedOnStartuphosted service registered after Wolverine (Development only). The before twin keeps its EF Core migrations.Two silent failures worth knowing about
NotifyBakerHandlerreturningStore<BakerTask>compiled, ran, logged nothing and wrote no row. The generated code says why:// No explicit update necessary with EF Core without a Version property. Wolverine's EF Core storage-action applier treatsStoreas update-of-a-tracked-entity, not upsert, so a new row never lands. Separately, a handler returning anISideEffecthas no DbContext dependency, soAutoApplyTransactionsnever notices it and nothing callsSaveChangesAsync. Both symptoms look identical from outside.So the baker notification is now
AddBakerTask : ISideEffectreturned by a pure[Transactional]handler. The side effect checks for an existing task by order id before adding, because the broker delivers at least once and EF Core inserts where identity-keyed upsert made redelivery harmless for free. The handler is still a pure function and the unit test still asserts on a returned value with no context, real or faked.The three read endpoints are
[NonTransactional]withAsNoTracking(), which is the EF Core way of saying whatIQuerySessionsaid before.Results
dotnet test LayerCake.slnxtests/LayerCake.ContractTests.Martentests/LayerCake.Slices.Marten.Teststests/LayerCake.ContractTests.CleanTemplateRoot suite is 13.6s to 16.9s warm, 22.1s cold after
dotnet clean. Both Marten projects are outsideLayerCake.slnx, so rootdotnet testis still exactly two hosts.Live run against
docker compose up -d: schema built and seeded on startup,/ping,/cakes,/cakes/{id},/coupons/bday10valid at 10 percent off,POST /orderswithBDAY10returning subtotal 68.00, discount 6.80, total 61.20, the baker task appearing on/baker/tasks?orderId=...after the message crossed RabbitMQ, and Swagger 200.Numbers the deck re-sources
Per-slice after-twin file counts are unchanged path for path: 5 created / 1 edited, 3 / 2, 6 / none, none / 1.
The ratio moves from about 3.0x to about 2.5x. The +141 raw is almost entirely the price of telling a relational store what a document store infers:
Program.csis one line shorter, the four table configurations are +70 / +62,NotifyBaker.csis +23 / +21 andSeedData.cs+18 / +16 for the redelivery and re-seed checks that identity upserts made free. File-by-file table in the inventory.LayerCakeDbContextis taken as a parameter by 9 methods in 7 files, or 8 in 6 counting feature code only.Packages
WolverineFx.EntityFrameworkCoreandWolverineFx.Postgresqladded at 6.30.0, tracking the existing Wolverine pin; EF Core 10.0.4 and Npgsql 10.0.3 reused from the before twin's pins. This is the one documented exception to the version freeze.MartenandWolverineFx.Martenstay pinned for the experiment.Test and doc changes
Two shared test files changed, both twin bindings and no scenario:
TwinHosts.cs(the after fixture wipes and re-seeds through the DbContext) andCakeScenarios.cs(AfterTwinCakes.InsertCakeDirectlyAsyncinserts through the DbContext). Both gainedAfterDb/BeforeDbusing-aliases because both twins now have a type calledLayerCakeDbContext. No assertion, scenario name or abstract member changed.The Marten experiment keeps its
LayerCake.Slicesnamespaces; only the project and assembly are renamed toLayerCake.Slices.Marten, andtests/LayerCake.ContractTests.Martenaliases the assembly the same way the template host does, because otherwise every shared type name would be ambiguous.Docs updated:
docs/file-inventory.md(new dated section with the full breakdown),docs/build-log.md(dated entry),README.md,CLAUDE.md, one stale line inopenspec/config.yaml, and a one-line pointer at the top of eachdocs/slices/spec.openspec/specs/is deliberately untouched: no built behaviour changed, so the archived specs still describe what ships.