diff --git a/.editorconfig b/.editorconfig
index a1a21a7a..d6665bff 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -40,7 +40,7 @@ dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:sil
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
# Modifier preferences
-dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
dotnet_style_readonly_field = true:suggestion
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
@@ -113,7 +113,7 @@ csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
# Expression-level preferences
-csharp_prefer_braces = true:silent
+csharp_prefer_braces = true:warning
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
@@ -245,11 +245,12 @@ dotnet_diagnostic.SA1309.severity = none
# Tuple element names should use correct casing
dotnet_diagnostic.SA1316.severity = none
-# File may only contain a single class
-dotnet_diagnostic.SA1402.severity = suggestion
+# File may only contain a single class. Off by default and warning for src alone, at the bottom
+# of this file: tests, samples and benchmarks keep their small helper types next to what uses them.
+dotnet_diagnostic.SA1402.severity = none
# Braces must not be omitted
-dotnet_diagnostic.SA1503.severity = suggestion
+dotnet_diagnostic.SA1503.severity = warning
# ElementsMustBeDocumented
dotnet_diagnostic.SA1600.severity = none
@@ -421,13 +422,13 @@ dotnet_diagnostic.SA1208.severity = none
dotnet_diagnostic.SA1135.severity = none
# SA1413: Use trailing comma in multi-line initializers
-dotnet_diagnostic.SA1413.severity = suggestion
+dotnet_diagnostic.SA1413.severity = warning
# SA1300: Element should begin with upper-case letter
-dotnet_diagnostic.SA1300.severity = suggestion
+dotnet_diagnostic.SA1300.severity = warning
# SA1117: Parameters should be on same line or separate lines
-dotnet_diagnostic.SA1117.severity = suggestion
+dotnet_diagnostic.SA1117.severity = warning
# CS8600: Converting null literal or possible null value to non-nullable type.
dotnet_diagnostic.CS8600.severity = error
@@ -471,3 +472,41 @@ dotnet_diagnostic.RS0041.severity = none
dotnet_diagnostic.RS0026.severity = none
dotnet_diagnostic.RS0027.severity = none
+# StyleCop.Analyzers ships ~5000 warnings' worth of rules. Every category below is off except
+# OrderingRules. A rule id beats its category here, so anything an exact id further up had already
+# switched off has to be named again to apply: SA1204 and SA1208 are, SA1200 and SA1649 are not.
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpecialRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpacingRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.ReadabilityRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.NamingRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.MaintainabilityRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.LayoutRules.severity = none
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = none
+
+# SA1649 (file name should match the first type) is already none further up, and stays that way:
+# it does not recognise the OfT suffix this repo uses for generic types, so it would ask to
+# rename CacheOfT.cs, ICacheOfT.cs and five more to Cache{T}.cs.
+#
+# SX1309, which asks every field to begin with an underscore, is already warning further up and
+# stays that way: it is this repo's field convention, and SA1309 is off above for the same reason.
+# Referencing the package is what makes it report, so the five fields that broke it are fixed.
+
+# SA1204 (static before instance) and SA1208 (System usings first) are none further up. A specific
+# id beats the category below, so each has to be named here to actually apply; the pass satisfies
+# both. SA1200 stays none: this repo puts using directives above the file-scoped namespace.
+dotnet_diagnostic.SA1204.severity = warning
+dotnet_diagnostic.SA1208.severity = warning
+
+# Ordering: the whole category, so SA1201 order by kind, SA1202 order by access, SA1204 static
+# first, SA1203 constants first, SA1214 readonly first and SA1210 (usings sorted) all apply
+# without a list of ids to maintain. Nothing off the shelf applies these: neither dotnet format nor
+# the Roslynator CLI can drive StyleCop's fixer — both answer that no code fix was found — and
+# Rider's layout engine ranks constants and statics above accessibility, which moves SA1202 the
+# wrong way. The tree was brought into line by a one-off Roslyn pass instead, and is clean now.
+dotnet_analyzer_diagnostic.category-StyleCop.CSharp.OrderingRules.severity = warning
+
+[src/**.cs]
+# One top-level type per file, for the shipped surface only. topLevelTypes in stylecop.json widens
+# SA1402 past its default of class alone. Tests, samples and benchmarks keep their small helper
+# types next to what uses them: 17 files would otherwise have to be broken up.
+dotnet_diagnostic.SA1402.severity = warning
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 46df16f7..0ecac077 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -101,8 +101,13 @@ jobs:
- name: Restore
run: dotnet restore UiPath.Caching.slnx
+ # -warnaserror is what keeps the analyzer rules in .editorconfig from drifting back:
+ # TreatWarningsAsErrors stays false so a local build still compiles while you work. It rides
+ # on this job alone. build-linux builds under the SonarScanner, whose analyzer package is not
+ # pinned here, so a new Sonar rule would fail the build instead of reporting; the analyzers
+ # this repo does pin run identically on both jobs, so gating one catches the same drift.
- name: Build
- run: dotnet build UiPath.Caching.slnx -c Release --no-restore
+ run: dotnet build UiPath.Caching.slnx -c Release --no-restore -warnaserror
# Redis-backed integration tests run on the Linux job (service containers are Linux-only).
# Windows is build-only to validate cross-platform compilation.
diff --git a/Directory.Build.props b/Directory.Build.props
index 3edf7292..bc92c985 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -44,4 +44,13 @@
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b7916d14..e5845155 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -59,6 +59,7 @@
+
diff --git a/benchmarks/UiPath.Caching.Benchmarks/CacheBenchmark.cs b/benchmarks/UiPath.Caching.Benchmarks/CacheBenchmark.cs
index 02d2b99f..23158a04 100644
--- a/benchmarks/UiPath.Caching.Benchmarks/CacheBenchmark.cs
+++ b/benchmarks/UiPath.Caching.Benchmarks/CacheBenchmark.cs
@@ -9,6 +9,8 @@ namespace UiPath.Caching.Benchmarks;
[HtmlExporter]
public class CacheBenchmark
{
+
+ private const int _batchSize = 50;
private Entry[] _entries = default!;
[Params(500, 2_500)]
@@ -25,7 +27,7 @@ public class CacheBenchmark
protected Func CreateRandomObject { get; set; } = default!;
- private const int _batchSize = 50;
+ private ICache RandomCache => _entries[Random.Shared.Next(0, _entries.Length)].Cache;
[GlobalSetup]
public void Setup()
@@ -35,13 +37,11 @@ public void Setup()
"Small" => CustomObject.RandomSmall,
"Medium" => CustomObject.RandomMedium,
"Large" => CustomObject.RandomLarge,
- _ => throw new NotSupportedException(ObjectSize)
+ _ => throw new NotSupportedException(ObjectSize),
};
_entries = SetupHelper.Setup(2, Cache, $"Redis{Topic}", NumKeys, CreateRandomObject);
}
- private ICache RandomCache => _entries[Random.Shared.Next(0, _entries.Length)].Cache;
-
[GlobalCleanup]
public void Cleanup() => SetupHelper.Cleanup(_entries);
diff --git a/benchmarks/UiPath.Caching.Benchmarks/CustomObject.cs b/benchmarks/UiPath.Caching.Benchmarks/CustomObject.cs
index 907e9491..3cca9399 100644
--- a/benchmarks/UiPath.Caching.Benchmarks/CustomObject.cs
+++ b/benchmarks/UiPath.Caching.Benchmarks/CustomObject.cs
@@ -18,7 +18,7 @@ public static CustomObject RandomLarge()
Property2 = GenerateRandomString(1_000),
Property3 = GenerateRandomDoubleArray(10_000),
UtcDateTime = DateTime.UtcNow,
- GuidList = GenerateRandomGuidList(5_000)
+ GuidList = GenerateRandomGuidList(5_000),
};
return customObject;
@@ -32,7 +32,7 @@ public static CustomObject RandomMedium()
Property2 = GenerateRandomString(100),
Property3 = GenerateRandomDoubleArray(10),
UtcDateTime = DateTime.UtcNow,
- GuidList = GenerateRandomGuidList(10)
+ GuidList = GenerateRandomGuidList(10),
};
return customObject;
diff --git a/benchmarks/UiPath.Caching.Benchmarks/SerializerBenchmark.cs b/benchmarks/UiPath.Caching.Benchmarks/SerializerBenchmark.cs
index 4e7c4dd4..e7badae2 100644
--- a/benchmarks/UiPath.Caching.Benchmarks/SerializerBenchmark.cs
+++ b/benchmarks/UiPath.Caching.Benchmarks/SerializerBenchmark.cs
@@ -11,11 +11,11 @@ namespace UiPath.Caching.Benchmarks;
[CategoriesColumn]
public class SerializerBenchmark
{
- [Params("Small", "Medium", "Large")]
- public string Size { get; set; } = "Medium";
private CustomObject _obj = default!;
private RedisValue _payload;
+ [Params("Small", "Medium", "Large")]
+ public string Size { get; set; } = "Medium";
[GlobalSetup]
public void Setup()
diff --git a/benchmarks/UiPath.Caching.Benchmarks/StreamNotifyDoorbellHarness.cs b/benchmarks/UiPath.Caching.Benchmarks/StreamNotifyDoorbellHarness.cs
index 1beda63c..6102b1d7 100644
--- a/benchmarks/UiPath.Caching.Benchmarks/StreamNotifyDoorbellHarness.cs
+++ b/benchmarks/UiPath.Caching.Benchmarks/StreamNotifyDoorbellHarness.cs
@@ -1,8 +1,8 @@
using System.Collections.Concurrent;
using System.Globalization;
-using UiPath.Caching.Benchmarks;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
+using UiPath.Caching.Benchmarks;
using UiPath.Caching.Redis;
namespace UiPath.Caching.Benchmarks;
@@ -31,6 +31,8 @@ namespace UiPath.Caching.Benchmarks;
// Run with: dotnet run -c Release --framework net8.0 -- doorbell [durationSec] [writeHz]
internal static class StreamNotifyDoorbellHarness
{
+
+ private const string TimestampPrefix = "ts:";
public static async Task RunAsync(int durationSec = 20, int writeHz = 5)
{
var cells = new (bool NotifyEnabled, string PollInterval)[]
@@ -170,6 +172,4 @@ private static async Task> MeasureCellAsync(bool notifyEnabled, s
// Hosts dispose via 'using'.
}
}
-
- private const string TimestampPrefix = "ts:";
}
diff --git a/samples/UiPath.Caching.Sample.ServiceDefaults/Extensions.cs b/samples/UiPath.Caching.Sample.ServiceDefaults/Extensions.cs
index 65a8504d..fd1cccce 100644
--- a/samples/UiPath.Caching.Sample.ServiceDefaults/Extensions.cs
+++ b/samples/UiPath.Caching.Sample.ServiceDefaults/Extensions.cs
@@ -83,25 +83,6 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w
return builder;
}
- private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder
- {
- var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
-
- if (useOtlpExporter)
- {
- builder.Services.AddOpenTelemetry().UseOtlpExporter();
- }
-
- // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
- //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
- //{
- // builder.Services.AddOpenTelemetry()
- // .UseAzureMonitor();
- //}
-
- return builder;
- }
-
public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks()
@@ -123,10 +104,29 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app)
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
{
- Predicate = r => r.Tags.Contains("live")
+ Predicate = r => r.Tags.Contains("live"),
});
}
return app;
}
+
+ private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
+ //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
+ //{
+ // builder.Services.AddOpenTelemetry()
+ // .UseAzureMonitor();
+ //}
+
+ return builder;
+ }
}
diff --git a/samples/UiPath.Caching.Sample/Program.cs b/samples/UiPath.Caching.Sample/Program.cs
index 676f2319..6424e5b6 100644
--- a/samples/UiPath.Caching.Sample/Program.cs
+++ b/samples/UiPath.Caching.Sample/Program.cs
@@ -45,7 +45,7 @@
builder.Services.AddRequestTimeouts(opt => opt.DefaultPolicy = new Microsoft.AspNetCore.Http.Timeouts.RequestTimeoutPolicy
{
Timeout = TimeSpan.FromMilliseconds(100),
- TimeoutStatusCode = 503
+ TimeoutStatusCode = 503,
});
var app = builder.Build();
diff --git a/src/UiPath.Caching.Abstractions/Broadcast/IChangeTokenFactory.cs b/src/UiPath.Caching.Abstractions/Broadcast/IChangeTokenFactory.cs
index 954b6eba..472e27be 100644
--- a/src/UiPath.Caching.Abstractions/Broadcast/IChangeTokenFactory.cs
+++ b/src/UiPath.Caching.Abstractions/Broadcast/IChangeTokenFactory.cs
@@ -2,5 +2,5 @@ namespace UiPath.Caching.Broadcast;
public interface IChangeTokenFactory
{
- public ICacheChangeToken Create(string token, ITopic topic, string cacheName, Type entryType);
+ ICacheChangeToken Create(string token, ITopic topic, string cacheName, Type entryType);
}
diff --git a/src/UiPath.Caching.Abstractions/Broadcast/ITopic.cs b/src/UiPath.Caching.Abstractions/Broadcast/ITopic.cs
index 2e5ce9fe..1a8adaff 100644
--- a/src/UiPath.Caching.Abstractions/Broadcast/ITopic.cs
+++ b/src/UiPath.Caching.Abstractions/Broadcast/ITopic.cs
@@ -5,9 +5,9 @@ public interface ITopic : IDisposable
{
TopicKey TopicKey { get; }
+ EventHandler? OnDisposed { get; set; }
+
IDisposable Subscribe(IObserver observer);
ValueTask PublishAsync(T @event, CancellationToken token = default);
-
- EventHandler? OnDisposed { get; set; }
}
diff --git a/src/UiPath.Caching.Abstractions/Broadcast/NullCacheChangeToken.cs b/src/UiPath.Caching.Abstractions/Broadcast/NullCacheChangeToken.cs
index c9f5c9e6..0f9b140a 100644
--- a/src/UiPath.Caching.Abstractions/Broadcast/NullCacheChangeToken.cs
+++ b/src/UiPath.Caching.Abstractions/Broadcast/NullCacheChangeToken.cs
@@ -4,11 +4,11 @@ namespace UiPath.Caching.Broadcast;
[ExcludeFromCodeCoverage]
public class NullCacheChangeToken : ICacheChangeToken
{
- public static NullCacheChangeToken Instance { get; } = new NullCacheChangeToken();
private NullCacheChangeToken()
{
}
+ public static NullCacheChangeToken Instance { get; } = new NullCacheChangeToken();
public bool HasChanged => false;
diff --git a/src/UiPath.Caching.Abstractions/Broadcast/NullCacheEventFactory.cs b/src/UiPath.Caching.Abstractions/Broadcast/NullCacheEventFactory.cs
index 41a6cd16..2460cce0 100644
--- a/src/UiPath.Caching.Abstractions/Broadcast/NullCacheEventFactory.cs
+++ b/src/UiPath.Caching.Abstractions/Broadcast/NullCacheEventFactory.cs
@@ -5,12 +5,12 @@ public sealed class NullCacheEventFactory : ICacheEventFactory
{
private static ICacheEvent NullEvent = new NullCacheEvent();
- public static NullCacheEventFactory Instance { get; } = new NullCacheEventFactory();
-
private NullCacheEventFactory()
{
}
+ public static NullCacheEventFactory Instance { get; } = new NullCacheEventFactory();
+
public ICacheEvent Create(string cacheName, string eventType, CacheEventData eventData, string? id = null) =>
NullEvent;
diff --git a/src/UiPath.Caching.Abstractions/Broadcast/TopicKey.cs b/src/UiPath.Caching.Abstractions/Broadcast/TopicKey.cs
index e1720c08..3b08eb9c 100644
--- a/src/UiPath.Caching.Abstractions/Broadcast/TopicKey.cs
+++ b/src/UiPath.Caching.Abstractions/Broadcast/TopicKey.cs
@@ -10,29 +10,22 @@ public TopicKey()
public TopicKey(string? name) =>
Name = name?.Trim().ToLowerInvariant() ?? string.Empty;
- public string Name { get; }
-
- public override bool Equals(object? obj) =>
- obj is TopicKey topicKey && Equals(topicKey);
+ public static TopicKey Null { get; } = new TopicKey(null);
- public bool Equals(TopicKey other) =>
- string.Equals(Name, other.Name, StringComparison.InvariantCultureIgnoreCase);
+ public string Name { get; }
public bool IsNull => string.IsNullOrEmpty(Name);
-
- public override string ToString() =>
- Name;
-
- public override int GetHashCode() =>
- HashCode.Combine(Name, IsNull);
-
public static implicit operator string(TopicKey topicKey) =>
topicKey.Name;
public static implicit operator TopicKey(string? value)
{
- if (value == null) return default;
+ if (value == null)
+ {
+ return default;
+ }
+
return new TopicKey(value);
}
@@ -42,5 +35,16 @@ public static implicit operator TopicKey(string? value)
public static bool operator !=(TopicKey left, TopicKey right) =>
!(left == right);
- public static TopicKey Null { get; } = new TopicKey(null);
+ public override bool Equals(object? obj) =>
+ obj is TopicKey topicKey && Equals(topicKey);
+
+ public bool Equals(TopicKey other) =>
+ string.Equals(Name, other.Name, StringComparison.InvariantCultureIgnoreCase);
+
+
+ public override string ToString() =>
+ Name;
+
+ public override int GetHashCode() =>
+ HashCode.Combine(Name, IsNull);
}
diff --git a/src/UiPath.Caching.Abstractions/CacheKey.cs b/src/UiPath.Caching.Abstractions/CacheKey.cs
index 32839bf8..c84d7ecf 100644
--- a/src/UiPath.Caching.Abstractions/CacheKey.cs
+++ b/src/UiPath.Caching.Abstractions/CacheKey.cs
@@ -6,19 +6,6 @@ namespace UiPath.Caching;
{
private static CacheKeyCasing _defaultCasing = CacheKeyCasing.Insensitive;
- ///
- /// Process-global casing for keys built without an explicit mode; seeded from CacheOptions.KeyCasing.
- /// Set only at startup. Rejects a value outside the enum on assignment rather than at the next key built,
- /// since this is global state and the throw would otherwise surface far from the assignment that caused it.
- ///
- public static CacheKeyCasing DefaultCasing
- {
- get => _defaultCasing;
- set => _defaultCasing = value is CacheKeyCasing.Insensitive or CacheKeyCasing.Sensitive
- ? value
- : throw new ArgumentOutOfRangeException(nameof(value), value, $"Unsupported {nameof(CacheKeyCasing)} value.");
- }
-
public CacheKey()
: this(string.Empty)
{
@@ -45,34 +32,38 @@ public CacheKey(string? name, CacheKeyCasing casing)
};
}
+ ///
+ /// Process-global casing for keys built without an explicit mode; seeded from CacheOptions.KeyCasing.
+ /// Set only at startup. Rejects a value outside the enum on assignment rather than at the next key built,
+ /// since this is global state and the throw would otherwise surface far from the assignment that caused it.
+ ///
+ public static CacheKeyCasing DefaultCasing
+ {
+ get => _defaultCasing;
+ set => _defaultCasing = value is CacheKeyCasing.Insensitive or CacheKeyCasing.Sensitive
+ ? value
+ : throw new ArgumentOutOfRangeException(nameof(value), value, $"Unsupported {nameof(CacheKeyCasing)} value.");
+ }
+
+ public static CacheKey Null { get; } = new CacheKey(null);
+
public string Name { get; }
/// Normalization mode this key was built with; not part of equality.
public CacheKeyCasing Casing { get; }
- /// New key from , preserving this key's casing mode.
- public CacheKey WithName(string? name) => new(name, Casing);
-
- public override bool Equals(object? obj) =>
- obj is CacheKey cacheKey && Equals(cacheKey);
-
- public bool Equals(CacheKey other) =>
- string.Equals(Name, other.Name, StringComparison.Ordinal);
-
public bool IsNull => string.IsNullOrEmpty(Name);
- public override string ToString() =>
- Name;
-
- public override int GetHashCode() =>
- HashCode.Combine(Name, IsNull);
-
public static implicit operator string(CacheKey cacheKey) =>
cacheKey.Name;
public static implicit operator CacheKey(string? cacheKey)
{
- if (cacheKey == null) return default;
+ if (cacheKey == null)
+ {
+ return default;
+ }
+
return new CacheKey(cacheKey);
}
@@ -91,5 +82,18 @@ public static implicit operator CacheKey(Guid value) =>
public static bool operator !=(CacheKey left, CacheKey right) =>
!(left == right);
- public static CacheKey Null { get; } = new CacheKey(null);
+ /// New key from , preserving this key's casing mode.
+ public CacheKey WithName(string? name) => new(name, Casing);
+
+ public override bool Equals(object? obj) =>
+ obj is CacheKey cacheKey && Equals(cacheKey);
+
+ public bool Equals(CacheKey other) =>
+ string.Equals(Name, other.Name, StringComparison.Ordinal);
+
+ public override string ToString() =>
+ Name;
+
+ public override int GetHashCode() =>
+ HashCode.Combine(Name, IsNull);
}
diff --git a/src/UiPath.Caching.Abstractions/CacheOfT.cs b/src/UiPath.Caching.Abstractions/CacheOfT.cs
index 879c6b39..13938b1b 100644
--- a/src/UiPath.Caching.Abstractions/CacheOfT.cs
+++ b/src/UiPath.Caching.Abstractions/CacheOfT.cs
@@ -64,13 +64,6 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token
where TState : notnull =>
_cache.GetOrAddAsync(MapKeys(entries), generator, expiration, Policy, token);
- /// Applies the key strategy to each entry's key and leaves its state alone.
- private KeyValuePair[] MapKeys(KeyValuePair[] entries)
- {
- ArgumentNullException.ThrowIfNull(entries);
- return Array.ConvertAll(entries, e => new KeyValuePair(GetCacheKey(e.Key), e.Value));
- }
-
public ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default) =>
_cache.RefreshAsync(GetCacheKey(cacheKey), policy: Policy, token: token);
@@ -95,6 +88,15 @@ public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration
public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default) =>
_cache.SetAsync(GetCacheKey(cacheKey), value, expiration, Policy, token);
+ public ValueTask SetAsync(KeyValuePair[] keyValues, CancellationToken token = default) =>
+ _cache.SetAsync(GetKeyValuePairs(keyValues), policy: Policy, token: token);
+
+ public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CancellationToken token = default) =>
+ _cache.SetAsync(GetKeyValuePairs(keyValues), expiration, Policy, token);
+
+ public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CancellationToken token = default) =>
+ _cache.SetAsync(GetKeyValuePairs(keyValues), expiration, Policy, token);
+
public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default) =>
_cache.TryAddAsync(GetCacheKey(cacheKey), value, policy: Policy, token: token);
@@ -105,21 +107,19 @@ public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset e
_cache.TryAddAsync(GetCacheKey(cacheKey), value, expiration, Policy, token);
- public ValueTask SetAsync(KeyValuePair[] keyValues, CancellationToken token = default) =>
- _cache.SetAsync(GetKeyValuePairs(keyValues), policy: Policy, token: token);
-
- public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CancellationToken token = default) =>
- _cache.SetAsync(GetKeyValuePairs(keyValues), expiration, Policy, token);
-
- public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CancellationToken token = default) =>
- _cache.SetAsync(GetKeyValuePairs(keyValues), expiration, Policy, token);
-
public ValueTask TimeToLiveAsync(CacheKey cacheKey, CancellationToken token = default) =>
_cache.TimeToLiveAsync(GetCacheKey(cacheKey), token);
public ValueTask ExpireTimeAsync(CacheKey cacheKey, CancellationToken token = default) =>
_cache.ExpireTimeAsync(GetCacheKey(cacheKey), token);
+ /// Applies the key strategy to each entry's key and leaves its state alone.
+ private KeyValuePair[] MapKeys(KeyValuePair[] entries)
+ {
+ ArgumentNullException.ThrowIfNull(entries);
+ return Array.ConvertAll(entries, e => new KeyValuePair(GetCacheKey(e.Key), e.Value));
+ }
+
private CacheKey GetCacheKey(CacheKey cacheKey) =>
_cacheKeyStrategy.GetCacheKey(cacheKey);
diff --git a/src/UiPath.Caching.Abstractions/Config/ICachePolicyFactory.cs b/src/UiPath.Caching.Abstractions/Config/ICachePolicyFactory.cs
index 0dc6cdb4..482c5918 100644
--- a/src/UiPath.Caching.Abstractions/Config/ICachePolicyFactory.cs
+++ b/src/UiPath.Caching.Abstractions/Config/ICachePolicyFactory.cs
@@ -13,11 +13,6 @@ namespace UiPath.Caching;
///
public interface ICachePolicyFactory
{
- ///
- /// Resolves the named cache policy. Returns null when no specific policy is registered
- /// for .
- ///
- CachePolicy? Resolve(string policyName);
///
/// The user-configured default policy. Returns null when no default is configured —
@@ -32,4 +27,9 @@ public interface ICachePolicyFactory
/// statically may return an empty sequence and opt out of validation.
///
IEnumerable Keys { get; }
+ ///
+ /// Resolves the named cache policy. Returns null when no specific policy is registered
+ /// for .
+ ///
+ CachePolicy? Resolve(string policyName);
}
diff --git a/src/UiPath.Caching.Abstractions/Config/NullCachePolicyFactory.cs b/src/UiPath.Caching.Abstractions/Config/NullCachePolicyFactory.cs
index 2fcd8de1..a4d0c9cf 100644
--- a/src/UiPath.Caching.Abstractions/Config/NullCachePolicyFactory.cs
+++ b/src/UiPath.Caching.Abstractions/Config/NullCachePolicyFactory.cs
@@ -7,9 +7,9 @@ public sealed class NullCachePolicyFactory : ICachePolicyFactory
private NullCachePolicyFactory() { }
- public CachePolicy? Resolve(string policyName) => default;
-
public CachePolicy? Default => null;
public IEnumerable Keys => Array.Empty();
+
+ public CachePolicy? Resolve(string policyName) => default;
}
diff --git a/src/UiPath.Caching.Abstractions/HashCacheSetOption.cs b/src/UiPath.Caching.Abstractions/HashCacheSetOption.cs
index 4471855a..bf3d50a9 100644
--- a/src/UiPath.Caching.Abstractions/HashCacheSetOption.cs
+++ b/src/UiPath.Caching.Abstractions/HashCacheSetOption.cs
@@ -9,5 +9,5 @@ public enum HashCacheSetOption
///
/// Option to remove the entire specified hash key and set the specified fields to their respective values in the hash stored at key.
///
- KeyReplace
+ KeyReplace,
}
diff --git a/src/UiPath.Caching.Abstractions/ICacheChangeToken.cs b/src/UiPath.Caching.Abstractions/ICacheChangeToken.cs
index 57858113..e10ac222 100644
--- a/src/UiPath.Caching.Abstractions/ICacheChangeToken.cs
+++ b/src/UiPath.Caching.Abstractions/ICacheChangeToken.cs
@@ -4,11 +4,11 @@ namespace UiPath.Caching;
public interface ICacheChangeToken : IChangeToken
{
+
+ IDictionary? Metadata { get; }
bool MetadataHasChanged { get; }
DateTimeOffset? Expiration { get; }
string? TransportId { get; }
-
- public IDictionary? Metadata { get; }
}
diff --git a/src/UiPath.Caching.Abstractions/ICacheEntry.cs b/src/UiPath.Caching.Abstractions/ICacheEntry.cs
index 5d6b0372..38acdcb7 100644
--- a/src/UiPath.Caching.Abstractions/ICacheEntry.cs
+++ b/src/UiPath.Caching.Abstractions/ICacheEntry.cs
@@ -7,8 +7,6 @@ public interface ICacheEntry
IDictionary? Metadata { get; }
- ICacheEntry NewEntry(DateTimeOffset? expiration = null, IDictionary? metadata = null);
-
object? Value { get; }
///
@@ -17,4 +15,6 @@ public interface ICacheEntry
/// if your implementation populates a real expiration on miss.
///
bool Found => Expiration > DateTimeOffset.MinValue;
+
+ ICacheEntry NewEntry(DateTimeOffset? expiration = null, IDictionary? metadata = null);
}
diff --git a/src/UiPath.Caching.Abstractions/NullCache.cs b/src/UiPath.Caching.Abstractions/NullCache.cs
index b1fbf442..95ecde42 100644
--- a/src/UiPath.Caching.Abstractions/NullCache.cs
+++ b/src/UiPath.Caching.Abstractions/NullCache.cs
@@ -96,6 +96,11 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok
return ValueTask.FromResult(default(TimeSpan?));
}
+ public void Dispose()
+ {
+ // Nothing to dispose
+ }
+
private static ValueTask ReturnTrueAsync()
{
NotCacheableException.ThrowIfNotCacheable();
@@ -108,11 +113,6 @@ private static ValueTask ReturnTrueAsync()
return await generator(token).ConfigureAwait(false);
}
- public void Dispose()
- {
- // Nothing to dispose
- }
-
private sealed record NullCacheEntry : ICacheEntry
{
[SuppressMessage("SonarLint.Rule", "S3218:Inner class members should not shadow outer class names")]
@@ -120,12 +120,12 @@ private sealed record NullCacheEntry : ICacheEntry
public T? Value => default;
- object? ICacheEntry.Value => default;
-
public DateTimeOffset Expiration => DateTimeOffset.MinValue;
public IDictionary? Metadata => default;
+ object? ICacheEntry.Value => default;
+
public ICacheEntry NewEntry(DateTimeOffset? expiration = null, IDictionary? metadata = null) =>
NullCacheEntry.Instance;
}
diff --git a/src/UiPath.Caching.Abstractions/NullHashCache.cs b/src/UiPath.Caching.Abstractions/NullHashCache.cs
index 7f3585c4..3accb21a 100644
--- a/src/UiPath.Caching.Abstractions/NullHashCache.cs
+++ b/src/UiPath.Caching.Abstractions/NullHashCache.cs
@@ -113,12 +113,12 @@ private sealed record NullCacheEntry : ICacheEntry
public T? Value => default;
- object? ICacheEntry.Value => default;
-
public DateTimeOffset Expiration => DateTimeOffset.MinValue;
public IDictionary? Metadata => default;
+ object? ICacheEntry.Value => default;
+
public ICacheEntry NewEntry(DateTimeOffset? expiration = null, IDictionary? metadata = null) =>
NullCacheEntry.Instance;
}
diff --git a/src/UiPath.Caching.Abstractions/Telemetry/ICachingTelemetryProvider.cs b/src/UiPath.Caching.Abstractions/Telemetry/ICachingTelemetryProvider.cs
index 17e423a5..edce077f 100644
--- a/src/UiPath.Caching.Abstractions/Telemetry/ICachingTelemetryProvider.cs
+++ b/src/UiPath.Caching.Abstractions/Telemetry/ICachingTelemetryProvider.cs
@@ -2,7 +2,7 @@ namespace UiPath.Caching.Telemetry;
public interface ICachingTelemetryProvider
{
- public ITelemetryOperation StartOperation(string providerName, Type cacheObject, string methodName = "")
+ ITelemetryOperation StartOperation(string providerName, Type cacheObject, string methodName = "")
{
var ret = new TelemetryOperation(providerName, methodName, cacheObject, this);
ret.Start();
diff --git a/src/UiPath.Caching.Abstractions/Telemetry/TelemetryOperation.cs b/src/UiPath.Caching.Abstractions/Telemetry/TelemetryOperation.cs
index 13493796..11e9c701 100644
--- a/src/UiPath.Caching.Abstractions/Telemetry/TelemetryOperation.cs
+++ b/src/UiPath.Caching.Abstractions/Telemetry/TelemetryOperation.cs
@@ -5,9 +5,6 @@ namespace UiPath.Caching.Telemetry;
public sealed class TelemetryOperation(string providerName, string callerMethod, Type cacheObjectType, ICachingTelemetryProvider telemetryProvider) : ITelemetryOperation
{
- private const string Prefix = "Caching.Stats.";
- private const string Hits = Prefix + "Hits.";
- private const string Misses = Prefix + "Misses.";
public const string DependencyType = "Redis";
public const string OutcomeTag = "Outcome";
@@ -16,6 +13,9 @@ public sealed class TelemetryOperation(string providerName, string callerMethod,
public const string TypeTag = "Type";
public const string KeysTag = "Keys";
public const string BatchIdTag = "BatchId";
+ private const string Prefix = "Caching.Stats.";
+ private const string Hits = Prefix + "Hits.";
+ private const string Misses = Prefix + "Misses.";
private const string HitOutcome = "Hit";
private const string MissOutcome = "Miss";
diff --git a/src/UiPath.Caching.Azure/AzureEntraConnectionConfigurator.cs b/src/UiPath.Caching.Azure/AzureEntraConnectionConfigurator.cs
index 8b83b987..b4138b5e 100644
--- a/src/UiPath.Caching.Azure/AzureEntraConnectionConfigurator.cs
+++ b/src/UiPath.Caching.Azure/AzureEntraConnectionConfigurator.cs
@@ -36,6 +36,11 @@ public async ValueTask ConfigureAsync(ConfigurationOptions configuration, Cancel
await ApplyAzureAuthenticationAsync(configuration, _credential.Value).ConfigureAwait(false);
}
+ /// Applies Entra authentication via Microsoft.Azure.StackExchangeRedis. Virtual for testability.
+ [ExcludeFromCodeCoverage(Justification = "Acquires a live Entra token via Microsoft.Azure.StackExchangeRedis — needs Azure to exercise.")]
+ protected virtual Task ApplyAzureAuthenticationAsync(ConfigurationOptions configuration, TokenCredential credential) =>
+ configuration.ConfigureForAzureWithTokenCredentialAsync(credential);
+
private static TokenCredential CreateCredential(AzureEntraOptions options, IAzureEntraCredentialFactory credentialFactory)
{
if (options.Credential is not null)
@@ -52,9 +57,4 @@ private static TokenCredential CreateCredential(AzureEntraOptions options, IAzur
? credentialFactory.CreateDefaultCredential()
: credentialFactory.CreateManagedIdentityCredential(options.ManagedIdentityClientId);
}
-
- /// Applies Entra authentication via Microsoft.Azure.StackExchangeRedis. Virtual for testability.
- [ExcludeFromCodeCoverage(Justification = "Acquires a live Entra token via Microsoft.Azure.StackExchangeRedis — needs Azure to exercise.")]
- protected virtual Task ApplyAzureAuthenticationAsync(ConfigurationOptions configuration, TokenCredential credential) =>
- configuration.ConfigureForAzureWithTokenCredentialAsync(credential);
}
diff --git a/src/UiPath.Caching.Azure/AzureEntraCredentialFactory.cs b/src/UiPath.Caching.Azure/AzureEntraCredentialFactory.cs
index d693bbff..a1f1bbb0 100644
--- a/src/UiPath.Caching.Azure/AzureEntraCredentialFactory.cs
+++ b/src/UiPath.Caching.Azure/AzureEntraCredentialFactory.cs
@@ -2,11 +2,11 @@ namespace UiPath.Caching.Azure;
internal sealed class AzureEntraCredentialFactory : IAzureEntraCredentialFactory
{
- public static AzureEntraCredentialFactory Instance { get; } = new();
private AzureEntraCredentialFactory()
{
}
+ public static AzureEntraCredentialFactory Instance { get; } = new();
public TokenCredential CreateDefaultCredential() => new DefaultAzureCredential();
diff --git a/src/UiPath.Caching.CloudEvents/CacheCloudEventWrapper.cs b/src/UiPath.Caching.CloudEvents/CacheCloudEventWrapper.cs
index 6660eda5..78cb556c 100644
--- a/src/UiPath.Caching.CloudEvents/CacheCloudEventWrapper.cs
+++ b/src/UiPath.Caching.CloudEvents/CacheCloudEventWrapper.cs
@@ -7,9 +7,6 @@ public CacheCloudEventWrapper(CloudEvent cloudEvent)
CloudEvent = cloudEvent;
Data = CloudEvent.Data as CacheEventData;
}
- internal CloudEvent CloudEvent { get; }
-
- public bool IsValid() => CloudEvent.IsValid && !string.IsNullOrWhiteSpace(Data?.Key);
public string? Id => CloudEvent.Id;
@@ -22,6 +19,9 @@ public CacheCloudEventWrapper(CloudEvent cloudEvent)
public string? TransportId { get; private set; }
public string? Key => Data?.Key;
+ internal CloudEvent CloudEvent { get; }
+
+ public bool IsValid() => CloudEvent.IsValid && !string.IsNullOrWhiteSpace(Data?.Key);
public void AttachTransportId(string? transportId)
{
diff --git a/src/UiPath.Caching.CloudEvents/CloudCacheEventFactory.cs b/src/UiPath.Caching.CloudEvents/CloudCacheEventFactory.cs
index 8b24acf8..b43512a2 100644
--- a/src/UiPath.Caching.CloudEvents/CloudCacheEventFactory.cs
+++ b/src/UiPath.Caching.CloudEvents/CloudCacheEventFactory.cs
@@ -27,7 +27,7 @@ public ICacheEvent Create(string cacheName, string eventType, CacheEventData eve
Type = eventType.Trim(),
Source = _sourceUri,
DataContentType = MediaTypeNames.Application.Json,
- Data = eventData
+ Data = eventData,
});
}
diff --git a/src/UiPath.Caching.Polly/GlobalUsings.cs b/src/UiPath.Caching.Polly/GlobalUsings.cs
index c07fa929..27ac8fae 100644
--- a/src/UiPath.Caching.Polly/GlobalUsings.cs
+++ b/src/UiPath.Caching.Polly/GlobalUsings.cs
@@ -1,10 +1,10 @@
-global using System.Diagnostics.CodeAnalysis;
+global using System.Diagnostics.CodeAnalysis;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection.Extensions;
global using Microsoft.Extensions.Logging;
-global using Microsoft.Extensions.Options;
global using Microsoft.Extensions.Logging.Abstractions;
+global using Microsoft.Extensions.Options;
global using Polly;
global using Polly.Timeout;
global using UiPath.Caching.Config;
diff --git a/src/UiPath.Caching.Polly/ResiliencePipelineFactory.cs b/src/UiPath.Caching.Polly/ResiliencePipelineFactory.cs
index 58c385a0..8bb2f200 100644
--- a/src/UiPath.Caching.Polly/ResiliencePipelineFactory.cs
+++ b/src/UiPath.Caching.Polly/ResiliencePipelineFactory.cs
@@ -42,7 +42,7 @@ protected virtual ResiliencePipelineBuilder GetBuilder(ILogger
{
logger.LogWarning("OnFallback. Operation key {OperationKey}", args.Context.OperationKey);
return default;
- }
+ },
});
}
@@ -67,7 +67,7 @@ protected virtual ResiliencePipelineBuilder GetBuilder(ILogger
{
logger.LogWarning("CircuitBreaker OnOpened. Operation key {OperationKey}. Breaking the circuit for {DurationOfBreak}!", args.Context.OperationKey, resilienceOptions.DurationOfBreak);
return default;
- }
+ },
});
}
@@ -83,7 +83,7 @@ protected virtual ResiliencePipelineBuilder GetBuilder(ILogger
{
logger.LogWarning("OnRetry, Attempt: {AttemptNumber}. Operation key {OperationKey}", args.AttemptNumber, args.Context.OperationKey);
return default;
- }
+ },
});
}
@@ -96,7 +96,7 @@ protected virtual ResiliencePipelineBuilder GetBuilder(ILogger
{
logger.LogWarning("Execution timed out after {TotalMilliseconds} ms. Operation key {OperationKey}", args.Timeout.TotalMilliseconds, args.Context.OperationKey);
return default;
- }
+ },
});
}
diff --git a/src/UiPath.Caching.Queue/InMemoryQueueCacheProvider.cs b/src/UiPath.Caching.Queue/InMemoryQueueCacheProvider.cs
index 1acf4c26..0b0a4373 100644
--- a/src/UiPath.Caching.Queue/InMemoryQueueCacheProvider.cs
+++ b/src/UiPath.Caching.Queue/InMemoryQueueCacheProvider.cs
@@ -18,10 +18,6 @@ public sealed class InMemoryQueueCacheProvider : IQueueCacheProvider
private readonly TimeProvider _clock;
private readonly Lazy _setCache;
- public string Name => KnownCacheProviderNames.InMemory;
-
- public bool Enabled { get; }
-
public InMemoryQueueCacheProvider(
IOptions optionsAccessor,
IMemoryCacheFactory memoryCacheFactory,
@@ -38,6 +34,10 @@ public InMemoryQueueCacheProvider(
Enabled = _options.Enabled;
}
+ public string Name => KnownCacheProviderNames.InMemory;
+
+ public bool Enabled { get; }
+
public ISetCache CreateSetCache() =>
_setCache.Value;
diff --git a/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheProvider.cs b/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheProvider.cs
index 57816fa9..293377c7 100644
--- a/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheProvider.cs
+++ b/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheProvider.cs
@@ -20,10 +20,6 @@ public sealed class InMemoryRedisQueueCacheProvider : IQueueCacheProvider
private readonly TimeProvider _clock;
private readonly Lazy _setCache;
- public string Name => KnownCacheProviderNames.InMemoryRedis;
-
- public bool Enabled { get; }
-
public InMemoryRedisQueueCacheProvider(
IOptions optionsAccessor,
IMemoryCacheFactory memoryCacheFactory,
@@ -42,6 +38,10 @@ public InMemoryRedisQueueCacheProvider(
Enabled = _options.Enabled;
}
+ public string Name => KnownCacheProviderNames.InMemoryRedis;
+
+ public bool Enabled { get; }
+
public ISetCache CreateSetCache() =>
_setCache.Value;
diff --git a/src/UiPath.Caching.Queue/MemorySetCache.cs b/src/UiPath.Caching.Queue/MemorySetCache.cs
index c33e3e43..05471bf6 100644
--- a/src/UiPath.Caching.Queue/MemorySetCache.cs
+++ b/src/UiPath.Caching.Queue/MemorySetCache.cs
@@ -1,4 +1,4 @@
-using System.Collections.Immutable;
+using System.Collections.Immutable;
using UiPath.Caching.Locking;
namespace UiPath.Caching;
@@ -11,21 +11,11 @@ internal sealed class MemorySetCache(
IMemoryCacheOptions memoryCacheOptions,
TimeProvider clock)
{
- private readonly bool _trackSize = memoryCacheOptions.SizeLimit.HasValue;
- private readonly string _localLockKeyPrefix = cacheName + ":";
private static readonly ImmutableHashSet EmptyMembers =
ImmutableHashSet.Create(ByteArrayEqualityComparer.Instance);
-
- private sealed record Snapshot(ImmutableHashSet Members, DateTimeOffset? Expiration);
-
- ///
- /// A passthrough serializer such as hands back the caller's
- /// own array. The snapshot hashes its members, so a caller mutating that array afterwards would
- /// change an element's hash while it sits in the set. Only the paths that store need this; the
- /// lookup paths may compare against the caller's array directly.
- ///
- private static byte[] Owned(byte[] value) => value.Length == 0 ? value : (byte[])value.Clone();
+ private readonly bool _trackSize = memoryCacheOptions.SizeLimit.HasValue;
+ private readonly string _localLockKeyPrefix = cacheName + ":";
public bool TryGetMembers(string key, [NotNullWhen(true)] out IReadOnlyCollection? members)
{
@@ -168,6 +158,14 @@ public async ValueTask RemoveKeyAsync(string key, CancellationToken token)
}
}
+ ///
+ /// A passthrough serializer such as hands back the caller's
+ /// own array. The snapshot hashes its members, so a caller mutating that array afterwards would
+ /// change an element's hash while it sits in the set. Only the paths that store need this; the
+ /// lookup paths may compare against the caller's array directly.
+ ///
+ private static byte[] Owned(byte[] value) => value.Length == 0 ? value : (byte[])value.Clone();
+
private bool TryGetSnapshot(string key, [NotNullWhen(true)] out Snapshot? snapshot) =>
memoryCache.TryGetValue(key, out snapshot) && snapshot is not null;
@@ -207,4 +205,6 @@ private void StoreSnapshot(string key, Snapshot snapshot)
}
return list;
}
+
+ private sealed record Snapshot(ImmutableHashSet Members, DateTimeOffset? Expiration);
}
diff --git a/src/UiPath.Caching.Queue/MultilayerSetCache.cs b/src/UiPath.Caching.Queue/MultilayerSetCache.cs
index 8fa712ef..f408e7da 100644
--- a/src/UiPath.Caching.Queue/MultilayerSetCache.cs
+++ b/src/UiPath.Caching.Queue/MultilayerSetCache.cs
@@ -42,11 +42,6 @@ public MultilayerSetCache(
_defaultExpiration = options.DefaultExpiration;
}
- private static IConnectionState GetConnectionMonitor(ISetCache inner, TimeSpan? period) =>
- inner is IConnectionState state
- ? new ConnectionStateMonitor(NullTelemetryProvider.Instance, period ?? TimeSpan.FromSeconds(5), state)
- : NullConnectionStateMonitor.Instance;
-
public string Name => _name;
public async ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? policy, CancellationToken token = default)
@@ -64,12 +59,6 @@ public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, Time
public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) =>
AddCoreAsync(cacheKey, items, CacheExpiration.ThrowIfNotFuture(expiration, _clock.GetUtcNow()), policy, token);
- private async ValueTask AddCoreAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token)
- {
- NotCacheableException.ThrowIfNotCacheable();
- return await InternalAddAsync(cacheKey, Materialize(items), expiration, policy, token).ConfigureAwait(false);
- }
-
public async ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default)
{
NotCacheableException.ThrowIfNotCacheable();
@@ -218,6 +207,33 @@ public void Dispose()
}
}
+ private static IConnectionState GetConnectionMonitor(ISetCache inner, TimeSpan? period) =>
+ inner is IConnectionState state
+ ? new ConnectionStateMonitor(NullTelemetryProvider.Instance, period ?? TimeSpan.FromSeconds(5), state)
+ : NullConnectionStateMonitor.Instance;
+
+ private static IEnumerable Materialize(IEnumerable items)
+ {
+ ArgumentNullException.ThrowIfNull(items);
+ return items as IReadOnlyCollection ?? items.ToArray();
+ }
+
+ private static string Key(CacheKey cacheKey, CancellationToken token)
+ {
+ if (cacheKey.IsNull)
+ {
+ throw new ArgumentNullException(nameof(cacheKey));
+ }
+ token.ThrowIfCancellationRequested();
+ return cacheKey.Name;
+ }
+
+ private async ValueTask AddCoreAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token)
+ {
+ NotCacheableException.ThrowIfNotCacheable();
+ return await InternalAddAsync(cacheKey, Materialize(items), expiration, policy, token).ConfigureAwait(false);
+ }
+
private bool GetInnerCacheDisconnected() => _inner is NullSetCache || (_useLocalOnlyWhenDisconnected && !_connectionState.IsConnected);
private DateTimeOffset? LocalWriteExpiration(DateTimeOffset? requested, CachePolicy? policy)
@@ -276,22 +292,6 @@ _inner is not NullSetCache && policy?.DistributedExpiration is null && _defaultE
private DateTimeOffset? FromTtl(TimeSpan? ttl) =>
ttl.HasValue ? _clock.ToDateTimeOffset(ttl.Value) : null;
- private static IEnumerable Materialize(IEnumerable items)
- {
- ArgumentNullException.ThrowIfNull(items);
- return items as IReadOnlyCollection ?? items.ToArray();
- }
-
private DateTimeOffset? LocalExpiration() =>
_localMaxExpiration.HasValue ? _clock.ToDateTimeOffset(_localMaxExpiration.Value) : null;
-
- private static string Key(CacheKey cacheKey, CancellationToken token)
- {
- if (cacheKey.IsNull)
- {
- throw new ArgumentNullException(nameof(cacheKey));
- }
- token.ThrowIfCancellationRequested();
- return cacheKey.Name;
- }
}
diff --git a/src/UiPath.Caching.Queue/RedisQueueCacheProvider.cs b/src/UiPath.Caching.Queue/RedisQueueCacheProvider.cs
index bb5379c4..05ab7c11 100644
--- a/src/UiPath.Caching.Queue/RedisQueueCacheProvider.cs
+++ b/src/UiPath.Caching.Queue/RedisQueueCacheProvider.cs
@@ -18,10 +18,6 @@ public sealed class RedisQueueCacheProvider : IQueueCacheProvider
private readonly TimeProvider _clock;
private readonly Lazy _setCache;
- public string Name => KnownCacheProviderNames.Redis;
-
- public bool Enabled { get; }
-
public RedisQueueCacheProvider(
IOptions redisCacheOptions,
IOptions cacheOptions,
@@ -48,6 +44,10 @@ public RedisQueueCacheProvider(
Enabled = _setCacheOptions.Enabled;
}
+ public string Name => KnownCacheProviderNames.Redis;
+
+ public bool Enabled { get; }
+
public ISetCache CreateSetCache() =>
_setCache.Value;
diff --git a/src/UiPath.Caching.Queue/RedisSetCache.cs b/src/UiPath.Caching.Queue/RedisSetCache.cs
index 1cdf13a3..fa716a07 100644
--- a/src/UiPath.Caching.Queue/RedisSetCache.cs
+++ b/src/UiPath.Caching.Queue/RedisSetCache.cs
@@ -56,70 +56,6 @@ public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, Time
public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) =>
AddCoreAsync(cacheKey, items, GetExpiration(expiration), token);
- private ValueTask AddCoreAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CancellationToken token)
- {
- NotCacheableException.ThrowIfNotCacheable();
- ArgumentNullException.ThrowIfNull(items);
- var values = items.Select(i => (RedisValue)_serializer.Serialize(i)).ToArray();
- return AddManyInnerAsync(cacheKey, values, expiration, token);
- }
-
- private async ValueTask AddManyInnerAsync(CacheKey cacheKey, RedisValue[] values, DateTimeOffset expiration, CancellationToken token)
- {
- var redisKey = ToRedisKey(cacheKey, token);
- var now = Clock.GetUtcNow();
- long ret = 0;
- if (!IsConnected || values.Length == 0)
- {
- return ret;
- }
-
- if (expiration < now)
- {
- _ = await _write.ExecuteAsync(async token =>
- {
- token.ThrowIfCancellationRequested();
- return await Database.KeyDeleteAsync(redisKey, CommandFlags.DemandMaster).ConfigureAwait(false);
- }, default, token).ConfigureAwait(false);
- return ret;
- }
-
- var operation = StartOperation(nameof(AddAsync));
- try
- {
- var transaction = Database.CreateTransaction();
- var addTask = transaction.SetAddAsync(redisKey, values, CommandFlags.DemandMaster);
- QueueExpirationUpdate(transaction, redisKey, expiration);
-
- var committed = await _write.ExecuteAsync(async token =>
- {
- token.ThrowIfCancellationRequested();
- return await transaction.ExecuteAsync(CommandFlags.DemandMaster).ConfigureAwait(false);
- }, default, token).ConfigureAwait(false);
-
- if (committed)
- {
- ret = await addTask.ConfigureAwait(false);
- }
- else
- {
- LogRedisTransactionFailed();
- }
- operation.Stop();
- }
- catch (Exception ex)
- {
- operation.Stop();
- LogRedisSetCacheException(ex);
- }
- finally
- {
- operation.Track(ret > 0);
- }
-
- return ret;
- }
-
public async ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default)
{
NotCacheableException.ThrowIfNotCacheable();
@@ -138,7 +74,9 @@ private async ValueTask AddManyInnerAsync(CacheKey cacheKey, RedisValue
{
token.ThrowIfCancellationRequested();
return await Database.SetPopAsync(redisKey, CommandFlags.DemandMaster).ConfigureAwait(false);
- }, RedisValue.Null, token).ConfigureAwait(false);
+ },
+ RedisValue.Null,
+ token).ConfigureAwait(false);
if (!value.IsNull)
{
ret = _serializer.Deserialize(value);
@@ -181,7 +119,9 @@ private async ValueTask AddManyInnerAsync(CacheKey cacheKey, RedisValue
{
token.ThrowIfCancellationRequested();
return await Database.SetPopAsync(redisKey, count, CommandFlags.DemandMaster).ConfigureAwait(false);
- }, Array.Empty(), token).ConfigureAwait(false);
+ },
+ Array.Empty(),
+ token).ConfigureAwait(false);
ret = Deserialize(values);
operation.Stop();
}
@@ -215,7 +155,9 @@ private async ValueTask AddManyInnerAsync(CacheKey cacheKey, RedisValue
{
token.ThrowIfCancellationRequested();
return await Database.SetMembersAsync(redisKey, CommandFlags.PreferReplica).ConfigureAwait(false);
- }, Array.Empty(), token).ConfigureAwait(false);
+ },
+ Array.Empty(),
+ token).ConfigureAwait(false);
ret = Deserialize(values);
operation.Stop();
}
@@ -245,7 +187,9 @@ public async ValueTask ContainsItemAsync(CacheKey cacheKey, T item, Can
{
token.ThrowIfCancellationRequested();
return await Database.SetContainsAsync(redisKey, value, CommandFlags.PreferReplica).ConfigureAwait(false);
- }, default, token).ConfigureAwait(false);
+ },
+ default,
+ token).ConfigureAwait(false);
operation.Stop();
}
catch (Exception ex)
@@ -273,7 +217,9 @@ public async ValueTask CountAsync(CacheKey cacheKey, CancellationToken
{
token.ThrowIfCancellationRequested();
return await Database.SetLengthAsync(redisKey, CommandFlags.PreferReplica).ConfigureAwait(false);
- }, default, token).ConfigureAwait(false);
+ },
+ default,
+ token).ConfigureAwait(false);
operation.Stop();
}
catch (Exception ex)
@@ -302,7 +248,9 @@ public async ValueTask RemoveItemAsync(CacheKey cacheKey, T item, Cance
{
token.ThrowIfCancellationRequested();
return await Database.SetRemoveAsync(redisKey, value, CommandFlags.DemandMaster).ConfigureAwait(false);
- }, default, token).ConfigureAwait(false);
+ },
+ default,
+ token).ConfigureAwait(false);
operation.Stop();
}
catch (Exception ex)
@@ -337,7 +285,9 @@ public async ValueTask RemoveItemsAsync(CacheKey cacheKey, IEnumerable<
{
token.ThrowIfCancellationRequested();
return await Database.SetRemoveAsync(redisKey, values, CommandFlags.DemandMaster).ConfigureAwait(false);
- }, default, token).ConfigureAwait(false);
+ },
+ default,
+ token).ConfigureAwait(false);
operation.Stop();
}
catch (Exception ex)
@@ -365,7 +315,9 @@ public async ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken
{
token.ThrowIfCancellationRequested();
return await Database.KeyDeleteAsync(redisKey, CommandFlags.DemandMaster).ConfigureAwait(false);
- }, default, token).ConfigureAwait(false);
+ },
+ default,
+ token).ConfigureAwait(false);
operation.Stop();
}
catch (Exception ex)
@@ -393,7 +345,9 @@ public async ValueTask ContainsAsync(CacheKey cacheKey, CancellationTok
{
token.ThrowIfCancellationRequested();
return await Database.KeyExistsAsync(redisKey, CommandFlags.PreferReplica).ConfigureAwait(false);
- }, default, token).ConfigureAwait(false);
+ },
+ default,
+ token).ConfigureAwait(false);
operation.Stop();
}
catch (Exception ex)
@@ -419,6 +373,74 @@ private static void QueueExpirationUpdate(ITransaction transaction, RedisKey red
_ = transaction.KeyPersistAsync(redisKey, CommandFlags.DemandMaster | CommandFlags.FireAndForget).ConfigureAwait(false);
}
+ private ValueTask AddCoreAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CancellationToken token)
+ {
+ NotCacheableException.ThrowIfNotCacheable();
+ ArgumentNullException.ThrowIfNull(items);
+ var values = items.Select(i => (RedisValue)_serializer.Serialize(i)).ToArray();
+ return AddManyInnerAsync(cacheKey, values, expiration, token);
+ }
+
+ private async ValueTask AddManyInnerAsync(CacheKey cacheKey, RedisValue[] values, DateTimeOffset expiration, CancellationToken token)
+ {
+ var redisKey = ToRedisKey(cacheKey, token);
+ var now = Clock.GetUtcNow();
+ long ret = 0;
+ if (!IsConnected || values.Length == 0)
+ {
+ return ret;
+ }
+
+ if (expiration < now)
+ {
+ _ = await _write.ExecuteAsync(async token =>
+ {
+ token.ThrowIfCancellationRequested();
+ return await Database.KeyDeleteAsync(redisKey, CommandFlags.DemandMaster).ConfigureAwait(false);
+ },
+ default,
+ token).ConfigureAwait(false);
+ return ret;
+ }
+
+ var operation = StartOperation(nameof(AddAsync));
+ try
+ {
+ var transaction = Database.CreateTransaction();
+ var addTask = transaction.SetAddAsync(redisKey, values, CommandFlags.DemandMaster);
+ QueueExpirationUpdate(transaction, redisKey, expiration);
+
+ var committed = await _write.ExecuteAsync(async token =>
+ {
+ token.ThrowIfCancellationRequested();
+ return await transaction.ExecuteAsync(CommandFlags.DemandMaster).ConfigureAwait(false);
+ },
+ default,
+ token).ConfigureAwait(false);
+
+ if (committed)
+ {
+ ret = await addTask.ConfigureAwait(false);
+ }
+ else
+ {
+ LogRedisTransactionFailed();
+ }
+ operation.Stop();
+ }
+ catch (Exception ex)
+ {
+ operation.Stop();
+ LogRedisSetCacheException(ex);
+ }
+ finally
+ {
+ operation.Track(ret > 0);
+ }
+
+ return ret;
+ }
+
private IReadOnlyCollection Deserialize(RedisValue[] values)
{
if (values is null || values.Length == 0)
diff --git a/src/UiPath.Caching/Broadcast/CacheEventFactory.cs b/src/UiPath.Caching/Broadcast/CacheEventFactory.cs
index a0dddf4f..cbe1f21a 100644
--- a/src/UiPath.Caching/Broadcast/CacheEventFactory.cs
+++ b/src/UiPath.Caching/Broadcast/CacheEventFactory.cs
@@ -25,7 +25,7 @@ public ICacheEvent Create(string cacheName, string eventType, CacheEventData eve
Id = id ?? Guid.NewGuid().ToString(),
Type = eventType.Trim(),
Source = _sourceUri,
- Data = eventData
+ Data = eventData,
};
}
diff --git a/src/UiPath.Caching/Broadcast/ChangeToken.cs b/src/UiPath.Caching/Broadcast/ChangeToken.cs
index f947d3d4..c11f300c 100644
--- a/src/UiPath.Caching/Broadcast/ChangeToken.cs
+++ b/src/UiPath.Caching/Broadcast/ChangeToken.cs
@@ -18,12 +18,6 @@ public sealed partial class ChangeToken : ICacheChangeToken, IKeyedObserver callback, object? state)> _callbacks = [];
- /// This token's own key: the caller's, rendered inside the composed key it subscribes with.
- private LoggedKey Logged() => LoggedKey.For(_masker, _callerKey, _key, _entryType);
-
- /// A key off the wire has no caller key to judge, so it is masked whole when masking is on.
- private LoggedKey LoggedForeign(string key) => LoggedKey.Composed(_masker, key, _entryType);
-
public ChangeToken(
string key,
ITopic topic,
@@ -66,8 +60,6 @@ internal ChangeToken(
public bool MetadataHasChanged { get; private set; }
- string IKeyedObserver.Key => _key;
-
public bool ActiveChangeCallbacks => true;
public DateTimeOffset? Expiration { get; private set; }
@@ -76,6 +68,8 @@ internal ChangeToken(
public string? TransportId { get; private set; }
+ string IKeyedObserver.Key => _key;
+
public void OnCompleted() =>
LogOnCompleted(Logged(), _topic);
@@ -111,6 +105,23 @@ public void OnNext(ICacheEvent cacheEvent)
}
}
+
+
+ public IDisposable RegisterChangeCallback(Action