File: SerializerTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Caching.Hybrid.Tests\Microsoft.Extensions.Caching.Hybrid.Tests.csproj (Microsoft.Extensions.Caching.Hybrid.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Caching.Hybrid.Internal;
using Microsoft.Extensions.DependencyInjection;
 
namespace Microsoft.Extensions.Caching.Hybrid.Tests;
 
public class SerializerTests
{
    [Fact]
    public void RoundTripString()
    {
        IHybridCacheSerializer<string> serializer = InbuiltTypeSerializer.Instance;
 
        using var target = RecyclableArrayBufferWriter<byte>.Create(int.MaxValue);
        serializer.Serialize("test value", target);
        Assert.True("test value"u8.SequenceEqual(target.GetCommittedMemory().Span));
        Assert.Equal("test value", serializer.Deserialize(target.AsSequence()));
 
        // and deserialize with multi-chunk
        Assert.Equal("test value", serializer.Deserialize(Split(target.AsSequence())));
    }
 
    [Fact]
    public void RoundTripByteArray()
    {
        IHybridCacheSerializer<byte[]> serializer = InbuiltTypeSerializer.Instance;
        var value = "test value"u8.ToArray();
        using var target = RecyclableArrayBufferWriter<byte>.Create(int.MaxValue);
        serializer.Serialize(value, target);
        Assert.True("test value"u8.SequenceEqual(target.GetCommittedMemory().Span));
        Assert.Equal(value, serializer.Deserialize(target.AsSequence()));
 
        // and deserialize with multi-chunk
        Assert.Equal(value, serializer.Deserialize(Split(target.AsSequence())));
    }
 
    [Theory]
    [InlineData(JsonSerializer.None, JsonSerializer.Default)]
    [InlineData(JsonSerializer.CustomGlobal, JsonSerializer.CustomGlobal)]
    [InlineData(JsonSerializer.CustomPerType, JsonSerializer.CustomPerType)]
    [InlineData(JsonSerializer.CustomPerType | JsonSerializer.CustomGlobal, JsonSerializer.CustomPerType)]
    public void RoundTripPoco(JsonSerializer addSerializers, JsonSerializer expectedSerializer)
    {
        var obj = RoundTrip(new MyPoco { X = 42, Y = "abc" }, """{"X":42,"Y":"abc"}"""{"X":42,"Y":"abc"}"""u8, expectedSerializer, addSerializers);
        Assert.Equal(42, obj.X);
        Assert.Equal("abc", obj.Y);
    }
 
    [Flags]
    public enum JsonSerializer
    {
        None = 0,
        CustomGlobal = 1 << 0,
        CustomPerType = 1 << 1,
        Default = 1 << 2,
        FieldEnabled = 1 << 3,
    }
 
    public class MyPoco
    {
        public int X { get; set; }
        public string Y { get; set; } = "";
    }
 
    [Theory]
    [InlineData(JsonSerializer.None, JsonSerializer.Default)]
    [InlineData(JsonSerializer.CustomGlobal, JsonSerializer.CustomGlobal)]
    [InlineData(JsonSerializer.CustomPerType, JsonSerializer.CustomPerType)]
    [InlineData(JsonSerializer.CustomPerType | JsonSerializer.CustomGlobal, JsonSerializer.CustomPerType)]
    public void RoundTripTuple(JsonSerializer addSerializers, JsonSerializer expectedSerializer)
    {
        var obj = RoundTrip(Tuple.Create(42, "abc"), """{"Item1":42,"Item2":"abc"}"""{"Item1":42,"Item2":"abc"}"""u8, expectedSerializer, addSerializers);
        Assert.Equal(42, obj.Item1);
        Assert.Equal("abc", obj.Item2);
    }
 
    [Theory]
    [InlineData(JsonSerializer.None, JsonSerializer.FieldEnabled)]
    [InlineData(JsonSerializer.CustomGlobal, JsonSerializer.FieldEnabled)]
    [InlineData(JsonSerializer.CustomPerType, JsonSerializer.FieldEnabled)]
    [InlineData(JsonSerializer.CustomPerType | JsonSerializer.CustomGlobal, JsonSerializer.FieldEnabled)]
    public void RoundTripValueTuple(JsonSerializer addSerializers, JsonSerializer expectedSerializer)
    {
        var obj = RoundTrip((42, "abc"), """{"Item1":42,"Item2":"abc"}"""{"Item1":42,"Item2":"abc"}"""u8, expectedSerializer, addSerializers);
        Assert.Equal(42, obj.Item1);
        Assert.Equal("abc", obj.Item2);
    }
 
    [Theory]
    [InlineData(JsonSerializer.None, JsonSerializer.FieldEnabled)]
    [InlineData(JsonSerializer.CustomGlobal, JsonSerializer.FieldEnabled)]
    [InlineData(JsonSerializer.CustomPerType, JsonSerializer.FieldEnabled)]
    [InlineData(JsonSerializer.CustomPerType | JsonSerializer.CustomGlobal, JsonSerializer.FieldEnabled)]
    public void RoundTripNamedValueTuple(JsonSerializer addSerializers, JsonSerializer expectedSerializer)
    {
        var obj = RoundTrip((X: 42, Y: "abc"), """{"Item1":42,"Item2":"abc"}"""{"Item1":42,"Item2":"abc"}"""u8, expectedSerializer, addSerializers);
        Assert.Equal(42, obj.X);
        Assert.Equal("abc", obj.Y);
    }
 
    private static T RoundTrip<T>(T value, ReadOnlySpan<byte> expectedBytes, JsonSerializer expectedJsonOptions, JsonSerializer addSerializers = JsonSerializer.None, bool binary = false)
    {
        var services = new ServiceCollection();
        services.AddHybridCache();
        JsonSerializerOptions? globalOptions = null;
        JsonSerializerOptions? perTypeOptions = null;
 
        if ((addSerializers & JsonSerializer.CustomGlobal) != JsonSerializer.None)
        {
            globalOptions = new();
            services.AddKeyedSingleton<JsonSerializerOptions>(typeof(IHybridCacheSerializer<>), globalOptions);
        }
 
        if ((addSerializers & JsonSerializer.CustomPerType) != JsonSerializer.None)
        {
            perTypeOptions = new();
            services.AddKeyedSingleton<JsonSerializerOptions>(typeof(IHybridCacheSerializer<T>), perTypeOptions);
        }
 
        JsonSerializerOptions? expectedOptionsObj = expectedJsonOptions switch
        {
            JsonSerializer.Default => JsonSerializerOptions.Default,
            JsonSerializer.FieldEnabled => DefaultJsonSerializerFactory.FieldEnabledJsonOptions,
            JsonSerializer.CustomGlobal => globalOptions,
            JsonSerializer.CustomPerType => perTypeOptions,
            _ => throw new ArgumentOutOfRangeException(nameof(expectedJsonOptions))
        };
        Assert.NotNull(expectedOptionsObj);
 
        using var provider = services.BuildServiceProvider();
        var cache = Assert.IsType<DefaultHybridCache>(provider.GetRequiredService<HybridCache>());
 
        var serializer = cache.GetSerializer<T>();
 
        if (serializer is DefaultJsonSerializerFactory.DefaultJsonSerializer<T> json)
        {
            Assert.Same(expectedOptionsObj, json.Options);
        }
 
        using var target = RecyclableArrayBufferWriter<byte>.Create(int.MaxValue);
        serializer.Serialize(value, target);
        var actual = target.GetCommittedMemory().Span;
        if (!expectedBytes.IsEmpty && !expectedBytes.SequenceEqual(actual))
        {
            if (!binary)
            {
                Assert.Equal(FormatText(expectedBytes), FormatText(actual));
            }
 
            Assert.Equal(FormatBytes(expectedBytes), FormatBytes(actual));
        }
 
        return serializer.Deserialize(target.AsSequence());
    }
 
    private static string FormatText(ReadOnlySpan<byte> value)
    {
        // not concerned about efficiency - only used in failure case
        return Encoding.UTF8.GetString(value.ToArray());
    }
 
    private static string FormatBytes(ReadOnlySpan<byte> value)
    {
        // not concerned about efficiency - only used in failure case
        return BitConverter.ToString(value.ToArray());
    }
 
    private static ReadOnlySequence<byte> Split(ReadOnlySequence<byte> value)
    {
        // ensure the value is a multi-chunk segment
        if (!value.IsSingleSegment || value.Length <= 1)
        {
            // already multiple chunks, or cannot be split
            return value;
        }
 
        var chunk = value.First; // actually, single
 
        Segment first = new(chunk.Slice(0, 1), null);
        Segment second = new(chunk.Slice(1), first);
        var result = new ReadOnlySequence<byte>(first, 0, second, chunk.Length - 1);
        Assert.False(result.IsSingleSegment, "should be multi-segment");
        return result;
    }
 
    private sealed class Segment : ReadOnlySequenceSegment<byte>
    {
        public Segment(ReadOnlyMemory<byte> memory, Segment? previous)
        {
            if (previous is not null)
            {
                RunningIndex = previous.RunningIndex + previous.Memory.Length;
                previous.Next = this;
            }
 
            Memory = memory;
        }
    }
 
}