Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Pozitron.QuerySpecification;

/// <summary>
/// Evaluator to apply the query tags to the query.
/// </summary>
public sealed class QueryTagEvaluator : IEvaluator
{

/// <summary>
/// Gets the singleton instance of the <see cref="QueryTagEvaluator"/> class.
/// </summary>
public static QueryTagEvaluator Instance = new();
private QueryTagEvaluator() { }

/// <inheritdoc/>
public IQueryable<T> Evaluate<T>(IQueryable<T> source, Specification<T> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public SpecificationEvaluator()
AsTrackingEvaluator.Instance,
AsSplitQueryEvaluator.Instance,
IgnoreAutoIncludesEvaluator.Instance,
QueryTagEvaluator.Instance,
];
}

Expand Down
75 changes: 75 additions & 0 deletions src/QuerySpecification/Builders/Builder_TagWith.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace Pozitron.QuerySpecification;

public static partial class SpecificationBuilderExtensions
{
/// <summary>
/// Adds a query tag to the specification.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <param name="builder">The specification builder.</param>
/// <param name="tag">The query tag.</param>
/// <returns>The updated specification builder.</returns>
public static ISpecificationBuilder<T, TResult> TagWith<T, TResult>(
this ISpecificationBuilder<T, TResult> builder,
string tag)
{
TagWith(builder, tag, true);
return builder;
}

/// <summary>
/// Adds a query tag to the specification if the condition is true.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <param name="builder">The specification builder.</param>
/// <param name="tag">The query tag.</param>
/// <param name="condition">The condition to evaluate.</param>
/// <returns>The updated specification builder.</returns>
public static ISpecificationBuilder<T, TResult> TagWith<T, TResult>(
this ISpecificationBuilder<T, TResult> builder,
string tag,
bool condition)
{
if (condition)
{
builder.Specification.AddInternal(ItemType.QueryTag, tag);
}

return builder;
}

/// <summary>
/// Adds a query tag to the specification.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <param name="builder">The specification builder.</param>
/// <param name="tag">The query tag.</param>
/// <returns>The updated specification builder.</returns>
public static ISpecificationBuilder<T> TagWith<T>(
this ISpecificationBuilder<T> builder,
string tag)
=> TagWith(builder, tag, true);

/// <summary>
/// Adds a query tag to the specification if the condition is true.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <param name="builder">The specification builder.</param>
/// <param name="tag">The query tag.</param>
/// <param name="condition">The condition to evaluate.</param>
/// <returns>The updated specification builder.</returns>
public static ISpecificationBuilder<T> TagWith<T>(
this ISpecificationBuilder<T> builder,
string tag,
bool condition)
{
if (condition)
{
builder.Specification.AddInternal(ItemType.QueryTag, tag);
}

return builder;
}
}
2 changes: 2 additions & 0 deletions src/QuerySpecification/Internals/ItemType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
7 changes: 7 additions & 0 deletions src/QuerySpecification/Specification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ public virtual bool IsSatisfiedBy(T entity)
? Enumerable.Empty<string>()
: new SpecSelectIterator<string, string>(_items, ItemType.IncludeString, (x, bag) => x);

/// <summary>
/// Gets the Query tags.
/// </summary>
public IEnumerable<string> QueryTags => _items is null
? Enumerable.Empty<string>()
: new SpecSelectIterator<string, string>(_items, ItemType.QueryTag, (x, bag) => x);

/// <summary>
/// Gets the number of items to take.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Country>();
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<Country>();
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<Country>();

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<Country>();
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<Country>();
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<Country>();
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public void GivenFullQuery()

var spec = new Specification<Store>();
spec.Query
.TagWith("full test query")
.TagWith("another tag")
.Where(x => x.Id > id)
.Where(x => x.Name == name)
.Like(x => x.Name, $"%{storeTerm}%")
Expand All @@ -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}%")
Expand Down Expand Up @@ -166,6 +170,7 @@ public void GivenFullQueryWithSelect()

var spec = new Specification<Store, string?>();
spec.Query
.TagWith("full test query with select")
.Where(x => x.Id > id)
.Where(x => x.Name == name)
.Like(x => x.Name, $"%{storeTerm}%")
Expand All @@ -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}%")
Expand Down Expand Up @@ -221,6 +227,7 @@ public void GivenFullQueryWithSelectMany()

var spec = new Specification<Store, string?>();
spec.Query
.TagWith("full test query with select many")
.Where(x => x.Id > id)
.Where(x => x.Name == name)
.Like(x => x.Name, $"%{storeTerm}%")
Expand All @@ -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}%")
Expand Down Expand Up @@ -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<LikeEvaluator>();
result[1].Should().BeOfType<WhereEvaluator>();
result[2].Should().BeOfType<LikeEvaluator>();
Expand All @@ -395,7 +403,8 @@ public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluator()
result[9].Should().BeOfType<AsTrackingEvaluator>();
result[10].Should().BeOfType<AsSplitQueryEvaluator>();
result[11].Should().BeOfType<IgnoreAutoIncludesEvaluator>();
result[12].Should().BeOfType<WhereEvaluator>();
result[12].Should().BeOfType<QueryTagEvaluator>();
result[13].Should().BeOfType<WhereEvaluator>();
}

private class SpecificationEvaluatorDerived : SpecificationEvaluator
Expand Down
Loading