Skip to content
20 changes: 13 additions & 7 deletions src/Components/Components/src/OwningComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected IServiceProvider ScopedServices
}
}

/// <inhertidoc />
/// <inheritdoc />
void IDisposable.Dispose()
{
Dispose(disposing: true);
Expand All @@ -69,12 +69,20 @@ protected virtual void Dispose(bool disposing)
}
}

/// <inhertidoc />
/// <inheritdoc />
async ValueTask IAsyncDisposable.DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);

Dispose(disposing: false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the fix just be Dispose(disposing: true) here?

if (!IsDisposed)
{
try
{
await DisposeAsyncCore().ConfigureAwait(false);
}
finally
{
Dispose(disposing: true);
}
}
Comment on lines +75 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we need the if here, that's already covered inside DisposeAsyncCore, same with the try_finally. It's overkill.

GC.SuppressFinalize(this);
}

Expand All @@ -89,8 +97,6 @@ protected virtual async ValueTask DisposeAsyncCore()
await _scope.Value.DisposeAsync().ConfigureAwait(false);
_scope = null;
}

IsDisposed = true;
}
}

Expand Down
157 changes: 157 additions & 0 deletions src/Components/Components/test/OwningComponentBaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,168 @@ public MyService(Counter counter)
void IDisposable.Dispose() => Counter.DisposedCount++;
}

[Fact]
public async Task DisposeAsync_CallsDispose_WithDisposingTrue()
{
var services = new ServiceCollection();
services.AddSingleton<Counter>();
services.AddTransient<MyService>();
var serviceProvider = services.BuildServiceProvider();

var renderer = new TestRenderer(serviceProvider);
var component = (ComponentWithDispose)renderer.InstantiateComponent<ComponentWithDispose>();

_ = component.MyService;
await ((IAsyncDisposable)component).DisposeAsync();
Assert.True(component.DisposingParameter);
}

[Fact]
public async Task DisposeAsync_ThenDispose_IsIdempotent()
{
var services = new ServiceCollection();
services.AddSingleton<Counter>();
services.AddTransient<MyService>();
var serviceProvider = services.BuildServiceProvider();

var counter = serviceProvider.GetRequiredService<Counter>();
var renderer = new TestRenderer(serviceProvider);
var component = (ComponentWithDispose)renderer.InstantiateComponent<ComponentWithDispose>();

_ = component.MyService;

await ((IAsyncDisposable)component).DisposeAsync();
var firstCallCount = component.DisposeCallCount;
Assert.Equal(1, counter.DisposedCount);

((IDisposable)component).Dispose();
Assert.True(component.DisposeCallCount >= firstCallCount);
Assert.Equal(1, counter.DisposedCount);
}

[Fact]
public async Task DisposeAsyncCore_Override_WithException_StillCallsDispose()
{
var services = new ServiceCollection();
services.AddSingleton<Counter>();
services.AddTransient<MyService>();
var serviceProvider = services.BuildServiceProvider();

var renderer = new TestRenderer(serviceProvider);
var component = (ComponentWithThrowingDisposeAsyncCore)renderer.InstantiateComponent<ComponentWithThrowingDisposeAsyncCore>();

_ = component.MyService;

await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await ((IAsyncDisposable)component).DisposeAsync());

Assert.True(component.DisposingParameter);
Assert.True(component.IsDisposedPublic);
}

private class ComponentWithDispose : OwningComponentBase<MyService>
{
public MyService MyService => Service;
public bool? DisposingParameter { get; private set; }
public int DisposeCallCount { get; private set; }

protected override void Dispose(bool disposing)
{
DisposingParameter = disposing;
DisposeCallCount++;
base.Dispose(disposing);
}
}

private class ComponentWithThrowingDisposeAsyncCore : OwningComponentBase<MyService>
{
public MyService MyService => Service;
public bool? DisposingParameter { get; private set; }
public bool IsDisposedPublic => IsDisposed;

protected override async ValueTask DisposeAsyncCore()
{
await base.DisposeAsyncCore();
throw new InvalidOperationException("Something went wrong in async disposal");
}

protected override void Dispose(bool disposing)
{
DisposingParameter = disposing;
base.Dispose(disposing);
}
}

private class MyOwningComponent : OwningComponentBase<MyService>
{
public MyService MyService => Service;

// Expose IsDisposed for testing
public bool IsDisposedPublic => IsDisposed;
}

[Fact]
public async Task ComplexComponent_DisposesResourcesOnlyWhenDisposingIsTrue()
{
var services = new ServiceCollection();
services.AddSingleton<Counter>();
services.AddTransient<MyService>();
var serviceProvider = services.BuildServiceProvider();

var renderer = new TestRenderer(serviceProvider);
var component = (ComplexComponent)renderer.InstantiateComponent<ComplexComponent>();

_ = component.MyService;

await ((IAsyncDisposable)component).DisposeAsync();

// Verify all managed resources were disposed because disposing=true
Assert.True(component.TimerDisposed);
Assert.True(component.CancellationTokenSourceDisposed);
Assert.True(component.EventUnsubscribed);
Assert.Equal(1, component.ManagedResourcesCleanedUpCount);
}

private class ComplexComponent : OwningComponentBase<MyService>
{
private readonly System.Threading.Timer _timer;
private readonly CancellationTokenSource _cts;
private bool _eventSubscribed;

public MyService MyService => Service;
public bool TimerDisposed { get; private set; }
public bool CancellationTokenSourceDisposed { get; private set; }
public bool EventUnsubscribed { get; private set; }
public int ManagedResourcesCleanedUpCount { get; private set; }

public ComplexComponent()
{
_timer = new System.Threading.Timer(_ => { }, null, Timeout.Infinite, Timeout.Infinite);
_cts = new CancellationTokenSource();
_eventSubscribed = true;
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_timer?.Dispose();
TimerDisposed = true;

_cts?.Cancel();
_cts?.Dispose();
CancellationTokenSourceDisposed = true;

if (_eventSubscribed)
{
EventUnsubscribed = true;
_eventSubscribed = false;
}

ManagedResourcesCleanedUpCount++;
}

base.Dispose(disposing);
}
}
}
Loading