Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dispose configuration providers #928

Merged
merged 1 commit into from
Jan 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ namespace Microsoft.Extensions.Configuration
/// <summary>
/// Base class for file based <see cref="ConfigurationProvider"/>.
/// </summary>
public abstract class FileConfigurationProvider : ConfigurationProvider
public abstract class FileConfigurationProvider : ConfigurationProvider, IDisposable
{
private readonly IDisposable _changeTokenRegistration;

/// <summary>
/// Initializes a new instance with the specified source.
/// </summary>
Expand All @@ -29,7 +31,7 @@ public FileConfigurationProvider(FileConfigurationSource source)

if (Source.ReloadOnChange && Source.FileProvider != null)
{
ChangeToken.OnChange(
_changeTokenRegistration = ChangeToken.OnChange(
() => Source.FileProvider.Watch(Source.Path),
() => {
Thread.Sleep(Source.ReloadDelay);
Expand Down Expand Up @@ -126,5 +128,17 @@ private void HandleException(Exception e)
throw e;
}
}

/// <inheritdoc />
public void Dispose() => Dispose(true);

/// <summary>
/// Dispose the provider.
/// </summary>
/// <param name="disposing"><c>true</c> if invoked from <see cref="IDisposable.Dispose"/>.</param>
protected virtual void Dispose(bool disposing)
{
_changeTokenRegistration?.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using Microsoft.Extensions.FileProviders;
using Xunit;

namespace Microsoft.Extensions.Configuration.Json
namespace Microsoft.Extensions.Configuration.FileExtensions.Test
{
public class FileConfigurationBuilderExtensionsTest
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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.Collections.Generic;
using System.IO;
using Microsoft.Extensions.Configuration.Test;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;

namespace Microsoft.Extensions.Configuration.FileExtensions.Test
{
public class FileConfigurationProviderTest
{
[Fact]
public void ProviderDisposesChangeTokenRegistration()
{
var changeToken = new ConfigurationRootTest.ChangeToken();
var fileProviderMock = new Mock<IFileProvider>();
fileProviderMock.Setup(fp => fp.Watch(It.IsAny<string>())).Returns(changeToken);

var provider = new FileConfigurationProviderImpl(new FileConfigurationSourceImpl
{
FileProvider = fileProviderMock.Object,
ReloadOnChange = true,
});

Assert.NotEmpty(changeToken.Callbacks);

provider.Dispose();

Assert.Empty(changeToken.Callbacks);
}

public class FileConfigurationProviderImpl : FileConfigurationProvider
{
public FileConfigurationProviderImpl(FileConfigurationSource source)
: base(source)
{ }

public override void Load(Stream stream)
{ }
}

public class FileConfigurationSourceImpl : FileConfigurationSource
{
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new FileConfigurationProviderImpl(this);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<TargetFrameworks>netcoreapp3.0;net472</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Config\test\Microsoft.Extensions.Configuration.Tests.csproj" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.Extensions.Configuration.FileExtensions" />
</ItemGroup>
Expand Down
22 changes: 20 additions & 2 deletions src/Configuration/Config/src/ConfigurationRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ namespace Microsoft.Extensions.Configuration
/// <summary>
/// The root node for a configuration.
/// </summary>
public class ConfigurationRoot : IConfigurationRoot
public class ConfigurationRoot : IConfigurationRoot, IDisposable
poke marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly IList<IConfigurationProvider> _providers;
private readonly IList<IDisposable> _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();

/// <summary>
Expand All @@ -29,10 +30,11 @@ public ConfigurationRoot(IList<IConfigurationProvider> providers)
}

_providers = providers;
_changeTokenRegistrations = new List<IDisposable>(providers.Count);
foreach (var p in providers)
{
p.Load();
ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged());
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}

Expand Down Expand Up @@ -115,5 +117,21 @@ private void RaiseChanged()
var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}

/// <inheritdoc />
public void Dispose()
{
// dispose change token registrations
foreach (var registration in _changeTokenRegistrations)
{
registration.Dispose();
}

// dispose providers
foreach (var provider in _providers)
{
(provider as IDisposable)?.Dispose();
}
}
}
}
111 changes: 111 additions & 0 deletions src/Configuration/Config/test/ConfigurationRootTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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.Collections.Generic;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;

namespace Microsoft.Extensions.Configuration.Test
{
public class ConfigurationRootTest
{
[Fact]
public void RootDisposesProviders()
{
var provider1 = new TestConfigurationProvider("foo", "foo-value");
var provider2 = new DisposableTestConfigurationProvider("bar", "bar-value");
var provider3 = new TestConfigurationProvider("baz", "baz-value");
var provider4 = new DisposableTestConfigurationProvider("qux", "qux-value");
var provider5 = new DisposableTestConfigurationProvider("quux", "quux-value");

var config = new ConfigurationRoot(new IConfigurationProvider[] {
provider1, provider2, provider3, provider4, provider5
});

Assert.Equal("foo-value", config["foo"]);
Assert.Equal("bar-value", config["bar"]);
Assert.Equal("baz-value", config["baz"]);
Assert.Equal("qux-value", config["qux"]);
Assert.Equal("quux-value", config["quux"]);

config.Dispose();

Assert.True(provider2.IsDisposed);
Assert.True(provider4.IsDisposed);
Assert.True(provider5.IsDisposed);
}

[Fact]
public void RootDisposesChangeTokenRegistrations()
{
var changeToken = new ChangeToken();
var providerMock = new Mock<IConfigurationProvider>();
providerMock.Setup(p => p.GetReloadToken()).Returns(changeToken);

var config = new ConfigurationRoot(new IConfigurationProvider[] {
providerMock.Object,
});

Assert.NotEmpty(changeToken.Callbacks);

config.Dispose();

Assert.Empty(changeToken.Callbacks);
}

private class TestConfigurationProvider : ConfigurationProvider
{
public TestConfigurationProvider(string key, string value)
=> Data.Add(key, value);
}

private class DisposableTestConfigurationProvider : ConfigurationProvider, IDisposable
{
public bool IsDisposed { get; set; }

public DisposableTestConfigurationProvider(string key, string value)
=> Data.Add(key, value);

public void Dispose()
=> IsDisposed = true;
}

public class ChangeToken : IChangeToken
{
public List<(Action<object>, object)> Callbacks { get; } = new List<(Action<object>, object)>();

public bool HasChanged => false;

public bool ActiveChangeCallbacks => true;

public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
var item = (callback, state);
Callbacks.Add(item);
return new DisposableAction(() => Callbacks.Remove(item));
}

private class DisposableAction : IDisposable
{
private Action _action;

public DisposableAction(Action action)
{
_action = action;
}

public void Dispose()
{
var a = _action;
if (a != null)
{
_action = null;
a();
}
}
}
}
}
}