-
Notifications
You must be signed in to change notification settings - Fork 4.6k
/
ChangeToken.cs
156 lines (135 loc) · 6.23 KB
/
ChangeToken.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Diagnostics;
using System.Threading;
namespace Microsoft.Extensions.Primitives
{
/// <summary>
/// Propagates notifications that a change has occurred.
/// </summary>
public static class ChangeToken
{
/// <summary>
/// Registers the <paramref name="changeTokenConsumer"/> action to be called whenever the token produced changes.
/// </summary>
/// <param name="changeTokenProducer">Produces the change token.</param>
/// <param name="changeTokenConsumer">Action called when the token changes.</param>
/// <returns></returns>
public static IDisposable OnChange(Func<IChangeToken?> changeTokenProducer, Action changeTokenConsumer)
{
if (changeTokenProducer is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenProducer);
}
if (changeTokenConsumer is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenConsumer);
}
return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
}
/// <summary>
/// Registers the <paramref name="changeTokenConsumer"/> action to be called whenever the token produced changes.
/// </summary>
/// <param name="changeTokenProducer">Produces the change token.</param>
/// <param name="changeTokenConsumer">Action called when the token changes.</param>
/// <param name="state">state for the consumer.</param>
/// <returns></returns>
public static IDisposable OnChange<TState>(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
{
if (changeTokenProducer is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenProducer);
}
if (changeTokenConsumer is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenConsumer);
}
return new ChangeTokenRegistration<TState>(changeTokenProducer, changeTokenConsumer, state);
}
private sealed class ChangeTokenRegistration<TState> : IDisposable
{
private readonly Func<IChangeToken?> _changeTokenProducer;
private readonly Action<TState> _changeTokenConsumer;
private readonly TState _state;
private IDisposable? _disposable;
private static readonly NoopDisposable _disposedSentinel = new NoopDisposable();
public ChangeTokenRegistration(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
{
_changeTokenProducer = changeTokenProducer;
_changeTokenConsumer = changeTokenConsumer;
_state = state;
IChangeToken? token = changeTokenProducer();
RegisterChangeTokenCallback(token);
}
private void OnChangeTokenFired()
{
// The order here is important. We need to take the token and then apply our changes BEFORE
// registering. This prevents us from possible having two change updates to process concurrently.
//
// If the token changes after we take the token, then we'll process the update immediately upon
// registering the callback.
IChangeToken? token = _changeTokenProducer();
try
{
_changeTokenConsumer(_state);
}
finally
{
// We always want to ensure the callback is registered
RegisterChangeTokenCallback(token);
}
}
private void RegisterChangeTokenCallback(IChangeToken? token)
{
if (token is null)
{
return;
}
IDisposable registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration<TState>?)s)!.OnChangeTokenFired(), this);
SetDisposable(registraton);
}
private void SetDisposable(IDisposable disposable)
{
// We don't want to transition from _disposedSentinel => anything since it's terminal
// but we want to allow going from previously assigned disposable, to another
// disposable.
IDisposable? current = Volatile.Read(ref _disposable);
// If Dispose was called, then immediately dispose the disposable
if (current == _disposedSentinel)
{
disposable.Dispose();
return;
}
// Otherwise, try to update the disposable
IDisposable? previous = Interlocked.CompareExchange(ref _disposable, disposable, current);
if (previous == _disposedSentinel)
{
// The subscription was disposed so we dispose immediately and return
disposable.Dispose();
}
else if (previous == current)
{
// We successfully assigned the _disposable field to disposable
}
else
{
// Sets can never overlap with other SetDisposable calls so we should never get into this situation
throw new InvalidOperationException("Somebody else set the _disposable field");
}
}
public void Dispose()
{
// If the previous value is disposable then dispose it, otherwise,
// now we've set the disposed sentinel
Interlocked.Exchange(ref _disposable, _disposedSentinel)?.Dispose();
}
private sealed class NoopDisposable : IDisposable
{
public void Dispose()
{
}
}
}
}
}