Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Expand Up @@ -9,9 +9,20 @@ private TagWithEvaluator() { }

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

if (spec.OneOrManyQueryTags.HasSingleItem)
{
query = query.TagWith(spec.OneOrManyQueryTags.Single);
return query;
}
}

foreach (var tag in specification.QueryTags)
{
query = query.TagWith(tag);
}

return query;
Expand Down
6 changes: 6 additions & 0 deletions src/Ardalis.Specification/Ardalis.Specification.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@
<PackageReference Include="System.Memory" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Ardalis.Specification.EntityFrameworkCore" />
<InternalsVisibleTo Include="Ardalis.Specification.EntityFramework6" />
<InternalsVisibleTo Include="Ardalis.Specification.Tests" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/Ardalis.Specification/Builders/Builder_TagWith.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public static ISpecificationBuilder<T> TagWith<T>(
{
if (condition)
{
builder.Specification.QueryTag = tag;
builder.Specification.AddQueryTag(tag);
}

return builder;
Expand Down
8 changes: 5 additions & 3 deletions src/Ardalis.Specification/ISpecification.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Ardalis.Specification;
using System.ComponentModel;

namespace Ardalis.Specification;

/// <summary>
/// Encapsulates query logic for <typeparamref name="T"/>,
Expand Down Expand Up @@ -84,9 +86,9 @@ public interface ISpecification<T>
Func<IEnumerable<T>, IEnumerable<T>>? PostProcessingAction { get; }

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

/// <summary>
/// Return whether or not the results should be cached.
Expand Down
82 changes: 82 additions & 0 deletions src/Ardalis.Specification/Internals/OneOrMany.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
namespace Ardalis.Specification;

internal struct OneOrMany<T>
{
private object? _value;

public readonly bool IsEmpty => _value is null;
public readonly bool HasSingleItem => _value is T;

public void Add(T item)
{
if (_value is null)
{
_value = item;
return;
}

if (_value is List<T> list)
{
list.Add(item);
return;
}

if (_value is T singleValue)
{
_value = new List<T>(2) { singleValue, item };
return;
}
}

public readonly T Single
{
get
{
if (_value is T singleValue)
{
return singleValue;
}

throw new InvalidOperationException("The value is not a single item.");
}
}

public readonly IEnumerable<T> Values
{
get
{
if (_value is null)
{
return Enumerable.Empty<T>();
}

if (_value is List<T> tags)
{
return tags;
}

if (_value is T singleValue)
{
return new[] { singleValue };
}

throw new InvalidOperationException("The value is neither a single item nor a list of items.");
}
}

public readonly OneOrMany<T> Clone()
{
var clone = new OneOrMany<T>();

if (_value is T singleValue)
{
clone._value = singleValue;
}
else if (_value is List<T> list)
{
clone._value = list.ToList();
}

return clone;
}
}
16 changes: 12 additions & 4 deletions src/Ardalis.Specification/Specification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class Specification<T> : ISpecification<T>
private List<IncludeExpressionInfo>? _includeExpressions;
private List<string>? _includeStrings;
private Dictionary<string, object>? _items;
private OneOrMany<string> _queryTags = new();

public ISpecificationBuilder<T> Query => new SpecificationBuilder<T>(this);
protected virtual IInMemorySpecificationEvaluator Evaluator => InMemorySpecificationEvaluator.Default;
Expand All @@ -56,9 +57,6 @@ 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 Down Expand Up @@ -121,6 +119,7 @@ internal void Add(SearchExpressionInfo<T> searchExpression)
_searchExpressions.Insert(index, searchExpression);
}
}
internal void AddQueryTag(string queryTag) => _queryTags.Add(queryTag);

/// <inheritdoc/>
public Dictionary<string, object> Items => _items ??= [];
Expand All @@ -140,6 +139,11 @@ internal void Add(SearchExpressionInfo<T> searchExpression)
/// <inheritdoc/>
public IEnumerable<string> IncludeStrings => _includeStrings ?? Enumerable.Empty<string>();

/// <inheritdoc/>
public IEnumerable<string> QueryTags => _queryTags.Values;

internal OneOrMany<string> OneOrManyQueryTags => _queryTags;

/// <inheritdoc/>
public virtual IEnumerable<T> Evaluate(IEnumerable<T> entities)
{
Expand All @@ -157,7 +161,6 @@ public virtual bool IsSatisfiedBy(T entity)
void ISpecification<T>.CopyTo(Specification<T> otherSpec)
{
otherSpec.PostProcessingAction = PostProcessingAction;
otherSpec.QueryTag = QueryTag;
otherSpec.CacheKey = CacheKey;
otherSpec.Take = Take;
otherSpec.Skip = Skip;
Expand Down Expand Up @@ -196,6 +199,11 @@ void ISpecification<T>.CopyTo(Specification<T> otherSpec)
otherSpec._searchExpressions = _searchExpressions.ToList();
}

if (!_queryTags.IsEmpty)
{
otherSpec._queryTags = _queryTags.Clone();
}

if (_items is not null)
{
otherSpec._items = new Dictionary<string, object>(_items);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void QueriesMatch_GivenTag()
}

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

Expand All @@ -42,4 +42,55 @@ public void Applies_GivenTag()

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.GetQuery(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.GetQuery(DbContext.Countries, spec)
.Expression
.ToString();

var expected = DbContext.Countries
.TagWith(tag1)
.TagWith(tag2)
.TagWith(tag3)
.Expression
.ToString();

actual.Should().Be(expected);
}
}
70 changes: 63 additions & 7 deletions tests/Ardalis.Specification.Tests/Builders/Builder_TagWith.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ public void DoesNothing_GivenNoTag()
var spec1 = new Specification<Customer>();
var spec2 = new Specification<Customer, string>();

spec1.QueryTag.Should().BeNull();
spec2.QueryTag.Should().BeNull();
spec1.QueryTags.Should().BeSameAs(Enumerable.Empty<string>());
spec2.QueryTags.Should().BeSameAs(Enumerable.Empty<string>());
}

[Fact]
Expand All @@ -25,12 +25,12 @@ public void DoesNothing_GivenTagWithFalseCondition()
spec2.Query
.TagWith("asd", false);

spec1.QueryTag.Should().BeNull();
spec2.QueryTag.Should().BeNull();
spec1.QueryTags.Should().BeSameAs(Enumerable.Empty<string>());
spec2.QueryTags.Should().BeSameAs(Enumerable.Empty<string>());
}

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

Expand All @@ -42,7 +42,63 @@ public void SetsTag_GivenTag()
spec2.Query
.TagWith(tag);

spec1.QueryTag.Should().Be(tag);
spec2.QueryTag.Should().Be(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<Customer>();
spec1.Query
.TagWith(tag1)
.TagWith(tag2);

var spec2 = new Specification<Customer, string>();
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<Customer>();
spec1.Query
.TagWith(tag1)
.TagWith(tag2)
.TagWith(tag3);

var spec2 = new Specification<Customer, string>();
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);
}
}
Loading