The short list of things XFTY can't do, or does in a way that might catch you out. Each one: what you tried, what you got, what to do instead.
Fixed bugs and design history are in CHANGELOG.md and contribute/porting-history.md, not here.
You wrote: an override template — new Invoice { Total = 0 }, or
new Invoice { Notes = null } — and your Provider has a default for that
field (Total => 100, Notes => "n/a").
You got: the Provider default. Total is 100, Notes is "n/a".
Why: XFTY can't tell "I set this to the empty value on purpose" from "I
never touched it". The empty value is 0 / false / default for a number,
bool, DateTime, Guid, or enum, and null for a string, an object, or a
Nullable<T> — and in every one of those cases it's what a blank record
already has, so the default fills it. Any other value in a template
(Total = 5, Notes = "see attached") is kept fine.
Do this instead, one of:
- Set it the tracked way on the call:
provider[x => x.Total] = 0,provider[x => x.Notes] = null(orprovider.Put(x => x.Total, 0)). That does win over a Provider default. - Drop the Provider's default for this call:
provider.RemoveFromMasterTemplate(x => x.Total)— then the field keeps whatever the template gave it (including the empty value). - If it comes up a lot, a Provider author can simply not default that field.
record struct and readonly record struct types generate fine for a plain
unit test (Mock or Never mode). But:
InsertMode.Nowwon't save astruct— the EF Core gateway needs a class it can track.- A
struct(or class) with only{ get; }properties can't be filled — XFTY needs aninitor asetto write each field. bundle.Inject(...)over a graph of structs isn't safe — every step copies the value, so a change made to a nested record later won't show up in the parent that holds it. Plain generation (noInject) is fine.
When Inject(...) reshapes a graph so you can read contact.Account.Name
straight off the record, it works out that Contact.AccountId pairs with a
Contact.Account property by name: SomethingId → Something, and a
child list → whichever property is a List<> of the child's type.
If your names don't follow that — Contact.AccountFk with no
Contact.Account, or two List<Case> properties — Inject(...) throws an
error naming the field it couldn't place. There's no attribute or setting to
tell it the right property.
(Your primary key is never guessed like this. It's whatever your Provider
declares — LedgerId, Reference, anything.)
provider.With(ChildProvider.For<Case>(x => x.SomeField)) — if SomeField
isn't actually a relationship back to the parent, XFTY doesn't stop you at
setup. You find out at generation time, when the child comes back with a
wrong or null link. There's no schema to validate against up front.
SharedAncestor.Put("hq", myAccount) needs to know: is myAccount a real
record to use as-is, or a template to generate from? It runs before your
Provider is in scope, so it checks one thing — is a property named Id set?
If your key is named something else (AccountId), a pre-built record
would be mistaken for a template. Do this instead: say which you mean —
SharedAncestor.PutAsValue("hq", myAccount) for a fixed record, or
SharedAncestor.PutAsTemplate("hq", myAccount) to generate from it. (With
PutAsTemplate, XFTY notices later that the real key is filled and uses the
record as-is anyway.)
SharedAncestor and DeferredInserter remember things for the whole test
run — .NET doesn't wipe static state between test methods the way Salesforce
did. A shared ancestor one test registers can bleed into the next.
Do this instead, one of:
- put
[IsolatesSharedAncestor]on the test class (from theXfty.Xunitpackage) — automatic; - call
SharedAncestor.ResetAllForTesting()in your test base class or fixture; - or give every test a unique shared-ancestor name
(
"hq-" + nameof(ThisTestMethod)).
Full detail in salesforce-considerations.
Xfty.VectorDatabases.Qdrant and .MicrosoftExtensionsVectorData (both
preview) treat the record's one float[] property as the embedding. A record
with two float[] fields, or an embedding typed ReadOnlyMemory<float>,
throws.
A few Apex/Salesforce features have no .NET equivalent. Only relevant if you're porting from the Apex library — salesforce-considerations has the detail.
- Detecting a Provider variant with zero setup. In Apex, RecordType
detection read Salesforce's schema for free. Here you register the
variants — a
FlavouredLookupKey/DiscriminatorLookupKeywith a condition on the record — and XFTY does then match your override template against them automatically (new Account { Type = "Big" }finds the "Big" variant).Xfty.EntityFrameworkCorecan even derive the keys from aDbContext's discriminator column. What's gone is only the "figure it out from metadata with no registration at all" part. - Org / environment seeding — XFTY generates for one test run — use/org-seeding.
- Bundled test-user helpers — no role/profile schema to resolve against.
- CPU / row-count budgets — no fixed per-run quota to track.
RecordInjectorBlob / compound-field / polymorphic machinery — reflection sets any property, so nothing needs special-casing.