[Bug][Prod] GameServer crashes on startup under NativeAOT — MongoPlaySessionRepository index creation reflectively builds DowncastingSerializer #443

Closed
opened 2026-07-06 21:01:13 +00:00 by spikerj · 0 comments
Owner

Live in production (Seq)

System.NotSupportedException: 'MongoDB.Bson.Serialization.Serializers.DowncastingSerializer\2[System.Object,System.DateTime]' is missing native code or metadata. This can happen for code that is not compatible with trimming or AOT.`

Thrown at SpikerSoft.GameServer.Services.MongoPlaySessionRepository..ctor(IMongoDatabase database)MongoIndexManager.CreateOne → LINQ index-key render → DowncastingSerializer.CreateActivator.CreateInstance. Because the repo is built at DI construction, IPlaySessionRepository (and its consumer PlayTimeTrackingService) can never be resolved — the GameServer container fails to start.

Root cause

SpikerSoft.GameServer is published NativeAOT (<PublishAot>true</PublishAot>, runtime-deps base image, native entrypoint — by design). MongoPlaySessionRepository's constructor built its indexes with strongly-typed lambdas:

Builders<PlaySession>.IndexKeys.Ascending(s => s.UserId).Descending(s => s.StartedAt)

IndexKeys.Ascending/Descending take Expression<Func<T, object>>, so s => s.StartedAt (a DateTime value type) compiles with a boxing Convert-to-object node. The driver's index renderer then reflectively constructs DowncastingSerializer<object, DateTime> via Activator.CreateInstance — a generic instantiation that AOT never compiledNotSupportedException.

Fix (keeps AOT)

Backend PR incoming: define the two indexes with BsonDocument index keys ({ "UserId": 1, "StartedAt": -1 }, { "UserId": 1, "IsActive": 1 }), which render straight to field names with no expression translation or serializer construction — AOT-safe. The repository's filters/updates already use non-boxing Func<T,TField> expressions, so they don't hit this path and are unchanged.

Scope: only SpikerSoft.GameServer is AOT-published, and only MongoPlaySessionRepository builds indexes there, so the crash is contained. (Note: the same boxing-lambda index pattern exists in several SpikerSoft.Business services — OfflineAttemptStore, AnonymousSessionService, AnonymousLessonProgressService, KeycloakEventService — but those run under JIT in the API/workers, so they're fine today; they'd only need the same treatment if that code is ever AOT-published.)

Acceptance

GameServer starts under NativeAOT; game_play_sessions indexes are created; play-time tracking works. Unit tests pin the index field names/directions to PlaySession's class map. Final proof is an AOT-published smoke test (the reflective failure only manifests in an AOT runtime, not a JIT unit test).

Severity: High (production startup crash for a whole service).

## Live in production (Seq) `System.NotSupportedException: 'MongoDB.Bson.Serialization.Serializers.DowncastingSerializer\`2[System.Object,System.DateTime]' is missing native code or metadata. This can happen for code that is not compatible with trimming or AOT.` Thrown at `SpikerSoft.GameServer.Services.MongoPlaySessionRepository..ctor(IMongoDatabase database)` → `MongoIndexManager.CreateOne` → LINQ index-key render → `DowncastingSerializer.Create` → `Activator.CreateInstance`. Because the repo is built at DI construction, `IPlaySessionRepository` (and its consumer `PlayTimeTrackingService`) can never be resolved — the GameServer container fails to start. ## Root cause `SpikerSoft.GameServer` is published **NativeAOT** (`<PublishAot>true</PublishAot>`, `runtime-deps` base image, native entrypoint — by design). `MongoPlaySessionRepository`'s constructor built its indexes with strongly-typed lambdas: ```csharp Builders<PlaySession>.IndexKeys.Ascending(s => s.UserId).Descending(s => s.StartedAt) ``` `IndexKeys.Ascending/Descending` take `Expression<Func<T, object>>`, so `s => s.StartedAt` (a `DateTime` **value type**) compiles with a boxing `Convert`-to-`object` node. The driver's index renderer then reflectively constructs `DowncastingSerializer<object, DateTime>` via `Activator.CreateInstance` — a **generic instantiation that AOT never compiled** → `NotSupportedException`. ## Fix (keeps AOT) Backend PR incoming: define the two indexes with **`BsonDocument` index keys** (`{ "UserId": 1, "StartedAt": -1 }`, `{ "UserId": 1, "IsActive": 1 }`), which render straight to field names with no expression translation or serializer construction — AOT-safe. The repository's filters/updates already use non-boxing `Func<T,TField>` expressions, so they don't hit this path and are unchanged. Scope: only `SpikerSoft.GameServer` is AOT-published, and only `MongoPlaySessionRepository` builds indexes there, so the crash is contained. (Note: the same boxing-lambda index pattern exists in several `SpikerSoft.Business` services — `OfflineAttemptStore`, `AnonymousSessionService`, `AnonymousLessonProgressService`, `KeycloakEventService` — but those run under JIT in the API/workers, so they're fine today; they'd only need the same treatment if that code is ever AOT-published.) ## Acceptance GameServer starts under NativeAOT; `game_play_sessions` indexes are created; play-time tracking works. Unit tests pin the index field names/directions to PlaySession's class map. **Final proof is an AOT-published smoke test** (the reflective failure only manifests in an AOT runtime, not a JIT unit test). Severity: **High** (production startup crash for a whole service).
Sign in to join this conversation.