Concepts
Cratis Applications provides seamless integration between Cratis Concepts and MongoDB through automatic serialization support. Concepts are domain-driven design primitives that wrap primitive types with business meaning.
What are Concepts?
Concepts are types that inherit from ConceptAs<T> and provide type-safe wrappers around primitive values:
public record UserId(Guid Value) : ConceptAs<Guid>(Value)
{
public static readonly UserId NotSet = new(Guid.Empty);
public static implicit operator UserId(Guid value) => new(value);
public static UserId New() => new(Guid.NewGuid());
}
public record ProductName(string Value) : ConceptAs<string>(Value)
{
public static readonly ProductName NotSet = new(string.Empty);
public static implicit operator ProductName(string value) => new(value);
}
Automatic Serialization
When you call UseCratisMongoDB(), all types implementing ConceptAs<T> are automatically configured for MongoDB serialization through the ConceptSerializationProvider.
How It Works
The ConceptSerializer<T> handles the serialization by:
- Detection: Automatically detects types that implement
ConceptAs<T> - Unwrapping: Serializes only the underlying value, not the wrapper
- Type Safety: Ensures type validation during serialization/deserialization
- Performance: Optimized to avoid unnecessary object creation
Example Usage
public class User
{
public UserId Id { get; set; }
public ProductName Name { get; set; }
public EmailAddress Email { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
// Usage
var user = new User
{
Id = UserId.New(),
Name = "John Doe", // Implicit conversion
Email = "john@example.com",
CreatedAt = DateTimeOffset.Now
};
// In MongoDB, this will be stored as:
// {
// "_id": "550e8400-e29b-41d4-a716-446655440000",
// "name": "John Doe",
// "email": "john@example.com",
// "createdAt": ISODate("2024-01-15T10:30:00Z")
// }
Supported Underlying Types
The ConceptSerializer<T> supports all primitive types that MongoDB can natively handle:
Numeric Types
int,uint,long,ulongfloat,double,decimalbyte,sbyte,short,ushort
String and Character Types
stringchar
Date and Time Types
DateTimeDateTimeOffset(uses the custom serializer)DateOnly(uses the custom serializer)TimeOnly(uses the custom serializer)
Other Types
boolGuid- Any type that has a registered MongoDB serializer
Error Handling
The concept serializer includes robust error handling:
Type Validation
// This will throw TypeIsNotAConcept exception
var serializer = new ConceptSerializer<string>(); // Invalid - string is not a concept
Null Safety
public record OptionalId(Guid? Value) : ConceptAs<Guid?>(Value)
{
public static readonly OptionalId NotSet = new(null);
}
// Properly handles null values during serialization
Performance Benefits
Storage Efficiency
Concepts are serialized as their underlying values, meaning:
- No wrapper overhead: Only the business value is stored
- Native MongoDB types: Uses optimal BSON types for each primitive
- Index compatibility: Underlying values can be indexed normally
Memory Efficiency
- Lazy initialization: Concept instances are created only when needed
- Value semantics: Record-based concepts minimize allocation overhead
- Implicit conversions: Reduce explicit casting requirements
Best Practices
Use Static Members for Common Values
public record Status(string Value) : ConceptAs<string>(Value)
{
public static readonly Status Active = new("Active");
public static readonly Status Inactive = new("Inactive");
public static readonly Status Pending = new("Pending");
public static implicit operator Status(string value) => new(value);
}
Implement Validation
public record EmailAddress(string Value) : ConceptAs<string>(Value)
{
public EmailAddress(string value) : this(Validate(value)) { }
static string Validate(string email)
{
if (string.IsNullOrWhiteSpace(email))
throw new ArgumentException("Email cannot be empty");
if (!email.Contains('@'))
throw new ArgumentException("Invalid email format");
return email;
}
public static implicit operator EmailAddress(string value) => new(value);
}
Event Source Integration
For concepts used as Event Source IDs:
public record CustomerId(Guid Value) : ConceptAs<Guid>(Value)
{
public static readonly CustomerId NotSet = new(Guid.Empty);
public static implicit operator CustomerId(Guid value) => new(value);
public static implicit operator EventSourceId(CustomerId id) => new(id.Value.ToString());
public static CustomerId New() => new(Guid.NewGuid());
}
Collection Examples
Concepts work seamlessly in collections:
public class Order
{
public OrderId Id { get; set; }
public CustomerId CustomerId { get; set; }
public IEnumerable<ProductId> ProductIds { get; set; }
public Dictionary<ProductId, Quantity> Products { get; set; }
}
// All concept types in collections are automatically handled
Query Support
Concepts can be used directly in MongoDB queries:
var customerId = CustomerId.New();
var orders = await collection
.Find(o => o.CustomerId == customerId)
.ToListAsync();
// The concept is automatically converted to its underlying value for the query
Next Steps
- Learn about Class Mapping for complex type configurations
- Explore Convention Packs for customizing serialization behavior
- Read about Naming Policies for consistent field naming