Skip to content

Commit 001b54f

Browse files
Ryan Nowakrynowak
authored andcommitted
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.
1 parent bff3f9e commit 001b54f

File tree

3 files changed

+174
-0
lines changed

3 files changed

+174
-0
lines changed

src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,19 @@ public partial class NavigationException : System.Exception
302302
public NavigationException(string uri) { }
303303
public string Location { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
304304
}
305+
public abstract partial class OwningComponentBase : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable
306+
{
307+
protected OwningComponentBase() { }
308+
protected bool IsDisposed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
309+
protected System.IServiceProvider ScopedServices { get { throw null; } }
310+
protected virtual void Dispose(bool disposing) { }
311+
void System.IDisposable.Dispose() { }
312+
}
313+
public abstract partial class OwningComponentBase<TService> : Microsoft.AspNetCore.Components.OwningComponentBase, System.IDisposable
314+
{
315+
protected OwningComponentBase() { }
316+
protected TService Service { get { throw null; } }
317+
}
305318
public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent
306319
{
307320
public PageDisplay() { }
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace Microsoft.AspNetCore.Components
8+
{
9+
/// <summary>
10+
/// A base class that creates a service provider scope.
11+
/// </summary>
12+
/// <remarks>
13+
/// Use the <see cref="OwningComponentBase"/> class as a base class to author components that control
14+
/// the lifetime of a service provider scope. This is useful when using a transient or scoped service that
15+
/// requires disposal such as a repository or database abstraction. Using <see cref="OwningComponentBase"/>
16+
/// as a base class ensures that the service provider scope is disposed with the component.
17+
/// </remarks>
18+
public abstract class OwningComponentBase : ComponentBase, IDisposable
19+
{
20+
private IServiceScope _scope;
21+
22+
[Inject] IServiceScopeFactory ScopeFactory { get; set; }
23+
24+
/// <summary>
25+
/// Gets a value determining if the component and associated services have been disposed.
26+
/// </summary>
27+
protected bool IsDisposed { get; private set; }
28+
29+
/// <summary>
30+
/// Gets the scoped <see cref="IServiceProvider"/> that is associated with this component.
31+
/// </summary>
32+
protected IServiceProvider ScopedServices
33+
{
34+
get
35+
{
36+
if (ScopeFactory == null)
37+
{
38+
throw new InvalidOperationException("Services cannot be accessed before the component is initialized.");
39+
}
40+
41+
if (IsDisposed)
42+
{
43+
throw new ObjectDisposedException(GetType().Name);
44+
}
45+
46+
_scope ??= ScopeFactory.CreateScope();
47+
return _scope.ServiceProvider;
48+
}
49+
}
50+
51+
void IDisposable.Dispose()
52+
{
53+
if (!IsDisposed)
54+
{
55+
_scope?.Dispose();
56+
_scope = null;
57+
Dispose(disposing: true);
58+
IsDisposed = true;
59+
}
60+
}
61+
62+
/// <inheritdoc />
63+
protected virtual void Dispose(bool disposing)
64+
{
65+
}
66+
}
67+
68+
/// <summary>
69+
/// A base class that creates a service provider scope, and resolves a service of type <typeparamref name="TService"/>.
70+
/// </summary>
71+
/// <typeparam name="TService">The service type.</typeparam>
72+
/// <remarks>
73+
/// Use the <see cref="OwningComponentBase{TService}"/> class as a base class to author components that control
74+
/// the lifetime of a service or multiple services. This is useful when using a transient or scoped service that
75+
/// requires disposal such as a repository or database abstraction. Using <see cref="OwningComponentBase{TService}"/>
76+
/// as a base class ensures that the service and relates services that share its scope are disposed with the component.
77+
/// </remarks>
78+
public abstract class OwningComponentBase<TService> : OwningComponentBase, IDisposable
79+
{
80+
private TService _item;
81+
82+
/// <summary>
83+
/// Gets the <typeparamref name="TService"/> that is associated with this component.
84+
/// </summary>
85+
protected TService Service
86+
{
87+
get
88+
{
89+
if (IsDisposed)
90+
{
91+
throw new ObjectDisposedException(GetType().Name);
92+
}
93+
94+
// We cache this because we don't know the lifetime. We have to assume that it could be transient.
95+
_item ??= ScopedServices.GetRequiredService<TService>();
96+
return _item;
97+
}
98+
}
99+
}
100+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Dynamic;
6+
using System.Linq;
7+
using Microsoft.AspNetCore.Components.RenderTree;
8+
using Microsoft.AspNetCore.Components.Test.Helpers;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Xunit;
11+
12+
namespace Microsoft.AspNetCore.Components
13+
{
14+
public class OwningComponentBaseTest
15+
{
16+
[Fact]
17+
public void CreatesScopeAndService()
18+
{
19+
var services = new ServiceCollection();
20+
services.AddSingleton<Counter>();
21+
services.AddTransient<MyService>();
22+
var serviceProvider = services.BuildServiceProvider();
23+
24+
var counter = serviceProvider.GetRequiredService<Counter>();
25+
var renderer = new TestRenderer(serviceProvider);
26+
var component1 = renderer.InstantiateComponent<MyOwningComponent>();
27+
28+
Assert.NotNull(component1.MyService);
29+
Assert.Equal(1, counter.CreatedCount);
30+
Assert.Equal(0, counter.DisposedCount);
31+
32+
((IDisposable)component1).Dispose();
33+
Assert.Equal(1, counter.CreatedCount);
34+
Assert.Equal(1, counter.DisposedCount);
35+
}
36+
37+
private class Counter
38+
{
39+
public int CreatedCount { get; set; }
40+
public int DisposedCount { get; set; }
41+
}
42+
43+
private class MyService : IDisposable
44+
{
45+
public MyService(Counter counter)
46+
{
47+
Counter = counter;
48+
Counter.CreatedCount++;
49+
}
50+
51+
public Counter Counter { get; }
52+
53+
void IDisposable.Dispose() => Counter.DisposedCount++;
54+
}
55+
56+
private class MyOwningComponent : OwningComponentBase<MyService>
57+
{
58+
public MyService MyService => Service;
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)