Skip to content

Instantly share code, notes, and snippets.

@TheCloudlessSky
Last active June 9, 2016 14:27
Show Gist options
  • Save TheCloudlessSky/f60d47ad2ca4dea72583 to your computer and use it in GitHub Desktop.
Save TheCloudlessSky/f60d47ad2ca4dea72583 to your computer and use it in GitHub Desktop.
JSON.NET implementation of ICacheSerializer for NHibernate.Caches.Redis
// IMPORTANT: This might not be a complete implementation. For example, if you use
// custom NHibernate types, you will have to modify this (e.g. inside of
// CustomContractResolver.CreateObjectContract and maybe writing a custom
// JsonConverter) to support your custom types. You'll want to test this
// implementation with your data and use cases.
public class NhJsonCacheSerializer : ICacheSerializer
{
// By default, JSON.NET will always use Int64/Double when deserializing numbers
// since there isn't an easy way to detect the proper number size. However,
// because NHibernate does casting to the correct number type, it will fail.
// Adding the type to the serialize object is what the "TypeNameHandling.All"
// option does except that it doesn't include certain types.
private class ExplicitTypesConverter : JsonConverter
{
// We shouldn't have to account for Nullable<T> because the serializer
// should see them as null.
private static readonly ISet<Type> explicitTypes = new HashSet<Type>(new[]
{
// Int64 and Double are correctly serialzied/deserialized by JSON.NET.
typeof(Byte), typeof(SByte),
typeof(UInt16), typeof(UInt32), typeof(UInt64),
typeof(Int16), typeof(Int32),
typeof(Single), typeof(Decimal),
typeof(Guid)
});
public override bool CanConvert(Type objectType)
{
return explicitTypes.Contains(objectType);
}
// JSON.NET will deserialize a value with the explicit type when
// the JSON object exists with $type/$value properties. So, we
// don't need to implement reading.
public override bool CanRead { get { return false; } }
public override bool CanWrite { get { return true; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// CanRead is false.
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("$type");
var typeName = value.GetType().FullName;
writer.WriteValue(typeName);
writer.WritePropertyName("$value");
writer.WriteValue(value);
writer.WriteEndObject();
}
}
private class CustomContractResolver : DefaultContractResolver
{
private static readonly ISet<Type> nhibernateCacheObjectTypes = new HashSet<Type>(new[]
{
typeof(CachedItem),
typeof(CacheLock),
typeof(CacheEntry),
typeof(CollectionCacheEntry)
});
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var result = base.CreateObjectContract(objectType);
// By default JSON.NET will only use the public constructors that
// require parameters such as ISessionImplementor. Because the
// NHibernate cache objects use internal constructors that don't
// do anything except initialize the fields, it's much easier
// (no constructor lookup) to just get an uninitialized object and
// fill in the fields.
if (nhibernateCacheObjectTypes.Contains(objectType))
{
result.DefaultCreator = () => FormatterServices.GetUninitializedObject(objectType);
}
return result;
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
if (nhibernateCacheObjectTypes.Contains(type))
{
// By default JSON.NET will serialize the NHibernate objects with
// their public properties. However, the backing fields/property
// names don't always match up. Therefore, we *only* use the fields
// so that we can get/set the correct value.
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Select(f => base.CreateProperty(f, memberSerialization));
var result = fields.Select(p =>
{
p.Writable = true;
p.Readable = true;
return p;
}).ToList();
return result;
}
else
{
return base.CreateProperties(type, memberSerialization);
}
}
}
private readonly JsonSerializerSettings settings;
public NhJsonCacheSerializer()
{
this.settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.All;
settings.Converters.Add(new ExplicitTypesConverter());
settings.ContractResolver = new CustomContractResolver();
}
public RedisValue Serialize(object value)
{
if (value == null) return RedisValue.Null;
var result = JsonConvert.SerializeObject(value, Formatting.None, settings);
return result;
}
public object Deserialize(RedisValue value)
{
if (value.IsNull) return null;
var result = JsonConvert.DeserializeObject(value, settings);
return result;
}
}
@nomaddamon
Copy link

Some small things:

  • Add typeof(Guid) to list of types as mentioned above - else deserialization will fail
  • Remove typeof(Int64) and typeof(Double) from the list of types - they are handled fine by JSON.NET (this does provide some memory savings in Redis, since almost all cached objects will have freshTimestamp property (typed Int64) - this will save at least 36b off every NHibernate.Cache.CachedItem you have in Redis)

@nomaddamon
Copy link

This serializer will fail with some types supported by NHibernate, one example being System.Globalization.CultureInfo - NHibernate supports it and NHibernate.Caches.Redis will happily (de)serialize it with NetDataContractSerializer but will fail on deserializing with NhJsonCacheSerializer (and the fix is not as simple as with Guid)

Prior to inclusion in NHibernate.Caches.Redis project, test should be written (and pass :)) for all supported types of NHibernate

@TheCloudlessSky
Copy link
Author

@jeffijoe @nomaddamon I've added Guid and removed Int64 and Double. Thanks for pointing those out! 😄

Any other types such as CultureInfo (or your own custom NHibernate types) will have to be implemented on your own. This class is a starting point for you to use this in your own project. This is why it's not directly included in NHibernate.Caches.Redis.

For example, we have Point class, that we partially implement inside of CustomContractResolver to tell JSON.NET exactly how to construct a serialized Point:

protected override JsonObjectContract CreateObjectContract(Type objectType)
{
    var result = base.CreateObjectContract(objectType);

    // JSON.NET uses the default constructor (or uninitialized objects)
    // by default. Since Point is immutable, we must use the parameterized
    // constructor. Since we don't need to take a dependency on JSON.NET
    // in the model, we can't use [JsonConstructor]. It is explicitly 
    // emulated here.
    if (objectType == typeof(Point))
    {
        result.CreatorParameters.Add(new JsonProperty()
        {
            PropertyName = "x",
            PropertyType = typeof(Int32)
        });
        result.CreatorParameters.Add(new JsonProperty()
        {
            PropertyName = "y",
            PropertyType = typeof(Int32)
        });
        result.OverrideCreator = (args) =>
        {
            return new Point((Int32)args[0], (Int32)args[1]);
        };
    }
    // By default JSON.NET will only use the public constructors that 
    // require parameters such as ISessionImplementor. Because the 
    // NHibernate cache objects use internal constructors that don't 
    // do anything except initialize the fields, it's much easier 
    // (no constructor lookup) to just get an uninitialized object and 
    // fill in the fields.
    else if (nhibernateCacheObjectTypes.Contains(objectType))
    {
        result.DefaultCreator = () => FormatterServices.GetUninitializedObject(objectType);
    }

    return result;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment