Caching issue in development

Getting the following exception when trying to pull an object out of the cache, it was added fine

Exception has occurred: CLR/System.InvalidCastException
An exception of type 'System.InvalidCastException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'Unable to cast object of type 'ᵀAɗƹϦŵɧҏҤũIԯŹӍijİαwʒԒȻҵʕɇԓӮťpǭԓͶӣӚЉҩԧđӓͲᴱ.ᵀrҪƆǺɸˋεӄԎΦǍаΚᴱ[System.Object]' to type 'ᵀAɗƹϦŵɧҏҤũIԯŹӍijİαwʒԒȻҵʕɇԓӮťpǭԓͶӣӚЉҩԧđӓͲᴱ.ᵀrҪƆǺɸˋεӄԎΦǍаΚᴱ[System.Nullable`1[FourRoads.VerintCommunity.ForumLastPost.Interfaces.LastReadPostInfo]]'.'
at System.Runtime.CompilerServices.CastHelpers.<IsInstanceOf_NoCacheLookup>g____PInvoke|4_0(Void* __toTypeHnd_native, Int32 __throwCastException_native, ObjectHandleOnStack __obj_native) at System.Runtime.CompilerServices.CastHelpers.<IsInstanceOf_NoCacheLookup>g____PInvoke|4_0(Void* __toTypeHnd_native, Int32 __throwCastException_native, ObjectHandleOnStack __obj_native) at System.Runtime.CompilerServices.CastHelpers.ChkCastAny_NoCacheLookup(Void* toTypeHnd, Object obj) at System.Runtime.CompilerServices.CastHelpers.ChkCast_Helper(Void* toTypeHnd, Object obj) at ᵀAɗƹϦŵɧҏҤũIԯŹӞŖѨћѤđΘɅιƚDŽċɘõãɔǷɦӡҲDZԦËϴЅʼnᴱ.ᵀĴǩӉVŹАζӓǼɞϬȾɧᴱ.Get[T](String key) at ᵀAɗƹϦŵɧҏҤũIԯŹӞŖѨћѤđΘɅιƚDŽċɘõãɔǷɦӡҲDZԦËϴЅʼnᴱ.ᵀÞҋĺòȐđçˬƁҩґԬʋᴱ.TryGet[T](String key, CacheScope scope, T& value, ConsistencyLevel consistency) at Telligent.Evolution.Extensibility.Caching.Version1.CacheService.TryGet[T](String key, CacheScope scope, T& value, GetOptions`1 options)

this is what it added to the cache

    public struct LastReadPostInfo
    {
        public int ReplyCount { get; set; }
        public Guid? ContentId { get; set; }
        public DateTime PostDate { get; set; }
    }
  • I've just implemented this and it is being called for every type in the system

    public
    class DistributedCacheSerializer<T> : IDistributedCacheSerializer<T>
        {
            public byte[] Serialize(T value)
            {
                return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value);
            }

            public T Deserialize(byte[] value)
            {
                return System.Text.Json.JsonSerializer.Deserialize<T>(value);
            }
        }
    Are you saying that for every entity we have defined in a project we will have to register a specific serializer?

    public class DistributedCacheSerializer : IDistributedCacheSerializer<RealType>
        {
            public byte[] Serialize(RealType value)
            {
                return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value);
            }

            public RealType Deserialize(byte[] value)
            {
                return System.Text.Json.JsonSerializer.Deserialize<RealType>(value);
            }
        }


  • The problem or interesting thing is that

    LastReadPostInfo lastreadPost = _cacheService.Get<LastReadPostInfo>(key);  

    works

    LastReadPostInfo? lastreadPost = _cacheService.Get<LastReadPostInfo?>(key);

    does not work because the underlying type being stored via the put is not LastReadPostInfo, this used to work.


    In our code I am going to add "where T : notnull" to all cache methods, so our code won't compile.
     

  • I’ve reviewed this again, and I still maintain that my original suggestion above is the solution: ensure the type used when putting the item into the cache matches the type used when getting it back out.

    For example, if you put LastReadPostInfo?, you should also get LastReadPostInfo?. If you put LastReadPostInfo, you should get LastReadPostInfo.

    I also cannot replicate any issue with caching nullable structs, and there have been no changes from previous versions. I’ve included full sample code below using your struct, as well as a similar cache service wrapper.

    Things to note about this sample code:

    The cache wrapper’s Put implementation is generic, as suggested. Changing the wrapper's Put to accept an object does result in your original exception.

    Since the cache wrapper enforces use of the distributed cache service via CacheScope.All, I’ve implemented IDistributedCacheSerializationProvider. However, the provider is intentionally limited to the specific type this plugin owns and understands: LastReadPostInfo.
    Are you saying that for every entity we have defined in a project we will have to register a specific serializer?

    You don't need a separate serializer for every type you own, but every type you own that you want to serialize must have a serializer you also own, even if they share the same custom serializer. It is very important that you not implement a generic, catch-all IDistributedCacheSerializationProvider, as shown in the earlier example, for a few reasons:

    • JSON is a good fit for small, simple, flat types like structs.
    • JSON is not a good fit for all classes.
    • JSON is a poor fit for complex nested types or types that contain binary data or streams.
    • JSON can be slower and more verbose than binary formats commonly used for remote caching.
    • Other serialization formats, such as MessagePack, are often faster and better suited to remote caching.
    • There is no universal way to define which parts of a type should and should not be serialized, or how they should be serialized across different serialization approaches.
    • For these reasons, a plugin must define its own IDistributedCacheSerializationProvider when using the distributed cache, since only the developer can know the intended serialization behavior for the plugin’s own types.
    • If you implement an IDistributedCacheSerializationProvider that matches all types, T, it will break other plugins as well as the underlying platform itself which often have their own specific serialization formats for given types.
    • You must ensure your IDistributedCacheSerializationProvider only handles the types you explicitly intend to support.
    using Telligent.Evolution.Extensibility;
    using Telligent.Evolution.Extensibility.Api.Version1;
    using Telligent.Evolution.Extensibility.Caching.Version1;
    using Telligent.Evolution.Extensibility.Version1;
    
    namespace FourRoads.CacheTest;
    
    public class CacheTestPlugin : IPlugin, IDistributedCacheSerializationProvider
    {
        public string Name => "Four Roads Cache Test";
    
        public string Description => Name;
    
        public void Initialize()
        {
            LastReadPostInfo? postInfoToCache = new LastReadPostInfo
            {
                ReplyCount = 5,
                ContentId = Guid.NewGuid(),
                PostDate = DateTime.UtcNow
            };
    
            string myCacheKey = "my-test-cache-key";
    
            // put in cache
            TestCacheWrapper.Insert(myCacheKey, postInfoToCache, ["tag1", "tag2"], TimeSpan.FromMinutes(10));
    
            // get from cache
            var cachedPostInfo = TestCacheWrapper.Get<LastReadPostInfo?>(myCacheKey);
    
            // verify that the cached item matches original
            bool success = cachedPostInfo?.ContentId == postInfoToCache?.ContentId &&
                cachedPostInfo?.ReplyCount == postInfoToCache?.ReplyCount &&
                cachedPostInfo?.PostDate == postInfoToCache?.PostDate;
    
            Apis.Get<IEventLog>().Write($"LastReadPostInfo cached? {success}", new EventLogEntryWriteOptions
            {
                EventType = "Information",
                Category = "FourRoadsCacheTest",
                EventId = 1001
            });
        }
    
        #region IDistributedCacheSerializationProvider
    
        private readonly HashSet<Type> _serializableTypes = new()
        {
            { typeof(LastReadPostInfo?) }
        };
    
        public IDistributedCacheSerializer<T> GetSerializer<T>()
        {
            if (_serializableTypes.Contains(typeof(T)))
                return new TestDistributedCacheSerializer<T>();
    
            return null!;
        }
    
        #endregion
    }
    
    public class TestDistributedCacheSerializer<T> : IDistributedCacheSerializer<T>
    {
        public byte[] Serialize(T value)
        {
            return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value);
        }
    
        public T Deserialize(byte[] value)
        {
            return System.Text.Json.JsonSerializer.Deserialize<T>(value)!;
        }
    }
    
    public static class TestCacheWrapper
    {
        public static void Insert<T>(string key, T value, string[] tags, TimeSpan timeout)
        {
            CacheService.Put(key, CacheScope.All, value, new PutOptions
            {
                ExpiresAfter = timeout,
                Tags = tags ?? Array.Empty<string>()
            });
        }
    
        public static T Get<T>(string key)
        {
            CacheService.TryGet<T>(key, CacheScope.All, out T result);
            return result;
        }
    }
    
    public struct LastReadPostInfo
    {
        public int ReplyCount { get; set; }
        public Guid? ContentId { get; set; }
        public DateTime PostDate { get; set; }
    }
    
  • Your sample is very close to what I have ended up with. You are right the Put and the Get's must match types and this is now happening because of things being strongly typed rather than just storing objects.  The risk here in projects is that with structs in particular you can fall into the trap of Putting Type and Getting Type? or vis-versa. 

  • Yes, and post c# 8, even reference types can be nullable, and would be subject to the same concern.