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,22 @@
namespace Ardalis.Specification.EntityFrameworkCore;

/// <summary>
/// This evaluator applies EF Core's IgnoreAutoIncludes to a given query
/// </summary>
public class IgnoreAutoIncludesEvaluator : IEvaluator
{
private IgnoreAutoIncludesEvaluator() { }
public static IgnoreAutoIncludesEvaluator Instance { get; } = new IgnoreAutoIncludesEvaluator();

public bool IsCriteriaEvaluator { get; } = true;

public IQueryable<T> GetQuery<T>(IQueryable<T> query, ISpecification<T> specification) where T : class
{
if (specification.IgnoreAutoIncludes)
{
query = query.IgnoreAutoIncludes();
}

return query;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ public SpecificationEvaluator()
AsNoTrackingWithIdentityResolutionEvaluator.Instance,
AsTrackingEvaluator.Instance,
IgnoreQueryFiltersEvaluator.Instance,
AsSplitQueryEvaluator.Instance
IgnoreAutoIncludesEvaluator.Instance,
AsSplitQueryEvaluator.Instance,
TagWithEvaluator.Instance,
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Ardalis.Specification.EntityFrameworkCore;

public class TagWithEvaluator : IEvaluator
{
private TagWithEvaluator() { }
public static TagWithEvaluator Instance { get; } = new TagWithEvaluator();

public bool IsCriteriaEvaluator { get; } = true;

public IQueryable<T> GetQuery<T>(IQueryable<T> query, ISpecification<T> specification) where T : class
{
if (specification.QueryTag is not null)
{
query = query.TagWith(specification.QueryTag);
}

return query;
}
}
60 changes: 60 additions & 0 deletions src/Ardalis.Specification/Builders/Builder_Flags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,66 @@ public static ISpecificationBuilder<T> IgnoreQueryFilters<T>(
return builder;
}

/// <summary>
/// Configures the specification to ignore auto includes.
/// </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>
/// <returns>The updated specification builder.</returns>
public static ISpecificationBuilder<T, TResult> IgnoreAutoIncludes<T, TResult>(
this ISpecificationBuilder<T, TResult> builder) where T : class
=> IgnoreAutoIncludes(builder, true);

/// <summary>
/// Configures the specification to ignore auto includes 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="condition">The condition to evaluate.</param>
/// <returns>The updated specification builder.</returns>
public static ISpecificationBuilder<T, TResult> IgnoreAutoIncludes<T, TResult>(
this ISpecificationBuilder<T, TResult> builder,
bool condition) where T : class
{
if (condition)
{
builder.Specification.IgnoreAutoIncludes = true;
}

return builder;
}

/// <summary>
/// Configures the specification to ignore auto includes.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <param name="builder">The specification builder.</param>
/// <returns>The updated specification builder.</returns>
public static ISpecificationBuilder<T> IgnoreAutoIncludes<T>(
this ISpecificationBuilder<T> builder) where T : class
=> IgnoreAutoIncludes(builder, true);

/// <summary>
/// Configures the specification to ignore auto includes if the condition is true.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <param name="builder">The specification builder.</param>
/// <param name="condition">The condition to evaluate.</param>
/// <returns>The updated specification builder.</returns>
public static ISpecificationBuilder<T> IgnoreAutoIncludes<T>(
this ISpecificationBuilder<T> builder,
bool condition) where T : class
{
if (condition)
{
builder.Specification.IgnoreAutoIncludes = true;
}

return builder;
}

/// <summary>
/// Configures the specification to use split queries.
/// </summary>
Expand Down
72 changes: 72 additions & 0 deletions src/Ardalis.Specification/Builders/Builder_TagWith.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
namespace Ardalis.Specification;

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);

/// <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.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.QueryTag = tag;
}

return builder;
}
}
10 changes: 10 additions & 0 deletions src/Ardalis.Specification/ISpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ public interface ISpecification<T>
/// </summary>
Func<IEnumerable<T>, IEnumerable<T>>? PostProcessingAction { get; }

/// <summary>
/// A query tag to help correlate specification with generated SQL queries captured in logs
/// </summary>
string? QueryTag { get; }

/// <summary>
/// Return whether or not the results should be cached.
/// </summary>
Expand Down Expand Up @@ -133,6 +138,11 @@ public interface ISpecification<T>
/// </remarks>
bool IgnoreQueryFilters { get; }

/// <summary>
/// Returns whether or not the query should ignore the defined AutoInclude configurations.
/// </summary>
bool IgnoreAutoIncludes { get; }

/// <summary>
/// Applies the query defined within the specification to the given objects.
/// This is specially helpful when unit testing specification classes
Expand Down
8 changes: 7 additions & 1 deletion src/Ardalis.Specification/Specification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public class Specification<T> : ISpecification<T>
/// <inheritdoc/>
public Func<IEnumerable<T>, IEnumerable<T>>? PostProcessingAction { get; internal set; }

/// <inheritdoc/>
public string? QueryTag { get; internal set; }

/// <inheritdoc/>
public string? CacheKey { get; internal set; }

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

// 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.
// And we'll have unnecessary overhead with enum flags for now. This will be reconsidered for version 10.
// 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.
// 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.

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

/// <inheritdoc/>
public bool IgnoreAutoIncludes { get; internal set; } = false;

/// <inheritdoc/>
public bool AsSplitQuery { get; internal set; } = false;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Tests.Evaluators;

[Collection("SharedCollection")]
public class IgnoreAutoIncludesEvaluatorTests(TestFactory factory) : IntegrationTest(factory)
{
private static readonly IgnoreAutoIncludesEvaluator _evaluator = IgnoreAutoIncludesEvaluator.Instance;

[Fact]
public void Applies_GivenIgnoreAutoIncludes()
{
var spec = new Specification<Country>();
spec.Query.IgnoreAutoIncludes();

var actual = _evaluator.GetQuery(DbContext.Countries, spec)
.Expression
.ToString();

var expected = DbContext.Countries
.IgnoreAutoIncludes()
.Expression
.ToString();

actual.Should().Be(expected);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace Tests.Evaluators;

[Collection("SharedCollection")]
public class TagWithEvaluatorTests(TestFactory factory) : IntegrationTest(factory)
{
private static readonly TagWithEvaluator _evaluator = TagWithEvaluator.Instance;

[Fact]
public void QueriesMatch_GivenTag()
{
var tag = "asd";

var spec = new Specification<Country>();
spec.Query.TagWith(tag);

var actual = _evaluator.GetQuery(DbContext.Countries, spec)
.ToQueryString();

var expected = DbContext.Countries
.TagWith(tag)
.ToQueryString();

actual.Should().Be(expected);
}

[Fact]
public void Applies_GivenTag()
{
var tag = "asd";

var spec = new Specification<Country>();
spec.Query.TagWith(tag);

var actual = _evaluator.GetQuery(DbContext.Countries, spec)
.Expression
.ToString();

var expected = DbContext.Countries
.TagWith(tag)
.Expression
.ToString();

actual.Should().Be(expected);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace Tests.Builders;

public class Builder_IgnoreAutoIncludes
{
public record Customer(int Id, string Name);

[Fact]
public void DoesNothing_GivenNoAutoIncludes()
{
var spec1 = new Specification<Customer>();
var spec2 = new Specification<Customer, string>();

spec1.IgnoreAutoIncludes.Should().Be(false);
spec2.IgnoreAutoIncludes.Should().Be(false);
}

[Fact]
public void DoesNothing_GivenAutoIncludesWithFalseCondition()
{
var spec1 = new Specification<Customer>();
spec1.Query
.IgnoreAutoIncludes(false);

var spec2 = new Specification<Customer, string>();
spec2.Query
.IgnoreAutoIncludes(false);

spec1.IgnoreAutoIncludes.Should().Be(false);
spec2.IgnoreAutoIncludes.Should().Be(false);
}

[Fact]
public void SetsAutoIncludes_GivenAutoIncludes()
{
var spec1 = new Specification<Customer>();
spec1.Query
.IgnoreAutoIncludes();

var spec2 = new Specification<Customer, string>();
spec2.Query
.IgnoreAutoIncludes();

spec1.IgnoreAutoIncludes.Should().Be(true);
spec2.IgnoreAutoIncludes.Should().Be(true);
}
}
Loading