Complete the #253 optimistic-concurrency contract's cross-editor ETag
rotation tail. The non-If-Match config siblings mutated editor-visible
state without bumping Version, so a concurrent editor of the same root
never invalidated. Now the Collection Add*/Remove handlers bump
Collection.Version, and UpdateCollection / UpdatePlayout / the three
ScheduleFile writers (which already force-wrote past a concurrent bump)
now bump too — all via SaveChangesForcingVersion (no If-Match → force
write, never 412/500).
No-op idempotence (Fable-caught trap): these gate reindex/BuildPlayout
fan-out on SaveChanges()>0, so an unconditional bump would fire spurious
rebuilds on an idempotent re-add / same-value re-submit. Each now
short-circuits a genuine no-op before the bump — Add handlers by an
explicit membership check (also fixing a latent duplicate-CollectionItem
insert), scalar writers by ChangeTracker.HasChanges().
Corrects #269's framing: the Add*ToCollection family is not
repository-mediated (IMediaCollectionRepository is read-only); each
handler writes via its own dbContext, so the scanner's separate
membership path is unaffected (a background scan does not rotate the
editor ETag).
Tests: CollectionEtagRotationTests + PlayoutScheduleFileEtagRotationTests
(rotation, no-op-without-bump-or-rebuild, force-write-past-concurrent-bump),
no-op guard proven non-vacuous by inverting the membership check.
Docs: api-conventions §7a + decisions.md. No new status codes / no
OpenAPI change (these endpoints take no If-Match, never 412).
The #265 RFC-7232 If-Match parser refinement is a separate PR.
fixes#269
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix validation in new form layout
* pin mediatr to last oss version
* update dependencies
* cleanup code in core
* cleanup code in ffmpeg
* cleanup code in infra
* cleanup code in scanner
* cleanup code in application
* cleanup main code
* cleanup test code
* solution-wide code cleanup