Skip to content
Merged
8 changes: 3 additions & 5 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,12 @@ protected virtual void Dispose(bool disposing)
}
}

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

Dispose(disposing: false);
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

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

IsDisposed = true;
}
}

Expand Down
118 changes: 118 additions & 0 deletions src/Components/Components/test/OwningComponentBaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,129 @@ 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);
}

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 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