Skip to content

Disposing disposable transient services in Blazor #20755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
DanJBower opened this issue Apr 11, 2020 · 2 comments
Closed

Disposing disposable transient services in Blazor #20755

DanJBower opened this issue Apr 11, 2020 · 2 comments
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. question Status: Resolved

Comments

@DanJBower
Copy link

Apologies if this is the wrong place to post this.

Describe the bug

Disposable transient / scoped services are not disposed when there are no more references to the object in Blazor. A simple example of this can be seen from having 2 disposable pages and 2 disposable transient services.

The pages are created and destroyed at the correct time and the services are created when the page is created. However, the services are not disposed until the page is refreshed or I leave the website. Navigating to a different page on the website does not dispose them (and even if it did, that would not handle the disposing of services in sub-components on a page).

I thought about disposing the injected service when the component is disposed, but this wouldn't work for scoped / singleton dependencies and [Inject] had no way of saying what scope the injected dependency had (and it shouldn't as why should the component care?)

The issue is, this leads to a lot of services that require disposing still running in the background. Is there a way to get around this issue?

Annotated log of when things are created and disposed

--------- Startup -------------

DependencyInjectionScopeTesting.Pages.PageOne was created.
DependencyInjectionScopeTesting.Shared.ServiceOne was created.
DependencyInjectionScopeTesting.Pages.PageOne was disposed.
DependencyInjectionScopeTesting.Shared.ServiceOne was disposed.
DependencyInjectionScopeTesting.Pages.PageOne was created.
DependencyInjectionScopeTesting.Shared.ServiceOne was created.

-------------------------------

----- Switch to page two ------

DependencyInjectionScopeTesting.Pages.PageTwo was created.
DependencyInjectionScopeTesting.Shared.ServiceTwo was created.
DependencyInjectionScopeTesting.Pages.PageOne was disposed.

--------------------------------

----- Switch to page one ------

DependencyInjectionScopeTesting.Pages.PageOne was created.
DependencyInjectionScopeTesting.Shared.ServiceOne was created.
DependencyInjectionScopeTesting.Pages.PageTwo was disposed.

--------------------------------

----- Switch to page two ------

DependencyInjectionScopeTesting.Pages.PageTwo was created.
DependencyInjectionScopeTesting.Shared.ServiceTwo was created.
DependencyInjectionScopeTesting.Pages.PageOne was disposed.

--------------------------------

----- Switch to page one ------

DependencyInjectionScopeTesting.Pages.PageOne was created.
DependencyInjectionScopeTesting.Shared.ServiceOne was created.
DependencyInjectionScopeTesting.Pages.PageTwo was disposed.

--------------------------------

----- Refresh or shutdown ------

DependencyInjectionScopeTesting.Pages.PageOne was disposed.
DependencyInjectionScopeTesting.Shared.ServiceOne was disposed.
DependencyInjectionScopeTesting.Shared.ServiceTwo was disposed.
DependencyInjectionScopeTesting.Shared.ServiceOne was disposed.
DependencyInjectionScopeTesting.Shared.ServiceTwo was disposed.
DependencyInjectionScopeTesting.Shared.ServiceOne was disposed.

--------------------------------

To Reproduce

It can be reproduced with two simple disposable razor pages and two services registered as transient, or it can be cloned from here.

Base.cs

using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Components;

namespace DependencyInjectionScopeTesting.Shared
{
	public class Base<T> : ComponentBase, IDisposable
	{
		[Inject]
		protected T Service { get; set; }

		public Base()
		{
			Debug.WriteLine($"{GetType()} was created.");
		}

		private bool _disposed;

		private void Dispose(bool disposing)
		{
			if (_disposed)
			{
				return;
			}

			if (disposing)
			{
				Service = default;
				Debug.WriteLine($"{GetType()} was disposed.");
			}

			_disposed = true;
		}

		~Base()
		{
			Dispose(false);
		}

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}
	}
}

PageOne.razor

@inherits Base<ServiceOne>
@page "/"
@page "/PageOne"

