Skip to content
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
113 changes: 113 additions & 0 deletions src/Ramstack.FileProviders.Composition/ChangeTokenComposer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;

namespace Ramstack.FileProviders.Composition;

/// <summary>
/// Provides helper methods for the <see cref="IChangeToken"/>.
/// </summary>
public static class ChangeTokenComposer
{
/// <summary>
/// Attempts to flatten the specified <see cref="IChangeToken"/> into a flat list of change tokens.
/// </summary>
/// <remarks>
/// If the <paramref name="changeToken"/> is not a <see cref="CompositeChangeToken"/>,
/// the same instance of the <paramref name="changeToken"/> is returned.
/// </remarks>
/// <param name="changeToken">The <see cref="IChangeToken"/> to flatten.</param>
/// <returns>
/// An <see cref="IChangeToken"/> representing the flattened version from the specified <see cref="IChangeToken"/>.
/// </returns>
public static IChangeToken Flatten(this IChangeToken changeToken) =>
FlattenChangeToken(changeToken);

/// <summary>
/// Attempts to flatten the specified <see cref="IChangeToken"/> into a flat list of change tokens.
/// </summary>
/// <remarks>
/// If the <paramref name="changeToken"/> is not a <see cref="CompositeChangeToken"/>,
/// the same instance of the <paramref name="changeToken"/> is returned.
/// </remarks>
/// <param name="changeToken">The <see cref="IChangeToken"/> to flatten.</param>
/// <returns>
/// An <see cref="IChangeToken"/> representing the flattened version from the specified <see cref="IChangeToken"/>.
/// </returns>
public static IChangeToken FlattenChangeToken(IChangeToken changeToken)
{
while (changeToken is CompositeChangeToken composite)
{
var changeTokens = composite.ChangeTokens;
if (changeTokens.Count == 0)
return NullChangeToken.Singleton;

if (changeTokens.Count == 1)
{
changeToken = changeTokens[0];
continue;
}

foreach (var t in changeTokens)
if (t is CompositeChangeToken or NullChangeToken)
return ComposeChangeTokens(changeTokens);

break;
}

return changeToken;
}

/// <summary>
/// Creates a change token from the specified list of <see cref="IChangeToken"/> instances and flattens it into a flat list of change tokens.
/// </summary>
/// <remarks>
/// This method returns a <see cref="CompositeChangeToken"/> if more than one token remains after flattening.
/// </remarks>
/// <param name="changeTokens">The list of <see cref="IChangeToken"/> instances to compose and flatten.</param>
/// <returns>
/// An <see cref="IChangeToken"/> representing the flattened version from the specified list of tokens.
/// </returns>
public static IChangeToken ComposeChangeTokens(params IChangeToken[] changeTokens) =>
ComposeChangeTokens(changeTokens.AsEnumerable());

/// <summary>
/// Creates a change token from the specified list of <see cref="IChangeToken"/> instances and flattens it into a flat list of change tokens.
/// </summary>
/// <remarks>
/// This method returns a <see cref="CompositeChangeToken"/> if more than one token remains after flattening.
/// </remarks>
/// <param name="changeTokens">The list of <see cref="IChangeToken"/> instances to compose and flatten.</param>
/// <returns>
/// An <see cref="IChangeToken"/> representing the flattened version from the specified list of tokens.
/// </returns>
public static IChangeToken ComposeChangeTokens(IEnumerable<IChangeToken> changeTokens)
{
var queue = new Queue<IChangeToken>();
var collection = new List<IChangeToken>();

foreach (var changeToken in changeTokens)
{
queue.Enqueue(changeToken);

while (queue.TryDequeue(out var current))
{
if (current is CompositeChangeToken composite)
{
foreach (var t in composite.ChangeTokens)
queue.Enqueue(t);
}
else if (current is not NullChangeToken)
{
collection.Add(current);
}
}
}

return collection.Count switch
{
0 => NullChangeToken.Singleton,
1 => collection[0],
_ => new CompositeChangeToken(collection.ToArray())
};
}
}
12 changes: 11 additions & 1 deletion src/Ramstack.FileProviders.Composition/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,19 @@ environment.ContentRootFileProvider = FileProviderComposer.ComposeProviders(
```

In this example, the `ComposeProviders` method handles any unnecessary nesting that might occur, including when the current
`environment.ContentRootFileProvider` is a `CompositeFileProvider`. This ensures that all file providers merged into a single
`environment.ContentRootFileProvider` is a `CompositeFileProvider`. This ensures that all file providers are merged into a single
flat structure, avoiding unnecessary indirectness.

## Flattening Change Tokens
The `Flatten` extension method optimizes the structure of change token hierarchies by flattening nested `CompositeChangeToken` instances
and, most importantly, automatically filters out `NullChangeToken` instances from the hierarchy. Unlike standard `CompositeChangeToken`
behavior, which retains and processes `NullChangeToken` instances unnecessarily, this utility removes them completely,
resulting in improved performance and simplified change notification chains.

```csharp
var changeToken = compositeFileProvider.Watch("**/*.json").Flatten();
```

## Related Packages
- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes.
- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `ZipFileProvider`, `PrefixedFileProvider`, and `SubFileProvider`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
namespace Ramstack.FileProviders.Composition;

[TestFixture]
public sealed class ChangeTokenComposerTests
{
[Test]
public void Flatten_ReturnsAsIs_WhenNoComposite()
{
var changeToken = new TestChangeToken();
var result = ChangeTokenComposer.FlattenChangeToken(changeToken);

Assert.That(result, Is.SameAs(changeToken));
}

[Test]
public void Flatten_ReturnsCompositeProvider_WhenNeedComposite()
{
var changeToken = CreateCompositeChangeToken(new TestChangeToken(), new TestChangeToken());

var result = ChangeTokenComposer.FlattenChangeToken(changeToken);
Assert.That(result, Is.InstanceOf<CompositeChangeToken>());
}

[Test]
public void Flatten_ReturnsAsIs_WhenAlreadyFlat()
{
var changeToken = CreateCompositeChangeToken(new TestChangeToken(), new TestChangeToken());

var result = ChangeTokenComposer.FlattenChangeToken(changeToken);
Assert.That(result, Is.SameAs(changeToken));
}

[Test]
public void Flatten_ReturnsCompositeChangeToken_Flattened()
{
var provider = CreateCompositeChangeToken(
new TestChangeToken(),
CreateCompositeChangeToken(
new TestChangeToken(),
new TestChangeToken(),
CreateCompositeChangeToken(
new TestChangeToken())));

var result = ChangeTokenComposer.FlattenChangeToken(provider);

Assert.That(result, Is.InstanceOf<CompositeChangeToken>());
Assert.That(((CompositeChangeToken)result).ChangeTokens.Count, Is.EqualTo(4));
Assert.That(((CompositeChangeToken)result).ChangeTokens, Is.All.InstanceOf<TestChangeToken>());
}

[Test]
public void Flatten_RemovesNullChangeToken()
{
var provider = CreateCompositeChangeToken(
new TestChangeToken(),
CreateCompositeChangeToken(
new TestChangeToken(),
NullChangeToken.Singleton,
new TestChangeToken()),
NullChangeToken.Singleton);

var result = ChangeTokenComposer.FlattenChangeToken(provider);

Assert.That(result, Is.InstanceOf<CompositeChangeToken>());
Assert.That(((CompositeChangeToken)result).ChangeTokens.Count, Is.EqualTo(3));
Assert.That(((CompositeChangeToken)result).ChangeTokens, Is.All.InstanceOf<TestChangeToken>());
}

[Test]
public void Flatten_ReturnsNullChangeToken_WhenNothingReturn()
{
var provider = CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton),
NullChangeToken.Singleton),
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton),
NullChangeToken.Singleton))),
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton),
NullChangeToken.Singleton)),
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton),
NullChangeToken.Singleton));

var result = ChangeTokenComposer.FlattenChangeToken(provider);
Assert.That(result, Is.InstanceOf<NullChangeToken>());
Assert.That(result, Is.SameAs(NullChangeToken.Singleton));
}

[Test]
public void Flatten_ReturnsSingleToken_WhenRemainOneToken()
{
var provider = CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton),
NullChangeToken.Singleton),
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
new TestChangeToken()),
NullChangeToken.Singleton))),
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton),
NullChangeToken.Singleton)),
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton),
NullChangeToken.Singleton));

var result = ChangeTokenComposer.FlattenChangeToken(provider);
Assert.That(result, Is.InstanceOf<TestChangeToken>());
}

[Test]
public void Flatten_MaintainOrder_WhenComposite()
{
var t1 = new TestChangeToken();
var t2 = new TestChangeToken();
var t3 = new TestChangeToken();
var t4 = new TestChangeToken();
var t5 = new TestChangeToken();
var t6 = new TestChangeToken();
var t7 = new TestChangeToken();
var t8 = new TestChangeToken();
var t9 = new TestChangeToken();

var changeToken = ChangeTokenComposer.ComposeChangeTokens(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
t1,
CreateCompositeChangeToken(
t2,
NullChangeToken.Singleton,
t3),
t4,
NullChangeToken.Singleton),
t5,
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
t6),
NullChangeToken.Singleton)),
t7),
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton),
NullChangeToken.Singleton,
t8)),
NullChangeToken.Singleton,
CreateCompositeChangeToken(
CreateCompositeChangeToken(
NullChangeToken.Singleton,
NullChangeToken.Singleton),
NullChangeToken.Singleton,
t9));

var composite = (CompositeChangeToken)changeToken;
var changeTokens = new IChangeToken[] {t1, t2, t3, t4, t5, t6, t7, t8, t9};

Assert.That(changeToken, Is.InstanceOf<CompositeChangeToken>());
Assert.That(composite.ChangeTokens, Is.EquivalentTo(changeTokens));
}

[Test]
public void Flatten_ReturnsSingleToken_WhenCompositeContainsOnlyOne()
{
var changeToken = CreateCompositeChangeToken(new TestChangeToken()).Flatten();
Assert.That(changeToken, Is.InstanceOf<TestChangeToken>());
}

[Test]
public void Flatten_EmptyComposite_ReturnsNullChangeToken()
{
var changeToken = CreateCompositeChangeToken().Flatten();
Assert.That(changeToken, Is.InstanceOf<NullChangeToken>());
Assert.That(changeToken, Is.SameAs(NullChangeToken.Singleton));
}

private static CompositeChangeToken CreateCompositeChangeToken(params IChangeToken[] changeTokens) =>
new(changeTokens);

private sealed class TestChangeToken : IChangeToken
{
public bool HasChanged => false;

public bool ActiveChangeCallbacks => false;

public IDisposable RegisterChangeCallback(Action<object> callback, object state) =>
NullChangeToken.Singleton.RegisterChangeCallback(callback, state);
}
}