MichaCo / CacheManager

CacheManager is an open source caching abstraction layer for .NET written in C#. It supports various cache providers and implements many advanced features.
http://cachemanager.michaco.net
Apache License 2.0
2.35k stars 456 forks source link

Deserialization fails for generic types when mixing .NET Core and .NET Framework #327

Open sebgrohn opened 3 years ago

sebgrohn commented 3 years ago

Hi!

We have an issue with CacheManager and our Redis instance when reading and writing cached values from both ASP.NET Core and ASP.NET Framework. The two applications both read and write the same keys, and CacheManager sometimes can't find the correct type when deserializing values. When we started digging into what goes wrong we saw that we had some values serialized with .NET Core assembly name and some with the .NET Framework one: System.Private.CoreLib vs. mscorlib.

I found this code in TypeCache.GetType which, according to the comment, it's supposed to handle exactly this issue. And for simple type strings it works fine:

System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

is simplified to System.Boolean => no problem to look up.

On the other hand, for generic types such as List<string>, it doesn't work:

System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

is simplified to System.Collections.Generic.List`1[[System.String instead of the correct System.Collections.Generic.List`1[[System.String]] => look-up fails. Considering that you can nest generic types arbitrarily, this gives me a headache to think about. šŸ˜µ

Right now I'm trying out workarounds via RegisterResolveType, such as this a bit hacky one:

TypeCache.RegisterResolveType(typeName => {
    const string NET_CORE_ASSEMBLY_NAME = "System.Private.CoreLib";
    const string NET_FRAMEWORK_ASSEMBLY_NAME = "mscorlib";

    type ??= Type.GetType(typeName.Replace(NET_CORE_ASSEMBLY_NAME, NET_FRAMEWORK_ASSEMBLY_NAME), false);
    type ??= Type.GetType(typeName.Replace(NET_FRAMEWORK_ASSEMBLY_NAME, NET_CORE_ASSEMBLY_NAME), false);

    return type;
});

What are your thoughts about this? šŸ˜ƒ

MichaCo commented 3 years ago

Hi @sebgrohn, Thanks for reporting this issue. Yeah, that looks like a bug.

As another workaround, maybe try arrays instead of list? Because, string[] should work just fine, and technically, for serialization there isn't a difference between array and list anyways, performance wise or for your app.

sebgrohn commented 3 years ago

Thanks for the suggestion, that's a good point!

I will admit that my example above was a bit simplified; what we have seen reported in exception logs is for example this:

System.Tuple<System.DateTime, {generic data collection type}<System.Collections.Generic.List<{DTO type}>>>

I'm afraid that we are caching similar types from other places in the code as well.

sebgrohn commented 3 years ago

Here's the full workaround that we're adding to our shared code, if it is of any use for someone.

When testing it, we saw that it is only a problem when .NET Framework tries to read .NET Core-serialized values, not the other way around: Type.GetType in .NET Core seem to have a built-in fallback for the "mscorlib" assembly name.

TypeCache.RegisterResolveType(typeName => {
    const string NET_CORE_ASSEMBLY_NAME = "System.Private.CoreLib";
    const string NET_FRAMEWORK_ASSEMBLY_NAME = "mscorlib";

    // Try to get the type from the full qualified name.
    var type = Type.GetType(typeName, false);

    // Try remove version from the type string and resolve it (should work even for signed assemblies).
    typeName = Regex.Replace(typeName, @", Version=\d+.\d+.\d+.\d+", string.Empty);
    type ??= Type.GetType(typeName, false);

    // Try replacing .NET Core's core-lib assembly name with corresponding
    // .NET Framework name. This is needed because some caches are shared
    // between .NET Core and .NET Framework
    // projects, with differing assembly names for the standard system types.
    type ??= Type.GetType(typeName.Replace(NET_CORE_ASSEMBLY_NAME, NET_FRAMEWORK_ASSEMBLY_NAME), false);

    return type;
});
MichaCo commented 3 years ago

This will be improved with the next PR. It's still not 100% going to work for all types because e.g. HashSet was in System.Core in .NET4x, not mscorlib. But it should work much better for lists and other generics.