Skip to content

Commit 5df1ff3

Browse files
authored
Cache interface support for factory arg (#351)
* interface support * functional default * to base * scopedasync test * cache default * tests * cleanup
1 parent 83732d3 commit 5df1ff3

12 files changed

+254
-42
lines changed

BitFaster.Caching.UnitTests/Atomic/AtomicFactoryScopedAsyncCacheTests.cs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,6 @@ public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue()
4646
this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
4747
}
4848

49-
[Fact]
50-
public async Task WhenKeyDoesNotExistGetOrAddArgAddsValueWithArg()
51-
{
52-
// TODO: move to base when interface supports factory arg
53-
var c = this.cache as AtomicFactoryScopedAsyncCache<int, Disposable>;
54-
55-
await c.ScopedGetOrAddAsync(
56-
1,
57-
(k, a) => Task.FromResult(new Scoped<Disposable>(new Disposable(a))),
58-
2);
59-
60-
this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
61-
lifetime.Value.State.Should().Be(2);
62-
}
63-
6449
[Fact]
6550
public async Task GetOrAddAsyncDisposedScopeThrows()
6651
{

BitFaster.Caching.UnitTests/Atomic/AtomicFactoryScopedCacheTests.cs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,6 @@ public void WhenKeyDoesNotExistGetOrAddAddsValue()
4646
this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
4747
}
4848

49-
[Fact]
50-
public void WhenKeyDoesNotExistGetOrAddArgAddsValueWithArg()
51-
{
52-
// TODO: move to base when interface supports factory arg
53-
var c = this.cache as AtomicFactoryScopedCache<int, Disposable>;
54-
55-
c.ScopedGetOrAdd(
56-
1,
57-
(k, a) => new Scoped<Disposable>(new Disposable(a)),
58-
2);
59-
60-
this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
61-
lifetime.Value.State.Should().Be(2);
62-
}
63-
6449
[Fact]
6550
public void GetOrAddDisposedScopeThrows()
6651
{
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+

2+
using System;
3+
using System.Threading.Tasks;
4+
using FluentAssertions;
5+
using Moq;
6+
using Xunit;
7+
8+
namespace BitFaster.Caching.UnitTests
9+
{
10+
// Tests for interface default implementations.
11+
public class CacheTests
12+
{
13+
// backcompat: remove conditional compile
14+
#if NETCOREAPP3_0_OR_GREATER
15+
[Fact]
16+
public void WhenCacheInterfaceDefaultGetOrAddFallback()
17+
{
18+
var cache = new Mock<ICache<int, int>>();
19+
cache.CallBase = true;
20+
21+
Func<int, Func<int, int>, int> evaluate = (k, f) => f(k);
22+
cache.Setup(c => c.GetOrAdd(It.IsAny<int>(), It.IsAny<Func<int, int>>())).Returns(evaluate);
23+
24+
cache.Object.GetOrAdd(
25+
1,
26+
(k, a) => k + a,
27+
2).Should().Be(3);
28+
}
29+
30+
[Fact]
31+
public async Task WhenAsyncCacheInterfaceDefaultGetOrAddFallback()
32+
{
33+
var cache = new Mock<IAsyncCache<int, int>>();
34+
cache.CallBase = true;
35+
36+
Func<int, Func<int, Task<int>>, ValueTask<int>> evaluate = (k, f) => new ValueTask<int>(f(k));
37+
cache.Setup(c => c.GetOrAddAsync(It.IsAny<int>(), It.IsAny<Func<int, Task<int>>>())).Returns(evaluate);
38+
39+
var r = await cache.Object.GetOrAddAsync(
40+
1,
41+
(k, a) => Task.FromResult(k + a),
42+
2);
43+
44+
r.Should().Be(3);
45+
}
46+
47+
[Fact]
48+
public void WhenScopedCacheInterfaceDefaultGetOrAddFallback()
49+
{
50+
var cache = new Mock<IScopedCache<int, Disposable>>();
51+
cache.CallBase = true;
52+
53+
Func<int, Func<int, Scoped<Disposable>>, Lifetime<Disposable>> evaluate = (k, f) =>
54+
{
55+
var scope = f(k);
56+
scope.TryCreateLifetime(out var lifetime).Should().BeTrue();
57+
return lifetime;
58+
};
59+
60+
cache.Setup(c => c.ScopedGetOrAdd(It.IsAny<int>(), It.IsAny<Func<int, Scoped<Disposable>>>())).Returns(evaluate);
61+
62+
var l = cache.Object.ScopedGetOrAdd(
63+
1,
64+
(k, a) => new Scoped<Disposable>(new Disposable(k + a)),
65+
2);
66+
67+
l.Value.State.Should().Be(3);
68+
}
69+
70+
[Fact]
71+
public async Task WhenScopedAsyncCacheInterfaceDefaultGetOrAddFallback()
72+
{
73+
var cache = new Mock<IScopedAsyncCache<int, Disposable>>();
74+
cache.CallBase = true;
75+
76+
Func<int, Func<int, Task<Scoped<Disposable>>>, ValueTask<Lifetime<Disposable>>> evaluate = async (k, f) =>
77+
{
78+
var scope = await f(k);
79+
scope.TryCreateLifetime(out var lifetime).Should().BeTrue();
80+
return lifetime;
81+
};
82+
83+
cache
84+
.Setup(c => c.ScopedGetOrAddAsync(It.IsAny<int>(), It.IsAny<Func<int, Task<Scoped<Disposable>>>>()))
85+
.Returns(evaluate);
86+
87+
var lifetime = await cache.Object.ScopedGetOrAddAsync(
88+
1,
89+
(k, a) => Task.FromResult(new Scoped<Disposable>(new Disposable(k + a))),
90+
2);
91+
92+
lifetime.Value.State.Should().Be(3);
93+
}
94+
#endif
95+
}
96+
}

BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Linq;
5-
using System.Text;
65
using System.Threading.Tasks;
76
using FluentAssertions;
87
using Xunit;
@@ -83,6 +82,20 @@ public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem()
8382
lifetime.Value.Should().Be(d);
8483
}
8584

