nhibernate/NHibernate-Caches

Consider defaulting to another serializer than binary

fredericDelaporte opened this issue · 13 comments

Binary serialization (BinaryFormatter) is made obsolete in .Net 5.

Some caches use it by default. They have to be made obsolete themselves for .Net 5, or changed for another serialization default when possible.

(Issue extracted from nhibernate/nhibernate-core#2603.)

Hi @fredericDelaporte ,
Thank you pointing out JsonSerializer sometimes we may miss "NH documentation", when I use "cache.serializer" I get below exception? Is it necessary open another issue about this?

    <property name="cache.region_strategy_factory">NHibernate.Caches.StackExchangeRedis.Tests.CacheRegionStrategyFactory, NHibernate.Caches.StackExchangeRedis</property>
    <property name="cache.region_strategy">NHibernate.Caches.StackExchangeRedis.DistributedLocalCacheRegionStrategy, NHibernate.Caches.StackExchangeRedis</property>
    <property name="cache.provider_class">NHibernate.Caches.StackExchangeRedis.RedisCacheProvider,NHibernate.Caches.StackExchangeRedis</property>
    <property name="cache.serializer">NHibernate.Caches.Util.JsonSerializer.JsonCacheSerializer, NHibernate.Caches.Util.JsonSerializer</property>

NHibernate.Caches.StackExchangeRedis.Messages.CacheSynchronizationMessage, NHibernate.Caches.StackExchangeRedis, Version=5.7.0.0, Culture=neutral, PublicKeyToken=6876f2ea66c9f443', use JsonCacheSerializer.RegisterType method to register it.

at NHibernate.Caches.Util.JsonSerializer.JsonCacheSerializer.ExplicitSerializationBinder.BindToName(Type serializedType, String& assemblyName, String& typeName)
   at Newtonsoft.Json.Utilities.ReflectionUtils.GetFullyQualifiedTypeName(Type t, ISerializationBinder binder)
   at Newtonsoft.Json.Utilities.ReflectionUtils.GetTypeName(Type t, TypeNameAssemblyFormatHandling assemblyFormat, ISerializationBinder binder)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.WriteTypeProperty(JsonWriter writer, Type type)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.WriteObjectStart(JsonWriter writer, Object value, JsonContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
   at NHibernate.Caches.Util.JsonSerializer.JsonCacheSerializer.Serialize(Object value)
   at NHibernate.Caches.StackExchangeRedis.DistributedLocalCacheRegionStrategy.ExecuteOperation(String cacheKey, Object value, Int64 timestamp, Int32 clientId, Boolean publish, Boolean remove)
   at NHibernate.Caches.StackExchangeRedis.DistributedLocalCacheRegionStrategy.TryPutLocal(String cacheKey, Object value, Int64 timestamp, Int32 clientId, Boolean publish)
   at NHibernate.Caches.StackExchangeRedis.DistributedLocalCacheRegionStrategy.ExecutePut(String cacheKey, Object value)
   at NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy.Put(Object key, Object value)
   at NHibernate.Caches.StackExchangeRedis.DistributedLocalCacheRegionStrategy.ExecutePutMany(Object[] keys, Object[] values)
   at NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy.PutMany(Object[] keys, Object[] values)
   at NHibernate.Caches.StackExchangeRedis.RedisCache.PutMany(Object[] keys, Object[] values)
   at NHibernate.Cache.UpdateTimestampsCache.SetSpacesTimestamp(IReadOnlyCollection`1 spaces, Int64 ts)
   at NHibernate.Cache.UpdateTimestampsCache.PreInvalidate(IReadOnlyCollection`1 spaces)

Yes that is another issue, or lack of knowledge/configuration. It seems to me your error message is incomplete by the way. I am hiding these comments as off-topic.

@gokhanabatay for security reasons each type that is serialized must be register. StackExchangeRedis strategies DistributedLocalCacheRegionStrategy and TwoLayerCacheRegionStrategy use additional types that needs to be registered. The following registrations should work:

public class RedisJsonCacheSerializer : JsonCacheSerializer
{
  public RedisJsonCacheSerializer()
  {
    // Registrations for DistributedLocalCacheRegionStrategy
    RegisterType(typeof(CacheSynchronizationMessage), "sm");
    RegisterType(typeof(LockData), "ld");
    RegisterType(typeof(LockManyData), "lmd");
    RegisterType(typeof(LockResultMessage), "lrm");
    RegisterType(typeof(PutData), "pd");

    // Registrations for TwoLayerCacheRegionStrategy
    RegisterType(typeof(CacheKeyInvalidationMessage), "kim");
  }
}

Alternative serializers like Message Pack require you to annotate the properties of your objects to identify which properties to serialize. Which properties of your entities do you need to serialize at minimum for NHibernate cache to function correctly? Child collections / Many-to-one properties / etc?

This is implementation specific. Some caches do not need serialization at all. Those needing it usually require serializing the complete state.

This state is not the entities or collections themselves, but their "dehydrated" versions, which means, simple arrays of atomic types. Some caches also adds some of their own state in the serialized data though.

Hi,
for .net6 there seems to be no workaround using the binary serializer and you have to switch (https://docs.microsoft.com/de-de/dotnet/standard/serialization/binaryformatter-security-guide)
I'm having trouble using the JsonCacheSerializer and IDistributedCache and cannot find much documentation or advice. Currently using MemoryDistributedCache and also want to use RedisCache.
It fails for me for simple types like Tuple<object, object>, CacheKey, QueryKey - and I'm not even sure I should register these types as they are NHibernates internals? Even if I do it fails later when deserializing CacheKey - do I need to add my own contracts for NH types?
Is there any further sample code or documentation to check out other than the docs stating "use JsonCacheSerializer" and the tests in this project?
Thanks

We ended up using the JsonCacheSerializer with StackExchangeRedisCache when moving away from binary serializer. It seemed the only reasonable option. Our configuration is :

cfg.AddProperties(new Dictionary<string, string> {
    {
        RedisEnvironment.Serializer,
        typeof(global::NHibernate.Caches.Util.JsonSerializer.JsonCacheSerializer).AssemblyQualifiedName
    }
});

Only other thing that needed to be done is to ensure the Assemble and Disassemble methods for custom types are correctly implemented.

This bit of code should be added by default somewhere. I'm not sure if it should be in the CoreDistributedCache, or in NHibernate.Core, or in the Utils.JsonCacheSerializer:

var serializer = new JsonCacheSerializer();
serializer.RegisterType(typeof(Tuple<string, object>), "tso");
CoreDistributedCacheProvider.DefaultSerializer = serializer;

See: https://github.com/nhibernate/nhibernate-core/blob/c6677a0cfaf3bc7949ce3429e40c3745345421a9/src/NHibernate.Test/CacheTest/JsonSerializerCacheFixture.cs

No need, see #117. But 5.9 is not yet released.

is there any plan for the 5.9 release?
or is it possible to use any other serializer compatible with .net 6 that does not have the JsonCacheSerializer issues?

I am having the same JsonCacheSerializer issues as above (required to register types). Trying to use a distributed redis cache (NHibernate.Caches.CoreDistributedCache.Redis) so that multiple instances of same services and other instances of other services could access the same NHibernate cache.

Is this possible with the current version of NHibernate.Caches? What are the steps to do it if yes?

You can add in your project the code shown here. You will have to remove the RegisterType line when upgrading to 5.9.0.

About 5.9.0, I should have released it sooner but I have not got the time to do it. (And I am currently away for one week.)

5.9.0 is now released.

Works beautifully without having to use my own ConfigureJsonCacheSerializer() code. Many thanks.