From 001b54f42e1d8ebf69902546781d8093295e081c Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 31 Jul 2019 21:22:18 -0700 Subject: [PATCH] Add component for managing a DI scope Fixes: #5496 Fixes: #10448 This change adds a *utility* base class that encourages you to do the right thing when you need to interact with a disposable scoped or transient service. This solution ties the lifetime of a DI scope and a service to a component instance. Note that this is not recursive - we expect users to pass services like this around (or as cascading values) if the design dictates it. --- ...ft.AspNetCore.Components.netstandard2.0.cs | 13 +++ .../Components/src/OwningComponentBase.cs | 100 ++++++++++++++++++ .../test/OwningComponentBaseTest.cs | 61 +++++++++++ 3 files changed, 174 insertions(+) create mode 100644 src/Components/Components/src/OwningComponentBase.cs create mode 100644 src/Components/Components/test/OwningComponentBaseTest.cs diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index 22307a3200..ac3a7d8ecd 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -302,6 +302,19 @@ namespace Microsoft.AspNetCore.Components public NavigationException(string uri) { } public string Location { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } + public abstract partial class OwningComponentBase : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable + { + protected OwningComponentBase() { } + protected bool IsDisposed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + protected System.IServiceProvider ScopedServices { get { throw null; } } + protected virtual void Dispose(bool disposing) { } + void System.IDisposable.Dispose() { } + } + public abstract partial class OwningComponentBase : Microsoft.AspNetCore.Components.OwningComponentBase, System.IDisposable + { + protected OwningComponentBase() { } + protected TService Service { get { throw null; } } + } public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent { public PageDisplay() { } diff --git a/src/Components/Components/src/OwningComponentBase.cs b/src/Components/Components/src/OwningComponentBase.cs new file mode 100644 index 0000000000..d5bcecb1f7 --- /dev/null +++ b/src/Components/Components/src/OwningComponentBase.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// A base class that creates a service provider scope. + /// + /// + /// Use the class as a base class to author components that control + /// the lifetime of a service provider scope. This is useful when using a transient or scoped service that + /// requires disposal such as a repository or database abstraction. Using + /// as a base class ensures that the service provider scope is disposed with the component. + /// + public abstract class OwningComponentBase : ComponentBase, IDisposable + { + private IServiceScope _scope; + + [Inject] IServiceScopeFactory ScopeFactory { get; set; } + + /// + /// Gets a value determining if the component and associated services have been disposed. + /// + protected bool IsDisposed { get; private set; } + + /// + /// Gets the scoped that is associated with this component. + /// + protected IServiceProvider ScopedServices + { + get + { + if (ScopeFactory == null) + { + throw new InvalidOperationException("Services cannot be accessed before the component is initialized."); + } + + if (IsDisposed) + { + throw new ObjectDisposedException(GetType().Name); + } + + _scope ??= ScopeFactory.CreateScope(); + return _scope.ServiceProvider; + } + } + + void IDisposable.Dispose() + { + if (!IsDisposed) + { + _scope?.Dispose(); + _scope = null; + Dispose(disposing: true); + IsDisposed = true; + } + } + + /// + protected virtual void Dispose(bool disposing) + { + } + } + + /// + /// A base class that creates a service provider scope, and resolves a service of type . + /// + /// The service type. + /// + /// Use the class as a base class to author components that control + /// the lifetime of a service or multiple services. This is useful when using a transient or scoped service that + /// requires disposal such as a repository or database abstraction. Using + /// as a base class ensures that the service and relates services that share its scope are disposed with the component. + /// + public abstract class OwningComponentBase : OwningComponentBase, IDisposable + { + private TService _item; + + /// + /// Gets the that is associated with this component. + /// + protected TService Service + { + get + { + if (IsDisposed) + { + throw new ObjectDisposedException(GetType().Name); + } + + // We cache this because we don't know the lifetime. We have to assume that it could be transient. + _item ??= ScopedServices.GetRequiredService(); + return _item; + } + } + } +} diff --git a/src/Components/Components/test/OwningComponentBaseTest.cs b/src/Components/Components/test/OwningComponentBaseTest.cs new file mode 100644 index 0000000000..155231b40b --- /dev/null +++ b/src/Components/Components/test/OwningComponentBaseTest.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Dynamic; +using System.Linq; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class OwningComponentBaseTest + { + [Fact] + public void CreatesScopeAndService() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddTransient(); + var serviceProvider = services.BuildServiceProvider(); + + var counter = serviceProvider.GetRequiredService(); + var renderer = new TestRenderer(serviceProvider); + var component1 = renderer.InstantiateComponent(); + + 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); + } + + 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++; + } + + private class MyOwningComponent : OwningComponentBase + { + public MyService MyService => Service; + } + } +}