Repository: dotnet/aspnetcore
File: src/Components/Components/src/PersistentStateValueProvider.cs (C#)

Code before:
        var propertyType = parameterInfo.PropertyType;
        _subscriptions[subscriber] = state.RegisterOnPersisting(async () =>
            {
                var storageKey = ComputeKey(subscriber, propertyName);
                var propertyGetter = ResolvePropertyGetter(subscriber.Component.GetType(), propertyName);
                var property = propertyGetter.GetValue(subscriber.Component);
                if (property == null)
                {
                    return;
                }

                // Try to get a custom serializer for this type first
                var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(propertyType);
                var customSerializer = serviceProvider.GetService(serializerType);
                
                if (customSerializer != null)
                {
                    // Use reflection to call the generic PersistAsync method with the custom serializer
                    var persistMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.PersistAsync), BindingFlags.Instance | BindingFlags.Public, [typeof(string), propertyType, serializerType, typeof(CancellationToken)]);
                    if (persistMethod != null)
                    {
                        var task = (Task)persistMethod.Invoke(state, [storageKey, property, customSerializer, CancellationToken.None])!;
                        await task;
                        return;
                    }
                }

                // Fallback to JSON serialization
                state.PersistAsJson(storageKey, property, propertyType);
            }, subscriber.Renderer.GetComponentRenderMode(subscriber.Component));
    }

    private static PropertyGetter ResolvePropertyGetter(Type type, string propertyName)
    {
        return _propertyGetterCache.GetOrAdd((type, propertyName), PropertyGetterFactory);
    }

    [UnconditionalSuppressMessage(
    "Trimming",
    "IL2077:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations.",
    Justification = "Properties of rendered components are preserved through other means and won't get trimmed.")]

    private static PropertyGetter PropertyGetterFactory((Type type, string propertyName) key)
    {
        var (type, propertyName) = key;
        var propertyInfo = GetPropertyInfo(type, propertyName);
        if (propertyInfo == null)
        {
            throw new InvalidOperationException($"Property {propertyName} not found on type {type.FullName}");
        }
        return new PropertyGetter(type, propertyInfo);

Review comment (performance):
Cache the serializers by type on a `ConcurrentDictionary<Type, IPersistentComponentStateSerializer>` (see how it's done in this file.

IPersistentComponentStateSerializer should be an internal interface, have the same methods as IPersistentComponentStateSerializer but take the `Type type` argument as a first argument.

`IPersistentComponentStateSerializer<T>` should implement `IPersistentComponentStateSerializer` with a default interface implementation that calls into Persist<T> no matter the Type t passed as first argument

Code after:
        // Resolve serializer outside the lambda
        var customSerializer = _serializerCache.GetOrAdd(propertyType, SerializerFactory);
        
        _subscriptions[subscriber] = state.RegisterOnPersisting(() =>
            {
                var storageKey = ComputeKey(subscriber, propertyName);
                var propertyGetter = ResolvePropertyGetter(subscriber.Component.GetType(), propertyName);
                var property = propertyGetter.GetValue(subscriber.Component);
                if (property == null)
                {
                    return Task.CompletedTask;
                }

                if (customSerializer != null)
                {
                    using var writer = new PooledArrayBufferWriter<byte>();
                    customSerializer.Persist(propertyType, property, writer);
                    state.PersistAsBytes(storageKey, writer.WrittenMemory.ToArray());
                    return Task.CompletedTask;
                }

                // Fallback to JSON serialization
                state.PersistAsJson(storageKey, property, propertyType);
                return Task.CompletedTask;
            }, subscriber.Renderer.GetComponentRenderMode(subscriber.Component));
    }

    private static PropertyGetter ResolvePropertyGetter(Type type, string propertyName)
    {
        return _propertyGetterCache.GetOrAdd((type, propertyName), PropertyGetterFactory);
    }

    private IPersistentComponentStateSerializer? SerializerFactory(Type type)
    {
        var serializerType = typeof(PersistentComponentStateSerializer<>).MakeGenericType(type);
        var serializer = serviceProvider.GetService(serializerType);
        
        // The generic class now inherits from the internal interface, so we can cast directly
        return serializer as IPersistentComponentStateSerializer;
    }

    [UnconditionalSuppressMessage(
    "Trimming",
    "IL2077:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations.",
    Justification = "Properties of rendered components are preserved through other means and won't get trimmed.")]

    private static PropertyGetter PropertyGetterFactory((Type type, string propertyName) key)
    {
        var (type, propertyName) = key;
        var propertyInfo = GetPropertyInfo(type, propertyName);
        if (propertyInfo == null)