Skip to content

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.

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);
}

When you call UseCratisMongoDB(), all types implementing ConceptAs<T> are automatically configured for MongoDB serialization through the ConceptSerializationProvider.

The ConceptSerializer<T> handles the serialization by:

  1. Detection: Automatically detects types that implement ConceptAs<T>
  2. Unwrapping: Serializes only the underlying value, not the wrapper
  3. Type Safety: Ensures type validation during serialization/deserialization
  4. Performance: Optimized to avoid unnecessary object creation
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")
// }

The ConceptSerializer<T> supports all primitive types that MongoDB can natively handle:

  • int, uint, long, ulong
  • float, double, decimal
  • byte, sbyte, short, ushort
  • string
  • char
  • DateTime
  • DateTimeOffset (uses the custom serializer)
  • DateOnly (uses the custom serializer)
  • TimeOnly (uses the custom serializer)
  • bool
  • Guid
  • Any type that has a registered MongoDB serializer

The concept serializer includes robust error handling:

// This will throw TypeIsNotAConcept exception
var serializer = new ConceptSerializer<string>(); // Invalid - string is not a concept
public record OptionalId(Guid? Value) : ConceptAs<Guid?>(Value)
{
public static readonly OptionalId NotSet = new(null);
}
// Properly handles null values during serialization

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
  • Lazy initialization: Concept instances are created only when needed
  • Value semantics: Record-based concepts minimize allocation overhead
  • Implicit conversions: Reduce explicit casting requirements
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);
}
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);
}

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());
}

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

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