-
Notifications
You must be signed in to change notification settings - Fork 4
Domain models
Your domain classes are plain C# objects, but to be persisted, lazy-loaded and change-tracked by Scrinium they must follow a few rules. This page is the complete contract for a persisted model.
| Kind | Implements | Identity | Mutability |
|---|---|---|---|
| Entity | IEntityModel<TKey> |
Yes — an Id of type TKey
|
Mutable through methods |
| Value object | IModel |
No | Immutable after construction; lives only inside an entity or another value object |
Only entities are stored as top-level documents and handled by repositories. Value objects are serialized inline within their owner.
Scrinium defines the interfaces; you implement them — usually via your own base classes, like the ones
below. (Etherna.Scrinium.Core.Domain.Models also ships ready-made ModelBase /
EntityModelBase<TKey> implementations with these exact names; the bases below show their essential
shape — the shipped EntityModelBase<TKey> also implements id-based Equals/GetHashCode.)
public interface IModel
{
IDictionary<string, object>? ExtraElements { get; } // MongoDB overflow bag
}
public interface IEntityModel : IModel
{ }
public interface IEntityModel<TKey> : IEntityModel
{
TKey Id { get; }
}-
ExtraElementscaptures document fields not mapped to a property while a document deserializes. It's what lets a schema drop a member without changing its id — deserialization tolerates added and removed fields (see versioned schemas) — and what a schema's fix function reads to migrate old values. The bag lives only inside the load: Scrinium empties it once deserialization (and the fix, when present) has run, and never writes extra data back on save. -
IEntityModelcarries no member of its own: it is the marker distinguishing entity models from plain embedded models. Unlinking related models on delete needs no hook on the model: each reference declares its own origin delete policy, propagated in background when the model is deleted through its repository.
Define two abstract bases and derive your domain from them (as in First steps):
public abstract class ModelBase : IModel
{
public virtual IDictionary<string, object>? ExtraElements { get; protected set; }
}
public abstract class EntityModelBase<TKey> : ModelBase, IEntityModel<TKey>
{
public virtual TKey Id { get; protected set; } = default!;
}Model dates are DateTimeOffset UTC instants (UtcNow); how to persist them as plain BSON
Dates — and when to derive a creation instant from an ObjectId id instead — is configured per
member in Model mapping.
Scrinium subclasses your models with a proxy, generated at compile time by the source generator
shipped inside the Etherna.Scrinium.Core package, to add lazy loading and change tracking
signaling. Only virtual members can be overridden, so all properties and methods must be
virtual. A non-virtual member silently loses lazy loading and change tracking.
The serializer needs to create an empty instance before filling it. Provide a protected (or
public) parameterless constructor alongside your real constructors:
public Cat(string name, DateTime birthday) { Name = name; Birthday = birthday; }
protected Cat() { } // for the serializerA member is serialized both ways only if it has a setter. Keep setters protected to protect your
invariants; the serializer sets them by reflection. A property without a setter (a computed value
like Age) is ignored by the serializer by default. A schema fix function writes a member behind a
non-public setter with ReflectionHelper.SetValue — see Versioned schemas.
Back a collection with a private field and expose it as IEnumerable<T> (or another read-only
interface), never null (empty at most). This keeps mutation under your control and avoids null
checks on load:
private List<Kitten> _kittens = [];
public virtual IEnumerable<Kitten> Kittens => _kittens;
public virtual void AddKitten(Kitten kitten) => _kittens.Add(kitten);Exposing a mutable collection type (List<T>, an array, IDictionary<>) is legal, but every
read of it flags the model for a diff at save: a change made through the handed-out collection
can't be intercepted, so Scrinium assumes one may have happened
(Change tracking and saving). With read-only exposure, reads stay free when the element type is
itself immutable or an entity (see the value object rules below). Casting a read-only view
back to its mutable type to sneak a change past detection is unsupported.
A summary reference fully loads its document when a missing member
is accessed. Property accesses are intercepted directly, and a method that reads or writes a
backing field (like AddKitten above) is analyzed at compile time: the source generator computes
the properties each method touches — direct backing field accesses included, following the non
virtual helpers of the model — so a summary loads the full document before the method runs on
partial state. A method whose source the generator can't see (a base class compiled in another
assembly) conservatively loads the full document when invoked on a summary.
Leave Id unset on new entities when its type has an id generator; it's assigned on insert. A common
setup stores a string id as a MongoDB ObjectId and generates it automatically, configured in the
model map:
schema.IdMemberMap.SetSerializer(new StringSerializer(BsonType.ObjectId))
.SetIdGenerator(new StringObjectIdGenerator());ObjectId and Guid ids get their driver generators without configuration (Guid also needs its
representation claimed with a custom serializer map — see Custom serializers). An id type without
a generator — an int, a custom serialized domain primitive — is assigned by the application before
insert.
An entity is addressed by an atomic key: repositories, identity map and references all key on the
id value, and a document valued id is the only shape MongoDB reads as an operator expression
instead of a value — a caller sending {"$ne": null} as id would otherwise match, delete or
overwrite an arbitrary document. So Scrinium refuses an id that doesn't serialize to a value:
-
Composite ids are not supported. A class mapped id, a dictionary, an interface, a
BsonDocumentorBsonValuemember fail fast at engine build withScriniumInvalidIdMemberException(see Exceptions reference). Serialize a composite key into a value — a string like"tenant:code"— and map its components as ordinary members if you need to query them. -
objectids are refused too, because they don't commit to a type: the values with no BSON type equivalent serialize as discriminated documents, and the others read back as the type of their BSON type — an enum id writes1and reads back anInt32. An application that really keys on heterogeneous values declares it mapping its own serializer forobject(Custom serializers). -
A custom serializer emitting a document is invisible to the engine build, so it's refused where
it renders: every operation addressing a document by its key throws a
FormatException, and so does the create, since a document written with an unaddressable id couldn't be read, updated or deleted afterwards.TryFindOneAsyncreads such an id like any other unparsable key, an id matching nothing.
A value object implements only IModel and is immutable — no Id, no setters exposed, constructed
complete:
public class GeoCoordinate : ModelBase // ModelBase : IModel
{
public GeoCoordinate(double lat, double lon) { Latitude = lat; Longitude = lon; }
protected GeoCoordinate() { }
public virtual double Latitude { get; protected set; }
public virtual double Longitude { get; protected set; }
}Keep value objects genuinely immutable: protected/init-only setters and no public mutating methods. A value object exposing public setters or business methods counts as mutable state, and reading any member that hands it out (directly, or nested inside a collection or another value object) flags the owner model for a diff at save (Change tracking and saving). References to other entities never count: their changes are tracked on their own repository.
Next: Model mapping to declare how these models serialize, Repositories to store and query entities, or References and denormalization to relate them.
Scrinium — source · issues (SCR) · GNU LGPL-3.0 · info@etherna.io
Getting started
Core concepts
Working with data
Serialization & mapping
Operations & maintenance
Advanced & reference