Tracks where RoutesGenerator.cs does not yet match
docs/features/api-contract.md.
Update when a gap closes or a new one surfaces.
GET /api/<entity>list,GET /:id,POST,PATCH /:id,PUT /:id,DELETE /:idmount underapiPrefix(default/api); entity segment is lowercased + pluralized.?limit=N&offset=Npagination — honoured on the list route.?sort=<field>:asc|desc— honoured against a per-entity staticSortAllowlistderived from scalar fields. Unknown field returns 400{ "error": "validation", "message": "unknown sort field: ..." }.?withCount=1— switches the list envelope from[<row>...]to{ rows, total }. The grid hook always sends this.totalreflects the filtered count whenfilter[...]is present.?filter[<field>][<op>]=<value>(and thefilter[<field>]=<value>sugar form foreq) — honoured against a per-entity<Entity>FilterAllowlistemitted byFilterAllowlistGenerator. Errors return 400{ "error": "invalid_filter_field" | "invalid_filter_op" | "invalid_filter_value" }. The list handler callsFilterParser.Parseand dispatches viaEfCoreFilterDispatch.ApplyFilter(both inMetaObjects.Codegen.Runtime). FR-009.- Both
PATCHandPUTmap to the same update handler (TS reference exposes both verbs; C# now matches). - 404 carries a JSON envelope:
{ "error": "not_found" }. - Projection (
object.projectionover a read-onlysource.rdb@kind: view) routes are read-only — onlyGETlist +GET /:idare mounted; noPOST/PATCH/PUT/DELETE. - HTTP status codes:
200/201(POST) /204(PATCH/PUT/DELETE) /400(validation) /404(not found). - Sort dispatch uses
EF.Property<object>(x, "<Name>")— no runtime reflection (AOT-safe). Filter dispatch uses the same approach viaEfCoreFilterDispatch.
Contract. filter[or]=[...] and filter[and]=[...] keys nest disjunctions / conjunctions. Reference: api-contract.md "Filter operators (8)".
Today. Out of scope of FR-009 (which ships the flat 9-operator surface). Boolean grouping would need a richer URL grammar; defer until a real consumer demand surfaces.
Contract. Single sort key only in the default contract (sort=<field>:asc|desc). Multi-field sort is NOT in scope for the contract today.
Today. Conformed — single sort key is the documented surface.
Contract. The exact error code vocabulary is NOT a hard Tier-1 invariant; consumers should treat any 4xx as user-facing and 5xx as retryable. Reference: api-contract.md "Error response".
Today. We emit { "error": "not_found" } for 404 and { "error": "validation", "message": "..." } for the sort-validation 400. FR-036 wired explicit body validation with the cross-port { "error": "validation" } envelope: a POST runs Validator.TryValidateObject(input, ...) and a PATCH runs Validator.TryValidateProperty(value, ...) per present value → 400 { "error": "validation" } (ASP.NET minimal-API never runs DataAnnotations on its own, so the annotations were previously decorative at the wire tier). The issues array is still TS-only idiomatic (Tier 2).
Contract. A field.object column (a value-object mapped to jsonb, single or @isArray) is EF-mapped as an owned navigation (.OwnsOne/.OwnsMany(...).ToJson).
field.map is not in this gap. A map column is not a navigation — it is a scalar-shaped property carrying a value converter (G9) — so FindProperty resolves it and the generic merge arm writes it on PATCH. Map columns are patch-settable here, as on Java; Kotlin alone stages them out of its patch set (see its KNOWN_GAPS.md).
Today. On the vanilla (non-TPH) PATCH/PUT handler a VO-typed column IS patch-settable. The handler passes the entity's VO fields into AppendPartialMergeLoop (RoutesGenerator.cs:256), which emits a Program D typed value-object arm for each one AHEAD of the generic entry.Metadata.FindProperty(prop.Name) arm — an owned navigation is invisible to FindProperty, so the generic scalar path alone would silently drop the column. Per arm: a present value is deserialized, recursively validated (ValueObjectValidator.Validate), and assigned to the CLR navigation property; a present null clears a nullable VO column — a nullable array-of-VO additionally takes a post-save raw UPDATE, since EF's OwnsMany(...).ToJson writes [] for a null collection nav rather than SQL NULL — or 400s a @required one; an absent key leaves the column untouched. This is the same Program D shipment Java's KNOWN_GAPS.md records as cross-port (TS / Python / Java / Kotlin / C#), gated by fixtures/api-contract-conformance/jsonb/scenarios/jsonb-value-object-patch.yaml in both lanes.
Residual. TPH only: the per-subtype partial-update path passes an empty VO list (RoutesGenerator.cs:961), so VO columns on a TPH-rooted entity are still skipped on PATCH — the same staging-out Java's entry records for TPH.
RULED (1.0.1, #359): narrowed to the TPH residual, and the residual is a deliberate cross-port stage-out — not a C# divergence. The question the issue could not answer from C# alone was whether C# now patches VO columns while Java and Kotlin still do not, which would make this an api-contract divergence rather than a gap. It does not: Java stages out the same columns on the same path. SpringDtoGenerator.scalarFields is documented as "every MetaField that is not an ObjectField", and the TPH union is built from it, so a field.object column never reaches a Java TPH per-subtype write path either — Java's own KNOWN_GAPS.md records the carve-out in the same terms. Both ports patch VO columns on the vanilla path and stage them out on TPH, which is exactly the uniform Day-1 simplification this entry was written to describe.
So the entry stays OPEN, scoped to TPH only, and its cross-port framing is intact rather than obsolete. What closed is the part that was stale: the vanilla path, corrected in Today above. Reopening the TPH half is a feature decision (extend the TPH union to carry owned navigations), not a bug fix, and it would want to move on Java and C# together.
Contract. gt / gte / lt / lte work uniformly for all numeric/date subtypes.
Today. EfCoreFilterDispatch.BuildComparison<T> tries long.TryParse first and falls through to lexicographic string.Compare. Works for long columns and for ISO 8601 string-backed dates (which sort lexicographically). Fragile for decimal / double / non-ISO date columns — those degrade silently to string-compare semantics. The contract still holds for the operator vocabulary; only the comparison precision is gappy.
Workaround. Consumers can hand-write a typed dispatcher in their repository layer that materializes Expression<Func<T,bool>> with the column's CLR type. A future closure pass would add decimal.TryParse + DateTime.TryParse to the dispatcher; tracked here rather than auto-closed because the right fix is per-port substrate-aware emit, not a runtime tweak.
Contract. Generated controllers should NOT enforce CORS themselves — the consumer's Program.cs wires app.UseCors(...) per their origin policy. The Angular dev-server case is documented in docs/recipes/csharp-angular18.md.
Today. Conformed — the generator emits no [EnableCors] attributes.
Contract. A write-through entity (writable table source + read-only replica view source + derived origin.* fields, FR-024 §7) generates a hybrid surface: a derived-free, table-mapped WRITE entity (<Entity>), plus a SECOND, view-mapped read model (<Entity>View) carrying the derived fields (EF Core cannot map one CLR type to both a table and a view). Reads (list / get / reverse finders) route to db.<Entity>Views; writes target db.<Entity>Plural; a create/update re-reads the row through the view by PK (read-your-writes).
Today. Conformed. Two notes:
-
EmitMappedClassoverride reach. The write entity is still emitted through theEmitMappedClassextension seam (an adopter override applies to it), but the<Entity>Viewread model is emitted through the shared private impl directly, so an adopter'sEmitMappedClass/EmitClassHeaderoverride does NOT reach the read model. If read-model customization is needed, overrideEmitWriteThroughReadModel. Acceptable Day-1 limitation. -
M:N on a write-through entity. The FR-018 M:N traversal route (
GET /<entity>/{id}/<relation>) on a write-through entity still addresses the source by the write-entity path and joins through the junction/target DbSets — it is not routed through the replica view (the view carries no junction). Not exercised by the shipped fixtures; revisit if a real consumer models an M:N on a write-through entity. -
<Entity>View/<Entity>Viewsis a RESERVED name.CSharpNaming.ViewModelClassName/ViewDbSetNamemint the read model as<Entity>View(class + file) and<Entity>Views(DbSet) unconditionally from the write-through entity's name. A user object literally named<Entity>View(e.g. a write-throughOrderalongside a separate object namedOrderView) collides — codegen would emit twoOrderView.g.csfiles / twoOrderViewtypes / twoOrderViewsDbSets. No code guard is added: the codegen runner already hard-errors on the duplicate output path, a clear, immediate failure (not silent corruption). Treat<Entity>View/<Entity>Viewsas reserved for a write-through entity's generated read model.
Was. @storage: flattened spreads a value object's members onto the owning table as
<parent field's column>_<member column>, and migrate-ts's flattenObjectField emits a column
for every member. field.map was the one member kind DbContextGenerator did not name, so
the migration created <prefix>_<map member column> while EF bound its own <Nav>_<Prop>
default — reads and writes touching that member failed at the engine (Postgres 42703). The
stated reason was that this port configured a top-level field.map nowhere either, so there was
no proven mapping to mirror.
Now. There is one. A top-level field.map gets .HasColumnType("jsonb") plus the shared
MapJsonb converter/comparer pair, and the flattened member takes the SAME suffix pinned to its
prefixed column name via HasColumnName — one MapJsonbSuffix, so the two tiers cannot drift.
Pinned by ObjectFieldCodegenTests.A_flattened_map_member_binds_its_prefixed_jsonb_column and
compile-proven against real EF Core 8 by DbContextCompileTests (whose fixture carries a map on
an entity, on a value object flattened into one, and on a read-only projection — three distinct
receivers).
Residual. The generator still warns for any member kind it cannot pin; field.map is no
longer one of them.
- Append a numbered
### G<N>section above. - Quote the contract clause it relates to and the file it lives in.
- State today's behaviour honestly (silently ignored / partially conforming / etc.).
- State the workaround (if any) so consumers aren't blocked.
- Reference the FR / sub-project where the closure work is tracked.