Description
Description
The static ChangeToken.OnChange
method throws a StackOverflowException
when its producer factory returns the same change token instance.
Reproduction Steps
You will need a project with the Microsoft.Extensions.Primitives
NuGet package installed. The following C# code demonstrates the bug:
using System;
using System.Threading;
using Microsoft.Extensions.Primitives;
CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);
Console.WriteLine($"Initial state of HasChanged: {cancellationChangeToken.HasChanged}");
Func<IChangeToken> producer = () =>
{
return cancellationChangeToken;
};
Action consumer = () => Console.WriteLine("The callback was invoked.");
using (ChangeToken.OnChange(producer, consumer))
{
cancellationTokenSource.Cancel();
}
Expected behavior
I would expect that this method would not throw a StackOverflowException
.
Actual behavior
A StackOverflowException
is thrown. The ChangeToken.OnChange
function registers the producer
and corresponding consumer
. When the producer triggers a change, i.e.; the cancellationTokenSource.Cancel
is called, the consumer
callback is executed and then the producer
is called again to get a new change token. However, the same change token is returned. And since it has already fired, it immediately causes the re-registration to invoke the callback inline. This creates an infinite loop, where the symptom is the SOE.
Regression?
It is my understanding that this has always been an issue, it's just never been raised before. This ChangeToken
is applicable to the following .NET builds:
.NET Platform Extensions | 1.0, 1.1, 2.0, 2.1, 2.2, 3.0, 3.1, 5.0, 6.0 RC 1 |
---|
Known Workarounds
The known workaround is to ensure that the producer
either returns a new change token, or at least evaluates the previous state and resets itself (if possible).
using System;
using System.Threading;
using Microsoft.Extensions.Primitives;
CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);
Console.WriteLine($"Initial state of HasChanged: {cancellationChangeToken.HasChanged}");
Func<IChangeToken> producer = () =>
{
// The producer factory should always return a new change token.
// If the token's already fired, get a new token.
if (cancellationTokenSource.IsCancellationRequested)
{
cancellationTokenSource = new();
cancellationChangeToken = new(cancellationTokenSource.Token);
}
return cancellationChangeToken;
};
Action consumer = () => Console.WriteLine("The callback was invoked.");
using (ChangeToken.OnChange(producer, consumer))
{
cancellationTokenSource.Cancel();
}
Configuration
No response
Other information
No response