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

Code before:
                }

                if (propertyInfo.GetMethod == null || !propertyInfo.GetMethod.IsPublic)
                {
                    throw new InvalidOperationException(
                        $"The type '{targetType.FullName}' declares a property matching the name '{propertyName}' that is not public. Persistent service properties must be public.");
                }

                var propertySetter = new PropertySetter(targetType, propertyInfo);
                var propertyGetter = new PropertyGetter(targetType, propertyInfo);

                _underlyingAccessors.Add(key, (propertySetter, propertyGetter));
            }

            _cachedKeysForService = [.. keys];
        }

        public (string, Type)[] KeyTypePairs => _cachedKeysForService;

        // Mapping for internal classes bundled in different assemblies during prerendering and WASM rendering.
        private static readonly Dictionary<string, string> _canonicalMap = new(StringComparer.OrdinalIgnoreCase)
        {
            { "Microsoft.AspNetCore.Components.Endpoints", "Microsoft.AspNetCore.Components" },
            { "Microsoft.AspNetCore.Components.WebAssembly", "Microsoft.AspNetCore.Components" }
        };

        private static string ComputeKey(Type keyType, string propertyName)
        {
            // This happens once per type and property combo, so allocations are ok.
            var assemblyName = keyType.Assembly.FullName;
            var assemblySimpleName = keyType.Assembly.GetName().Name ?? "";
            if (_canonicalMap.TryGetValue(assemblySimpleName, out var canonicalAssembly))
            {
                assemblyName = canonicalAssembly;
            }

            var typeName = keyType.FullName;
            var inputString = string.Join(".", assemblyName, typeName, propertyName);
            var input = Encoding.UTF8.GetBytes(inputString);
            var hash = SHA256.HashData(input);
            return Convert.ToBase64String(hash);
        }

        internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties(
            [DynamicallyAccessedMembers(LinkerFlags.Component)] Type targetType)
            => MemberAssignment.GetPropertiesIncludingInherited(targetType, BindablePropertyFlags);

        internal (PropertySetter setter, PropertyGetter getter) GetAccessor(string key) =>
            _underlyingAccessors.TryGetValue(key, out var result) ? result : default;
    }


Review comment (suggestion):
This works, but ideally, we don't want to do it this way. Microsoft.AspNetCore.Components shouldn't really depend on Endpoints or WebAssembly (by depend on I mean make a reference to those or include some built-in knowledge about them).

This makes it problematic the next time we add one of these things. It's "interaction at a distance" where someone will ask themselves why something doesn't work and it's because they are missing an update to this mapping.

Instead, maybe we can consider some heuristic, but I don't know exactly which one it is. We could do something like "if the type is internal, we could consider the type name only.

Code after:
                }

                if (propertyInfo.GetMethod == null || !propertyInfo.GetMethod.IsPublic)
                {
                    throw new InvalidOperationException(
                        $"The type '{targetType.FullName}' declares a property matching the name '{propertyName}' that is not public. Persistent service properties must be public.");
                }

                var propertySetter = new PropertySetter(targetType, propertyInfo);
                var propertyGetter = new PropertyGetter(targetType, propertyInfo);

                _underlyingAccessors.Add(key, (propertySetter, propertyGetter));
            }

            _cachedKeysForService = [.. keys];
        }

        public (string, Type)[] KeyTypePairs => _cachedKeysForService;

        private static string ComputeKey(Type keyType, string propertyName)
        {
            // This happens once per type and property combo, so allocations are ok.
            var assemblyName = keyType.Assembly.FullName;
            var typeName = keyType.FullName;

            // Internal classes can be bundled in different assemblies during prerendering and WASM rendering.
            bool isTypeInternal = (!keyType.IsPublic && !keyType.IsNested) || keyType.IsNestedAssembly;
            var inputString = isTypeInternal
                ? string.Join(".", typeName, propertyName)
                : string.Join(".", assemblyName, typeName, propertyName);

            var input = Encoding.UTF8.GetBytes(inputString);
            var hash = SHA256.HashData(input);
            return Convert.ToBase64String(hash);
        }

        internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties(
            [DynamicallyAccessedMembers(LinkerFlags.Component)] Type targetType)
            => MemberAssignment.GetPropertiesIncludingInherited(targetType, BindablePropertyFlags);

        internal (PropertySetter setter, PropertyGetter getter) GetAccessor(string key) =>
            _underlyingAccessors.TryGetValue(key, out var result) ? result : default;
    }

    [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
    private class PersistentServiceRegistration : IPersistentServiceRegistration
    {
        public string Assembly { get; set; } = "";

        public string FullTypeName { get; set; } = "";