Dear Enchev, I am convinced that there is a memory leak in the Radzen Blazor Component, with two main manifestations: 1. Repeatedly opening and closing the same page leads to a continuous increase in memory usage. 2. Memory is not released after closing the browser. Both tests were conducted using automatically generated code from Radzen Blazor Studio, without any other modifications. I scanned the entire project with Cursor and had Cursor fix the memory leak points. Cursor provided the following repair report for reference. Meanwhile, if you can confirm the memory leak and fix it, I would be very grateful!
Below is the fixed report for your reference!
Full Fix Report (Memory Leak Sweep + Test Stabilization)
Scope
A full deep scan of Radzen.Blazor components, services, base classes, and demo layouts. The sweep focused on memory leak vectors including: event subscriptions, timers, DotNetObjectReference lifecycle, cancellation tokens, and JS interop resources. Test failures on Windows were also stabilized.
1) Radzen.Blazor/PagedDataBoundComponent.cs
Issue: Old INotifyCollectionChanged subscription was not removed when Data changed.
Before
set
{
if (_data != value)
{
_data = value;
if (_data != null && _data is INotifyCollectionChanged)
{
((INotifyCollectionChanged)_data).CollectionChanged += OnCollectionChanged;
}
OnDataChanged();
StateHasChanged();
}
}
After
set
{
if (_data != value)
{
if (_data != null && _data is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= OnCollectionChanged;
}
_data = value;
if (_data != null && _data is INotifyCollectionChanged newCollection)
{
newCollection.CollectionChanged += OnCollectionChanged;
}
OnDataChanged();
StateHasChanged();
}
}
2) Radzen.Blazor/RadzenLayout.cs
Issue: ThemeService.ThemeChanged subscribed but never unsubscribed.
Before
public partial class RadzenLayout : RadzenComponentWithChildren
{
protected override void OnInitialized()
{
themeService = ServiceProvider?.GetService<ThemeService>();
if (themeService != null)
{
themeService.ThemeChanged += OnThemeChanged;
}
base.OnInitialized();
}
private void OnThemeChanged()
{
StateHasChanged();
}
}
After
public partial class RadzenLayout : RadzenComponentWithChildren, IDisposable
{
protected override void OnInitialized()
{
themeService = ServiceProvider?.GetService<ThemeService>();
if (themeService != null)
{
themeService.ThemeChanged += OnThemeChanged;
}
base.OnInitialized();
}
private void OnThemeChanged()
{
StateHasChanged();
}
public override void Dispose()
{
if (themeService != null)
{
themeService.ThemeChanged -= OnThemeChanged;
}
base.Dispose();
GC.SuppressFinalize(this);
}
}
3) Radzen.Blazor/RadzenToc.razor.cs
Issue: Base RadzenComponent cleanup not executed.
Before
public async ValueTask DisposeAsync()
{
await UnregisterScrollListenerAsync();
}
After
public async ValueTask DisposeAsync()
{
await UnregisterScrollListenerAsync();
Dispose();
}
4) Radzen.Blazor/CookieThemeService.cs
Issue: ThemeService.ThemeChanged never unsubscribed in scoped service.
Before
public class CookieThemeService
{
public CookieThemeService(IJSRuntime jsRuntime, ThemeService themeService, IOptions<CookieThemeServiceOptions>? options)
{
this.jsRuntime = jsRuntime;
this.themeService = themeService;
if (themeService != null)
{
themeService.ThemeChanged += OnThemeChanged;
}
_ = InitializeAsync();
}
}
After
public class CookieThemeService : IDisposable
{
public CookieThemeService(IJSRuntime jsRuntime, ThemeService themeService, IOptions<CookieThemeServiceOptions>? options)
{
this.jsRuntime = jsRuntime;
this.themeService = themeService;
if (themeService != null)
{
themeService.ThemeChanged += OnThemeChanged;
}
_ = InitializeAsync();
}
public void Dispose()
{
themeService.ThemeChanged -= OnThemeChanged;
GC.SuppressFinalize(this);
}
}
5) Radzen.Blazor/RadzenAIChat.razor.cs
Issue: Old CancellationTokenSource canceled but not disposed.
Before
IsLoading = true;
#if NET8_0_OR_GREATER
await cts.CancelAsync();
#else
cts.Cancel();
#endif
cts = new CancellationTokenSource();
After
IsLoading = true;
var previousCts = cts;
#if NET8_0_OR_GREATER
await previousCts.CancelAsync();
#else
previousCts.Cancel();
#endif
previousCts.Dispose();
cts = new CancellationTokenSource();
6) RadzenBlazorDemos/Shared/MainLayout.razor
Issue: NavigationManager.LocationChanged not unsubscribed.
Before
@inherits LayoutComponentBase
@code {
protected override void OnInitialized()
{
UriHelper.LocationChanged += OnLocationChanged;
}
}
After
@inherits LayoutComponentBase
@implements IDisposable
@code {
protected override void OnInitialized()
{
UriHelper.LocationChanged += OnLocationChanged;
}
public void Dispose()
{
UriHelper.LocationChanged -= OnLocationChanged;
}
}
7) RadzenBlazorDemos/Shared/HomeLayout.razor
Issue: Same as MainLayout.
Before
@inherits LayoutComponentBase
@code {
protected override void OnInitialized()
{
UriHelper.LocationChanged += OnLocationChanged;
}
}
After
@inherits LayoutComponentBase
@implements IDisposable
@code {
protected override void OnInitialized()
{
UriHelper.LocationChanged += OnLocationChanged;
}
public void Dispose()
{
UriHelper.LocationChanged -= OnLocationChanged;
}
}
8) Radzen.Blazor/RadzenNotificationMessage.razor
Issue: Timer handler not detached; delayed close could run after disposal.
Before
Timer? timer;
protected override void OnInitialized()
{
if (Message?.ShowProgress == true)
{
timer = new Timer() { Enabled = true };
timer.Elapsed += (sender, args) =>
{
progress = progress + 100;
InvokeAsync(StateHasChanged);
};
}
Task.Delay(Convert.ToInt32(Message?.Duration ?? 3000))
.ContinueWith(r => InvokeAsync(Close));
}
public void Dispose()
{
timer?.Stop();
timer?.Dispose();
}
After
Timer? timer;
ElapsedEventHandler? timerElapsedHandler;
CancellationTokenSource? closeCts;
protected override void OnInitialized()
{
if (Message?.ShowProgress == true)
{
timer = new Timer() { Enabled = true };
timerElapsedHandler = OnTimerElapsed;
timer.Elapsed += timerElapsedHandler;
}
closeCts = new CancellationTokenSource();
_ = CloseAfterDelayAsync(Convert.ToInt32(Message?.Duration ?? 3000), closeCts.Token);
}
private void OnTimerElapsed(object? sender, ElapsedEventArgs args)
{
progress = progress + 100;
_ = InvokeAsync(StateHasChanged);
}
private async Task CloseAfterDelayAsync(int duration, CancellationToken token)
{
try
{
await Task.Delay(duration, token);
if (!token.IsCancellationRequested)
{
await InvokeAsync(Close);
}
}
catch (TaskCanceledException)
{
// Ignore
}
}
public void Dispose()
{
if (closeCts != null)
{
closeCts.Cancel();
closeCts.Dispose();
closeCts = null;
}
if (timer != null && timerElapsedHandler != null)
{
timer.Elapsed -= timerElapsedHandler;
timerElapsedHandler = null;
}
timer?.Stop();
timer?.Dispose();
timer = null;
}
9) Radzen.Blazor/Debouncer.cs
Issue: Timer handler not detached; replaced timers could retain references.
Before
private System.Timers.Timer? timer;
public void Debounce(int interval, Func<Task> action)
{
timer?.Stop();
timer = null;
timer = new System.Timers.Timer() { Interval = interval, Enabled = false, AutoReset = false };
timer.Elapsed += (s, e) =>
{
if (timer == null) return;
timer?.Stop();
timer = null;
Task.Run(action);
};
timer.Start();
}
public void Dispose()
{
if (timer != null)
{
timer.Stop();
timer.Dispose();
timer = null;
}
}
After
private System.Timers.Timer? timer;
private System.Timers.ElapsedEventHandler? timerElapsedHandler;
public void Debounce(int interval, Func<Task> action)
{
ClearTimer();
timer = new System.Timers.Timer() { Interval = interval, Enabled = false, AutoReset = false };
timerElapsedHandler = (s, e) =>
{
if (timer == null) return;
timer?.Stop();
timer = null;
Task.Run(action);
};
timer.Elapsed += timerElapsedHandler;
timer.Start();
}
public void Dispose()
{
ClearTimer();
}
private void ClearTimer()
{
if (timer == null) return;
if (timerElapsedHandler != null)
{
timer.Elapsed -= timerElapsedHandler;
timerElapsedHandler = null;
}
timer.Stop();
timer.Dispose();
timer = null;
}
10) Radzen.Blazor/Rendering/DialogContainer.razor
Issue: DotNetObjectReference created but not disposed.
Before
public void Dispose()
{
if (Service != null)
{
Service.OnRefresh -= OnRefresh;
}
if (Dialog != null)
{
Dialog.PropertyChanged -= OnPropertyChanged;
}
if (Dialog?.Options != null)
Dialog.Options.PropertyChanged -= OnPropertyChanged;
}
After
public void Dispose()
{
if (Service != null)
{
Service.OnRefresh -= OnRefresh;
}
if (Dialog != null)
{
Dialog.PropertyChanged -= OnPropertyChanged;
}
if (Dialog?.Options != null)
Dialog.Options.PropertyChanged -= OnPropertyChanged;
dotNetReference?.Dispose();
dotNetReference = null;
}
11) Radzen.Blazor/RadzenDialog.razor
Issue: JS interop handle not reliably disposed; async cleanup not wired.
Before
@implements IDisposable
public void Dispose()
{
if (Service is null) return;
Service.OnOpen -= OnOpen;
Service.OnClose -= OnClose;
Service.OnSideOpen -= OnSideOpen;
Service.OnSideClose -= OnSideClose;
}
// existed but component did not implement IAsyncDisposable:
public async ValueTask DisposeAsync()
{
try
{
if (sideDialogResizeHandleJsModule != null)
{
await sideDialogResizeHandleJsModule.InvokeVoidAsync("dispose");
}
}
catch { /* Ignore */ }
}
After
@implements IDisposable
@implements IAsyncDisposable
public void Dispose()
{
if (Service != null)
{
Service.OnOpen -= OnOpen;
Service.OnClose -= OnClose;
Service.OnSideOpen -= OnSideOpen;
Service.OnSideClose -= OnSideClose;
}
_ = DisposeSideDialogResizeHandleAsync();
}
public async ValueTask DisposeAsync()
{
Dispose();
await DisposeSideDialogResizeHandleAsync();
}
private async Task DisposeSideDialogResizeHandleAsync()
{
if (sideDialogResizeHandleJsModule == null) return;
try
{
await sideDialogResizeHandleJsModule.InvokeVoidAsync("dispose");
}
catch { /* Ignore */ }
try
{
await sideDialogResizeHandleJsModule.DisposeAsync();
}
catch { /* Ignore */ }
sideDialogResizeHandleJsModule = null;
}
12) Radzen.Blazor/RadzenDataGrid.razor.cs
Issue: sortDescriptors.CollectionChanged not detached in Dispose().
Before
public override void Dispose()
{
base.Dispose();
if (groups != null)
{
groups.CollectionChanged -= GroupsCollectionChanged;
}
// ...
}
After
public override void Dispose()
{
base.Dispose();
if (groups != null)
{
groups.CollectionChanged -= GroupsCollectionChanged;
}
if (sortDescriptors != null)
{
sortDescriptors.CollectionChanged -= SortsCollectionChanged;
}
// ...
}
13) Radzen.Blazor.Tests/Markdown/XmlVisitor.cs
Issue: Markdown tests failed on Windows due to \r\n vs \n.
Before
writer = XmlWriter.Create(xml, new XmlWriterSettings { OmitXmlDeclaration = true, Indent = true, IndentChars = " ", });
After
writer = XmlWriter.Create(xml, new XmlWriterSettings
{
OmitXmlDeclaration = true,
Indent = true,
IndentChars = " ",
NewLineChars = "\n",
NewLineHandling = NewLineHandling.Replace
});
Testing
- Command: dotnet test "Radzen.Blazor.Tests/Radzen.Blazor.Tests.csproj"
- Result: Passed
- Summary: 1625 passed, 0 failed, 0 skipped