Skip to content

Type Handlers

Paul Stovell edited this page Apr 20, 2020 · 6 revisions

ℹ️ Not sure if you need a type handler? Learn about the different ways to extend mapping in Nevermore.

Type Handlers are used to help Nevermore when mapping data between SQL result sets/parameters and CLR types.

When your data is stored in the JSON blob, it's serialized with Newtonsoft.Json, so controlling how it's serialized or mapping it to different types is easy - just write a JsonConverter and register it with the JsonSerializerSettings on RelationalStoreConfiguration. You only need a Type Handler for data that's stored in a column, or data you're using as parameters to queries.

Nevermore natively knows how to deal with most standard types: ints and strings, datetimes, and even enums (they are stored as strings).

For other types, you can register an ITypeHandler. For example, suppose we want to store a Uri. Here's a type handler:

public class UriTypeHandler : ITypeHandler
{
    public bool CanConvert(Type objectType)
    {
        return objectType == typeof(Uri);
    }

    public object ReadDatabase(DbDataReader reader, int columnIndex)
    {
        if (reader.IsDBNull(columnIndex))
            return default(Uri);
        var text = reader.GetString(columnIndex);
        return new Uri(text);
    }

    public void WriteDatabase(DbParameter parameter, object value)
    {
        parameter.DbType = DbType.String;
        parameter.Value = ((Uri) value)?.ToString();
    }
}

When reading, you're given the DbDataReader, and the index of the column that you should read from. This allows you to use whichever method on DbDataReader makes the most sense to you.

When writing, you're given the DbParameter. You should set the value on the DbParameter, and also set the DbType.

You can optionally add a Priority to your type handler. This allows you to provide handlers for specific types, with a fallback for unmapped types. For example, you might add support for a handler that knows how to store all IFormattable types, but set it with a higher priority number so it runs last (Priority 0 means first, Priority 1 second, etc.)

Your TypeHandler should then be registered with the store configuration:

store.Configuration.TypeHandlerRegistry.Register(new UriTypeHandler());

ℹ️ Tip:

If you want to control both JSON and SQL reading/writing, your type handler can also be a JsonConverter!

When are type handlers called?

Your type handler will be called everywhere that we need to convert to/from the CLR types you support, and SQL types. Essentially, any time we find ourselves with a DbDataReader trying to figure out how to map a column to a type (e.g., for a property, a contructor parameter, etc.) we will call your type handler.

That includes:

  • When reading documents with Load, TableQuery, and so on as we translate result set columns to properties
  • When writing documents with Insert, Update, etc. as we translate property values to column parameters
  • When reading tuples, plain old classes, etc. using Stream

CanConvert is typically called once, and cached. Nevermore remembers which type handler to use for each type, to speed up queries.

What can you not do with type handlers?

You cannot supply type handlers for basic types like string or DateTime. These fields are very common, and Nevermore builds expression trees that know how to deal with them as efficiently as possible.

ITypeHandler vs. IPropertyReaderWriter

With a type handler, you return an instance of the type. Nevermore then uses IPropertyReaderWriter when setting the value your type handler returns to set it on the property.

In pseudocode:

propertyReaderWriter.Write(documentInstance, typeHandler.ReadDatabase(reader, index));

If you want to control how the value is set on the target object property, you need to use an IPropertyReaderWriter and set it as part of the DocumentMap. Otherwise, Nevermore will simply call the property setter and pass the value your TypeHandler returns.