85+
// backcompat: remove conditional compile
86+
#if NETCOREAPP3_0_OR_GREATER
87+
[Fact]
88+
public async Task WhenKeyDoesNotExistGetOrAddArgAddsValueWithArg()
89+
{
90+
await this.cache.ScopedGetOrAddAsync(
91+
1,
92+
(k, a) => Task.FromResult(new Scoped<Disposable>(new Disposable(a))),
93+
2);
94+
95+
this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
96+
lifetime.Value.State.Should().Be(2);
97+
}
98+
#endif
8699
[Fact]
87100
public void WhenKeyExistsAddOrUpdateUpdatesExistingItem()
88101
{

BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,19 @@ public async Task GetOrAddAsyncDisposedScopeThrows()
5151

5252
await getOrAdd.Should().ThrowAsync<InvalidOperationException>();
5353
}
54+
55+
// backcompat: remove conditional compile
56+
#if NETCOREAPP3_0_OR_GREATER
57+
[Fact]
58+
public async Task GetOrAddAsyncArgDisposedScopeThrows()
59+
{
60+
var scope = new Scoped<Disposable>(new Disposable());
61+
scope.Dispose();
62+
63+
Func<Task> getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, (k, a) => Task.FromResult(scope), 2); };
64+
65+
await getOrAdd.Should().ThrowAsync<InvalidOperationException>();
66+
}
67+
#endif
5468
}
5569
}

BitFaster.Caching.UnitTests/ScopedCacheTestBase.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem()
8181
lifetime.Value.Should().Be(d);
8282
}
8383

84+
// backcompat: remove conditional compile
85+
#if NETCOREAPP3_0_OR_GREATER
86+
[Fact]
87+
public void WhenKeyDoesNotExistGetOrAddArgAddsValueWithArg()
88+
{
89+
this.cache.ScopedGetOrAdd(
90+
1,
91+
(k, a) => new Scoped<Disposable>(new Disposable(a)),
92+
2);
93+
94+
this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue();
95+
lifetime.Value.State.Should().Be(2);
96+
}
97+
#endif
98+
8499
[Fact]
85100
public void WhenKeyExistsAddOrUpdateUpdatesExistingItem()
86101
{

BitFaster.Caching/IAsyncCache.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
53
using System.Threading.Tasks;
64

75
namespace BitFaster.Caching
@@ -55,6 +53,20 @@ public interface IAsyncCache<K, V> : IEnumerable<KeyValuePair<K, V>>
5553
/// <returns>A task that represents the asynchronous GetOrAdd operation.</returns>
5654
ValueTask<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory);
5755

56+
// backcompat: remove conditional compile
57+
#if NETCOREAPP3_0_OR_GREATER
58+
/// <summary>
59+
/// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
60+
/// existing value if the key already exists.
61+
/// </summary>
62+
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
63+
/// <param name="key">The key of the element to add.</param>
64+
/// <param name="valueFactory">The factory function used to asynchronously generate a value for the key.</param>
65+
/// <param name="factoryArgument">An argument value to pass into valueFactory.</param>
66+
/// <returns>A task that represents the asynchronous GetOrAdd operation.</returns>
67+
ValueTask<V> GetOrAddAsync<TArg>(K key, Func<K, TArg, Task<V>> valueFactory, TArg factoryArgument) => this.GetOrAddAsync(key, k => valueFactory(k, factoryArgument));
68+
#endif
69+
5870
/// <summary>
5971
/// Attempts to remove the value that has the specified key.
6072
/// </summary>

