File: OwningComponentBaseTest.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.Extensions.DependencyInjection;
 
namespace Microsoft.AspNetCore.Components;
 
public class OwningComponentBaseTest
{
    [Fact]
    public void CreatesScopeAndService()
    {
        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 component1 = (MyOwningComponent)renderer.InstantiateComponent<MyOwningComponent>();
 
        Assert.NotNull(component1.MyService);
        Assert.Equal(1, counter.CreatedCount);
        Assert.Equal(0, counter.DisposedCount);
 
        ((IDisposable)component1).Dispose();
        Assert.Equal(1, counter.CreatedCount);
        Assert.Equal(1, counter.DisposedCount);
    }
 
    [Fact]
    public async Task DisposeAsyncReleasesScopeAndService()
    {
        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 component1 = (MyOwningComponent)renderer.InstantiateComponent<MyOwningComponent>();
 
        Assert.NotNull(component1.MyService);
        Assert.Equal(1, counter.CreatedCount);
        Assert.Equal(0, counter.DisposedCount);
        Assert.False(component1.IsDisposedPublic);
 
        await ((IAsyncDisposable)component1).DisposeAsync();
        Assert.Equal(1, counter.CreatedCount);
        Assert.Equal(1, counter.DisposedCount);
        Assert.True(component1.IsDisposedPublic);
    }
 
    [Fact]
    public void ThrowsWhenAccessingScopedServicesAfterDispose()
    {
        var services = new ServiceCollection();
        services.AddSingleton<Counter>();
        services.AddTransient<MyService>();
        var serviceProvider = services.BuildServiceProvider();
 
        var renderer = new TestRenderer(serviceProvider);
        var component1 = (MyOwningComponent)renderer.InstantiateComponent<MyOwningComponent>();
 
        // Access service first to create scope
        var service = component1.MyService;
 
        ((IDisposable)component1).Dispose();
 
        // Should throw when trying to access services after disposal
        Assert.Throws<ObjectDisposedException>(() => component1.MyService);
    }
 
    [Fact]
    public async Task ThrowsWhenAccessingScopedServicesAfterDisposeAsync()
    {
        var services = new ServiceCollection();
        services.AddSingleton<Counter>();
        services.AddTransient<MyService>();
        var serviceProvider = services.BuildServiceProvider();
 
        var renderer = new TestRenderer(serviceProvider);
        var component1 = (MyOwningComponent)renderer.InstantiateComponent<MyOwningComponent>();
 
        // Access service first to create scope
        var service = component1.MyService;
 
        await ((IAsyncDisposable)component1).DisposeAsync();
 
        // Should throw when trying to access services after disposal
        Assert.Throws<ObjectDisposedException>(() => component1.MyService);
    }
 
    private class Counter
    {
        public int CreatedCount { get; set; }
        public int DisposedCount { get; set; }
    }
 
    private class MyService : IDisposable
    {
        public MyService(Counter counter)
        {
            Counter = counter;
            Counter.CreatedCount++;
        }
 
        public Counter Counter { get; }
 
        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);
        }
    }
}