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
Section titled “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
Section titled “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
Section titled “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
Section titled “Automatic Discovery”The framework automatically discovers and registers all implementations of IBsonClassMapFor<T>:
// This happens automatically during UseCratisMongoDB() setupvar types = Types.Instance;var classMaps = types.FindMultiple(typeof(IBsonClassMapFor<>));
foreach (var classMapType in classMaps){ // Automatic registration RegisterClassMap(classMapType);}Registration Process
Section titled “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
Section titled “Advanced Mapping Examples”Inheritance Mapping
Section titled “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
Section titled “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
Section titled “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
Section titled “Convention Integration”Class maps work seamlessly with the convention system:
Applying Conventions
Section titled “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
Section titled “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
Section titled “Error Handling”Duplicate Registration Prevention
Section titled “Duplicate Registration Prevention”Class maps are automatically protected against duplicate registration:
// This check happens automaticallyif (BsonClassMap.IsClassMapRegistered(typeof(T))){ return; // Skip if already registered}Validation
Section titled “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
Section titled “Best Practices”Use AutoMap First
Section titled “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
Section titled “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
Section titled “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
Section titled “Use Appropriate Serializers”Choose serializers that match your data requirements:
// For high-precision decimalsclassMap.GetMemberMap(p => p.Price) .SetSerializer(new DecimalSerializer(BsonType.Decimal128));
// For large integersclassMap.GetMemberMap(p => p.LargeNumber) .SetSerializer(new Int64Serializer(BsonType.Int64));
// For enum valuesclassMap.GetMemberMap(p => p.Status) .SetSerializer(new EnumSerializer<OrderStatus>(BsonType.String));Testing Class Maps
Section titled “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
Section titled “Next Steps”- Learn about Convention Packs for system-wide customizations
- Explore Concepts for domain-driven design patterns
- Understand Naming Policies for consistent field naming