diff --git a/src/QuerySpecification/Builders/Builder_Cache.cs b/src/QuerySpecification/Builders/Builder_Cache.cs
new file mode 100644
index 00000000..94297dc9
--- /dev/null
+++ b/src/QuerySpecification/Builders/Builder_Cache.cs
@@ -0,0 +1,75 @@
+namespace Pozitron.QuerySpecification;
+
+public static partial class SpecificationBuilderExtensions
+{
+ ///
+ /// Sets the cache key for the specification.
+ ///
+ /// The type of the entity.
+ /// The type of the result.
+ /// The specification builder.
+ /// The cache key to be used.
+ /// The updated specification builder.
+ public static ISpecificationBuilder WithCacheKey(
+ this ISpecificationBuilder builder,
+ string cacheKey) where T : class
+ {
+ WithCacheKey(builder, cacheKey, true);
+ return (SpecificationBuilder)builder;
+ }
+
+ ///
+ /// Sets the cache key for the specification.
+ ///
+ /// The type of the entity.
+ /// The type of the result.
+ /// The specification builder.
+ /// The cache key to be used.
+ /// The condition to evaluate.
+ /// The updated specification builder.
+ public static ISpecificationBuilder WithCacheKey(
+ this ISpecificationBuilder builder,
+ string cacheKey,
+ bool condition) where T : class
+ {
+ if (condition)
+ {
+ builder.Specification.AddOrUpdateInternal(ItemType.CacheKey, cacheKey);
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Sets the cache key for the specification.
+ ///
+ /// The type of the entity.
+ /// The specification builder.
+ /// The cache key to be used.
+ /// The updated specification builder.
+ public static ISpecificationBuilder WithCacheKey(
+ this ISpecificationBuilder builder,
+ string cacheKey) where T : class
+ => WithCacheKey(builder, cacheKey, true);
+
+ ///
+ /// Sets the cache key for the specification.
+ ///
+ /// The type of the entity.
+ /// The specification builder.
+ /// The cache key to be used.
+ /// The condition to evaluate.
+ /// The updated specification builder.
+ public static ISpecificationBuilder WithCacheKey(
+ this ISpecificationBuilder builder,
+ string cacheKey,
+ bool condition) where T : class
+ {
+ if (condition)
+ {
+ builder.Specification.AddOrUpdateInternal(ItemType.CacheKey, cacheKey);
+ }
+
+ return builder;
+ }
+}
diff --git a/src/QuerySpecification/Internals/ItemType.cs b/src/QuerySpecification/Internals/ItemType.cs
index 07345ac5..69c9fe16 100644
--- a/src/QuerySpecification/Internals/ItemType.cs
+++ b/src/QuerySpecification/Internals/ItemType.cs
@@ -15,4 +15,5 @@ internal static class ItemType
public const int Flags = -8; // Stored in the bag
public const int QueryTag = -9;
+ public const int CacheKey = -10;
}
diff --git a/src/QuerySpecification/Specification.cs b/src/QuerySpecification/Specification.cs
index 25db04a9..2a1e786a 100644
--- a/src/QuerySpecification/Specification.cs
+++ b/src/QuerySpecification/Specification.cs
@@ -183,6 +183,16 @@ public virtual bool IsSatisfiedBy(T entity)
? Enumerable.Empty()
: new SpecSelectIterator(_items, ItemType.QueryTag, (x, bag) => x);
+ ///
+ /// Return whether or not a cache key is set.
+ ///
+ public bool HasCacheKey => Contains(ItemType.CacheKey);
+
+ ///
+ /// Gets the cache key.
+ ///
+ public string? CacheKey => FirstOrDefault(ItemType.CacheKey);
+
///
/// Gets the number of items to take.
///
@@ -306,6 +316,20 @@ public IEnumerable OfType(int type) => _items is null
internal ReadOnlySpan Items => _items ?? ReadOnlySpan.Empty;
+ internal bool Contains(int type)
+ {
+ if (IsEmpty) return false;
+
+ foreach (var item in _items)
+ {
+ if (item.Type == type)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
[MemberNotNull(nameof(_items))]
internal void AddInternal(int type, object value, int bag = 0)
{
diff --git a/tests/QuerySpecification.Tests/Builders/Builder_WithCacheKey.cs b/tests/QuerySpecification.Tests/Builders/Builder_WithCacheKey.cs
new file mode 100644
index 00000000..cef83fcf
--- /dev/null
+++ b/tests/QuerySpecification.Tests/Builders/Builder_WithCacheKey.cs
@@ -0,0 +1,61 @@
+namespace Tests.Builders;
+
+public class Builder_WithCacheKey
+{
+ public record Customer(int Id, string Name);
+
+ [Fact]
+ public void DoesNothing_GivenNoWithCacheKey()
+ {
+ var spec1 = new Specification();
+ var spec2 = new Specification();
+
+ spec1.CacheKey.Should().BeNull();
+ spec1.HasCacheKey.Should().BeFalse();
+
+ spec2.CacheKey.Should().BeNull();
+ spec2.HasCacheKey.Should().BeFalse();
+ }
+
+ [Fact]
+ public void DoesNothing_GivenWithCacheKeyWithFalseCondition()
+ {
+ var key = "someKey";
+
+ var spec1 = new Specification();
+ spec1.Query
+ .Where(x=> x.Id > 0)
+ .WithCacheKey(key, false);
+
+ var spec2 = new Specification();
+ spec2.Query
+ .Where(x=> x.Id > 0)
+ .WithCacheKey(key, false);
+
+ spec1.CacheKey.Should().BeNull();
+ spec1.HasCacheKey.Should().BeFalse();
+
+ spec2.CacheKey.Should().BeNull();
+ spec2.HasCacheKey.Should().BeFalse();
+ }
+
+ [Fact]
+ public void SetsCacheKey_GivenWithCacheKey()
+ {
+ var key = "someKey";
+
+ var spec1 = new Specification();
+ spec1.Query
+ .WithCacheKey(key);
+
+ var spec2 = new Specification();
+ spec2.Query
+ .WithCacheKey(key);
+
+ spec1.CacheKey.Should().Be(key);
+ spec1.HasCacheKey.Should().BeTrue();
+
+ spec1.CacheKey.Should().Be(key);
+ spec2.HasCacheKey.Should().BeTrue();
+ }
+}