Skip to content

Commit

Permalink
Support caching root-level parameter expressions. (#2168/#2173)
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremySkinner committed Nov 24, 2023
1 parent b3b95c4 commit e2c38e3
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Changelog.txt
@@ -1,6 +1,8 @@
11.9.0 -
Fix memory leak in NotEmptyValidator/EmptyValidator (#2174)
Add more descriptive error messages if a rule throws a NullReferenceException (#2152)
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
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 e2c38e3

Please sign in to comment.