# 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.