-
Notifications
You must be signed in to change notification settings - Fork 10.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Ryan Nowak
committed
Aug 1, 2019
1 parent
b42ebf1
commit b1cd697
Showing
3 changed files
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// 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, and resolves a service of type <typeparamref name="TService"/>. | ||
/// </summary> | ||
/// <typeparam name="TService">The service type.</typeparam> | ||
/// <remarks> | ||
/// Use the <see cref="OwningComponent{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="OwningComponent{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 OwningComponent<TService> : ComponentBase, IDisposable | ||
{ | ||
private IServiceScope _scope; | ||
private TService _item; | ||
|
||
[Inject] IServiceScopeFactory ScopeFactory { get; 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."); | ||
} | ||
|
||
_scope ??= ScopeFactory.CreateScope(); | ||
return _scope.ServiceProvider; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Gets the <typeparamref name="TService"/> that is associated with this component. | ||
/// </summary> | ||
protected TService Service | ||
{ | ||
get | ||
{ | ||
// 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; | ||
} | ||
} | ||
|
||
void IDisposable.Dispose() | ||
{ | ||
_scope?.Dispose(); | ||
_scope = null; | ||
Dispose(disposing: true); | ||
} | ||
|
||
/// <inheritdoc /> | ||
protected virtual void Dispose(bool disposing) | ||
{ | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// 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 OwningComponentTest | ||
{ | ||
[Fact] | ||
public void CreatesScopeAndService() | ||
{ | ||
var services = new ServiceCollection(); | ||
services.AddTransient<MyService>(); | ||
|
||
var renderer = new TestRenderer(services.BuildServiceProvider()); | ||
|
||
var i = 0; | ||
var component = new TestComponent(builder => | ||
{ | ||
if (i % 2 == 0) | ||
{ | ||
builder.OpenComponent<MyOwningComponent>(1); | ||
builder.CloseComponent(); | ||
} | ||
|
||
builder.AddContent(2, "--"); | ||
|
||
if (i % 2 == 1) | ||
{ | ||
builder.OpenComponent<MyOwningComponent>(3); | ||
builder.CloseComponent(); | ||
} | ||
|
||
i++; | ||
}); | ||
|
||
var componentId = renderer.AssignRootComponentId(component); | ||
renderer.RenderRootComponent(componentId); | ||
|
||
var batch = renderer.Batches[0]; | ||
var frames = batch.ReferenceFrames; | ||
Assert.Collection( | ||
frames, | ||
frame => AssertFrame.Component<MyOwningComponent>(frame, 1, 1), | ||
frame => AssertFrame.Text(frame, "--", 2), | ||
frame => AssertFrame.Text(frame, "Created: 1 - Disposed: 0", 1)); | ||
|
||
component.TriggerRender(); | ||
|
||
batch = renderer.Batches[1]; | ||
frames = batch.ReferenceFrames; | ||
Assert.Collection( | ||
frames, | ||
frame => AssertFrame.Component<MyOwningComponent>(frame, 1, 3), | ||
frame => AssertFrame.Text(frame, "Created: 2 - Disposed: 1", 1)); | ||
} | ||
|
||
private class MyService : IDisposable | ||
{ | ||
private static int CreatedCount = 0; | ||
|
||
private static int DisposedCount = 0; | ||
|
||
public MyService() => CreatedCount++; | ||
|
||
void IDisposable.Dispose() => DisposedCount++; | ||
|
||
public string Message => $"Created: {CreatedCount} - Disposed: {DisposedCount}"; | ||
} | ||
|
||
private class MyOwningComponent : OwningComponent<MyService> | ||
{ | ||
protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
{ | ||
builder.AddContent(1, Service.Message); | ||
} | ||
} | ||
|
||
class TestComponent : AutoRenderComponent | ||
{ | ||
private readonly RenderFragment _renderFragment; | ||
|
||
public TestComponent(RenderFragment renderFragment) | ||
{ | ||
_renderFragment = renderFragment; | ||
} | ||
|
||
protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
=> _renderFragment(builder); | ||
} | ||
} | ||
} |