Skip to content

Close the API gaps a side-by-side consumer found, and release them as preview.2 - #1

Merged
sadeqabuhattem merged 8 commits into
mainfrom
fix/api-gaps
Oct 10, 2026
Merged

sadeqabuhattem merged 8 commits into
mainfrom
fix/api-gaps

Conversation

@sadeqabuhattem

Copy link
Copy Markdown
Member

Why

An application composing this library with DotNetBoost.Media had to reimplement the fallback rule and the whole translation CRUD in its own code. Both now ship here. Running Media's conformance suite against this library for the first time also turned up a key-formatting bug neither repo's own tests could see.

What changed

New API

  • IMultiLingualManager.For(entityName, entityId) / ForMany(entityName, entityIds) — translations for a subject addressed by name and id rather than by instance, for per-language text about a type the application does not own. ITranslationSubject reads with the same current-language-then-default-language fallback the EF Core interceptors apply, from the same code, so the two can no longer answer the same rows differently. ForMany(...).GetAsync() is one store read for a whole page.
  • app.MapTranslationEndpoints() — the translation CRUD every consumer was hand-writing: GET, GET …/resolved, PUT (every language in one batch), POST …/resolve (many ids, one query), DELETE.
  • .ExposeOverHttp(e => e.Allow("MediaFile").Allow<Product>()) — the opt-in those endpoints require. An open {entityName} route would let any caller write translation rows for any entity in the system, so nothing is served until it is listed; an unlisted name answers 404, not 403. Authorization stays the application's.
  • EntityKey.AddressingVersion — DotNetBoost Entity Addressing, now written out as a versioned spec in the README. Every DotNetBoost library sharing subject addressing publishes the same constant, so a consumer joining rows across two of them asserts one equality instead of writing its own theory table.

Fixed

  • A whitespace-only string key was accepted and filed rows under it. " " is not "unset" — it has a length — so it passed ToKeyString unchanged, while Media refused the same value as its spec has always said. Rows written under such a key could never be joined to another library's. Found only by running Media's suite against this library, which is the whole argument for the suite.
  • The README claimed entity names are compared case-insensitively. They are compared as written, with the database's own collation deciding — case-sensitive under PostgreSQL's default, insensitive under SQL Server's.

Deprecated

  • IMultiLingualDbContext is obsolete and read by nothing; UseEntityFrameworkCore<TContext>() no longer constrains TContext to it. The provider always read the two tables through context.Set<T>(), so the constraint only made contexts declare members nothing called. Kept for one release, with an Upgrading section in the README showing what to delete.

Release

  • The entries move from [Unreleased] to 1.0.0-preview.2. preview.1 is on nuget.org and immutable, so this needs its own tag before a consumer can take it up. The date is the day it was prepared — adjust if the tag is cut later.

Verification

Run locally on the merge candidate:

Check Result
dotnet build -c Release -warnaserror (net8.0 + net10.0) 0 warnings, 0 errors
UnitTests 227 passed, both TFMs
ApiTests 55 (net8) / 56 (net10) passed
ProviderTests 214 passed, both TFMs
IntegrationTests (Testcontainers: SQL Server, PostgreSQL, MongoDB) 162 passed
dotnet pack × 6 projects all succeed, with .snupkg symbols

Unit-test coverage is 86.45% line / 78.59% branch. Note the Coverage Gate job declares COVERAGE_THRESHOLD: 80 — line coverage clears it, branch coverage does not, so that job is worth watching on this run.

Every relative Markdown link in the README, CHANGELOG, CONTRIBUTING and docs/ resolves.

🤖 Generated with Claude Code

sadeqabuhattem and others added 7 commits October 5, 2026 14:56
An application composing this library with DotNetBoost.Media had to reimplement
the fallback rule and the whole translation CRUD in its own code. Both ship here
now, and the addressing convention the two libraries share by hand becomes a
versioned spec.

Address a subject by name and id. For(entityName, entityId) and ForMany(...)
carry per-language text about something whose CLR type the application does not
own — another library's MediaFile, a row read with raw SQL. They read through
TranslationManager.Plan, the one place the current-language-then-default-language
rule lives, so a subject read and an entity read can no longer answer the same
rows differently; Plan gains a columnBacked flag for the one real difference,
that a subject has no table of its own. ForMany issues one store read for the
whole page rather than one per id. The key is formatted by the library, so
For("X", 42) and For("X", "42") reach the same rows.

