Skip to content

Commit edde86b

Browse files
authored
Add TagWith and IgnoreAutoIncludes features. (#451)
* Added TagWith feature. * Added IgnoreAutoIncludes feature. * Rename Tag to QueryTag.
1 parent b65baa5 commit edde86b

File tree

12 files changed

+358
-3
lines changed

12 files changed

+358
-3
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Ardalis.Specification.EntityFrameworkCore;
2+
3+
/// <summary>
4+
/// This evaluator applies EF Core's IgnoreAutoIncludes to a given query
5+
/// </summary>
6+
public class IgnoreAutoIncludesEvaluator : IEvaluator
7+
{
8+
private IgnoreAutoIncludesEvaluator() { }
9+
public static IgnoreAutoIncludesEvaluator Instance { get; } = new IgnoreAutoIncludesEvaluator();
10+
11+
public bool IsCriteriaEvaluator { get; } = true;
12+
13+
public IQueryable<T> GetQuery<T>(IQueryable<T> query, ISpecification<T> specification) where T : class
14+
{
15+
if (specification.IgnoreAutoIncludes)
16+
{
17+
query = query.IgnoreAutoIncludes();
18+
}
19+
20+
return query;
21+
}
22+
}

src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ public SpecificationEvaluator()
2626
AsNoTrackingWithIdentityResolutionEvaluator.Instance,
2727
AsTrackingEvaluator.Instance,
2828
IgnoreQueryFiltersEvaluator.Instance,
29-
AsSplitQueryEvaluator.Instance
29+
IgnoreAutoIncludesEvaluator.Instance,
30+
AsSplitQueryEvaluator.Instance,
31+
TagWithEvaluator.Instance,
3032
});
3133
}
3234

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace Ardalis.Specification.EntityFrameworkCore;
2+
3+
public class TagWithEvaluator : IEvaluator
4+
{
5+
private TagWithEvaluator() { }
6+
public static TagWithEvaluator Instance { get; } = new TagWithEvaluator();
7+
8+
public bool IsCriteriaEvaluator { get; } = true;
9+
10+
public IQueryable<T> GetQuery<T>(IQueryable<T> query, ISpecification<T> specification) where T : class
11+
{
12+
if (specification.QueryTag is not null)
13+
{
14+
query = query.TagWith(specification.QueryTag);
15+
}
16+
17+
return query;
18+
}
19+
}

src/Ardalis.Specification/Builders/Builder_Flags.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,66 @@ public static ISpecificationBuilder<T> IgnoreQueryFilters<T>(
6262
return builder;
6363
}
6464

