-
Notifications
You must be signed in to change notification settings - Fork 4
Custom serializers
Scrinium ships serializers for common shapes and lets you register your own for value types. You attach a serializer in one of two places:
-
On a member, inside a model map:
map.GetMemberMap(x => x.Member).SetSerializer(...). -
On a type, engine-wide:
dbContextEngine.MapRegistry.AddCustomSerializerMap(new MySerializer()).
All live in Etherna.Scrinium.Core.Serialization.Serializers.
| Serializer | For | Notes |
|---|---|---|
EnumerableSerializer<TItem> |
IEnumerable<TItem> |
Wrap a child item serializer — e.g. a collection of references. |
DictionarySerializer<TKey, TValue> |
IDictionary<TKey, TValue> |
Configurable representation and key/value serializers. |
ReadOnlyDictionarySerializer<TKey, TValue> |
IReadOnlyDictionary<TKey, TValue> |
As above, read-only. |
HexToBinaryDataSerializer |
string (hex) |
Stores a hex string as BSON Binary — compact for hashes/addresses. |
GeoPointSerializer<TInModel> |
a model with lon/lat members | Stores it as a GeoJSON Point for geospatial indexes/queries. |
ExtraElementsSerializer |
object |
Migration helper: read legacy values out of ExtraElements. |
ReferenceSerializer<TModelBase, TKey> |
an entity reference | Denormalized summary + lazy load — see References and denormalization. |
Wrap another serializer to serialize a collection of it. The key use is a collection of references:
map.GetMemberMap(t => t.Members)
.SetSerializer(new EnumerableSerializer<User>(UserReference(dbContextEngine)));With no argument it serializes items with their default serializer.
map.GetMemberMap(x => x.Labels)
.SetSerializer(new DictionarySerializer<string, string>(DictionaryRepresentation.Document));Pass key/value serializers to the three-arg constructor when the values need special handling (e.g. a dictionary whose values are references).
Store a hex string (a hash, an address) as binary instead of text:
map.GetMemberMap(x => x.Hash).SetSerializer(new HexToBinaryDataSerializer());Serialize a model's longitude/latitude pair as a GeoJSON point (enabling 2dsphere queries):
map.GetMemberMap(x => x.Location)
.SetSerializer(new GeoPointSerializer<Location>(dbContextEngine, l => l.Longitude, l => l.Latitude));A helper for schema fix functions and migrations: pull a typed
value out of the raw ExtraElements bag of a legacy document.
var serializer = new ExtraElementsSerializer(dbContextEngine);
var legacy = serializer.DeserializeValue<LegacyShape>(model.ExtraElements!["oldField"]);Derive from the driver's SerializerBase<T> and register it type-wide with AddCustomSerializerMap.
A common case is representing a value differently on disk — money as Decimal128, an encrypted string,
a domain primitive:
public sealed class MoneySerializer : SerializerBase<decimal>
{
private readonly IBsonSerializer<decimal> inner = new DecimalSerializer(BsonType.Decimal128);
public override decimal Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) =>
inner.Deserialize(context, args);
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, decimal value) =>
inner.Serialize(context, args, value);
}class MoneyMap : IModelMapsCollector
{
public void Register(IDbContextEngine dbContextEngine) =>
dbContextEngine.MapRegistry.AddCustomSerializerMap(new MoneySerializer());
}A member typed object — or an object valued dictionary, the usual metadata bag — serializes
through the driver's ObjectSerializer, which accepts only the types of its allow list: the driver's
default framework types — primitives, common value types, framework collections, and arrays and
enums of allowed types — and nothing else by default. A document naming any other type in its _t is
refused on read, and a value of another type is refused on write, so a document can't decide which
type materializes in your model.
Widen it deliberately, for the types you accept, by registering an ObjectSerializer of your own:
using Etherna.MongoDB.Bson.Serialization.Serializers;
dbContextEngine.MapRegistry.AddCustomSerializerMap<object>(
new ObjectSerializer(type => ObjectSerializer.DefaultAllowedTypes(type) ||
type == typeof(MyAllowedType)));The registered ObjectSerializer is also what makes interface typed members work: the driver's
interface serializer installs itself only over it and writes the member through it, so only allowed
implementations persist; on read the implementation is resolved from the document's discriminator.
Note. An entity model never belongs in an
objectmember: entities are stored as references, and the allow list refuses a mapped model type in anobjectmember, on write and on read — never widen it with an entity type. (The engine-build freeze validates declared member serializers only, so it can't see what anobjectmember holds — see References and denormalization for the freeze.)
An id typed object is a different matter, and is refused at engine build: it doesn't commit to
an id type, the contract repositories and references key on. Registering a custom serializer map for
object, as above, is what declares how those values serialize and deserialize, and makes such an id
accepted — its rendered value must still be a value, never a document (Domain models).
A custom serialized type can also key an entity — be the TKey of an IEntityModel<TKey>.
Repositories, references and the identity map address the entity with the typed value, and the
document stores _id in the custom representation. A domain primitive as id:
public readonly struct Isbn(string value)
{
public string Value { get; } = value;
}
public sealed class IsbnSerializer : SerializerBase<Isbn>
{
public override Isbn Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) =>
new(context.Reader.ReadString());
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Isbn value) =>
context.Writer.WriteString(value.Value);
}
public class Book : EntityModelBase<Isbn>
{
public Book(Isbn isbn, string title)
{
Id = isbn;
Title = title;
}
protected Book() { }
public virtual string Title { get; set; }
}class BookMap : IModelMapsCollector
{
public void Register(IDbContextEngine dbContextEngine)
{
dbContextEngine.MapRegistry.AddCustomSerializerMap(new IsbnSerializer());
dbContextEngine.MapRegistry.AddModelMap<EntityModelBase<Isbn>>(
"0e18ba21-6b7c-4e5c-9e0a-3d2f41c88b57");
dbContextEngine.MapRegistry.AddModelMap<Book>(
"5a7d90cf-2143-48f2-a5b8-6c1e0d9b374a");
}
}An Isbn id has no id generator: the application assigns it before insert (Id = isbn above). The
same serializer serves every other Isbn member of the db context, and the type works in
queries and indexes like any other member type.
An id serializer writes a value. Whatever the id type, its serializer must emit a BSON value — the
WriteStringabove — never a document or an array (the rationale is on Domain models). A serializer emitting a document is refused when it renders, by every operation addressing a document by its key and by the create itself; the id types whose serializer declares a composite fail fast at engine build.
Note.
AddCustomSerializerMapclaims the serializer of its type when invoked: register custom serializer maps before the model maps of the entities they key, likeBookMapabove. Mapping the entity resolves the id type's serializer immediately; for a type the driver otherwise serves with a default serializer (likeGuidbelow), a claim arriving after that resolution conflicts with it and fails at startup with aBsonSerializationException.
The driver's default Guid serializer has an unspecified representation and fails at use: to key
entities with Guid, claim the representation with a custom serializer map. The driver id generator
assigns the value on insert.
dbContextEngine.MapRegistry.AddCustomSerializerMap(new GuidSerializer(GuidRepresentation.Standard));
dbContextEngine.MapRegistry.AddModelMap<EntityModelBase<Guid>>(
"c9f2e8d4-7b16-45a3-9c58-0e6a2d31f7b9");Next: Model mapping for attaching serializers to members, References and denormalization for reference serializers, or Indexes for geospatial and other indexes.
Scrinium — source · issues (SCR) · GNU LGPL-3.0 · info@etherna.io
Getting started
Core concepts
Working with data
Serialization & mapping
Operations & maintenance
Advanced & reference