-
-
Save TheCloudlessSky/f60d47ad2ca4dea72583 to your computer and use it in GitHub Desktop.
// 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; | |
} | |
} |
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
@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;
}
Some small things:
typeof(Guid)
to list of types as mentioned above - else deserialization will failtypeof(Int64)
andtypeof(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 havefreshTimestamp
property (typedInt64
) - this will save at least 36b off everyNHibernate.Cache.CachedItem
you have in Redis)