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/field.map column (a value-object mapped to jsonb, single or @isArray) is EF-mapped as an owned navigation (.OwnsOne/.OwnsMany(...).ToJson).
Today. The partial-PATCH merge loop keys off entry.Metadata.FindProperty(prop.Name), which returns null for a navigation, so a VO-typed column is skipped on PATCH (present values too). This is a deliberate cross-port Day-1 simplification, consistent with Java + Kotlin (both exclude ObjectField from the patch set) — NOT a C#-specific bug (FR-036 assessed a C#-only fix and rejected it: it would break the api-contract byte-identical parity). Making VO-typed columns PATCHable (bind the owned nav via entry.Navigation(...).CurrentValue, cross-port) is a separately-scoped follow-up FR.
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.
- 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.