Skip to content

[Blazor] DisplayName/Label: ExpressionMemberAccessor caches every accessor expression instance (memory leak on Blazor Server) #70031

Open

Description

@pavolmaly66-alt

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

ExpressionMemberAccessor._memberInfoCache is a static ConcurrentDictionary<Expression, MemberInfo>. Expression uses reference equality, and a lambda such as () => Model.Code builds a new expression tree on every evaluation, so every parent render of <DisplayName For="..."> or <Label For="..."> adds a new entry. Each entry keeps the expression alive, and with it the captured closure / component instance and everything it references. On Blazor Server this grows for the lifetime of the process. InputNumber / InputDate hit the same path on every parsing error (#68667).

The _displayNameCache keyed by (MemberInfo, culture) is fine; only the expression-keyed cache is the problem.

Expected Behavior

The cache size is bounded by the number of distinct members, e.g. resolve the MemberInfo from the expression without caching by expression instance.

Steps To Reproduce

net11.0 console app with <FrameworkReference Include="Microsoft.AspNetCore.App" /> (reflection only reads the internal cache size):

using System.Collections;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;

var type = typeof(Microsoft.AspNetCore.Components.Forms.DisplayName<>).Assembly
    .GetType("Microsoft.AspNetCore.Components.Forms.ExpressionMemberAccessor", true)!;
var get = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
    .First(m => m.Name == "GetDisplayName" && m.IsGenericMethod).MakeGenericMethod(typeof(string));
var cache = (ICollection)type.GetField("_memberInfoCache", BindingFlags.NonPublic | BindingFlags.Static)!.GetValue(null)!;
var page = new Page();
for (var i = 0; i < 1000; i++)
{
    Expression<Func<string>> accessor = () => page.Model.Code; // new instance, like every render
    get.Invoke(null, [accessor]);
}
Console.WriteLine(cache.Count); // prints 1000

class Page { public Model Model { get; } = new(); }
class Model { [DisplayName("Code")] public string Code { get; set; } = ""; }

The same happens with <DisplayName For="@(() => Model.Code)" /> inside a form that re-renders on every field change.

Exceptions (if any)

None.

.NET Version

11.0.100-rc.2.26504.105 (Microsoft.AspNetCore.App 11.0.0-rc.2.26504.105)

Anything else?

src/Components/Web/src/Forms/ExpressionMemberAccessor.cs, same on main.

Activity

  1. github-actions commented on Oct 24, 2026

    @github-actions
    Contributor

    Triage Summary

    Area: area-blazor (ExpressionMemberAccessor in src/Components/Web/src/Forms, used by DisplayName/Label/InputNumber/InputDate)
    Type: Bug (preserved)

    Potential Duplicates

    • None found

    Generated by Issue Triage Agent for dotnet/aspnetcore for #70031 · copilot · auto · 20.9 AIC · ⌖ 1.01 AIC · ⊞ 7.3K · ◷

  2. self-assigned this
    on Oct 24, 2026
  3. added this to the Backlog milestone on Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-blazorIncludes: Blazor, Razor Components

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions