Skip to content

Commit 5924695

Browse files
authored
Merge pull request #27 from rameel/compositechangetoken
Add `ChangeTokenComposer` utility class
2 parents f3f965d + 7e360b4 commit 5924695

File tree

3 files changed

+364
-1
lines changed

3 files changed

+364
-1
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using Microsoft.Extensions.FileProviders;
2+
using Microsoft.Extensions.Primitives;
3+
4+
namespace Ramstack.FileProviders.Composition;
5+
6+
/// <summary>
7+
/// Provides helper methods for the <see cref="IChangeToken"/>.
8+
/// </summary>
9+
public static class ChangeTokenComposer
10+
{
11+
/// <summary>
12+
/// Attempts to flatten the specified <see cref="IChangeToken"/> into a flat list of change tokens.
13+
/// </summary>
14+
/// <remarks>
15+
/// If the <paramref name="changeToken"/> is not a <see cref="CompositeChangeToken"/>,
16+
/// the same instance of the <paramref name="changeToken"/> is returned.
17+
/// </remarks>
18+
/// <param name="changeToken">The <see cref="IChangeToken"/> to flatten.</param>
19+
/// <returns>
20+
/// An <see cref="IChangeToken"/> representing the flattened version from the specified <see cref="IChangeToken"/>.
21+
/// </returns>
22+
public static IChangeToken Flatten(this IChangeToken changeToken) =>
23+
FlattenChangeToken(changeToken);
24+
25+
/// <summary>
26+
/// Attempts to flatten the specified <see cref="IChangeToken"/> into a flat list of change tokens.
27+
/// </summary>
28+
/// <remarks>
29+
/// If the <paramref name="changeToken"/> is not a <see cref="CompositeChangeToken"/>,
30+
/// the same instance of the <paramref name="changeToken"/> is returned.
31+
/// </remarks>
32+
/// <param name="changeToken">The <see cref="IChangeToken"/> to flatten.</param>
33+
/// <returns>
34+
/// An <see cref="IChangeToken"/> representing the flattened version from the specified <see cref="IChangeToken"/>.
35+
/// </returns>
36+
public static IChangeToken FlattenChangeToken(IChangeToken changeToken)
37+
{
38+
while (changeToken is CompositeChangeToken composite)
39+
{
40+
var changeTokens = composite.ChangeTokens;
41+
if (changeTokens.Count == 0)
42+
return NullChangeToken.Singleton;
43+
44+
if (changeTokens.Count == 1)
45+
{
46+
changeToken = changeTokens[0];
47+
continue;
48+
}
49+
50+
foreach (var t in changeTokens)
51+
if (t is CompositeChangeToken or NullChangeToken)
52+
return ComposeChangeTokens(changeTokens);
53+
54+
break;
55+
}
56+
57+
return changeToken;
58+
}
59+
60+
/// <summary>
61+
/// Creates a change token from the specified list of <see cref="IChangeToken"/> instances and flattens it into a flat list of change tokens.
62+
/// </summary>
63+
/// <remarks>
64+
/// This method returns a <see cref="CompositeChangeToken"/> if more than one token remains after flattening.
65+
/// </remarks>
66+
/// <param name="changeTokens">The list of <see cref="IChangeToken"/> instances to compose and flatten.</param>
67+
/// <returns>
68+
/// An <see cref="IChangeToken"/> representing the flattened version from the specified list of tokens.
69+
/// </returns>
70+
public static IChangeToken ComposeChangeTokens(params IChangeToken[] changeTokens) =>
71+
ComposeChangeTokens(changeTokens.AsEnumerable());
72+
73+
/// <summary>
74+
/// Creates a change token from the specified list of <see cref="IChangeToken"/> instances and flattens it into a flat list of change tokens.
75+
/// </summary>
76+
/// <remarks>
77+
/// This method returns a <see cref="CompositeChangeToken"/> if more than one token remains after flattening.
78+
/// </remarks>
79+
/// <param name="changeTokens">The list of <see cref="IChangeToken"/> instances to compose and flatten.</param>
80+
/// <returns>
81+
/// An <see cref="IChangeToken"/> representing the flattened version from the specified list of tokens.
82+
/// </returns>
83+
public static IChangeToken ComposeChangeTokens(IEnumerable<IChangeToken> changeTokens)
84+
{
85+
var queue = new Queue<IChangeToken>();
86+
var collection = new List<IChangeToken>();
87+
88+
foreach (var changeToken in changeTokens)
89+
{
90+
queue.Enqueue(changeToken);
91+
92+
while (queue.TryDequeue(out var current))
93+
{
94+
if (current is CompositeChangeToken composite)
95+
{
96+
foreach (var t in composite.ChangeTokens)
97+
queue.Enqueue(t);
98+
}
99+
else if (current is not NullChangeToken)
100+
{
101+
collection.Add(current);
102+
}
103+
}
104+
}
105+
106+
return collection.Count switch
107+
{
108+
0 => NullChangeToken.Singleton,
109+
1 => collection[0],
110+
_ => new CompositeChangeToken(collection.ToArray())
111+
};
112+
}
113+
}

src/Ramstack.FileProviders.Composition/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,19 @@ environment.ContentRootFileProvider = FileProviderComposer.ComposeProviders(
4848
```
4949

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

54+
## Flattening Change Tokens
55+
The `Flatten` extension method optimizes the structure of change token hierarchies by flattening nested `CompositeChangeToken` instances
56+
and, most importantly, automatically filters out `NullChangeToken` instances from the hierarchy. Unlike standard `CompositeChangeToken`
57+
behavior, which retains and processes `NullChangeToken` instances unnecessarily, this utility removes them completely,
58+
resulting in improved performance and simplified change notification chains.
59+
60+
```csharp
61+
var changeToken = compositeFileProvider.Watch("**/*.json").Flatten();
62+
```
63+
5464
## Related Packages
5565
- [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.
5666
- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `ZipFileProvider`, `PrefixedFileProvider`, and `SubFileProvider`.
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
namespace Ramstack.FileProviders.Composition;
2+
3+
[TestFixture]
4+
public sealed class ChangeTokenComposerTests
5+
{
6+
[Test]
7+
public void Flatten_ReturnsAsIs_WhenNoComposite()
8+
{
9+
var changeToken = new TestChangeToken();
10+
var result = ChangeTokenComposer.FlattenChangeToken(changeToken);
11+
12+
Assert.That(result, Is.SameAs(changeToken));
13+
}
14+
15+
[Test]
16+
public void Flatten_ReturnsCompositeProvider_WhenNeedComposite()
17+
{
18+
var changeToken = CreateCompositeChangeToken(new TestChangeToken(), new TestChangeToken());
19+
20+
var result = ChangeTokenComposer.FlattenChangeToken(changeToken);
21+
Assert.That(result, Is.InstanceOf<CompositeChangeToken>());
22+
}
23+
24+
[Test]
25+
public void Flatten_ReturnsAsIs_WhenAlreadyFlat()
26+
{
27+
var changeToken = CreateCompositeChangeToken(new TestChangeToken(), new TestChangeToken());
28+
29+
var result = ChangeTokenComposer.FlattenChangeToken(changeToken);
30+
Assert.That(result, Is.SameAs(changeToken));
31+
}
32+
33+
[Test]
34+
public void Flatten_ReturnsCompositeChangeToken_Flattened()
35+
{
36+
var provider = CreateCompositeChangeToken(
37+
new TestChangeToken(),
38+
CreateCompositeChangeToken(
39+
new TestChangeToken(),
40+
new TestChangeToken(),
41+
CreateCompositeChangeToken(
42+
new TestChangeToken())));
43+
44+
var result = ChangeTokenComposer.FlattenChangeToken(provider);
45+
46+
Assert.That(result, Is.InstanceOf<CompositeChangeToken>());
47+
Assert.That(((CompositeChangeToken)result).ChangeTokens.Count, Is.EqualTo(4));
48+
Assert.That(((CompositeChangeToken)result).ChangeTokens, Is.All.InstanceOf<TestChangeToken>());
49+
}
50+
51+
[Test]
52+
public void Flatten_RemovesNullChangeToken()
53+
{
54+
var provider = CreateCompositeChangeToken(
55+
new TestChangeToken(),
56+
CreateCompositeChangeToken(
57+
new TestChangeToken(),
58+
NullChangeToken.Singleton,
59+
new TestChangeToken()),
60+
NullChangeToken.Singleton);
61+
62+
var result = ChangeTokenComposer.FlattenChangeToken(provider);
63+
64+
Assert.That(result, Is.InstanceOf<CompositeChangeToken>());
65+
Assert.That(((CompositeChangeToken)result).ChangeTokens.Count, Is.EqualTo(3));
66+
Assert.That(((CompositeChangeToken)result).ChangeTokens, Is.All.InstanceOf<TestChangeToken>());
67+
}
68+
69+
[Test]
70+
public void Flatten_ReturnsNullChangeToken_WhenNothingReturn()
71+
{
72+
var provider = CreateCompositeChangeToken(
73+
CreateCompositeChangeToken(
74+
NullChangeToken.Singleton,
75+
NullChangeToken.Singleton,
76+
CreateCompositeChangeToken(
77+
CreateCompositeChangeToken(
78+
NullChangeToken.Singleton,
79+
NullChangeToken.Singleton),
80+
NullChangeToken.Singleton),
81+
CreateCompositeChangeToken(
82+
NullChangeToken.Singleton,
83+
NullChangeToken.Singleton,
84+
CreateCompositeChangeToken(
85+
CreateCompositeChangeToken(
86+
NullChangeToken.Singleton,
87+
NullChangeToken.Singleton),
88+
NullChangeToken.Singleton))),
89+
NullChangeToken.Singleton,
90+
NullChangeToken.Singleton,
91+
CreateCompositeChangeToken(
92+
NullChangeToken.Singleton,
93+
NullChangeToken.Singleton,
94+
CreateCompositeChangeToken(
95+
CreateCompositeChangeToken(
96+
NullChangeToken.Singleton,
97+
NullChangeToken.Singleton),
98+
NullChangeToken.Singleton)),
99+
NullChangeToken.Singleton,
100+
CreateCompositeChangeToken(
101+
CreateCompositeChangeToken(
102+
NullChangeToken.Singleton,
103+
NullChangeToken.Singleton),
104+
NullChangeToken.Singleton));
105+
106+
var result = ChangeTokenComposer.FlattenChangeToken(provider);
107+
Assert.That(result, Is.InstanceOf<NullChangeToken>());
108+
Assert.That(result, Is.SameAs(NullChangeToken.Singleton));
109+
}
110+
111+
[Test]
112+
public void Flatten_ReturnsSingleToken_WhenRemainOneToken()
113+
{
114+
var provider = CreateCompositeChangeToken(
115+
CreateCompositeChangeToken(
116+
NullChangeToken.Singleton,
117+
NullChangeToken.Singleton,
118+
CreateCompositeChangeToken(
119+
CreateCompositeChangeToken(
120+
NullChangeToken.Singleton,
121+
NullChangeToken.Singleton),
122+
NullChangeToken.Singleton),
123+
CreateCompositeChangeToken(
124+
NullChangeToken.Singleton,
125+
NullChangeToken.Singleton,
126+
CreateCompositeChangeToken(
127+
CreateCompositeChangeToken(
128+
NullChangeToken.Singleton,
129+
new TestChangeToken()),
130+
NullChangeToken.Singleton))),
131+
NullChangeToken.Singleton,
132+
NullChangeToken.Singleton,
133+
CreateCompositeChangeToken(
134+
NullChangeToken.Singleton,
135+
NullChangeToken.Singleton,
136+
CreateCompositeChangeToken(
137+
CreateCompositeChangeToken(
138+
NullChangeToken.Singleton,
139+
NullChangeToken.Singleton),
140+
NullChangeToken.Singleton)),
141+
NullChangeToken.Singleton,
142+
CreateCompositeChangeToken(
143+
CreateCompositeChangeToken(
144+
NullChangeToken.Singleton,
145+
NullChangeToken.Singleton),
146+
NullChangeToken.Singleton));
147+
148+
var result = ChangeTokenComposer.FlattenChangeToken(provider);
149+
Assert.That(result, Is.InstanceOf<TestChangeToken>());
150+
}
151+
152+
[Test]
153+
public void Flatten_MaintainOrder_WhenComposite()
154+
{
155+
var t1 = new TestChangeToken();
156+
var t2 = new TestChangeToken();
157+
var t3 = new TestChangeToken();
158+
var t4 = new TestChangeToken();
159+
var t5 = new TestChangeToken();
160+
var t6 = new TestChangeToken();
161+
var t7 = new TestChangeToken();
162+
var t8 = new TestChangeToken();
163+
var t9 = new TestChangeToken();
164+
165+
var changeToken = ChangeTokenComposer.ComposeChangeTokens(
166+
CreateCompositeChangeToken(
167+
NullChangeToken.Singleton,
168+
NullChangeToken.Singleton,
169+
CreateCompositeChangeToken(
170+
t1,
171+
CreateCompositeChangeToken(
172+
t2,
173+
NullChangeToken.Singleton,
174+
t3),
175+
t4,
176+
NullChangeToken.Singleton),
177+
t5,
178+
CreateCompositeChangeToken(
179+
NullChangeToken.Singleton,
180+
NullChangeToken.Singleton,
181+
CreateCompositeChangeToken(
182+
CreateCompositeChangeToken(
183+
NullChangeToken.Singleton,
184+
t6),
185+
NullChangeToken.Singleton)),
186+
t7),
187+
NullChangeToken.Singleton,
188+
NullChangeToken.Singleton,
189+
CreateCompositeChangeToken(
190+
NullChangeToken.Singleton,
191+
NullChangeToken.Singleton,
192+
CreateCompositeChangeToken(
193+
CreateCompositeChangeToken(
194+
NullChangeToken.Singleton,
195+
NullChangeToken.Singleton),
196+
NullChangeToken.Singleton,
197+
t8)),
198+
NullChangeToken.Singleton,
199+
CreateCompositeChangeToken(
200+
CreateCompositeChangeToken(
201+
NullChangeToken.Singleton,
202+
NullChangeToken.Singleton),
203+
NullChangeToken.Singleton,
204+
t9));
205+
206+
var composite = (CompositeChangeToken)changeToken;
207+
var changeTokens = new IChangeToken[] {t1, t2, t3, t4, t5, t6, t7, t8, t9};
208+
209+
Assert.That(changeToken, Is.InstanceOf<CompositeChangeToken>());
210+
Assert.That(composite.ChangeTokens, Is.EquivalentTo(changeTokens));
211+
}
212+
213+
[Test]
214+
public void Flatten_ReturnsSingleToken_WhenCompositeContainsOnlyOne()
215+
{
216+
var changeToken = CreateCompositeChangeToken(new TestChangeToken()).Flatten();
217+
Assert.That(changeToken, Is.InstanceOf<TestChangeToken>());
218+
}
219+
220+
[Test]
221+
public void Flatten_EmptyComposite_ReturnsNullChangeToken()
222+
{
223+
var changeToken = CreateCompositeChangeToken().Flatten();
224+
Assert.That(changeToken, Is.InstanceOf<NullChangeToken>());
225+
Assert.That(changeToken, Is.SameAs(NullChangeToken.Singleton));
226+
}
227+
228+
private static CompositeChangeToken CreateCompositeChangeToken(params IChangeToken[] changeTokens) =>
229+
new(changeTokens);
230+
231+
private sealed class TestChangeToken : IChangeToken
232+
{
233+
public bool HasChanged => false;
234+
235+
public bool ActiveChangeCallbacks => false;
236+
237+
public IDisposable RegisterChangeCallback(Action<object> callback, object state) =>
238+
NullChangeToken.Singleton.RegisterChangeCallback(callback, state);
239+
}
240+
}

0 commit comments

Comments
 (0)