# Data Models and AOT Safety This guide explains how to define MongoDB document models, register them for AOT-safe serialization, and avoid the common patterns that break Native AOT compilation. ## Why AOT matters The project is compiled with `true`. AOT (Ahead-of-Time) compilation strips out the JIT and eliminates reflection-based code paths at runtime. Any code that relies on `Type.GetProperties()`, `Activator.CreateInstance()`, `Expression.Compile()`, or similar reflection primitives will either: - Produce a build warning during `dotnet publish`, or - Throw a `MissingMethodException` / `InvalidOperationException` at runtime The two main risks in this project are MongoDB BSON serialization and System.Text.Json serialization. Both require explicit registration rather than auto-discovery. --- ## Defining a document model A document class is a plain C# class annotated with BSON attribute hints. Keep it simple: ```csharp using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace Htmx.ApiDemo.Data; public sealed class AppUser { [BsonId] public ObjectId Id { get; set; } = ObjectId.GenerateNewId(); [BsonElement("email")] public string Email { get; set; } = ""; [BsonElement("normalizedEmail")] public string NormalizedEmail { get; set; } = ""; [BsonElement("passwordHash")] public string PasswordHash { get; set; } = ""; [BsonElement("displayName")] public string? DisplayName { get; set; } [BsonElement("createdAtUtc")] public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow; } ``` Rules: - Always annotate the primary key with `[BsonId]` - Always annotate every persisted property with `[BsonElement("fieldName")]` — this makes the MongoDB field name explicit and independent of C# naming conventions - Use `SetIgnoreExtraElements(true)` in the class map (see below) so old documents with extra fields do not crash deserialization --- ## Registering the class map (AOT-safe) MongoDB's default `AutoMap()` uses reflection to discover properties at runtime. This is not AOT-safe. Instead, register an explicit `BsonClassMap` in `Program.cs` **before** `WebApplication.CreateSlimBuilder`: ```csharp // Program.cs — must appear before builder construction BsonClassMap.RegisterClassMap(cm => { cm.MapIdProperty(u => u.Id).SetSerializer(new ObjectIdSerializer()); cm.MapProperty(u => u.Email).SetElementName("email"); cm.MapProperty(u => u.NormalizedEmail).SetElementName("normalizedEmail"); cm.MapProperty(u => u.PasswordHash).SetElementName("passwordHash"); cm.MapProperty(u => u.DisplayName).SetElementName("displayName"); cm.MapProperty(u => u.CreatedAtUtc).SetElementName("createdAtUtc"); cm.SetIgnoreExtraElements(true); }); ``` This replaces AutoMap entirely. Every property you want persisted must be listed here. ### Adding a new model 1. Create the model class in `Data/` with `[BsonId]` and `[BsonElement]` attributes 2. Add a `BsonClassMap.RegisterClassMap(...)` block in `Program.cs` before the builder 3. Wire the collection into `MongoDbService` --- ## MongoDbService pattern `MongoDbService` is the single place that owns typed MongoDB collections. Add new collections here: ```csharp // Data/MongoDbService.cs public sealed class MongoDbService { private readonly IMongoCollection _users; // add more collections here public MongoDbService(IMongoClient client, IConfiguration configuration) { var db = client.GetDatabase(configuration["MongoDbName"] ?? "HtmxAppDb"); _users = db.GetCollection("users"); // _posts = db.GetCollection("posts"); } } ``` All queries use the strongly-typed `Builders` API: ```csharp // Exact-match lookup — no LINQ translation, no reflection at runtime var filter = Builders.Filter.Eq(u => u.NormalizedEmail, normalizedEmail); return await _users.Find(filter).FirstOrDefaultAsync(ct); ``` The `Builders` API compiles query expressions to BSON at build time via source generators in the MongoDB driver — it does not require runtime reflection. --- ## Index management Indexes are created via `EnsureIndexesAsync`, called once at startup from `Program.cs`: ```csharp using (var scope = app.Services.CreateScope()) await scope.ServiceProvider.GetRequiredService().EnsureIndexesAsync(); ``` Add new indexes to `EnsureIndexesAsync`: ```csharp public async Task EnsureIndexesAsync(CancellationToken ct = default) { var indexKeys = Builders.IndexKeys.Ascending(u => u.NormalizedEmail); var indexOptions = new CreateIndexOptions { Unique = true, Name = "uq_normalizedEmail" }; var model = new CreateIndexModel(indexKeys, indexOptions); await _users.Indexes.CreateOneAsync(model, cancellationToken: ct); // add more index creation calls here — CreateOneAsync is idempotent } ``` The call is idempotent: if the index already exists with the same definition, MongoDB silently succeeds. --- ## AOT anti-patterns to avoid | Pattern | Why it breaks AOT | Safe alternative | |---|---|---| | `BsonClassMap.RegisterClassMap()` without explicit mapping | Uses AutoMap reflection | Explicit `cm.MapProperty(...)` for every field | | `collection.AsQueryable().Where(...)` | EF-style LINQ translation uses reflection | `Builders.Filter.Eq(...)` | | `JsonSerializer.Deserialize(json)` without a type resolver | Reflects on T at runtime | Register T in `AppJsonSerializerContext` | | `Activator.CreateInstance(type)` | Requires reflection metadata | Use `new T()` directly | | `typeof(T).GetProperties()` | Stripped by trimmer | Not needed with explicit class maps | --- ## Checking for AOT warnings Run a Release publish and watch the output: ```bash dotnet publish Htmx.ApiDemo/Htmx.ApiDemo.csproj -c Release ``` Any trim/AOT warning in the output is a potential runtime failure. Fix each warning before it reaches production. The most common suppressable case is third-party libraries that are not fully AOT-annotated — suppress only after manually verifying the code path is not exercised at runtime.