Ship the translation endpoints. MapTranslationEndpoints() maps the CRUD every
consumer was hand-writing, built on the subject API so it stays thin. An open
{entityName} route would let any caller write rows for any entity in the system,
so the names are listed at registration with ExposeOverHttp(...) and nothing is
served until they are; an unlisted name answers 404, not 403, which would
confirm which names exist. With nothing exposed, nothing is mapped and a warning
says so at startup.

Publish the addressing spec. EntityKey.AddressingVersion names the version of
DotNetBoost Entity Addressing, now written out in the README, so a consumer
joining rows across two libraries asserts one equality instead of writing its
own theory table over key formats — and a deliberate format change fails loudly
everywhere rather than quietly stranding stored rows. TestSupport carries the
conformance suite itself, framework-agnostic so it also works as a startup check.

Drop the IMultiLingualDbContext constraint. The provider always read the two
tables through context.Set<T>(), never through the interface's properties, so
the constraint only made contexts declare members nothing called. The interface
is obsolete for one release; a consuming context now needs nothing but
ApplyMultiLingual() in OnModelCreating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cross-library examples name members of the other library, and neither build
compiles them, so nothing catches a wrong name. Read against
dotnetboost-media/src, three were wrong or incomplete.

The conformance example passed MediaMetadata.MaxEntityNameLength as the
entity-name limit. DotNetBoost.Media does have a MediaMetadata, but it is the
JSON serializer for MediaFile.Metadata and its MaxLength is 4000 — so the line
as written does not compile, and the obvious correction to MediaMetadata.MaxLength
WOULD compile and then assert 128 against 4000, reporting a disagreement that
does not exist. The entity-name limit is MediaEntityNames.MaxLength.

That example also leaned on a MediaKey alias nothing introduced, as did the
one-liners in the README and on the constant's own doc. The README's was the
most misleading: it shows a using line for this library's namespace only, so the
omission reads as deliberate. The one-liners are now fully qualified on the Media
side, matching how media's own EntityKey doc writes the same assertion; the
four-argument example gets the two aliases instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A whitespace-only string key was accepted and filed rows under it. It is not
unset — "   " has a length — so it passed ToKeyString unchanged, while
DotNetBoost.Media refused the same value as its spec row has always said: a
key must not be empty or whitespace. Translations written under such a key
could never be joined to another library's rows.

ToKeyString refuses empty and whitespace now, at the call that made the
mistake, for the same reason SubjectName checks a name's length early. The
LENGTH limit deliberately stays where it was: TranslationManager refuses an
over-long key at the write, where it can name the entity as well as the key
and its length, which is a better error than this method could give.

Found by running Media's conformance suite against this library for the
first time. Neither repo's own tests could see it, because each only tests
itself — which is the whole argument for the suite.

The spec gains the Key discovery rule. The [Key] -> Id -> {TypeName}Id order
was always implemented and never written down, so nothing versioned it and
nothing tested it; that is how Media came to resolve a composite key where
this library refuses one, with both still publishing v1.
AddressingVersion's "bump it for ANY change" list now names all five rule
areas.

EntityAddressing gains AssertUnsetDetection and AssertKeyDiscovery, and
AssertAgreesWith takes both as optional arguments: the constants cannot
speak for behaviour, and behaviour is where two libraries actually drift.
Both throw InvalidOperationException naming the difference, like the rest of
the suite, so they stay usable from any test framework or a startup check.

Also corrects the Name length row, which claimed entity names are compared
case-insensitively. They are compared as written, by a plain `==` the
database's collation decides — case-sensitive under PostgreSQL's default.
Media behaves identically, so nothing diverged; the claim was just stronger
than the code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README offered DotNetBoost.MultiLingual.TestSupport's conformance suite —
EntityAddressing.KeyCases, AssertAgreesWith — to a consumer asserting that two
libraries address a subject the same way, and pitched it as framework-agnostic so
it would work as a startup check too. That project is IsPackable=false: it is never
packed, never reaches a feed, and no consumer can reference it. The suggestion was
a documented API nobody outside this repository can take up.

The assertion above it needs nothing else — EntityKey.AddressingVersion is a public
constant and is bumped for any change to the rules — so the paragraph now says that
one equality is the whole of the dependency, and points at the two public length
limits for anyone wanting to compare those as well.

