Skip to content

Commit

Permalink
Support caching root-level parameter expressions.
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremySkinner committed Nov 24, 2023
1 parent 04f1a42 commit bea7b21
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 5 deletions.
6 changes: 4 additions & 2 deletions Changelog.txt
@@ -1,5 +1,7 @@
11.8.2 -
11.8.2 -
Fix memory leak in NotEmptyValidator/EmptyValidator (#2174)
Add support for caching root parameter expressions (eg RuleFor(x => x)) (#2168)
Add builds for .net 8

11.8.1 - 22 Nov 2023
Fix unintentional behavioural changes in introduced in the previous release as part of #2158
Expand Down Expand Up @@ -34,7 +36,7 @@ Deprecated the ability to disable the root model null check via overriding Abstr
Deprecated the Transform and TransformAsync methods (#2072)

11.5.0 - 13 Feb 2023
MemberNameValidatorSelector now supports wildcard indexes in property paths (#2056)
MemberNameValidatorSelector now supports wildcard indexes in property paths (#2056)
Added overload of TestValidateAsync that accepts a context (#2052)
Minor optimization to regex validator (#2035)
Added Kazakh translations (#2036)
Expand Down
26 changes: 26 additions & 0 deletions src/FluentValidation.Tests/AccessorCacheTests.cs
Expand Up @@ -50,6 +50,32 @@ public class AccessorCacheTests {
Assert.Equal(compiled3, compiled4);
}

[Fact]
public void Gets_accessor_for_parameter_expression() {
Expression<Func<Person, Person>> expr1 = x => x;

var compiled1 = expr1.Compile();
var compiled2 = expr1.Compile();

Assert.NotEqual(compiled1, compiled2);

var compiled3 = AccessorCache<Person>.GetCachedAccessor(null, expr1);
var compiled4 = AccessorCache<Person>.GetCachedAccessor(null, expr1);

// Expression should have been cached.
Assert.Equal(compiled3, compiled4);

Expression<Func<Person, Person>> expr2 = x => x;
var compiled5 = AccessorCache<Person>.GetCachedAccessor(null, expr2);

// Technically a different expression, but for the same type, so should still be cached.
Assert.Equal(compiled3, compiled5);

// Different expression for a different type. Shouldn't try and cache and cast.
Expression<Func<Address, Address>> expr3 = x => x;
var compiled6 = AccessorCache<Address>.GetCachedAccessor(null, expr3);
}

[Fact]
public void Equality_comparison_check() {
Expression<Func<Person, string>> expr1 = x => x.Surname;
Expand Down
23 changes: 20 additions & 3 deletions src/FluentValidation/Internal/AccessorCache.cs
Expand Up @@ -10,7 +10,8 @@ namespace FluentValidation.Internal;
/// </summary>
/// <typeparam name="T"></typeparam>
public static class AccessorCache<T> {
private static readonly ConcurrentDictionary<Key, Delegate> _cache = new ConcurrentDictionary<Key, Delegate>();
private static readonly ConcurrentDictionary<Key, Delegate> _cache = new();
private static readonly ConcurrentDictionary<Type, Delegate> _parameterAccessorCache = new();

/// <summary>
/// Gets an accessor func based on an expression
Expand All @@ -22,11 +23,27 @@ public static class AccessorCache<T> {
/// <param name="cachePrefix">Cache prefix</param>
/// <returns>Accessor func</returns>
public static Func<T, TProperty> GetCachedAccessor<TProperty>(MemberInfo member, Expression<Func<T, TProperty>> expression, bool bypassCache = false, string cachePrefix = null) {
if (member == null || bypassCache || ValidatorOptions.Global.DisableAccessorCache) {
if (bypassCache || ValidatorOptions.Global.DisableAccessorCache) {
return expression.Compile();
}

var key = new Key(member, expression, cachePrefix);
Key key;

if (member == null) {
// If the expression doesn't reference a property we don't support
// caching it, The only exception is parameter expressions referencing the same object (eg RuleFor(x => x))
if (expression.IsParameterExpression() && typeof(T) == typeof(TProperty)) {
key = new Key(null, expression, typeof(T).FullName + ":" + cachePrefix);
}
else {
// Unsupported expression type. Non cacheable.
return expression.Compile();
}
}
else {
key = new Key(member, expression, cachePrefix);
}

return (Func<T,TProperty>)_cache.GetOrAdd(key, k => expression.Compile());
}

Expand Down

0 comments on commit bea7b21

Please sign in to comment.