BitFaster.Caching/ICache.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
63

74
namespace BitFaster.Caching
85
{
@@ -56,6 +53,21 @@ public interface ICache<K, V> : IEnumerable<KeyValuePair<K, V>>
5653
/// in the cache, or the new value if the key was not in the cache.</returns>
5754
V GetOrAdd(K key, Func<K, V> valueFactory);
5855

56+
// backcompat: remove conditional compile
57+
#if NETCOREAPP3_0_OR_GREATER
58+
/// <summary>
59+
/// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the
60+
/// existing value if the key already exists.
61+
/// </summary>
62+
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
63+
/// <param name="key">The key of the element to add.</param>
64+
/// <param name="valueFactory">The factory function used to generate a value for the key.</param>
65+
/// <param name="factoryArgument">An argument value to pass into valueFactory.</param>
66+
/// <returns>The value for the key. This will be either the existing value for the key if the key is already
67+
/// in the cache, or the new value if the key was not in the cache.</returns>
68+
V GetOrAdd<TArg>(K key, Func<K, TArg, V> valueFactory, TArg factoryArgument) => this.GetOrAdd(key, k => valueFactory(k, factoryArgument));
69+
#endif
70+
5971
/// <summary>
6072
/// Attempts to remove the value that has the specified key.
6173
/// </summary>

BitFaster.Caching/IScopedAsyncCache.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
53
using System.Threading.Tasks;
64

75
namespace BitFaster.Caching
@@ -60,6 +58,20 @@ public interface IScopedAsyncCache<K, V> : IEnumerable<KeyValuePair<K, Scoped<V>
6058
/// <returns>A task that represents the asynchronous ScopedGetOrAdd operation.</returns>
6159
ValueTask<Lifetime<V>> ScopedGetOrAddAsync(K key, Func<K, Task<Scoped<V>>> valueFactory);
6260

61+
// backcompat: remove conditional compile
62+
#if NETCOREAPP3_0_OR_GREATER
63+
/// <summary>
64+
/// Adds a key/scoped value pair to the cache if the key does not already exist. Returns a lifetime for either
65+
/// the new value, or the existing value if the key already exists.
66+
/// </summary>
67+
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
68+
/// <param name="key">The key of the element to add.</param>
69+
/// <param name="valueFactory">The factory function used to asynchronously generate a scoped value for the key.</param>
70+
/// <param name="factoryArgument"></param>
71+
/// <returns>A task that represents the asynchronous ScopedGetOrAdd operation.</returns>
72+
ValueTask<Lifetime<V>> ScopedGetOrAddAsync<TArg>(K key, Func<K, TArg, Task<Scoped<V>>> valueFactory, TArg factoryArgument) => this.ScopedGetOrAddAsync(key, (k) => valueFactory(k, factoryArgument));
73+
#endif
74+
6375
/// <summary>
6476
/// Attempts to remove the value that has the specified key.
6577
/// </summary>

BitFaster.Caching/IScopedCache.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
63

74
namespace BitFaster.Caching
85
{
@@ -62,6 +59,22 @@ public interface IScopedCache<K, V> : IEnumerable<KeyValuePair<K, Scoped<V>>> wh
6259
/// the cache.</returns>
6360
Lifetime<V> ScopedGetOrAdd(K key, Func<K, Scoped<V>> valueFactory);
6461

62+
// backcompat: remove conditional compile
63+
#if NETCOREAPP3_0_OR_GREATER
64+
/// <summary>
65+
/// Adds a key/scoped value pair to the cache if the key does not already exist. Returns a lifetime for either
66+
/// the new value, or the existing value if the key already exists.
67+
/// </summary>
68+
/// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
69+
/// <param name="key">The key of the element to add.</param>
70+
/// <param name="valueFactory">The factory function used to generate a scoped value for the key.</param>
71+
/// <param name="factoryArgument"></param>
72+
/// <returns>The lifetime for the value associated with the key. The lifetime will be either reference the
73+
/// existing value for the key if the key is already in the cache, or the new value if the key was not in
74+
/// the cache.</returns>
75+
Lifetime<V> ScopedGetOrAdd<TArg>(K key, Func<K, TArg, Scoped<V>> valueFactory, TArg factoryArgument) => this.ScopedGetOrAdd(key, k => valueFactory(k, factoryArgument));
76+
#endif
77+
6578
/// <summary>
6679
/// Attempts to remove the value that has the specified key.
6780
/// </summary>

0 commit comments

Comments
 (0)