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.
This commit is contained in:
parent
bff3f9e0ac
commit
001b54f42e
|
|
@ -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<TService> : Microsoft.AspNetCore.Components.OwningComponentBase, System.IDisposable
|
||||
{
|
||||
protected OwningComponentBase() { }
|
||||
protected TService Service { get { throw null; } }
|
||||
}
|
||||
public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent
|
||||
{
|
||||
public PageDisplay() { }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class that creates a service provider scope.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use the <see cref="OwningComponentBase"/> 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 <see cref="OwningComponentBase"/>
|
||||
/// as a base class ensures that the service provider scope is disposed with the component.
|
||||
/// </remarks>
|
||||
public abstract class OwningComponentBase : ComponentBase, IDisposable
|
||||
{
|
||||
private IServiceScope _scope;
|
||||
|
||||
[Inject] IServiceScopeFactory ScopeFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value determining if the component and associated services have been disposed.
|
||||
/// </summary>
|
||||
protected bool IsDisposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scoped <see cref="IServiceProvider"/> that is associated with this component.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A base class that creates a service provider scope, and resolves a service of type <typeparamref name="TService"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">The service type.</typeparam>
|
||||
/// <remarks>
|
||||
/// Use the <see cref="OwningComponentBase{TService}"/> 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 <see cref="OwningComponentBase{TService}"/>
|
||||
/// as a base class ensures that the service and relates services that share its scope are disposed with the component.
|
||||
/// </remarks>
|
||||
public abstract class OwningComponentBase<TService> : OwningComponentBase, IDisposable
|
||||
{
|
||||
private TService _item;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <typeparamref name="TService"/> that is associated with this component.
|
||||
/// </summary>
|
||||
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<TService>();
|
||||
return _item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Counter>();
|
||||
services.AddTransient<MyService>();
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
var counter = serviceProvider.GetRequiredService<Counter>();
|
||||
var renderer = new TestRenderer(serviceProvider);
|
||||
var component1 = 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);
|
||||
}
|
||||
|
||||
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<MyService>
|
||||
{
|
||||
public MyService MyService => Service;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue