简体   繁体   中英

Custom IBsonSerializer exception thrown in mongodb C# driver

I have written a custom IBsonSerializer for the System.Security.Claims.Claim class, which is very bloated and needs custom deserialization. This works when called as a one-off but I am hitting problems when registering it and calling from Unit tests and getting the dreaded exception "There is already a serializer registered for type Claim".

The problem is, there seems to be no way to check whether a serializer is registered without it creating one! This seems completely wrong to me. If you call BsonSerializer.LookupSerializer() , the code calls the BsonSerializerRegistry.GetSerializer() , which calls GetOrAdd() . In other words, I cannot check and then add my custom serializer without the exception and if I don't check, I might get the error if it gets added a second time.

Have I missed something really obvious here? The Unit Tests are called in a multi-threaded process so even though my setup is in Startup.cs and is only generally called once, it is possible to be called in parallel when the unit tests run.

lock (serializerLock)
{
    // I can't call the following, otherwise it creates a default serializer
    //if ( BsonSerializer.LookupSerializer<System.Security.Claims.Claim>().GetType() != typeof(MongoClaimSerializer))
    {
         BsonSerializer.RegisterSerializer(new MongoClaimSerializer());
    }
}

The following code should provide sufficient encapsulation for the serializer registration and lookups that you'd need.

public static class SafeBsonSerializerRegistry
{
    private static ConcurrentDictionary<Type, IBsonSerializer> _cache = new ConcurrentDictionary<Type,IBsonSerializer>();

    public static IBsonSerializer<T> LookupSerializer<T>()
    {
         return (IBsonSerializer<T>)LookupSerializer(typeof(T));
    }

    public static IBsonSerializer LookupSerializer(Type type)
    {
        if (type == null)
            throw new ArgumentNullException("type");

        var typeInfo = type.GetTypeInfo();
        if (typeInfo.IsGenericType && typeInfo.ContainsGenericParameters)
        {
            var message = string.Format("Generic type {0} has unassigned type parameters.", BsonUtils.GetFriendlyTypeName(type));
            throw new ArgumentException(message, "type");
        }

        IBsonSerializer serializer;

        if(_cache.TryGetValue(type, out serializer))
            return serializer;
        return null; //Note, this is where the BsonSerializerRegistry would GetOrAdd().
    }



    public static void RegisterSerializer<T>(IBsonSerializer<T> serializer)
    {
        RegisterSerializer(typeof(T), serializer);
    }

    public static void RegisterSerializer(Type type, IBsonSerializer serializer)
    {
        if (type == null)
            throw new ArgumentNullException("type");
        if (serializer == null)
            throw new ArgumentNullException("serializer");

        var typeInfo = type.GetTypeInfo();
        if (typeof(BsonValue).GetTypeInfo().IsAssignableFrom(type))
        {
            var message = string.Format("A serializer cannot be registered for type {0} because it is a subclass of BsonValue.", BsonUtils.GetFriendlyTypeName(type));
            throw new BsonSerializationException(message);
        }

        if (typeInfo.IsGenericType && typeInfo.ContainsGenericParameters)
        {
            var message = string.Format("Generic type {0} has unassigned type parameters.", BsonUtils.GetFriendlyTypeName(type));
            throw new ArgumentException(message, "type");
        }

        if (!_cache.TryAdd(type, serializer))
        {
            var message = string.Format("There is already a serializer registered for type {0}.", BsonUtils.GetFriendlyTypeName(type));
            throw new BsonSerializationException(message);
        }

        //call to the actual BsonSerializer
        BsonSerializer.RegisterSerializer(type, serializer);
    }

}

What this doesn't do is replace the existing BsonSerializer or underlying BsonSerializerRegistry. It only guarantees the LookupSerializer methods providing the caller used this to register them in the first place.

Let me know if this isn't suitable and I'll get back to the drawing board.

My workaround was instead of calling RegisterSerializer to rather call RegisterSerializationProvider(new ClaimSerializationProvider()) with a new class that will provide the serializer directly to the system during resolution. This doesn't check for duplicates so will not error if in a multi-threaded scenario, RegisterSerializationProvider gets called twice. The provider looks like this:

public class ClaimSerializationProvider : IBsonSerializationProvider
{
    /// <summary>
    /// <see cref="IBsonSerializationProvider.GetSerializer(Type)"/>
    /// </summary>
    public IBsonSerializer GetSerializer(Type type)
    {
        // We only provide a custom serializer for Claim
        if (type == typeof(Claim))
        {
            return new MongoClaimSerializer();
        }

        return null;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM