Skip to content

Commit

Permalink
Add component for managing a DI scope
Browse files Browse the repository at this point in the history
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
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,14 @@ public partial class NavigationException : System.Exception
public NavigationException(string uri) { }
public string Location { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public abstract partial class OwningComponent<TService> : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable
{
protected OwningComponent() { }
protected System.IServiceProvider ScopedServices { get { throw null; } }
protected TService Service { get { throw null; } }
protected virtual void Dispose(bool disposing) { }
void System.IDisposable.Dispose() { }
}
public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent
{
public PageDisplay() { }
Expand Down
68 changes: 68 additions & 0 deletions src/Components/Components/src/OwningComponent.cs
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)
{
}
}
}
99 changes: 99 additions & 0 deletions src/Components/Components/test/OwningComponentTest.cs
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);
}
}
}

0 comments on commit b1cd697

Please sign in to comment.