65+
/// <summary>
66+
/// Configures the specification to ignore auto includes.
67+
/// </summary>
68+
/// <typeparam name="T">The type of the entity.</typeparam>
69+
/// <typeparam name="TResult">The type of the result.</typeparam>
70+
/// <param name="builder">The specification builder.</param>
71+
/// <returns>The updated specification builder.</returns>
72+
public static ISpecificationBuilder<T, TResult> IgnoreAutoIncludes<T, TResult>(
73+
this ISpecificationBuilder<T, TResult> builder) where T : class
74+
=> IgnoreAutoIncludes(builder, true);
75+
76+
/// <summary>
77+
/// Configures the specification to ignore auto includes if the condition is true.
78+
/// </summary>
79+
/// <typeparam name="T">The type of the entity.</typeparam>
80+
/// <typeparam name="TResult">The type of the result.</typeparam>
81+
/// <param name="builder">The specification builder.</param>
82+
/// <param name="condition">The condition to evaluate.</param>
83+
/// <returns>The updated specification builder.</returns>
84+
public static ISpecificationBuilder<T, TResult> IgnoreAutoIncludes<T, TResult>(
85+
this ISpecificationBuilder<T, TResult> builder,
86+
bool condition) where T : class
87+
{
88+
if (condition)
89+
{
90+
builder.Specification.IgnoreAutoIncludes = true;
91+
}
92+
93+
return builder;
94+
}
95+
96+
/// <summary>
97+
/// Configures the specification to ignore auto includes.
98+
/// </summary>
99+
/// <typeparam name="T">The type of the entity.</typeparam>
100+
/// <param name="builder">The specification builder.</param>
101+
/// <returns>The updated specification builder.</returns>
102+
public static ISpecificationBuilder<T> IgnoreAutoIncludes<T>(
103+
this ISpecificationBuilder<T> builder) where T : class
104+
=> IgnoreAutoIncludes(builder, true);
105+
106+
/// <summary>
107+
/// Configures the specification to ignore auto includes if the condition is true.
108+
/// </summary>
109+
/// <typeparam name="T">The type of the entity.</typeparam>
110+
/// <param name="builder">The specification builder.</param>
111+
/// <param name="condition">The condition to evaluate.</param>
112+
/// <returns>The updated specification builder.</returns>
113+
public static ISpecificationBuilder<T> IgnoreAutoIncludes<T>(
114+
this ISpecificationBuilder<T> builder,
115+
bool condition) where T : class
116+
{
117+
if (condition)
118+
{
119+
builder.Specification.IgnoreAutoIncludes = true;
120+
}
121+
122+
return builder;
123+
}
124+
65125
/// <summary>
66126
/// Configures the specification to use split queries.
67127
/// </summary>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
namespace Ardalis.Specification;
2+
3+
public static partial class SpecificationBuilderExtensions
4+
{
5+
/// <summary>
6+
/// Adds a query tag to the specification.
7+
/// </summary>
8+
/// <typeparam name="T">The type of the entity.</typeparam>
9+
/// <typeparam name="TResult">The type of the result.</typeparam>
10+
/// <param name="builder">The specification builder.</param>
11+
/// <param name="tag">The query tag.</param>
12+
/// <returns>The updated specification builder.</returns>
13+
public static ISpecificationBuilder<T, TResult> TagWith<T, TResult>(
14+
this ISpecificationBuilder<T, TResult> builder,
15+
string tag)
16+
=> TagWith(builder, tag, true);
17+
18+
/// <summary>
19+
/// Adds a query tag to the specification if the condition is true.
20+
/// </summary>
21+
/// <typeparam name="T">The type of the entity.</typeparam>
22+
/// <typeparam name="TResult">The type of the result.</typeparam>
23+
/// <param name="builder">The specification builder.</param>
24+
/// <param name="tag">The query tag.</param>
25+
/// <param name="condition">The condition to evaluate.</param>
26+
/// <returns>The updated specification builder.</returns>
27+
public static ISpecificationBuilder<T, TResult> TagWith<T, TResult>(
28+
this ISpecificationBuilder<T, TResult> builder,
29+
string tag,
30+
bool condition)
31+
{
32+
if (condition)
33+
{
34+
builder.Specification.QueryTag = tag;
35+
}
36+
37+
return builder;
38+
}
39+
40+
/// <summary>
41+
/// Adds a query tag to the specification.
42+
/// </summary>
43+
/// <typeparam name="T">The type of the entity.</typeparam>
44+
/// <param name="builder">The specification builder.</param>
45+
/// <param name="tag">The query tag.</param>
46+
/// <returns>The updated specification builder.</returns>
47+
public static ISpecificationBuilder<T> TagWith<T>(
48+
this ISpecificationBuilder<T> builder,
49+
string tag)
50+
=> TagWith(builder, tag, true);
51+
52+
/// <summary>
53+
/// Adds a query tag to the specification if the condition is true.
54+
/// </summary>
55+
/// <typeparam name="T">The type of the entity.</typeparam>
56+
/// <param name="builder">The specification builder.</param>
57+
/// <param name="tag">The query tag.</param>
58+
/// <param name="condition">The condition to evaluate.</param>
59+
/// <returns>The updated specification builder.</returns>
60+
public static ISpecificationBuilder<T> TagWith<T>(
61+
this ISpecificationBuilder<T> builder,
62+
string tag,
63+
bool condition)
64+
{
65+
if (condition)
66+
{
67+
builder.Specification.QueryTag = tag;
68+
}
69+
70+
return builder;
71+
}
72+
}

src/Ardalis.Specification/ISpecification.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ public interface ISpecification<T>
8383
/// </summary>
8484
Func<IEnumerable<T>, IEnumerable<T>>? PostProcessingAction { get; }
8585

86+
/// <summary>
87+
/// A query tag to help correlate specification with generated SQL queries captured in logs
88+
/// </summary>
89+
string? QueryTag { get; }
90+
8691
/// <summary>
8792
/// Return whether or not the results should be cached.
8893
/// </summary>
@@ -133,6 +138,11 @@ public interface ISpecification<T>
133138
/// </remarks>
134139
bool IgnoreQueryFilters { get; }
135140

141+
/// <summary>
142+
/// Returns whether or not the query should ignore the defined AutoInclude configurations.
143+
/// </summary>
144+
bool IgnoreAutoIncludes { get; }
145+
136146
/// <summary>
137147
/// Applies the query defined within the specification to the given objects.
138148
/// This is specially helpful when unit testing specification classes

src/Ardalis.Specification/Specification.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public class Specification<T> : ISpecification<T>
5656
/// <inheritdoc/>
5757
public Func<IEnumerable<T>, IEnumerable<T>>? PostProcessingAction { get; internal set; }
5858

59+
/// <inheritdoc/>
60+
public string? QueryTag { get; internal set; }
61+
5962
/// <inheritdoc/>
6063
public string? CacheKey { get; internal set; }
6164

