Created
June 25, 2018 16:07
-
-
Save afscrome/e55e5f4e3712bfcac5b02ae137ae0d7f to your computer and use it in GitHub Desktop.
Background Refreshable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using NUnit.Framework; | |
using System; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace BackgroundRefreshable | |
{ | |
///<summary> | |
/// Provides support for loading, storing and refreshing a value asyncronously. | |
/// | |
/// Once Value has been loaded once, Value is guaranteed to complete syncronously, never blocking. | |
///</summary> | |
///<typeparam name="T"></typeparam> | |
public class BackgroundRefreshable<T> | |
{ | |
private readonly Func<Task<T>> _loadFunction; | |
private readonly object _initaliseLock = new object(); | |
private Task<T> _value; | |
public BackgroundRefreshable(Func<Task<T>> loadFunction) | |
{ | |
_loadFunction = loadFunction; | |
} | |
public Task<T> Value => _value ?? Initialise(); | |
private Task<T> Initialise() | |
{ | |
lock(_initaliseLock) | |
{ | |
if (_value != null) | |
return _value; | |
var loader = _loadFunction(); | |
loader.ContinueWith(_ => _value = null, TaskContinuationOptions.NotOnRanToCompletion); | |
_value = loader; | |
} | |
return _value; | |
} | |
public async Task<T> Refresh() | |
{ | |
var loaderTask = _loadFunction(); | |
await loaderTask; | |
_value = loaderTask; | |
return loaderTask.Result; | |
} | |
} | |
public class Tests | |
{ | |
[Test] | |
public async Task When_Making_First_Call_To_Value_Then_Blocked_Until_Value_Is_Loaded() | |
{ | |
var tcs = new TaskCompletionSource<int>(); | |
var refreshable = RefreshableLoadingSequence( | |
tcs.Task | |
); | |
var initialValueTask = refreshable.Value; | |
//Ensure value has not been loaded prematurely | |
await Task.Delay(TimeSpan.FromMilliseconds(1)); | |
Assert.False(initialValueTask.IsCompleted); | |
//Complete the load | |
tcs.SetResult(45); | |
//Verify value is returned | |
Assert.That(await refreshable.Value, Is.EqualTo(45)); | |
} | |
[Test] | |
public async Task When_Making_Multiple_Calls_To_Value_Before_Initial_Load_Then_Value_Only_Loaded_Once() | |
{ | |
var tcs = new TaskCompletionSource<string>(); | |
var refreshable = RefreshableLoadingSequence( | |
tcs.Task, | |
Task.FromResult("Now you don't") | |
); | |
var consumer1 = refreshable.Value; | |
var consumer2 = refreshable.Value; | |
var consumer3 = refreshable.Value; | |
//Ensure values have not been loaded prematurely | |
await Task.Delay(TimeSpan.FromMilliseconds(1)); | |
Assert.False(consumer1.IsCompleted); | |
Assert.False(consumer2.IsCompleted); | |
Assert.False(consumer3.IsCompleted); | |
//Complete the load | |
tcs.SetResult("Now you see me"); | |
//Verify all consumers got expected value | |
Assert.That(await consumer1, Is.EqualTo("Now you see me")); | |
Assert.That(await consumer2, Is.EqualTo("Now you see me")); | |
Assert.That(await consumer3, Is.EqualTo("Now you see me")); | |
} | |
[Test] | |
public async Task When_Getting_Value_Then_Value_Is_Cached() | |
{ | |
var refreshable = RefreshableLoadingSequence( | |
Task.FromResult("Alice"), | |
Task.FromResult("Bob") | |
); | |
Assert.That(await refreshable.Value, Is.EqualTo("Alice")); | |
Assert.That(await refreshable.Value, Is.EqualTo("Alice")); | |
Assert.That(await refreshable.Value, Is.EqualTo("Alice")); | |
Assert.That(await refreshable.Value, Is.EqualTo("Alice")); | |
} | |
[Test] | |
public async Task When_Refresh_Succeeds_Then_Value_Is_Updated() | |
{ | |
var refreshable = RefreshableLoadingSequence( | |
Task.FromResult("first"), | |
Task.FromResult("second") | |
); | |
Assert.That(await refreshable.Value, Is.EqualTo("first")); | |
var refresh = await refreshable.Refresh(); | |
Assert.That(refresh, Is.EqualTo("second")); | |
Assert.That(await refreshable.Value, Is.EqualTo("second")); | |
} | |
[Test] | |
public async Task When_Refreshing_Then_Previous_Value_Returned_Until_Refresh_Completes() | |
{ | |
var tcs = new TaskCompletionSource<double>(); | |
var refreshable = RefreshableLoadingSequence( | |
Task.FromResult(3.142), | |
tcs.Task | |
); | |
//Initial load | |
Assert.That(await refreshable.Value, Is.EqualTo(3.142)); | |
//Start Refresh | |
var refreshTask = refreshable.Refresh(); | |
//Ensure old value is being returned | |
await Task.Delay(TimeSpan.FromMilliseconds(1)); | |
var initialValueTask = refreshable.Value; | |
Assert.True(initialValueTask.IsCompleted); | |
Assert.That(initialValueTask.Result, Is.EqualTo(3.142)); | |
//Complete Refresh | |
tcs.SetResult(2.718); | |
Assert.That(await refreshTask, Is.EqualTo(2.718)); | |
//Value now returns updated result | |
var postRefreshValueTask = refreshable.Value; | |
Assert.True(postRefreshValueTask.IsCompleted); | |
Assert.That(postRefreshValueTask.Result, Is.EqualTo(2.718)); | |
} | |
[Test] | |
public async Task When_Refresh_Throws_Exception_Then_Old_Value_Persists() | |
{ | |
var exception = new Exception("Simulated failure"); | |
var refreshable = RefreshableLoadingSequence( | |
Task.FromResult('%'), | |
Task.FromException<char>(exception) | |
); | |
//Initial load | |
Assert.That(await refreshable.Value, Is.EqualTo('%')); | |
//Ensure Refresh throws expected exception | |
var caughtException = Assert.ThrowsAsync<Exception>(() => refreshable.Refresh()); | |
Assert.That(caughtException, Is.EqualTo(exception)); | |
//But Value still returns previous result | |
var postRefreshValueTask = refreshable.Value; | |
Assert.True(postRefreshValueTask.IsCompleted); | |
Assert.That(postRefreshValueTask.Result, Is.EqualTo('%')); | |
} | |
[Test] | |
public async Task When_Refresh_Is_Cancelled_Then_Old_Value_Persists() | |
{ | |
var cancellationSource = new CancellationTokenSource(); | |
cancellationSource.Cancel(); | |
var refreshable = RefreshableLoadingSequence( | |
Task.FromResult('%'), | |
Task.FromCanceled<char>(cancellationSource.Token) | |
); | |
//Initial load | |
Assert.That(await refreshable.Value, Is.EqualTo('%')); | |
//Ensure Refresh is canceleld | |
Assert.ThrowsAsync<TaskCanceledException>(() => refreshable.Refresh()); | |
//But Value still returns previous result | |
var postRefreshValueTask = refreshable.Value; | |
Assert.True(postRefreshValueTask.IsCompleted); | |
Assert.That(postRefreshValueTask.Result, Is.EqualTo('%')); | |
} | |
[Test] | |
public async Task When_Initial_Load_Throws_Exception_Then_Next_Call_To_Value_Retries() | |
{ | |
var initialException = new Exception("Initial Exception"); | |
var refreshable = RefreshableLoadingSequence( | |
Task.FromException<string>(initialException), | |
Task.FromResult("I'm alive") | |
); | |
//First Load | |
var caughtInitialValueException = Assert.ThrowsAsync<Exception>(() => refreshable.Value); | |
Assert.That(caughtInitialValueException, Is.EqualTo(initialException)); | |
//Second Load | |
Assert.That(await refreshable.Value, Is.EqualTo("I'm alive")); | |
} | |
[Test] | |
public async Task When_Initial_Load_Is_Cancelled_Then_Next_Call_To_Value_Retries() | |
{ | |
var cancellationSource = new CancellationTokenSource(); | |
cancellationSource.Cancel(); | |
var refreshable = RefreshableLoadingSequence( | |
Task.FromCanceled<decimal>(cancellationSource.Token), | |
Task.FromResult(93.42m) | |
); | |
//First Load | |
Assert.ThrowsAsync<TaskCanceledException>(() => refreshable.Value); | |
//Second Load | |
Assert.That(await refreshable.Value, Is.EqualTo(93.42m)); | |
} | |
private BackgroundRefreshable<T> RefreshableLoadingSequence<T>(params Task<T>[] sequence) | |
{ | |
int index = 0; | |
return new BackgroundRefreshable<T>(() => | |
{ | |
if (index >= sequence.Length) | |
throw new IndexOutOfRangeException(); | |
return sequence[index++]; | |
}); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment