diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs
new file mode 100644
index 00000000..5066f881
--- /dev/null
+++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs
@@ -0,0 +1,28 @@
+namespace Pozitron.QuerySpecification;
+
+///
+/// Evaluator to apply the query tags to the query.
+///
+public sealed class QueryTagEvaluator : IEvaluator
+{
+
+ ///
+ /// Gets the singleton instance of the class.
+ ///
+ public static QueryTagEvaluator Instance = new();
+ private QueryTagEvaluator() { }
+
+ ///
+ public IQueryable Evaluate(IQueryable source, Specification specification) where T : class
+ {
+ foreach (var item in specification.Items)
+ {
+ if (item.Type == ItemType.QueryTag && item.Reference is string tag)
+ {
+ source = source.TagWith(tag);
+ }
+ }
+
+ return source;
+ }
+}
diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs
index 9b9590ae..aa3ab776 100644
--- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs
+++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs
@@ -33,6 +33,7 @@ public SpecificationEvaluator()
AsTrackingEvaluator.Instance,
AsSplitQueryEvaluator.Instance,
IgnoreAutoIncludesEvaluator.Instance,
+ QueryTagEvaluator.Instance,
];
}
diff --git a/src/QuerySpecification/Builders/Builder_TagWith.cs b/src/QuerySpecification/Builders/Builder_TagWith.cs
new file mode 100644
index 00000000..375598ba
--- /dev/null
+++ b/src/QuerySpecification/Builders/Builder_TagWith.cs
@@ -0,0 +1,75 @@
+namespace Pozitron.QuerySpecification;
+
+public static partial class SpecificationBuilderExtensions
+{
+ ///
+ /// Adds a query tag to the specification.
+ ///
+ /// The type of the entity.
+ /// The type of the result.
+ /// The specification builder.
+ /// The query tag.
+ /// The updated specification builder.
+ public static ISpecificationBuilder TagWith(
+ this ISpecificationBuilder builder,
+ string tag)
+ {
+ TagWith(builder, tag, true);
+ return builder;
+ }
+
+ ///
+ /// Adds a query tag to the specification if the condition is true.
+ ///
+ /// The type of the entity.
+ /// The type of the result.
+ /// The specification builder.
+ /// The query tag.
+ /// The condition to evaluate.
+ /// The updated specification builder.
+ public static ISpecificationBuilder TagWith(
+ this ISpecificationBuilder builder,
+ string tag,
+ bool condition)
+ {
+ if (condition)
+ {
+ builder.Specification.AddInternal(ItemType.QueryTag, tag);
+ }
+
+ return builder;
+ }
+
+ ///
+ /// Adds a query tag to the specification.
+ ///
+ /// The type of the entity.
+ /// The specification builder.
+ /// The query tag.
+ /// The updated specification builder.
+ public static ISpecificationBuilder TagWith(
+ this ISpecificationBuilder builder,
+ string tag)
+ => TagWith(builder, tag, true);
+
+ ///
+ /// Adds a query tag to the specification if the condition is true.
+ ///
+ /// The type of the entity.
+ /// The specification builder.
+ /// The query tag.
+ /// The condition to evaluate.
+ /// The updated specification builder.
+ public static ISpecificationBuilder TagWith(
+ this ISpecificationBuilder builder,
+ string tag,
+ bool condition)
+ {
+ if (condition)
+ {
+ builder.Specification.AddInternal(ItemType.QueryTag, tag);
+ }
+
+ return builder;
+ }
+}
diff --git a/src/QuerySpecification/Internals/ItemType.cs b/src/QuerySpecification/Internals/ItemType.cs
index 4cc0421d..07345ac5 100644
--- a/src/QuerySpecification/Internals/ItemType.cs
+++ b/src/QuerySpecification/Internals/ItemType.cs
@@ -13,4 +13,6 @@ internal static class ItemType
// We can save 16 bytes (on x64) by storing both Flags and Paging in the same item.
public const int Paging = -8; // Stored in the reference
public const int Flags = -8; // Stored in the bag
+
+ public const int QueryTag = -9;
}
diff --git a/src/QuerySpecification/Specification.cs b/src/QuerySpecification/Specification.cs
index 014e4bc0..25db04a9 100644
--- a/src/QuerySpecification/Specification.cs
+++ b/src/QuerySpecification/Specification.cs
@@ -176,6 +176,13 @@ public virtual bool IsSatisfiedBy(T entity)
? Enumerable.Empty()
: new SpecSelectIterator(_items, ItemType.IncludeString, (x, bag) => x);
+ ///
+ /// Gets the Query tags.
+ ///
+ public IEnumerable QueryTags => _items is null
+ ? Enumerable.Empty()
+ : new SpecSelectIterator(_items, ItemType.QueryTag, (x, bag) => x);
+
///
/// Gets the number of items to take.
///
diff --git a/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/QueryTagEvaluatorTests.cs b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/QueryTagEvaluatorTests.cs
new file mode 100644
index 00000000..bc31dfd9
--- /dev/null
+++ b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/QueryTagEvaluatorTests.cs
@@ -0,0 +1,135 @@
+namespace Tests.Evaluators;
+
+[Collection("SharedCollection")]
+public class QueryTagEvaluatorTests(TestFactory factory) : IntegrationTest(factory)
+{
+ private static readonly QueryTagEvaluator _evaluator = QueryTagEvaluator.Instance;
+
+ [Fact]
+ public void QueriesMatch_GivenTag()
+ {
+ var tag = "asd";
+
+ var spec = new Specification();
+ spec.Query.TagWith(tag);
+
+ var actual = _evaluator.Evaluate(DbContext.Countries, spec)
+ .ToQueryString();
+
+ var expected = DbContext.Countries
+ .TagWith(tag)
+ .ToQueryString();
+
+ actual.Should().Be(expected);
+ }
+
+ [Fact]
+ public void QueriesMatch_GivenMultipleTags()
+ {
+ var tag1 = "asd";
+ var tag2 = "qwe";
+
+ var spec = new Specification();
+ spec.Query.TagWith(tag1);
+ spec.Query.TagWith(tag2);
+
+ var actual = _evaluator.Evaluate(DbContext.Countries, spec)
+ .ToQueryString();
+
+ var expected = DbContext.Countries
+ .TagWith(tag1)
+ .TagWith(tag2)
+ .ToQueryString();
+
+ actual.Should().Be(expected);
+ }
+
+
+ [Fact]
+ public void DoesNothing_GivenNoTag()
+ {
+ var spec = new Specification();
+
+ var actual = _evaluator.Evaluate(DbContext.Countries, spec)
+ .Expression
+ .ToString();
+
+ var expected = DbContext.Countries
+ .AsQueryable()
+ .Expression
+ .ToString();
+
+ actual.Should().Be(expected);
+ }
+
+ [Fact]
+ public void Applies_GivenSingleTag()
+ {
+ var tag = "asd";
+
+ var spec = new Specification();
+ spec.Query.TagWith(tag);
+
+ var actual = _evaluator.Evaluate(DbContext.Countries, spec)
+ .Expression
+ .ToString();
+
+ var expected = DbContext.Countries
+ .TagWith(tag)
+ .Expression
+ .ToString();
+
+ actual.Should().Be(expected);
+ }
+
+ [Fact]
+ public void Applies_GivenTwoTags()
+ {
+ var tag1 = "asd";
+ var tag2 = "qwe";
+
+ var spec = new Specification();
+ spec.Query
+ .TagWith(tag1)
+ .TagWith(tag2);
+
+ var actual = _evaluator.Evaluate(DbContext.Countries, spec)
+ .Expression
+ .ToString();
+
+ var expected = DbContext.Countries
+ .TagWith(tag1)
+ .TagWith(tag2)
+ .Expression
+ .ToString();
+
+ actual.Should().Be(expected);
+ }
+
+ [Fact]
+ public void Applies_GivenMultipleTags()
+ {
+ var tag1 = "asd";
+ var tag2 = "qwe";
+ var tag3 = "zxc";
+
+ var spec = new Specification();
+ spec.Query
+ .TagWith(tag1)
+ .TagWith(tag2)
+ .TagWith(tag3);
+
+ var actual = _evaluator.Evaluate(DbContext.Countries, spec)
+ .Expression
+ .ToString();
+
+ var expected = DbContext.Countries
+ .TagWith(tag1)
+ .TagWith(tag2)
+ .TagWith(tag3)
+ .Expression
+ .ToString();
+
+ actual.Should().Be(expected);
+ }
+}
diff --git a/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs
index f55c6ea2..7cd7101b 100644
--- a/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs
+++ b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs
@@ -60,6 +60,8 @@ public void GivenFullQuery()
var spec = new Specification();
spec.Query
+ .TagWith("full test query")
+ .TagWith("another tag")
.Where(x => x.Id > id)
.Where(x => x.Name == name)
.Like(x => x.Name, $"%{storeTerm}%")
@@ -81,6 +83,8 @@ public void GivenFullQuery()
// The expression in the spec are applied in a predefined order.
var expected = DbContext.Stores
+ .TagWith("full test query")
+ .TagWith("another tag")
.Where(x => x.Id > id)
.Where(x => x.Name == name)
.Where(x => EF.Functions.Like(x.Name, $"%{storeTerm}%")
@@ -166,6 +170,7 @@ public void GivenFullQueryWithSelect()
var spec = new Specification();
spec.Query
+ .TagWith("full test query with select")
.Where(x => x.Id > id)
.Where(x => x.Name == name)
.Like(x => x.Name, $"%{storeTerm}%")
@@ -188,6 +193,7 @@ public void GivenFullQueryWithSelect()
// The expression in the spec are applied in a predefined order.
var expected = DbContext.Stores
+ .TagWith("full test query with select")
.Where(x => x.Id > id)
.Where(x => x.Name == name)
.Where(x => EF.Functions.Like(x.Name, $"%{storeTerm}%")
@@ -221,6 +227,7 @@ public void GivenFullQueryWithSelectMany()
var spec = new Specification();
spec.Query
+ .TagWith("full test query with select many")
.Where(x => x.Id > id)
.Where(x => x.Name == name)
.Like(x => x.Name, $"%{storeTerm}%")
@@ -243,6 +250,7 @@ public void GivenFullQueryWithSelectMany()
// The expression in the spec are applied in a predefined order.
var expected = DbContext.Stores
+ .TagWith("full test query with select many")
.Where(x => x.Id > id)
.Where(x => x.Name == name)
.Where(x => EF.Functions.Like(x.Name, $"%{storeTerm}%")
@@ -382,7 +390,7 @@ public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluator()
var evaluator = new SpecificationEvaluatorDerived();
var result = EvaluatorsOf(evaluator);
- result.Should().HaveCount(13);
+ result.Should().HaveCount(14);
result[0].Should().BeOfType();
result[1].Should().BeOfType();
result[2].Should().BeOfType();
@@ -395,7 +403,8 @@ public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluator()
result[9].Should().BeOfType();
result[10].Should().BeOfType();
result[11].Should().BeOfType();
- result[12].Should().BeOfType();
+ result[12].Should().BeOfType();
+ result[13].Should().BeOfType();
}
private class SpecificationEvaluatorDerived : SpecificationEvaluator
diff --git a/tests/QuerySpecification.Tests/Builders/Builder_TagWith.cs b/tests/QuerySpecification.Tests/Builders/Builder_TagWith.cs
new file mode 100644
index 00000000..b00dbe23
--- /dev/null
+++ b/tests/QuerySpecification.Tests/Builders/Builder_TagWith.cs
@@ -0,0 +1,104 @@
+namespace Tests.Builders;
+
+public class Builder_TagWith
+{
+ public record Customer(int Id, string Name);
+
+ [Fact]
+ public void DoesNothing_GivenNoTag()
+ {
+ var spec1 = new Specification();
+ var spec2 = new Specification();
+
+ spec1.QueryTags.Should().BeSameAs(Enumerable.Empty());
+ spec2.QueryTags.Should().BeSameAs(Enumerable.Empty());
+ }
+
+ [Fact]
+ public void DoesNothing_GivenTagWithFalseCondition()
+ {
+ var spec1 = new Specification();
+ spec1.Query
+ .TagWith("asd", false);
+
+ var spec2 = new Specification();
+ spec2.Query
+ .TagWith("asd", false);
+
+ spec1.QueryTags.Should().BeSameAs(Enumerable.Empty());
+ spec2.QueryTags.Should().BeSameAs(Enumerable.Empty());
+ }
+
+ [Fact]
+ public void SetsTag_GivenSingleTag()
+ {
+ var tag = "asd";
+
+ var spec1 = new Specification();
+ spec1.Query
+ .TagWith(tag);
+
+ var spec2 = new Specification();
+ spec2.Query
+ .TagWith(tag);
+
+ spec1.QueryTags.Should().ContainSingle();
+ spec1.QueryTags.First().Should().Be(tag);
+ spec2.QueryTags.Should().ContainSingle();
+ spec2.QueryTags.First().Should().Be(tag);
+ }
+
+ [Fact]
+ public void SetsTags_GivenTwoTags()
+ {
+ var tag1 = "asd";
+ var tag2 = "qwe";
+
+ var spec1 = new Specification();
+ spec1.Query
+ .TagWith(tag1)
+ .TagWith(tag2);
+
+ var spec2 = new Specification();
+ spec2.Query
+ .TagWith(tag1)
+ .TagWith(tag2);
+
+ spec1.QueryTags.Should().HaveCount(2);
+ spec1.QueryTags.First().Should().Be(tag1);
+ spec1.QueryTags.Skip(1).First().Should().Be(tag2);
+ spec2.QueryTags.Should().HaveCount(2);
+ spec2.QueryTags.First().Should().Be(tag1);
+ spec2.QueryTags.Skip(1).First().Should().Be(tag2);
+ }
+
+ [Fact]
+ public void SetsTags_GivenMultipleTags()
+ {
+ var tag1 = "asd";
+ var tag2 = "qwe";
+ var tag3 = "zxc";
+
+ var spec1 = new Specification();
+ spec1.Query
+ .TagWith(tag1)
+ .TagWith(tag2)
+ .TagWith(tag3);
+
+ var spec2 = new Specification();
+ spec2.Query
+ .TagWith(tag1)
+ .TagWith(tag2)
+ .TagWith(tag3);
+
+ spec1.QueryTags.Should().HaveCount(3);
+ spec1.QueryTags.First().Should().Be(tag1);
+ spec1.QueryTags.Skip(1).First().Should().Be(tag2);
+ spec1.QueryTags.Skip(2).First().Should().Be(tag3);
+
+ spec2.QueryTags.Should().HaveCount(3);
+ spec2.QueryTags.First().Should().Be(tag1);
+ spec2.QueryTags.Skip(1).First().Should().Be(tag2);
+ spec2.QueryTags.Skip(2).First().Should().Be(tag3);
+ }
+}