@@ -71,11 +74,14 @@ public class Specification<T> : ISpecification<T>
7174

7275
// We may store all the flags in a single byte. But, based on the object alignment of 8 bytes, we won't save any space anyway.
7376
// And we'll have unnecessary overhead with enum flags for now. This will be reconsidered for version 10.
74-
// Based on the alignment of 8 bytes (on x64) we can store 8 flags here. So, we have space for 3 more flags for free.
77+
// Based on the alignment of 8 bytes (on x64) we can store 8 flags here. So, we have space for 2 more flags for free.
7578

7679
/// <inheritdoc/>
7780
public bool IgnoreQueryFilters { get; internal set; } = false;
7881

82+
/// <inheritdoc/>
83+
public bool IgnoreAutoIncludes { get; internal set; } = false;
84+
7985
/// <inheritdoc/>
8086
public bool AsSplitQuery { get; internal set; } = false;
8187

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Tests.Evaluators;
2+
3+
[Collection("SharedCollection")]
4+
public class IgnoreAutoIncludesEvaluatorTests(TestFactory factory) : IntegrationTest(factory)
5+
{
6+
private static readonly IgnoreAutoIncludesEvaluator _evaluator = IgnoreAutoIncludesEvaluator.Instance;
7+
8+
[Fact]
9+
public void Applies_GivenIgnoreAutoIncludes()
10+
{
11+
var spec = new Specification<Country>();
12+
spec.Query.IgnoreAutoIncludes();
13+
14+
var actual = _evaluator.GetQuery(DbContext.Countries, spec)
15+
.Expression
16+
.ToString();
17+
18+
var expected = DbContext.Countries
19+
.IgnoreAutoIncludes()
20+
.Expression
21+
.ToString();
22+
23+
actual.Should().Be(expected);
24+
}
25+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace Tests.Evaluators;
2+
3+
[Collection("SharedCollection")]
4+
public class TagWithEvaluatorTests(TestFactory factory) : IntegrationTest(factory)
5+
{
6+
private static readonly TagWithEvaluator _evaluator = TagWithEvaluator.Instance;
7+
8+
[Fact]
9+
public void QueriesMatch_GivenTag()
10+
{
11+
var tag = "asd";
12+
13+
var spec = new Specification<Country>();
14+
spec.Query.TagWith(tag);
15+
16+
var actual = _evaluator.GetQuery(DbContext.Countries, spec)
17+
.ToQueryString();
18+
19+
var expected = DbContext.Countries
20+
.TagWith(tag)
21+
.ToQueryString();
22+
23+
actual.Should().Be(expected);
24+
}
25+
26+
[Fact]
27+
public void Applies_GivenTag()
28+
{
29+
var tag = "asd";
30+
31+
var spec = new Specification<Country>();
32+
spec.Query.TagWith(tag);
33+
34+
var actual = _evaluator.GetQuery(DbContext.Countries, spec)
35+
.Expression
36+
.ToString();
37+
38+
var expected = DbContext.Countries
39+
.TagWith(tag)
40+
.Expression
41+
.ToString();
42+
43+
actual.Should().Be(expected);
44+
}
45+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace Tests.Builders;
2+
3+
public class Builder_IgnoreAutoIncludes
4+
{
5+
public record Customer(int Id, string Name);
6+
7+
[Fact]
8+
public void DoesNothing_GivenNoAutoIncludes()
9+
{
10+
var spec1 = new Specification<Customer>();
11+
var spec2 = new Specification<Customer, string>();
12+
13+
spec1.IgnoreAutoIncludes.Should().Be(false);
14+
spec2.IgnoreAutoIncludes.Should().Be(false);
15+
}
16+
17+
[Fact]
18+
public void DoesNothing_GivenAutoIncludesWithFalseCondition()
19+
{
20+
var spec1 = new Specification<Customer>();
21+
spec1.Query
22+
.IgnoreAutoIncludes(false);
23+
24+
var spec2 = new Specification<Customer, string>();
25+
spec2.Query
26+
.IgnoreAutoIncludes(false);
27+
28+
spec1.IgnoreAutoIncludes.Should().Be(false);
29+
spec2.IgnoreAutoIncludes.Should().Be(false);
30+
}
31+
32+
[Fact]
33+
public void SetsAutoIncludes_GivenAutoIncludes()
34+
{
35+
var spec1 = new Specification<Customer>();
36+
spec1.Query
37+
.IgnoreAutoIncludes();
38+
39+
var spec2 = new Specification<Customer, string>();
40+
spec2.Query
41+
.IgnoreAutoIncludes();
42+
43+
spec1.IgnoreAutoIncludes.Should().Be(true);
44+
spec2.IgnoreAutoIncludes.Should().Be(true);
45+
}
46+
}

0 commit comments

Comments
 (0)