Skip to content

Commit

Permalink
Improved in-memory Like extension.
Browse files Browse the repository at this point in the history
  • Loading branch information
fiseni committed Oct 11, 2024
1 parent 478a5d7 commit 5119e33
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ private ParameterReplacerVisitor(ParameterExpression oldParameter, Expression ne
_oldParameter = oldParameter;
_newExpression = newExpression;
}
protected override Expression VisitParameter(ParameterExpression node)
=> node == _oldParameter ? _newExpression : node;

internal static Expression Replace(Expression expression, ParameterExpression oldParameter, Expression newExpression)
=> new ParameterReplacerVisitor(oldParameter, newExpression).Visit(expression);

protected override Expression VisitParameter(ParameterExpression node)
=> node == _oldParameter ? _newExpression : node;
}
35 changes: 34 additions & 1 deletion src/QuerySpecification/Evaluators/LikeExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Pozitron.QuerySpecification;

internal static class LikeExtension
{
private static readonly ConcurrentDictionary<string, Regex> _regexCache = new();
private static readonly RegexCache _regexCache = new();

private static Regex BuildRegex(string pattern)
{
Expand All @@ -28,6 +28,10 @@ public static bool Like(this string input, string pattern)
{
try
{
// The pattern is dynamic and arbitrary, the consumer might even compose it by an end-user input.
// We can not cache all Regex objects, but at least we can try to reuse the most "recent" ones. We'll cache 10 of them.
// This might improve the performance within the same closed loop for the in-memory evaluator and validator.

var regex = _regexCache.GetOrAdd(pattern, BuildRegex);
return regex.IsMatch(input);
}
Expand All @@ -37,6 +41,35 @@ public static bool Like(this string input, string pattern)
}
}

private class RegexCache
{
private const int _maxSize = 10;
private readonly ConcurrentDictionary<string, Regex> _dictionary = new();

public Regex GetOrAdd(string key, Func<string, Regex> valueFactory)
{
if (_dictionary.TryGetValue(key, out var regex))
return regex;

// It might happen we end up with more items than max (concurrency), but we won't be too strict.
// We're just trying to avoid indefinite growth.
for (int i = _dictionary.Count - _maxSize; i >= 0; i--)
{
// Avoid being smart, just remove sequentially from the start.
var firstKey = _dictionary.Keys.FirstOrDefault();
if (firstKey is not null)
{
_dictionary.TryRemove(firstKey, out _);
}

}

var newRegex = valueFactory(key);
_dictionary.TryAdd(key, newRegex);
return newRegex;
}
}

#pragma warning disable IDE0051 // Remove unused private members
// This C# implementation of SQL Like operator is based on the following SO post https://stackoverflow.com/a/8583383/10577116
// It covers almost all of the scenarios, and it's faster than regex based implementations.
Expand Down
20 changes: 18 additions & 2 deletions src/QuerySpecification/Validators/LikeValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,25 @@ private LikeValidator() { }

public bool IsValid<T>(T entity, Specification<T> specification)
{
foreach (var likeGroup in specification.LikeExpressions.GroupBy(x => x.Group))
// There are benchmarks in QuerySpecification.Benchmarks project.
// This implementation was the most efficient one.

var groups = specification.LikeExpressions.GroupBy(x => x.Group);

foreach (var group in groups)
{
if (likeGroup.Any(c => c.KeySelectorFunc(entity)?.Like(c.Pattern) ?? false) == false) return false;
var match = false;
foreach (var like in group)
{
if (like.KeySelectorFunc(entity)?.Like(like.Pattern) ?? false)
{
match = true;
break;
}
}

if (match is false)
return false;
}

return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace QuerySpecification.Benchmarks;

// Benchmarks measuring the in-memory Like evaluator.
// Benchmarks measuring the in-memory Like evaluator implementations.
[MemoryDiagnoser]
public class LikeInMemoryBenchmark
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
namespace QuerySpecification.Benchmarks;

// Benchmarks measuring the in-memory Like evaluator implementations.
[MemoryDiagnoser]
public class LikeValidatorBenchmark
{
public record Customer(int Id, string FirstName, string? LastName);
private class CustomerSpec : Specification<Customer>
{
public CustomerSpec()
{
Query
.Like(x => x.FirstName, "%xx%", 1)
.Like(x => x.LastName, "%xy%", 2)
.Like(x => x.LastName, "%xz%", 2);
}
}

private CustomerSpec _specification = default!;
private Customer _customer = default!;

[GlobalSetup]
public void Setup()
{
_specification = new CustomerSpec();
_customer = new(1, "axxa", "axza");
}

[Benchmark(Baseline = true)]
public bool ValidateOption1()
{
var entity = _customer;

var groups = _specification.LikeExpressions.GroupBy(x => x.Group);

foreach (var likeGroup in groups)
{
if (likeGroup.Any(c => c.KeySelectorFunc(entity)?.Like(c.Pattern) ?? false) == false) return false;
}

return true;
}

[Benchmark]
public bool ValidateOption2()
{
var entity = _customer;

var groups = _specification.LikeExpressions.GroupBy(x => x.Group).ToList();

foreach (var group in groups)
{
var match = false;
foreach (var like in group)
{
if (like.KeySelectorFunc(entity)?.Like(like.Pattern) ?? false)
{
match = true;
break;
}
}

if (match is false)
return false;
}

return true;
}

[Benchmark]
public bool ValidateOption3()
{
var entity = _customer;

var groups = _specification.LikeExpressions.GroupBy(x => x.Group);

foreach (var group in groups)
{
var match = false;
foreach (var like in group)
{
if (like.KeySelectorFunc(entity)?.Like(like.Pattern) ?? false)
{
match = true;
break;
}
}

if (match is false)
return false;
}

return true;
}
}

0 comments on commit 5119e33

Please sign in to comment.