Skip to content

Should EFMessagePackDBNullFormatter call reader.ReadNil() before returning DBNull.Value? #334

@inkysquid

Description

@inkysquid

Using UseStackExchangeRedisCacheProvider, which uses MessagePack by default, I had issues with null values throwing exceptions. I resolved this by providing my own implementation of IEFDataSerializer, which replaces EFMessagePackDBNullFormatter with a formatter which calls reader.ReadNil(), and this has fixed the bug for me.

I don't have a good enough understanding of MessagePack to confidently issue a PR for this but my DbNull formatter looks like this:

/// <summary>
///     Replaces <c>EFMessagePackDBNullFormatter</c> which has a bug: its <c>Deserialize</c>
///     returns <see cref="DBNull.Value" /> without consuming the nil token from the reader,
///     leaving the reader misaligned and causing subsequent reads to fail.
/// </summary>
public sealed class DbNullFormatter : IMessagePackFormatter<DBNull?>
{
    public static readonly DbNullFormatter Instance = new();

    private DbNullFormatter()
    {
    }

    public void Serialize(ref MessagePackWriter writer, DBNull? value, MessagePackSerializerOptions options)
        => writer.WriteNil();

    public DBNull Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
    {
        reader.ReadNil();
        return DBNull.Value;
    }
}

And my IEFDataSerializer implementation looks like this

/// <summary>
///     Replaces <c>EFMessagePackSerializer</c> with an similar implementation that fixes DBNull handling.
/// </summary>
public sealed class CacheSerializer : IEFDataSerializer
{
    private static readonly IFormatterResolver _customResolvers = CompositeResolver.Create(
        [
            // The EFMessagePackDBNullFormatter has a bug where it returns DBNull.Value without consuming the nil token from the reader.
            DbNullFormatter.Instance,
            NativeDateTimeArrayFormatter.Instance,
            NativeDateTimeFormatter.Instance,
            NativeDecimalFormatter.Instance,
            NativeGuidFormatter.Instance,
            TypelessFormatter.Instance,
        ],
        [
            NativeDateTimeResolver.Instance,
            NativeDecimalResolver.Instance,
            NativeGuidResolver.Instance,
            GeometryResolver.Instance,
            ContractlessStandardResolver.Instance,
            StandardResolverAllowPrivate.Instance,
            TypelessContractlessStandardResolver.Instance,
            DynamicGenericResolver.Instance,
        ]);

    private readonly bool _enableCompression;

    /// <summary>
    ///     High-Level API of MessagePack for C#.
    /// </summary>
    public CacheSerializer(IOptions<EFCoreSecondLevelCacheSettings> cacheSettings)
    {
        var options =
            cacheSettings.Value.AdditionalData as EFRedisCacheConfigurationOptions ??
            throw new InvalidOperationException(message: "Please call the UseStackExchangeRedisCacheProvider() method.");

        _enableCompression = options.EnableCompression;
    }

    private MessagePackSerializerOptions EfMessagePackSerializerOptions
        => field ??= GetSerializerOptions();

    /// <summary>
    ///     Serializes a given value with the specified buffer writer.
    /// </summary>
    /// <param name="obj"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public byte[] Serialize<T>(T? obj) => MessagePackSerializer.Serialize(obj, EfMessagePackSerializerOptions);

    /// <summary>
    ///     Deserializes a value of a given type from a sequence of bytes.
    /// </summary>
    /// <param name="data"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public T? Deserialize<T>(byte[]? data)
        => data is null ? default : MessagePackSerializer.Deserialize<T>(data, EfMessagePackSerializerOptions);

    private MessagePackSerializerOptions GetSerializerOptions()
        => _enableCompression ?
            MessagePackSerializerOptions
                .Standard
                .WithCompression(MessagePackCompression.Lz4BlockArray)
                .WithResolver(_customResolvers):

            MessagePackSerializerOptions
                .Standard
                .WithResolver(_customResolvers);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions