Every image in Art Studio 404s on learn.spikersoft.com, for every user including staff. Requests look like:
GET /api/artstudio/6a651f4a644fc165b7a8b961/artifacts//download → 404
Note the empty path segment — a missing identifier, not a missing file. 8/8 assets affected, every artifact, back through the oldest asset.
Root cause
SpikerSoft.Data/Contexts/SpikerDbContext.cs:813-816 — ArtAsset.Artifacts is an EF JSON-converted column deserialized with null options, i.e. PropertyNameCaseInsensitive = false:
So ObjectKey stays at its = string.Empty default. Every sibling field survives; only the storage key is lost. The API then emits "gridFsId": "", and Angular's art-asset-card.component.ts:80 → art-studio.service.ts:717 builds /artifacts//download.
[BsonElement("gridFsId")] is irrelevant here — this column is a JSON string, not BSON-mapped.
No data loss
The keys are intact in Mongo. Nothing to restore from backups.
Why CI was green
ArtStudioStorageKeyNamingTests and ArtAssetLegacyDocumentTests build fixtures via JsonSerializer.Serialize(...)using the current code, so they write gridFsId and read gridFsId — a perfect round-trip that never meets historical PascalCase bytes. #849's guard does assert serialized names, just not against real legacy data.
Fix
Deserialize the Artifacts conversion with PropertyNameCaseInsensitive = true so "GridFsId" matches gridFsId. Restores every asset with no data migration. Serialization keeps writing canonical lowercase.
Regression test built from literal PascalCase JSON, not a round-trip.
Scope checked
Only Artifacts is affected. QuarantinedArtifacts is a native column, and the other #849 renames (SourceImageKey, SourceArtifactKey, StagedObjectKey) are native columns correctly pinned with [BsonElement].
Follow-up (separate)
One-time migration rewriting stored artifact JSON to canonical names — the real "get off gridFsId" work.
## Symptom
Every image in Art Studio 404s on `learn.spikersoft.com`, for every user including staff. Requests look like:
```
GET /api/artstudio/6a651f4a644fc165b7a8b961/artifacts//download → 404
```
Note the **empty path segment** — a missing identifier, not a missing file. 8/8 assets affected, every artifact, back through the oldest asset.
## Root cause
`SpikerSoft.Data/Contexts/SpikerDbContext.cs:813-816` — `ArtAsset.Artifacts` is an EF JSON-converted column deserialized with **null options**, i.e. `PropertyNameCaseInsensitive = false`:
```csharp
entity.Property(e => e.Artifacts)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null!),
v => JsonSerializer.Deserialize<List<ArtAssetArtifactRef>>(v, (JsonSerializerOptions?)null!) ?? new())
```
On disk (verified in prod Mongo, `spikersoft.art-assets`, all 8 docs) the value is a JSON **string** in PascalCase, written before #849:
```json
[{"Stage":"develop","GridFsId":"6a6015c9cf74746094dfce42","Kind":"image-tiff","FileName":"frame_000_....tif",...}]
```
**#849** renamed `GridFsId` → `ObjectKey` and pinned it `[JsonPropertyName("gridFsId")]` — lowercase `g`. Deserializing case-sensitively:
| stored | target | match |
|---|---|---|
| `"Stage"`, `"Kind"`, `"FileName"`, `"SizeBytes"`, `"CreatedAt"` | same property names | ✅ exact |
| `"GridFsId"` | `ObjectKey` pinned to `gridFsId` | ❌ **no match** |
So `ObjectKey` stays at its `= string.Empty` default. Every sibling field survives; only the storage key is lost. The API then emits `"gridFsId": ""`, and Angular's `art-asset-card.component.ts:80` → `art-studio.service.ts:717` builds `/artifacts//download`.
`[BsonElement("gridFsId")]` is irrelevant here — this column is a JSON string, not BSON-mapped.
## No data loss
The keys are intact in Mongo. Nothing to restore from backups.
## Why CI was green
`ArtStudioStorageKeyNamingTests` and `ArtAssetLegacyDocumentTests` build fixtures via `JsonSerializer.Serialize(...)` **using the current code**, so they write `gridFsId` and read `gridFsId` — a perfect round-trip that never meets historical PascalCase bytes. #849's guard does assert serialized names, just not against real legacy data.
## Fix
1. Deserialize the `Artifacts` conversion with `PropertyNameCaseInsensitive = true` so `"GridFsId"` matches `gridFsId`. Restores every asset with **no data migration**. Serialization keeps writing canonical lowercase.
2. Regression test built from **literal PascalCase JSON**, not a round-trip.
## Scope checked
Only `Artifacts` is affected. `QuarantinedArtifacts` is a native column, and the other #849 renames (`SourceImageKey`, `SourceArtifactKey`, `StagedObjectKey`) are native columns correctly pinned with `[BsonElement]`.
## Follow-up (separate)
One-time migration rewriting stored artifact JSON to canonical names — the real "get off `gridFsId`" work.
One-line read-side change (PropertyNameCaseInsensitive) plus a regression test seeded from literal PascalCase JSON rather than a round-trip. Proven to fail without the fix (2 failed / 17 passed) and pass with it (19/19). dotnet build SpikerSoft.UnitTests.slnf → 0 errors.
No data migration, no bytes rewritten — the keys were never lost, only unbound.
Wants a deploy as soon as it merges; nothing else restores images.
One thing found along the way and not fixed here — worth its own ticket: the converter's write side emits mixed case (PascalCase for every property except the pinned gridFsId), so a case-sensitive read cannot round-trip even its own output. The case-insensitive read makes that harmless, but normalising it is part of the follow-up migration.
Fix open: spikersoft-backend PR **#484** (`fix/artstudio-legacy-artifact-key-852`).
One-line read-side change (`PropertyNameCaseInsensitive`) plus a regression test seeded from **literal PascalCase JSON** rather than a round-trip. Proven to fail without the fix (2 failed / 17 passed) and pass with it (19/19). `dotnet build SpikerSoft.UnitTests.slnf` → 0 errors.
No data migration, no bytes rewritten — the keys were never lost, only unbound.
Wants a deploy as soon as it merges; nothing else restores images.
One thing found along the way and **not** fixed here — worth its own ticket: the converter's write side emits **mixed case** (PascalCase for every property except the pinned `gridFsId`), so a case-sensitive read cannot round-trip even its own output. The case-insensitive read makes that harmless, but normalising it is part of the follow-up migration.
Resolved. The emergency shim shipped in spikersoft-backend PR #484 (b48a5fa2) and restored production; #853 then removed it as designed. Verified against origin/master:
SpikerDbContext.cs:884-891 now uses a single shared ArtifactColumnJson instance (:33-39) for both read and write, deliberately case-sensitive. LegacyTolerantArtifactJson is gone — the shim is not lingering.
The literal-PascalCase regression test survives at ArtAssetLegacyDocumentTests.cs:574-578 but inverted: it now asserts an un-migrated document throws JsonException (:596-612) rather than silently yielding an empty key. That's a strictly stronger guarantee than the original ticket asked for — the failure mode that caused this incident (silent empty key → /artifacts//download) can no longer happen quietly.
Root-cause linkage confirmed for the record: #849's [JsonPropertyName("gridFsId")] on the Artifacts JSON column is exactly what orphaned the stored PascalCase "GridFsId". The [BsonElement] attribute is inert on an EF JSON-string column, which is why the rename passed review and round-trip tests.
Closing.
Resolved. The emergency shim shipped in spikersoft-backend PR #484 (`b48a5fa2`) and restored production; #853 then removed it as designed. Verified against `origin/master`:
- `SpikerDbContext.cs:884-891` now uses a single shared `ArtifactColumnJson` instance (`:33-39`) for both read and write, deliberately case-sensitive. `LegacyTolerantArtifactJson` is gone — the shim is not lingering.
- The literal-PascalCase regression test survives at `ArtAssetLegacyDocumentTests.cs:574-578` but **inverted**: it now asserts an un-migrated document throws `JsonException` (`:596-612`) rather than silently yielding an empty key. That's a strictly stronger guarantee than the original ticket asked for — the failure mode that caused this incident (silent empty key → `/artifacts//download`) can no longer happen quietly.
Root-cause linkage confirmed for the record: #849's `[JsonPropertyName("gridFsId")]` on the `Artifacts` JSON column is exactly what orphaned the stored PascalCase `"GridFsId"`. The `[BsonElement]` attribute is inert on an EF JSON-string column, which is why the rename passed review and round-trip tests.
Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Symptom
Every image in Art Studio 404s on
learn.spikersoft.com, for every user including staff. Requests look like:Note the empty path segment — a missing identifier, not a missing file. 8/8 assets affected, every artifact, back through the oldest asset.
Root cause
SpikerSoft.Data/Contexts/SpikerDbContext.cs:813-816—ArtAsset.Artifactsis an EF JSON-converted column deserialized with null options, i.e.PropertyNameCaseInsensitive = false:On disk (verified in prod Mongo,
spikersoft.art-assets, all 8 docs) the value is a JSON string in PascalCase, written before #849:#849 renamed
GridFsId→ObjectKeyand pinned it[JsonPropertyName("gridFsId")]— lowercaseg. Deserializing case-sensitively:"Stage","Kind","FileName","SizeBytes","CreatedAt""GridFsId"ObjectKeypinned togridFsIdSo
ObjectKeystays at its= string.Emptydefault. Every sibling field survives; only the storage key is lost. The API then emits"gridFsId": "", and Angular'sart-asset-card.component.ts:80→art-studio.service.ts:717builds/artifacts//download.[BsonElement("gridFsId")]is irrelevant here — this column is a JSON string, not BSON-mapped.No data loss
The keys are intact in Mongo. Nothing to restore from backups.
Why CI was green
ArtStudioStorageKeyNamingTestsandArtAssetLegacyDocumentTestsbuild fixtures viaJsonSerializer.Serialize(...)using the current code, so they writegridFsIdand readgridFsId— a perfect round-trip that never meets historical PascalCase bytes. #849's guard does assert serialized names, just not against real legacy data.Fix
Artifactsconversion withPropertyNameCaseInsensitive = trueso"GridFsId"matchesgridFsId. Restores every asset with no data migration. Serialization keeps writing canonical lowercase.Scope checked
Only
Artifactsis affected.QuarantinedArtifactsis a native column, and the other #849 renames (SourceImageKey,SourceArtifactKey,StagedObjectKey) are native columns correctly pinned with[BsonElement].Follow-up (separate)
One-time migration rewriting stored artifact JSON to canonical names — the real "get off
gridFsId" work.Fix open: spikersoft-backend PR #484 (
fix/artstudio-legacy-artifact-key-852).One-line read-side change (
PropertyNameCaseInsensitive) plus a regression test seeded from literal PascalCase JSON rather than a round-trip. Proven to fail without the fix (2 failed / 17 passed) and pass with it (19/19).dotnet build SpikerSoft.UnitTests.slnf→ 0 errors.No data migration, no bytes rewritten — the keys were never lost, only unbound.
Wants a deploy as soon as it merges; nothing else restores images.
One thing found along the way and not fixed here — worth its own ticket: the converter's write side emits mixed case (PascalCase for every property except the pinned
gridFsId), so a case-sensitive read cannot round-trip even its own output. The case-insensitive read makes that harmless, but normalising it is part of the follow-up migration.Resolved. The emergency shim shipped in spikersoft-backend PR #484 (
b48a5fa2) and restored production; #853 then removed it as designed. Verified againstorigin/master:SpikerDbContext.cs:884-891now uses a single sharedArtifactColumnJsoninstance (:33-39) for both read and write, deliberately case-sensitive.LegacyTolerantArtifactJsonis gone — the shim is not lingering.ArtAssetLegacyDocumentTests.cs:574-578but inverted: it now asserts an un-migrated document throwsJsonException(:596-612) rather than silently yielding an empty key. That's a strictly stronger guarantee than the original ticket asked for — the failure mode that caused this incident (silent empty key →/artifacts//download) can no longer happen quietly.Root-cause linkage confirmed for the record: #849's
[JsonPropertyName("gridFsId")]on theArtifactsJSON column is exactly what orphaned the stored PascalCase"GridFsId". The[BsonElement]attribute is inert on an EF JSON-string column, which is why the rename passed review and round-trip tests.Closing.