<h1>PageOne</h1>

<div>
	Go to <a href="/PageTwo">page two</a>!
</div>

<div>
	The count: @Service.Counter
</div>

<div>
	<button @onclick="() => { Service.Counter++; }">Increment</button>
</div>

PageTwo.razor

@inherits Base<ServiceTwo>
@page "/PageTwo"

<h1>PageTwo</h1>

Go to <a href="/PageOne">page one</a>!

<div>
	The count: @Service.Counter
</div>

<div>
	<button @onclick="StateHasChanged">Show current count</button>
</div>

Services.cs

using System;
using System.Diagnostics;
using System.Timers;

namespace DependencyInjectionScopeTesting.Shared
{
	public abstract class Services : IDisposable
	{
		protected Services()
		{
			Debug.WriteLine($"{GetType()} was created.");
		}

		private bool _disposed;

		protected Action ExtraDispose;

		private void Dispose(bool disposing)
		{
			if (_disposed)
			{
				return;
			}

			if (disposing)
			{
				ExtraDispose?.Invoke();
				Debug.WriteLine($"{GetType()} was disposed.");
			}

			_disposed = true;
		}

		~Services()
		{
			Dispose(false);
		}

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		public int Counter { get; set; } = 3;
	}

	public class ServiceOne : Services
	{
	}

	public class ServiceTwo : Services
	{
		public ServiceTwo()
		{
			Timer timer = new Timer(1000);
			timer.Elapsed += TimerElapsed;
			timer.Start();

			ExtraDispose = () => { timer.Stop(); };
		}

		private void TimerElapsed(object sender, ElapsedEventArgs e)
		{
			Counter++;
		}
	}
}

Services Registered in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddTransient<ServiceOne>();
    services.AddTransient<ServiceTwo>();
}

Further technical details

dotnet --info

.NET Core SDK (reflecting any global.json):
 Version:   3.1.300-preview-015048
 Commit:    13f19b4682

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.18363
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\3.1.300-preview-015048\

Host (useful for support):
  Version: 3.1.3
  Commit:  4a9f85e9f8

.NET Core SDKs installed:
  2.1.202 [C:\Program Files\dotnet\sdk]
  2.1.509 [C:\Program Files\dotnet\sdk]
  2.1.511 [C:\Program Files\dotnet\sdk]
  2.1.512 [C:\Program Files\dotnet\sdk]
  3.1.201 [C:\Program Files\dotnet\sdk]
  3.1.300-preview-015048 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.15 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.15 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.15 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.0.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

To install additional .NET Core runtimes or SDKs:
  https://aka.ms/dotnet-download

Using Visual Studio 2019 (16.5.3)

@mkArtakMSFT mkArtakMSFT added the area-blazor Includes: Blazor, Razor Components label Apr 13, 2020
@javiercn
Copy link
Member

@DanJBower thanks for contacting us.

The general rule is that you should be disposing the things that you inject unless they are singletons. If you have scoped services, there is an OwningComponentBase class you can use to scope the service to your component.

The only thing that Blazor will do is to call Dispose on your component when it gets rid of it if it implements IDisposable, but other than that Blazor does nothing.

With that in mind, singletons/scoped services will last for the lifetime of the application/scope respectively (which by default matches the application unless you are using OwningComponentBase).

For server-side blazor services will last until the application is shut down or the circuit is shut down, for singleton/scoped or until the component is shutdown if you are using OwningComponentBase

For transient services I believe the container does no tracking and you should dispose them themselves within the dispose in your component.

See here for more info.

@javiercn javiercn added ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. question labels Apr 13, 2020
@ghost ghost added the Status: Resolved label Apr 13, 2020
@ghost
Copy link

ghost commented Apr 14, 2020

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

@ghost ghost closed this as completed Apr 14, 2020
@ghost ghost locked as resolved and limited conversation to collaborators May 14, 2020
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. question Status: Resolved
Projects
None yet
Development

No branches or pull requests

3 participants