Skip to content

Commit 76967e8

Browse files
authored
Razor component disposal article (#34998)
1 parent 56ea051 commit 76967e8

File tree

12 files changed

+369
-356
lines changed

12 files changed

+369
-356
lines changed
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
---
2+
title: ASP.NET Core Razor component disposal
3+
author: guardrex
4+
description: Learn about ASP.NET Core Razor component component disposal with IDisposable and IAsyncDisposable.
5+
monikerRange: '>= aspnetcore-3.1'
6+
ms.author: riande
7+
ms.custom: mvc
8+
ms.date: 03/18/2025
9+
uid: blazor/components/component-disposal
10+
---
11+
# ASP.NET Core Razor component disposal
12+
13+
[!INCLUDE[](~/includes/not-latest-version.md)]
14+
15+
This article explains the ASP.NET Core Razor component disposal with with <xref:System.IDisposable> and <xref:System.IAsyncDisposable>.
16+
17+
If a component implements <xref:System.IDisposable> or <xref:System.IAsyncDisposable>, the framework calls for resource disposal when the component is removed from the UI. Don't rely on the exact timing of when these methods are executed. For example, <xref:System.IAsyncDisposable> can be triggered before or after an asynchronous <xref:System.Threading.Tasks.Task> awaited in [`OnInitalizedAsync`](xref:blazor/components/lifecycle#component-initialization-oninitializedasync) or [`OnParametersSetAsync`](xref:blazor/components/lifecycle#after-parameters-are-set-onparameterssetasync) is called or completes. Also, object disposal code shouldn't assume that objects created during initialization or other lifecycle methods exist.
18+
19+
Components shouldn't need to implement <xref:System.IDisposable> and <xref:System.IAsyncDisposable> simultaneously. If both are implemented, the framework only executes the asynchronous overload.
20+
21+
Developer code must ensure that <xref:System.IAsyncDisposable> implementations don't take a long time to complete.
22+
23+
For more information, see the introductory remarks of <xref:blazor/components/sync-context>.
24+
25+
## Disposal of JavaScript interop object references
26+
27+
Examples throughout the [JavaScript (JS) interop articles](xref:blazor/js-interop/index) demonstrate typical object disposal patterns:
28+
29+
* When calling JS from .NET, as described in <xref:blazor/js-interop/call-javascript-from-dotnet>, dispose any created <xref:Microsoft.JSInterop.IJSObjectReference>/<xref:Microsoft.JSInterop.IJSInProcessObjectReference>/<xref:Microsoft.JSInterop.Implementation.JSObjectReference> either from .NET or from JS to avoid leaking JS memory.
30+
31+
* When calling .NET from JS, as described in <xref:blazor/js-interop/call-dotnet-from-javascript>, dispose of a created <xref:Microsoft.JSInterop.DotNetObjectReference> either from .NET or from JS to avoid leaking .NET memory.
32+
33+
JS interop object references are implemented as a map keyed by an identifier on the side of the JS interop call that creates the reference. When object disposal is initiated from either the .NET or JS side, Blazor removes the entry from the map, and the object can be garbage collected as long as no other strong reference to the object is present.
34+
35+
At a minimum, always dispose objects created on the .NET side to avoid leaking .NET managed memory.
36+
37+
## DOM cleanup tasks during component disposal
38+
39+
For more information, see <xref:blazor/js-interop/index#dom-cleanup-tasks-during-component-disposal>.
40+
41+
For guidance on <xref:Microsoft.JSInterop.JSDisconnectedException> when a circuit is disconnected, see <xref:blazor/js-interop/index#javascript-interop-calls-without-a-circuit>. For general JavaScript interop error handling guidance, see the *JavaScript interop* section in <xref:blazor/fundamentals/handle-errors#javascript-interop>.
42+
43+
## Synchronous `IDisposable`
44+
45+
For synchronous disposal tasks, use <xref:System.IDisposable.Dispose%2A?displayProperty=nameWithType>.
46+
47+
The following component:
48+
49+
* Implements <xref:System.IDisposable> with the [`@implements`](xref:mvc/views/razor#implements) Razor directive.
50+
* Disposes of `obj`, which is a type that implements <xref:System.IDisposable>.
51+
* A null check is performed because `obj` is created in a lifecycle method (not shown).
52+
53+
```razor
54+
@implements IDisposable
55+
56+
...
57+
58+
@code {
59+
...
60+
61+
public void Dispose()
62+
{
63+
obj?.Dispose();
64+
}
65+
}
66+
```
67+
68+
If a single object requires disposal, a lambda can be used to dispose of the object when <xref:System.IDisposable.Dispose%2A> is called. The following example appears in the <xref:blazor/components/rendering#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system> article and demonstrates the use of a lambda expression for the disposal of a <xref:System.Timers.Timer>.
69+
70+
:::moniker range=">= aspnetcore-9.0"
71+
72+
`TimerDisposal1.razor`:
73+
74+
:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal1.razor":::
75+
76+
:::moniker-end
77+
78+
:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0"
79+
80+
`TimerDisposal1.razor`:
81+
82+
:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal1.razor":::
83+
84+
:::moniker-end
85+
86+
:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"
87+
88+
`CounterWithTimerDisposal1.razor`:
89+
90+
:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor":::
91+
92+
:::moniker-end
93+
94+
:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0"
95+
96+
`CounterWithTimerDisposal1.razor`:
97+
98+
:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor":::
99+
100+
:::moniker-end
101+
102+
:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0"
103+
104+
`CounterWithTimerDisposal1.razor`:
105+
106+
:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor":::
107+
108+
:::moniker-end
109+
110+
:::moniker range="< aspnetcore-5.0"
111+
112+
`CounterWithTimerDisposal1.razor`:
113+
114+
:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal1.razor":::
115+
116+
:::moniker-end
117+
118+
> [!NOTE]
119+
> In the preceding example, the call to <xref:Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged%2A> is wrapped by a call to <xref:Microsoft.AspNetCore.Components.ComponentBase.InvokeAsync%2A?displayProperty=nameWithType> because the callback is invoked outside of Blazor's synchronization context. For more information, see <xref:blazor/components/rendering#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system>.
120+
121+
If the object is created in a lifecycle method, such as [`OnInitialized{Async}`](xref:blazor/components/lifecycle#component-initialization-oninitializedasync), check for `null` before calling `Dispose`.
122+
123+
:::moniker range=">= aspnetcore-9.0"
124+
125+
`TimerDisposal2.razor`:
126+
127+
:::code language="razor" source="~/../blazor-samples/9.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal2.razor":::
128+
129+
:::moniker-end
130+
131+
:::moniker range=">= aspnetcore-8.0 < aspnetcore-9.0"
132+
133+
`TimerDisposal2.razor`:
134+
135+
:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/TimerDisposal2.razor":::
136+
137+
:::moniker-end
138+
139+
:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"
140+
141+
`CounterWithTimerDisposal2.razor`:
142+
143+
:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor":::
144+
145+
:::moniker-end
146+
147+
:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0"
148+
149+
`CounterWithTimerDisposal2.razor`:
150+
151+
:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor":::
152+
153+
:::moniker-end
154+
155+
:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0"
156+
157+
`CounterWithTimerDisposal2.razor`:
158+
159+
:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor":::
160+
161+
:::moniker-end
162+
163+
:::moniker range="< aspnetcore-5.0"
164+
165+
`CounterWithTimerDisposal2.razor`:
166+
167+
:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/lifecycle/CounterWithTimerDisposal2.razor":::
168+
169+
:::moniker-end
170+
171+
For more information, see:
172+
173+
* [Cleaning up unmanaged resources (.NET documentation)](/dotnet/standard/garbage-collection/unmanaged)
174+
* [Null-conditional operators ?. and ?[]](/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-)
175+
176+
## Asynchronous `IAsyncDisposable`
177+
178+
For asynchronous disposal tasks, use <xref:System.IAsyncDisposable.DisposeAsync%2A?displayProperty=nameWithType>.
179+
180+
The following component:
181+
182+
* Implements <xref:System.IAsyncDisposable> with the [`@implements`](xref:mvc/views/razor#implements) Razor directive.
183+
* Disposes of `obj`, which is an unmanaged type that implements <xref:System.IAsyncDisposable>.
184+
* A null check is performed because `obj` is created in a lifecycle method (not shown).
185+
186+
```razor
187+
@implements IAsyncDisposable
188+
189+
...
190+
191+
@code {
192+
...
193+
194+
public async ValueTask DisposeAsync()
195+
{
196+
if (obj is not null)
197+
{
198+
await obj.DisposeAsync();
199+
}
200+
}
201+
}
202+
```
203+
204+
For more information, see:
205+
206+
* <xref:blazor/components/sync-context>
207+
* [Cleaning up unmanaged resources (.NET documentation)](/dotnet/standard/garbage-collection/unmanaged)
208+
* [Null-conditional operators ?. and ?[]](/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-)
209+
210+
## Assignment of `null` to disposed objects
211+
212+
Usually, there's no need to assign `null` to disposed objects after calling <xref:System.IDisposable.Dispose%2A>/<xref:System.IAsyncDisposable.DisposeAsync%2A>. Rare cases for assigning `null` include the following:
213+
214+
* If the object's type is poorly implemented and doesn't tolerate repeat calls to <xref:System.IDisposable.Dispose%2A>/<xref:System.IAsyncDisposable.DisposeAsync%2A>, assign `null` after disposal to gracefully skip further calls to <xref:System.IDisposable.Dispose%2A>/<xref:System.IAsyncDisposable.DisposeAsync%2A>.
215+
* If a long-lived process continues to hold a reference to a disposed object, assigning `null` allows the [garbage collector](/dotnet/standard/garbage-collection/fundamentals) to free the object in spite of the long-lived process holding a reference to it.
216+
217+
These are unusual scenarios. For objects that are implemented correctly and behave normally, there's no point in assigning `null` to disposed objects. In the rare cases where an object must be assigned `null`, we recommend documenting the reason and seeking a solution that prevents the need to assign `null`.
218+
219+
## `StateHasChanged`
220+
221+
> [!NOTE]
222+
> Calling <xref:Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged%2A> in `Dispose` and `DisposeAsync` isn't supported. <xref:Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged%2A> might be invoked as part of tearing down the renderer, so requesting UI updates at that point isn't supported.
223+
224+
## Event handlers
225+
226+
Always unsubscribe event handlers from .NET events. The following [Blazor form](xref:blazor/forms/index) examples show how to unsubscribe an event handler in the `Dispose` method.
227+
228+
Private field and lambda approach:
229+
230+
```razor
231+
@implements IDisposable
232+
233+
<EditForm ... EditContext="editContext" ...>
234+
...
235+
<button type="submit" disabled="@formInvalid">Submit</button>
236+
</EditForm>
237+
238+
@code {
239+
...
240+
241+
private EventHandler<FieldChangedEventArgs>? fieldChanged;
242+
243+
protected override void OnInitialized()
244+
{
245+
editContext = new(model);
246+
247+
fieldChanged = (_, __) =>
248+
{
249+
...
250+
};
251+
252+
editContext.OnFieldChanged += fieldChanged;
253+
}
254+
255+
public void Dispose()
256+
{
257+
editContext.OnFieldChanged -= fieldChanged;
258+
}
259+
}
260+
```
261+
262+
Private method approach:
263+
264+
```razor
265+
@implements IDisposable
266+
267+
<EditForm ... EditContext="editContext" ...>
268+
...
269+
<button type="submit" disabled="@formInvalid">Submit</button>
270+
</EditForm>
271+
272+
@code {
273+
...
274+
275+
protected override void OnInitialized()
276+
{
277+
editContext = new(model);
278+
editContext.OnFieldChanged += HandleFieldChanged;
279+
}
280+
281+
private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
282+
{
283+
...
284+
}
285+
286+
public void Dispose()
287+
{
288+
editContext.OnFieldChanged -= HandleFieldChanged;
289+
}
290+
}
291+
```
292+
293+
For more information on the <xref:Microsoft.AspNetCore.Components.Forms.EditForm> component and forms, see <xref:blazor/forms/index> and the other forms articles in the *Forms* node.
294+
295+
## Anonymous functions, methods, and expressions
296+
297+
When [anonymous functions](/dotnet/csharp/programming-guide/statements-expressions-operators/anonymous-functions), methods, or expressions, are used, it isn't necessary to implement <xref:System.IDisposable> and unsubscribe delegates. However, failing to unsubscribe a delegate is a problem **when the object exposing the event outlives the lifetime of the component registering the delegate**. When this occurs, a memory leak results because the registered delegate keeps the original object alive. Therefore, only use the following approaches when you know that the event delegate disposes quickly. When in doubt about the lifetime of objects that require disposal, subscribe a delegate method and properly dispose the delegate as the earlier examples show.
298+
299+
Anonymous lambda method approach (explicit disposal not required):
300+
301+
```csharp
302+
private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
303+
{
304+
formInvalid = !editContext.Validate();
305+
StateHasChanged();
306+
}
307+
308+
protected override void OnInitialized()
309+
{
310+
editContext = new(starship);
311+
editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
312+
}
313+
```
314+
315+
Anonymous lambda expression approach (explicit disposal not required):
316+
317+
```csharp
318+
private ValidationMessageStore? messageStore;
319+
320+
[CascadingParameter]
321+
private EditContext? CurrentEditContext { get; set; }
322+
323+
protected override void OnInitialized()
324+
{
325+
...
326+
327+
messageStore = new(CurrentEditContext);
328+
329+
CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
330+
CurrentEditContext.OnFieldChanged += (s, e) =>
331+
messageStore.Clear(e.FieldIdentifier);
332+
}
333+
```
334+
335+
The full example of the preceding code with anonymous lambda expressions appears in the <xref:blazor/forms/validation#validator-components> article.
336+
337+
For more information, see [Cleaning up unmanaged resources](/dotnet/standard/garbage-collection/unmanaged) and the topics that follow it on implementing the `Dispose` and `DisposeAsync` methods.
338+
339+
## Disposal during JS interop
340+
341+
Trap <xref:Microsoft.JSInterop.JSDisconnectedException> in potential cases where loss of Blazor's SignalR circuit prevents JS interop calls and results an unhandled exception.
342+
343+
For more information, see the following resources:
344+
345+
* [JavaScript isolation in JavaScript modules](xref:blazor/js-interop/call-javascript-from-dotnet#javascript-isolation-in-javascript-modules)
346+
* [JavaScript interop calls without a circuit](xref:blazor/js-interop/index#javascript-interop-calls-without-a-circuit)

0 commit comments

Comments
 (0)