From fe936caaeaae836cd9f390fef906bbe93b90d1de Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 15 Jun 2026 09:28:04 +0000 Subject: [PATCH 1/2] fix: use IsNullOrDefault instead of != null in GetOrSet/GetOrSetAsync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The early-return optimization added in 0.0.17 used 'value != null' to check for a cache hit. For non-nullable value types (int, bool, etc.), default(T) is not null when boxed, so the check always returned true on cache miss — returning default(T) without ever calling the factory. This broke commerce-admin where GetOrSetAsync was used to cache Application.HostClientId: the factory was never invoked, returning 0 instead of the real client ID, causing 'Unknown application id' errors. Fix: use HzMemoryCache.IsNullOrDefault() consistently, matching the check the inner HzMemoryCache.GetOrSet already uses. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- RedisBackedHzCache/RedisBackedHzCache.cs | 2 +- RedisBackedHzCache/RedisBackedHzCacheAsync.cs | 2 +- UnitTests/IntegrationTests.cs | 65 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/RedisBackedHzCache/RedisBackedHzCache.cs b/RedisBackedHzCache/RedisBackedHzCache.cs index fa6c6f3..d4f216c 100644 --- a/RedisBackedHzCache/RedisBackedHzCache.cs +++ b/RedisBackedHzCache/RedisBackedHzCache.cs @@ -220,7 +220,7 @@ public void Set(string key, T value, TimeSpan ttl) public T GetOrSet(string key, Func valueFactory, TimeSpan ttl, long maxMsToWaitForFactory = 10000) { var value = hzCache.Get(key); - if (value != null) + if (!HzMemoryCache.IsNullOrDefault(value)) { return value; } diff --git a/RedisBackedHzCache/RedisBackedHzCacheAsync.cs b/RedisBackedHzCache/RedisBackedHzCacheAsync.cs index 84c26ce..a42efd2 100644 --- a/RedisBackedHzCache/RedisBackedHzCacheAsync.cs +++ b/RedisBackedHzCache/RedisBackedHzCacheAsync.cs @@ -58,7 +58,7 @@ public Task SetAsync(string key, T value, TimeSpan ttl) public async Task GetOrSetAsync(string key, Func> valueFactory, TimeSpan ttl, long maxMsToWaitForFactory = 10000) { var value = await hzCache.GetAsync(key); - if (value != null) + if (!HzMemoryCache.IsNullOrDefault(value)) { return value; } diff --git a/UnitTests/IntegrationTests.cs b/UnitTests/IntegrationTests.cs index c413eaf..660f953 100644 --- a/UnitTests/IntegrationTests.cs +++ b/UnitTests/IntegrationTests.cs @@ -306,6 +306,71 @@ public async Task TestRedisGetOrSet() } + [TestMethod] + [TestCategory("Integration")] + public async Task TestRedisGetOrSet_ValueType_CallsFactory() + { + // Arrange + var c1 = new RedisBackedHzCache( + new RedisBackedHzCacheOptions { redisConnectionString = ConnectionString, applicationCachePrefix = "test", instanceId = "c1", useRedisAs2ndLevelCache = false }); + var factoryCalled = false; + + // Act + var result = c1.GetOrSet("int-key", _ => + { + factoryCalled = true; + return 42; + }, TimeSpan.FromMinutes(1)); + + // Assert + Assert.IsTrue(factoryCalled, "Factory should be called on cache miss for value types"); + Assert.AreEqual(42, result); + await c1.RemoveAsync("int-key"); + } + + [TestMethod] + [TestCategory("Integration")] + public async Task TestRedisGetOrSetAsync_ValueType_CallsFactory() + { + // Arrange + var c1 = new RedisBackedHzCache( + new RedisBackedHzCacheOptions { redisConnectionString = ConnectionString, applicationCachePrefix = "test", instanceId = "c1", useRedisAs2ndLevelCache = false }); + var factoryCalled = false; + + // Act + var result = await c1.GetOrSetAsync("int-key-async", _ => + { + factoryCalled = true; + return Task.FromResult(42); + }, TimeSpan.FromMinutes(1)); + + // Assert + Assert.IsTrue(factoryCalled, "Factory should be called on cache miss for value types"); + Assert.AreEqual(42, result); + await c1.RemoveAsync("int-key-async"); + } + + [TestMethod] + [TestCategory("Integration")] + public async Task TestRedisGetOrSetAsync_ValueType_ReturnsCachedValue() + { + // Arrange + var c1 = new RedisBackedHzCache( + new RedisBackedHzCacheOptions { redisConnectionString = ConnectionString, applicationCachePrefix = "test", instanceId = "c1", useRedisAs2ndLevelCache = false }); + await c1.GetOrSetAsync("int-cached", _ => Task.FromResult(42), TimeSpan.FromMinutes(1)); + + // Act — second call should return cached value, not call factory + var result = await c1.GetOrSetAsync("int-cached", _ => + { + Assert.Fail("Factory should not be called when value is already cached"); + return Task.FromResult(0); + }, TimeSpan.FromMinutes(1)); + + // Assert + Assert.AreEqual(42, result); + await c1.RemoveAsync("int-cached"); + } + [TestMethod] [TestCategory("Integration")] public async Task TestRedisBackplaneDelete() From 2b0a6338a51c606a6a94f9926f06aed77946f06e Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 15 Jun 2026 10:04:39 +0000 Subject: [PATCH 2/2] add ConfigureAwait(false) to GetOrSetAsync for consistency Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- RedisBackedHzCache/RedisBackedHzCacheAsync.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RedisBackedHzCache/RedisBackedHzCacheAsync.cs b/RedisBackedHzCache/RedisBackedHzCacheAsync.cs index a42efd2..243ab35 100644 --- a/RedisBackedHzCache/RedisBackedHzCacheAsync.cs +++ b/RedisBackedHzCache/RedisBackedHzCacheAsync.cs @@ -57,7 +57,7 @@ public Task SetAsync(string key, T value, TimeSpan ttl) public async Task GetOrSetAsync(string key, Func> valueFactory, TimeSpan ttl, long maxMsToWaitForFactory = 10000) { - var value = await hzCache.GetAsync(key); + var value = await hzCache.GetAsync(key).ConfigureAwait(false); if (!HzMemoryCache.IsNullOrDefault(value)) { return value; @@ -65,16 +65,16 @@ public async Task GetOrSetAsync(string key, Func> valueFac if (options.useRedisAs2ndLevelCache) { - var redisValue = await GetRedisValueAsync(key); + var redisValue = await GetRedisValueAsync(key).ConfigureAwait(false); if (!redisValue.IsNull) { - var ttlValue = await TTLValue.FromRedisValueAsync(Encoding.ASCII.GetBytes(redisValue.ToString())); + var ttlValue = await TTLValue.FromRedisValueAsync(Encoding.ASCII.GetBytes(redisValue.ToString())).ConfigureAwait(false); hzCache.SetRaw(key, ttlValue); return (T)ttlValue.value; } } - return await hzCache.GetOrSetAsync(key, valueFactory, ttl, maxMsToWaitForFactory); + return await hzCache.GetOrSetAsync(key, valueFactory, ttl, maxMsToWaitForFactory).ConfigureAwait(false); } public Task> GetOrSetBatchAsync(IList keys, Func, Task>>> valueFactory)