The suite stays where it is and keeps holding this library to the spec it
publishes; it is simply not advertised as something a consumer can run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README stopped pointing consumers at DotNetBoost.MultiLingual.TestSupport,
which is IsPackable=false and reaches no feed. The same offer was in two more
places, one of them worse than the README: the XML doc on
EntityKey.AddressingVersion ships inside the nupkg, so it reaches a consumer as
IntelliSense on exactly the constant they are reading when they go looking.

The CHANGELOG bullet was the furthest reach — it listed AssertAgreesWith and
AssertKey by name and pitched the suite as framework-agnostic so it would "also
work as a startup check", which is an API no consumer can call from anywhere, let
alone from startup. It is still under [Unreleased], so nothing shipped with it.

Both now say what is true and useful instead: the conformance suite runs against
this library as part of its own test suite, so the version it publishes is one it is
held to rather than one it merely claims, and comparing the two constants is all a
consumer needs.

The suite is unchanged and still gates this library. CONTRIBUTING.md and
docs/architecture.md keep naming the project in their repository-layout trees, since
that is accurate for a contributor reading them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README named neither MapTranslationEndpoints() nor ExposeOverHttp(), so the
branch's largest addition reached a reader only through a docs-index row retitled
to "language and translation endpoints". The language endpoints get a quick-start
line and a warning about anonymous access; the translation endpoints get neither,
and they are the ones that write content. The new section under "Using it in your
own code" sits beneath the table rows that introduce For(entityName, id), shows
the exposure opt-in in the builder chain, lists the five routes, and carries the
same callout: the list decides which entities are reachable, not which callers,
so compose a policy on the group.

The "bump it for ANY change" promise named three of the five rule areas — name
resolution, key formatting, either limit — in three places, while the spec table
directly above it carries rows for key discovery and unset keys too. A rule left
off that list is how two libraries drift while both still claim the same version,
which is the failure the constant exists to prevent, so all three now name all
five. The one in the XML doc matters most: it ships inside the nupkg.

Preview.1's README had readers implement IMultiLingualDbContext. This branch
dropped it from the quick start and said nothing about deleting it, so anyone who
followed those instructions gets an obsolete warning with no note explaining it —
a build error in a project that treats warnings as errors. An Upgrading section
shows the hunk to delete and says the interface still compiles for one release.

The status line named 1.0.0-preview.1 explicitly, which is wrong the moment a tag
is pushed and wrong again at every release after. The vpre badge above it already
reports the current version, so the line now says preview.* and points at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The branch's entries sat under [Unreleased], which says what changed but not what
to install it as. They ship as 1.0.0-preview.2: preview.1 is already on nuget.org
and a published version is immutable, so the subject API, the translation
endpoints and the whitespace-key fix need a tag of their own before any consumer
can take them up.

[Unreleased] stays at the top, empty, per Keep a Changelog, and the compare links
are rewired so the new heading resolves and [Unreleased] spans preview.2...HEAD.

The date is the day the release was prepared, not one the tag can confirm — the
tag does not exist yet. Adjust it if v1.0.0-preview.2 is cut on another day.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread tests/DotNetBoost.MultiLingual.TestSupport/EntityAddressing.cs Fixed
AssertKeyDiscovery asserted that a composite key is refused by calling the
accessor and catching Exception. Any failure inside discoverKey therefore read as
a deliberate refusal: a library whose accessor threw NullReferenceException on the
two-[Key] type — a plain bug — passed this check as conforming, which is the
opposite of what a conformance suite is for.

It now catches the three exceptions a library refuses with, NotSupportedException
(what this library throws), InvalidOperationException and ArgumentException, and
lets anything else propagate so the caller sees the bug. The filter follows the
idiom already used in MultiLingualValidation, LanguageCatalog and
DapperMigrationHostedService. The spec still does not fix the exception type — it
fixes that one value cannot be chosen from two — and the remarks say so.

CodeQL flagged the generic catch on the pull request (cs/catch-of-all-exceptions),
which is how it surfaced; the rule has two older open alerts on main that predate
this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sadeqabuhattem
sadeqabuhattem merged commit 8196cbb into main Oct 10, 2026
10 checks passed
@sadeqabuhattem
sadeqabuhattem deleted the fix/api-gaps branch October 10, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants