Skip to content

Advanced Tips

Paul Stovell edited this page Apr 18, 2020 · 11 revisions

Tip 1: Serialization binders

ℹ️ This is actually a Newtonsoft.Json feature, not a Nevermore feature - but it's worth considering.

When a document contains a property or collection, and the instances within them are different to what the property or collection is declared as, Newtonsoft.Json (the default serializer in Nevermore) will add a $type property. For example, perhaps we have something like this:

class OrderHistory
{
    public string Id { get; set; }
    public List<IAuditEvent> AuditEvents { get; } = new List<IAuditEvent>(); 
}

interface IAuditEvent  { }
class CreatedEvent : IAuditEvent  { ... }
class EditedEvent : IAuditEvent  { ... }

The serialized JSON will end up something like this:

{
  "AuditEvents": [
    {
      "$type": "Company.Product.Namespace.CreatedEvent, Company.Product"
      ...
    },
    {
      "$type": "Company.Product.Namespace.EditedEvent, Company.Product"
      ...
    },
    {
      "$type": "Company.Product.Namespace.EditedEvent, Company.Product",
      ...
    }
  ]
}

Not only does this look messy, but it also makes data migration difficult over time.

A feature you should make use of in Json.Net is a custom ISerializationBinder. This allows you to control the format used in the $type property.

Some things this would let you do:

  • Serialize just the class name (EditedEvent for example), and on deserialization, look them up by searching for types with that name in the assemblies we'd expect.
  • Handle situations where a type has moved, by translating one name to another.

One thing to keep in mind though is that the serialization binder is called each time you deserialize, so you may want to cache the mappings.

Here's an example serialization binder:

class MySerializationBinder : ISerializationBinder
{
    readonly ISerializationBinder fallback;

    public MySerializationBinder(ISerializationBinder fallback)
    {
        this.fallback = fallback ?? new DefaultSerializationBinder();
    }
    
    public Type BindToType(string? assemblyName, string typeName)
    {
        if (typeName == "Created" && assemblyName == null)
            return typeof(CreatedEvent);
        if (typeName == "Edited" && assemblyName == null)
            return typeof(EditedEvent);
        return fallback.BindToType(assemblyName, typeName);
    }

    public void BindToName(Type serializedType, out string? assembly, out string? typeName)
    {
        if (serializedType == typeof(CreatedEvent))
        {
            typeName = "Created";
            assemblyName = null;
            return;
        }
        if (serializedType == typeof(EditedEvent))
        {
            typeName = "Edited";
            assemblyName = null;
            return;
        }

        fallback.BindToName(serializedType, out assemblyName, out typeName);
    }
}

It's registered like this:

configuration.UseJsonNetSerialization(settings =>
{
    settings.SerializationBinder = new MySerializationBinder(settings.SerializationBinder);
});

Tip 2: Consider using check constraints

From SQL 2016, the IsJson function can be used to check that a document is valid JSON (though not that it has all the documents we expect). You might consider adding this as a constraint:

create table Customer
(
    Id nvarchar(200) primary key not null, 
    -- ... other properties...

    [JSON] nvarchar(max) not null 
        constraint CK_Customer_JSON check (IsJson([JSON]) > 0)
)

If you use Compression you can check those too:

[JSONBlob] varbinary(max) not null 
    constraint CK_Customer_JSONBlob 
      check (IsJson(cast(DECOMPRESS([JSONBlob]) as nvarchar(max))) > 0)

The benefit to this is that it will prevent someone adding improperly-formatted JSON, or JSON that the application understands (e.g., UTF-8 encoded) but SQL doesn't.

⚠️ Note that there is a performance penalty to this - for JSON stored as text, it's about 20-50% overhead (like 0.2ms for a small document, and 2ms for a large 65K document). For compressed JSON, it's closer to 100%. But this only affects writes, not reads, so it may be worth it.

Alternatively, if the application isn't busy, you could use ISJSON to do an offline check on a schedule to pick up errors.

Clone this wiki locally