Class Mapping
Class mapping in Cratis Applications provides a powerful way to customize how your .NET types are serialized to and from MongoDB BSON documents. The framework includes automatic discovery and registration of custom mappings.
Overview
Class mapping allows you to:
- Customize field names: Override property names in the database
- Configure serialization: Specify custom serializers for properties
- Set up inheritance: Configure polymorphic type hierarchies
- Control indexes: Define which properties should be indexed
- Ignore properties: Exclude certain properties from serialization
IBsonClassMapFor Interface
To create a custom class map, implement the IBsonClassMapFor<T> interface:
public interface IBsonClassMapFor<T>
{
void Configure(BsonClassMap<T> classMap);
}
Basic Class Map Example
public class User
{
public UserId Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginAt { get; set; }
public string PasswordHash { get; set; } // We don't want to serialize this
}
public class UserClassMap : IBsonClassMapFor<User>
{
public void Configure(BsonClassMap<User> classMap)
{
classMap.AutoMap();
// Set the ID field
classMap.SetIdMember(classMap.GetMemberMap(u => u.Id));
// Custom field name
classMap.GetMemberMap(u => u.UserName)
.SetElementName("username");
// Ignore sensitive data
classMap.UnmapMember(u => u.PasswordHash);
// Custom serializer for dates
classMap.GetMemberMap(u => u.CreatedAt)
.SetSerializer(new DateTimeOffsetSupportingBsonDateTimeSerializer());
}
}
Automatic Discovery
The framework automatically discovers and registers all implementations of IBsonClassMapFor<T>:
// This happens automatically during UseCratisMongoDB() setup
var types = Types.Instance;
var classMaps = types.FindMultiple(typeof(IBsonClassMapFor<>));
foreach (var classMapType in classMaps)
{
// Automatic registration
RegisterClassMap(classMapType);
}
Registration Process
Class maps are registered during MongoDB initialization:
- Discovery: All
IBsonClassMapFor<T>implementations are found - Instantiation: Each class map provider is created
- Configuration: The
Configuremethod is called with aBsonClassMap<T> - Convention Application: Configured conventions are applied to the class map
- Registration: The class map is registered with MongoDB's
BsonClassMap.RegisterClassMap<T>()
Advanced Mapping Examples
Inheritance Mapping
public abstract class Document
{
public DocumentId Id { get; set; }
public string Title { get; set; }
public DateTime CreatedAt { get; set; }
}
public class TextDocument : Document
{
public string Content { get; set; }
}
public class ImageDocument : Document
{
public byte[] ImageData { get; set; }
public string MimeType { get; set; }
}
public class DocumentClassMap : IBsonClassMapFor<Document>
{
public void Configure(BsonClassMap<Document> classMap)
{
classMap.AutoMap();
classMap.SetIdMember(classMap.GetMemberMap(d => d.Id));
// Configure inheritance
classMap.SetIsRootClass(true);
classMap.AddKnownType(typeof(TextDocument));
classMap.AddKnownType(typeof(ImageDocument));
}
}
public class TextDocumentClassMap : IBsonClassMapFor<TextDocument>
{
public void Configure(BsonClassMap<TextDocument> classMap)
{
classMap.AutoMap();
classMap.SetDiscriminator("text");
}
}
public class ImageDocumentClassMap : IBsonClassMapFor<ImageDocument>
{
public void Configure(BsonClassMap<ImageDocument> classMap)
{
classMap.AutoMap();
classMap.SetDiscriminator("image");
// Custom handling for binary data
classMap.GetMemberMap(i => i.ImageData)
.SetSerializer(new ByteArraySerializer());
}
}
Complex Property Mapping
public class OrderItem
{
public ProductId ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => Quantity * UnitPrice; // Computed property
}
public class Order
{
public OrderId Id { get; set; }
public CustomerId CustomerId { get; set; }
public List<OrderItem> Items { get; set; } = [];
public Address ShippingAddress { get; set; }
public decimal Total => Items.Sum(i => i.Total); // Computed property
}
public class OrderClassMap : IBsonClassMapFor<Order>
{
public void Configure(BsonClassMap<Order> classMap)
{
classMap.AutoMap();
classMap.SetIdMember(classMap.GetMemberMap(o => o.Id));
// Don't serialize computed properties
classMap.UnmapMember(o => o.Total);
// Custom collection handling
classMap.GetMemberMap(o => o.Items)
.SetElementName("orderItems")
.SetIgnoreIfNull(true);
}
}
public class OrderItemClassMap : IBsonClassMapFor<OrderItem>
{
public void Configure(BsonClassMap<OrderItem> classMap)
{
classMap.AutoMap();
// Don't serialize computed properties
classMap.UnmapMember(i => i.Total);
// Custom decimal handling
classMap.GetMemberMap(i => i.UnitPrice)
.SetSerializer(new DecimalSerializer(BsonType.Decimal128));
}
}
Custom Serializers in Class Maps
public class EventRecord
{
public EventId Id { get; set; }
public EventType Type { get; set; }
public object Data { get; set; } // Polymorphic data
public Dictionary<string, object> Metadata { get; set; }
public DateTimeOffset Timestamp { get; set; }
}
public class EventRecordClassMap : IBsonClassMapFor<EventRecord>
{
public void Configure(BsonClassMap<EventRecord> classMap)
{
classMap.AutoMap();
classMap.SetIdMember(classMap.GetMemberMap(e => e.Id));
// Custom polymorphic serialization
classMap.GetMemberMap(e => e.Data)
.SetSerializer(new ObjectSerializer(
type => type == typeof(object),
ObjectSerializationOptions.Default));
// Custom dictionary serialization
classMap.GetMemberMap(e => e.Metadata)
.SetSerializer(new DictionaryInterfaceImplementerSerializer<Dictionary<string, object>>());
// Use custom DateTime serializer
classMap.GetMemberMap(e => e.Timestamp)
.SetSerializer(new DateTimeOffsetSupportingBsonDateTimeSerializer());
}
}
Convention Integration
Class maps work seamlessly with the convention system:
Applying Conventions
After configuration, conventions are automatically applied:
public void Configure(BsonClassMap<User> classMap)
{
classMap.AutoMap();
// Your custom configuration...
}
// Conventions are applied automatically after Configure() returns
// This includes naming policies, ignore conventions, etc.
Extension Method
The framework provides an extension method to manually apply conventions:
public void Configure(BsonClassMap<User> classMap)
{
classMap.AutoMap();
// Manual configuration
classMap.GetMemberMap(u => u.UserName).SetElementName("custom_name");
// Apply conventions manually if needed
classMap.ApplyConventions();
}
Error Handling
Duplicate Registration Prevention
Class maps are automatically protected against duplicate registration:
// This check happens automatically
if (BsonClassMap.IsClassMapRegistered(typeof(T)))
{
return; // Skip if already registered
}
Validation
The framework validates class map configurations:
public class InvalidClassMap : IBsonClassMapFor<User>
{
public void Configure(BsonClassMap<User> classMap)
{
// This would throw an exception during registration
classMap.SetIdMember(null);
}
}
Best Practices
Use AutoMap First
Always call AutoMap() first, then customize:
public void Configure(BsonClassMap<User> classMap)
{
classMap.AutoMap(); // Set up defaults first
// Then customize as needed
classMap.SetIdMember(classMap.GetMemberMap(u => u.Id));
classMap.UnmapMember(u => u.PasswordHash);
}
Organize by Domain
Group related class maps together:
namespace MyApp.MongoDB.UserMappings
{
public class UserClassMap : IBsonClassMapFor<User> { }
public class UserProfileClassMap : IBsonClassMapFor<UserProfile> { }
public class UserSettingsClassMap : IBsonClassMapFor<UserSettings> { }
}
Handle Computed Properties
Don't serialize computed properties:
public void Configure(BsonClassMap<Order> classMap)
{
classMap.AutoMap();
// These are computed at runtime
classMap.UnmapMember(o => o.Total);
classMap.UnmapMember(o => o.ItemCount);
classMap.UnmapMember(o => o.IsComplete);
}
Use Appropriate Serializers
Choose serializers that match your data requirements:
// For high-precision decimals
classMap.GetMemberMap(p => p.Price)
.SetSerializer(new DecimalSerializer(BsonType.Decimal128));
// For large integers
classMap.GetMemberMap(p => p.LargeNumber)
.SetSerializer(new Int64Serializer(BsonType.Int64));
// For enum values
classMap.GetMemberMap(p => p.Status)
.SetSerializer(new EnumSerializer<OrderStatus>(BsonType.String));
Testing Class Maps
You can test your class maps to ensure they work correctly:
[Test]
public void should_serialize_user_correctly()
{
// Arrange
var user = new User
{
Id = UserId.New(),
UserName = "testuser",
Email = "test@example.com"
};
// Act
var document = user.ToBsonDocument();
// Assert
document.Contains("_id").ShouldBeTrue();
document.Contains("username").ShouldBeTrue(); // Custom element name
document.Contains("PasswordHash").ShouldBeFalse(); // Should be ignored
}
Next Steps
- Learn about Convention Packs for system-wide customizations
- Explore Concepts for domain-driven design patterns
- Understand Naming Policies for consistent field naming