Open davidfowl opened 2 years ago
Tagging subscribers to this area: @mangod9 See info in area-owners.md if you want to be subscribed.
Author: | davidfowl |
---|---|
Assignees: | - |
Labels: | `api-suggestion`, `area-System.Threading`, `untriaged` |
Milestone: | - |
Tagging subscribers to this area: @dotnet/area-extensions-primitives See info in area-owners.md if you want to be subscribed.
Author: | davidfowl |
---|---|
Assignees: | - |
Labels: | `api-suggestion`, `untriaged`, `area-Extensions-Primitives` |
Milestone: | - |
Marked for future. @davidfowl is this a critical feature for 7.0?
Critical, no but I can take it to API review and do the implementation if needed for .NET 7. It's currently a pit of failure (I filed this because I saw someone use it in a way that would fail).
This would be highly appreciated!
Another use case. There's double detection of changes to appsettings.json
. There are still open issues for that. Those issues and the docs recommend debouncing the detections. But using the change token approach needs an async overload. (I documented the problem here).
I worked around this a while ago: https://github.com/dazinator/Changify/blob/develop/src/Changify/Extensions/ChangeTokenExtensions.cs
Basically, in order to use ChangeToken.OnChange
you need:
Func<IChangeToken>
produces the token.So I wrote the above extension method for Func<IChangeToken>
so you can do this:
Func<IChangeToken> changeTokenProducer = GetOrInjectThis();
await changeTokenProducer.WaitOneAsync();
// token has been signalled do your async work here.
await MyStuff();
// One draw back of this approach is that it's one execution at a time, i.e so to listen for the next signal you have to make this call again or refactor this into a loop so can be used similar to ChangeToken.OnChange style.
await changeTokenProducer.WaitOneAsync();
One other idea is an IAsyncEnumerable approach?
Here is the implementation I landed on: https://github.com/dazinator/Changify/blob/develop/src/Changify/Extensions/ChangeTokenExtensions.cs#L78
The reason the solution looks like this, to me, is basically because I was already familiar with this WaitOneAsync
api - and did not want the cognitive load of trying to make an async equivalent to this: https://github.com/dotnet/runtime/blob/3aaebebb9b766bf15e936f29ab90686de7c21840/src/libraries/Microsoft.Extensions.Primitives/src/ChangeToken.cs#L55 or solving from first principles ;-)
and a test: https://github.com/dazinator/Changify/blob/develop/src/Changify.Tests/ChangeTokenExtensionsTests.cs#L76
Saying that, even though it seems to be functional, I don't like the fact it's creating two TCS's per async callback.
All in all I think this will serve my purposes for now, until either:
IAsyncEnumerable
based, I could imagine enumerating through an IAsyncEnumerable in a long running background thread for example to process something asynchronously in each iteration in response to a change token firing - basically this would not look like the current ChangeToken.OnChange api.@davidfowl
one thing which I was curious about is that.. the consumer could unregister
from callbacks at any point on another thread for example. Meanwhile an async callback could about to be invoked, or could even be running. It felt to me like it might be nice to supply a CancellationToken
into the callback which could be signalled if the consumer unsubscribes. However this isn't part of the api at present. Use case would be, the async task does something resource intensive, and if the consumer is no longer interested in being notified, perhaps thats another way of them indicating that even if a callback is currently running, they wish it to be terminated? I am not sure about this perhaps this a seperate thing that they can workaround via their own mechanisms - passing state and such.
@davidfowl
I am also just geenrally curious as to why this OnChange
method was not implemented as an extension method to Func<IChangeToken>
to aid discoverability e.g current usage is like:
var producer = new Func<IChangeToken>(() =>
{
return someToken
});
ChangeToken.OnChange(producer, () => { });
No big deal but you have to be aware ChangeToken
concrete class exists with this static method to discover it.
The Extension method style is obvioulsy similar but has the advantage of removing an explicit argument, and is better for discoverability imho:
var producer = new Func<IChangeToken>(() =>
{
return someToken
});
producer.OnChange(async () => { });
This is why the solution I linked above I have gone with an exension method approach, but want to know if there is some valid reasons against that..
We don't usually add extension methods on delegate types.
Some arguments for introducung an IChangeTokenProducer
interface:
OnChange
or OnChangeAsync
, OnWaitOneAsync
etc could be introduced without violating your policy.CompositeChangeToken
is already a thing, but no builder api to compose seperate producers (sources) together into a single composite producer is available.Func<IChangeToken>
is nice for composability but not so good for lifetime management What would the implementation be? Await every single consumer sequentially? Or Task.WhenAll? I think the latter is a much better option. We don't need to artifically delay the callbacks until some other unrelated previous callback finished executing.
Is this still on the radar?
Background and motivation
Today it's not possible to use
ChangeToken.OnChange
to execute asynchronous logic before re-subscribing for new updates. The current API has aAction<T>
based callback only (https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.primitives.changetoken.onchange?view=dotnet-plat-ext-6.0). We need an async overload to run async logic so people don't end up using async void or blocking when using this helper.API Proposal
API Usage
Alternative Designs
No response
Risks
None