File: Internal\DefaultHybridCache.Serialization.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Caching.Hybrid\Microsoft.Extensions.Caching.Hybrid.csproj (Microsoft.Extensions.Caching.Hybrid)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
 
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
 
internal partial class DefaultHybridCache
{
    // Per instance cache of typed serializers; each serializer is a
    // IHybridCacheSerializer<T> for the corresponding Type, but we can't
    // know which here - and undesirable to add an artificial non-generic
    // IHybridCacheSerializer base that serves no other purpose.
    private readonly ConcurrentDictionary<Type, object> _serializers = new();
 
    internal int MaximumPayloadBytes { get; }
 
    internal IHybridCacheSerializer<T> GetSerializer<T>()
    {
        return _serializers.TryGetValue(typeof(T), out var serializer)
            ? Unsafe.As<IHybridCacheSerializer<T>>(serializer) : ResolveAndAddSerializer(this);
 
        static IHybridCacheSerializer<T> ResolveAndAddSerializer(DefaultHybridCache @this)
        {
            // It isn't critical that we get only one serializer instance during start-up; what matters
            // is that we don't get a new serializer instance *every time*.
            var serializer = @this._services.GetService<IHybridCacheSerializer<T>>();
            if (serializer is null)
            {
                foreach (var factory in @this._serializerFactories)
                {
                    if (factory.TryCreateSerializer<T>(out var current))
                    {
                        serializer = current;
                        break; // we've already reversed the factories, so: the first hit is what we want
                    }
                }
            }
 
            if (serializer is null)
            {
                throw new InvalidOperationException($"No {nameof(IHybridCacheSerializer<T>)} configured for type '{typeof(T).Name}'");
            }
 
            // store the result so we don't repeat this in future
            @this._serializers[typeof(T)] = serializer;
            return serializer;
        }
    }
 
    [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Intentional for logged failure mode")]
    private bool TrySerialize<T>(T value, out BufferChunk buffer, out IHybridCacheSerializer<T>? serializer)
    {
        // note: also returns the serializer we resolved, because most-any time we want to serialize, we'll also want
        // to make sure we use that same instance later (without needing to re-resolve and/or store the entire HC machinery)
 
        RecyclableArrayBufferWriter<byte>? writer = null;
        buffer = default;
        try
        {
            writer = RecyclableArrayBufferWriter<byte>.Create(MaximumPayloadBytes); // note this lifetime spans the SetL2Async
            serializer = GetSerializer<T>();
 
            serializer.Serialize(value, writer);
 
            buffer = new(writer.DetachCommitted(out var length), length, returnToPool: true); // remove buffer ownership from the writer
            writer.Dispose(); // we're done with the writer
            return true;
        }
        catch (Exception ex)
        {
            bool knownCause = false;
 
            // ^^^ if we know what happened, we can record directly via cause-specific events
            // and treat as a handled failure (i.e. return false) - otherwise, we'll bubble
            // the fault up a few layers *in addition to* logging in a failure event
 
            if (writer is not null)
            {
                if (writer.QuotaExceeded)
                {
                    _logger.MaximumPayloadBytesExceeded(ex, MaximumPayloadBytes);
                    knownCause = true;
                }
 
                writer.Dispose();
            }
 
            if (!knownCause)
            {
                _logger.SerializationFailure(ex);
                throw;
            }
 
            buffer = default;
            serializer = null;
            return false;
        }
    }
}