From b7e59b5bbd61afbcff3a47533cfb99779b16b0f1 Mon Sep 17 00:00:00 2001 From: iatsuta Date: Thu, 5 Feb 2026 22:15:42 +0100 Subject: [PATCH] add-cache-to-EfFetchService --- src/Directory.Packages.props | 4 +- .../Fetching/FetchPath.cs | 5 - .../Fetching/FetchRule.cs | 6 +- .../Fetching/IPropertyFetchRule.cs | 6 +- .../Fetching/PropertyFetchRule.cs | 20 +++- .../PropertyFetchRuleExtensions.cs | 4 +- .../EfFetchService.cs | 106 +++++++++--------- .../Fetching/FetchRuleExpanderExtensions.cs | 7 ++ src/__SolutionItems/CommonAssemblyInfo.cs | 2 +- 9 files changed, 94 insertions(+), 66 deletions(-) delete mode 100644 src/GenericQueryable.Abstractions/Fetching/FetchPath.cs create mode 100644 src/GenericQueryable.Runtime/Fetching/FetchRuleExpanderExtensions.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index abc9b23..5003ca7 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -3,8 +3,8 @@ true - - + + diff --git a/src/GenericQueryable.Abstractions/Fetching/FetchPath.cs b/src/GenericQueryable.Abstractions/Fetching/FetchPath.cs deleted file mode 100644 index 195e738..0000000 --- a/src/GenericQueryable.Abstractions/Fetching/FetchPath.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System.Linq.Expressions; - -namespace GenericQueryable.Fetching; - -public record FetchPath(IReadOnlyList Properties); \ No newline at end of file diff --git a/src/GenericQueryable.Abstractions/Fetching/FetchRule.cs b/src/GenericQueryable.Abstractions/Fetching/FetchRule.cs index 96f8a31..20de869 100644 --- a/src/GenericQueryable.Abstractions/Fetching/FetchRule.cs +++ b/src/GenericQueryable.Abstractions/Fetching/FetchRule.cs @@ -1,5 +1,7 @@ using System.Linq.Expressions; +using CommonFramework; + namespace GenericQueryable.Fetching; public abstract record FetchRule @@ -11,8 +13,8 @@ public static FetchRule Create(string path) public static PropertyFetchRule Create(Expression> prop) { - return new PropertyFetchRule([new FetchPath([prop])]); + return new PropertyFetchRule([new LambdaExpressionPath([prop])]); } - public static PropertyFetchRule Empty { get; } = new ([]); + public static PropertyFetchRule Empty { get; } = new([]); } \ No newline at end of file diff --git a/src/GenericQueryable.Abstractions/Fetching/IPropertyFetchRule.cs b/src/GenericQueryable.Abstractions/Fetching/IPropertyFetchRule.cs index 14e5984..9771e8e 100644 --- a/src/GenericQueryable.Abstractions/Fetching/IPropertyFetchRule.cs +++ b/src/GenericQueryable.Abstractions/Fetching/IPropertyFetchRule.cs @@ -1,8 +1,10 @@ -namespace GenericQueryable.Fetching; +using CommonFramework; + +namespace GenericQueryable.Fetching; public interface IPropertyFetchRule : IPropertyFetchRule; public interface IPropertyFetchRule { - IReadOnlyList Paths { get; } + DeepEqualsCollection Paths { get; } } \ No newline at end of file diff --git a/src/GenericQueryable.Abstractions/Fetching/PropertyFetchRule.cs b/src/GenericQueryable.Abstractions/Fetching/PropertyFetchRule.cs index 2395d5c..9d342b2 100644 --- a/src/GenericQueryable.Abstractions/Fetching/PropertyFetchRule.cs +++ b/src/GenericQueryable.Abstractions/Fetching/PropertyFetchRule.cs @@ -1,13 +1,27 @@ using System.Linq.Expressions; +using CommonFramework; + namespace GenericQueryable.Fetching; -public record PropertyFetchRule(IReadOnlyList Paths) : FetchRule, IPropertyFetchRule +public record PropertyFetchRule(DeepEqualsCollection Paths) : FetchRule, IPropertyFetchRule { + public PropertyFetchRule(IEnumerable paths) : + this(DeepEqualsCollection.Create(paths)) + { + } + public PropertyFetchRule Fetch(Expression> path) { - return new PropertyFetchRule(this.Paths.Concat([new FetchPath([path])]).ToList()); + return new PropertyFetchRule(this.Paths.Concat([new LambdaExpressionPath([path])])); } } -public record PropertyFetchRule(IReadOnlyList Paths) : PropertyFetchRule(Paths), IPropertyFetchRule; \ No newline at end of file +public record PropertyFetchRule(DeepEqualsCollection Paths) + : PropertyFetchRule(Paths), IPropertyFetchRule +{ + public PropertyFetchRule(IEnumerable paths) : + this(DeepEqualsCollection.Create(paths)) + { + } +} \ No newline at end of file diff --git a/src/GenericQueryable.Abstractions/PropertyFetchRuleExtensions.cs b/src/GenericQueryable.Abstractions/PropertyFetchRuleExtensions.cs index ec3646a..caa1cf4 100644 --- a/src/GenericQueryable.Abstractions/PropertyFetchRuleExtensions.cs +++ b/src/GenericQueryable.Abstractions/PropertyFetchRuleExtensions.cs @@ -2,6 +2,8 @@ using System.Linq.Expressions; +using CommonFramework; + namespace GenericQueryable; public static class PropertyFetchRuleExtensions @@ -29,7 +31,7 @@ private static PropertyFetchRule ThenFetchInternal(prevPaths.Concat([newLastPath]).ToList()); } diff --git a/src/GenericQueryable.EntityFramework/EfFetchService.cs b/src/GenericQueryable.EntityFramework/EfFetchService.cs index fcef9b9..3787578 100644 --- a/src/GenericQueryable.EntityFramework/EfFetchService.cs +++ b/src/GenericQueryable.EntityFramework/EfFetchService.cs @@ -1,8 +1,9 @@ -using System.Linq.Expressions; +using System.Collections.Concurrent; +using System.Linq.Expressions; using System.Reflection; using CommonFramework; - +using CommonFramework.ExpressionEvaluate; using GenericQueryable.Fetching; using Microsoft.EntityFrameworkCore; @@ -13,48 +14,73 @@ namespace GenericQueryable.EntityFramework; public class EfFetchService([FromKeyedServices(RootFetchRuleExpander.Key)] IFetchRuleExpander fetchRuleExpander) : IFetchService { + private readonly ConcurrentDictionary> rootCache = []; + public virtual IQueryable ApplyFetch(IQueryable source, FetchRule fetchRule) where TSource : class { - var expandedFetchRule = fetchRuleExpander.TryExpand(fetchRule) ?? fetchRule; - - return expandedFetchRule switch + return fetchRule switch { UntypedFetchRule untypedFetchRule => source.Include(untypedFetchRule.Path), - PropertyFetchRule propertyFetchRule => this.ApplyFetch(source, propertyFetchRule), - - _ => throw new ArgumentOutOfRangeException(nameof(fetchRule)) + _ => this.GetApplyFetchFunc(fetchRule).Invoke(source) }; } - protected IQueryable ApplyFetch(IQueryable source, PropertyFetchRule fetchRule) - where TSource : class - { - return fetchRule.Paths.Aggregate(source, this.ApplyFetch); - } + private Func, IQueryable> GetApplyFetchFunc(FetchRule fetchRule) + where TSource : class + { + return this.rootCache + .GetOrAdd(typeof(TSource), _ => new ConcurrentDictionary()) + .GetOrAdd(fetchRule.GetType(), _ => new ConcurrentDictionary, Func, IQueryable>>()) + .Pipe(v => (ConcurrentDictionary, Func, IQueryable>>)v) + .GetOrAdd(fetchRule, _ => + { + var fetchExpr = this.GetApplyFetchExpression(fetchRuleExpander.Expand(fetchRule)); + + return fetchExpr.Compile(); + }); + } - private IQueryable ApplyFetch(IQueryable source, FetchPath fetchPath) - where TSource : class - { - return fetchPath - .Properties - .ZipStrong(new LambdaExpression?[] { null }.Concat(fetchPath.Properties.SkipLast(1)), (prop, prevProp) => new { prop, prevProp }) - .Aggregate(source, (q, pair) => this.ApplyFetch(q, pair.prop, pair.prevProp)); - } + private Expression, IQueryable>> GetApplyFetchExpression(PropertyFetchRule fetchRule) + where TSource : class + { + var startState = ExpressionHelper.GetIdentity>(); - private IQueryable ApplyFetch(IQueryable source, LambdaExpression prop, LambdaExpression? prevProp) - where TSource : class - { - return this.GetFetchMethod(prop, prevProp).Invoke>(this, source, prop); - } + return fetchRule.Paths.Aggregate(startState, (state, path) => + { + var nextApplyFunc = GetApplyFetchExpression(path); - private MethodInfo GetFetchMethod(LambdaExpression prop, LambdaExpression? prevProp) + return ExpressionEvaluateHelper.InlineEvaluate, IQueryable>>(ee => + + q => ee.Evaluate(nextApplyFunc, ee.Evaluate(state, q))); + }); + } + + private static Expression, IQueryable>> GetApplyFetchExpression(LambdaExpressionPath fetchPath) + where TSource : class + { + LambdaExpression startState = ExpressionHelper.GetIdentity>(); + + var resultBody = fetchPath + .Properties + .ZipStrong(new LambdaExpression?[] { null }.Concat(fetchPath.Properties.SkipLast(1)), (prop, prevProp) => new { prop, prevProp }) + .Aggregate(startState.Body, (state, pair) => + { + var fetchMethod = GetFetchMethod(pair.prop, pair.prevProp); + + return Expression.Call(fetchMethod, state, pair.prop); + }); + + return Expression.Lambda, IQueryable>>(resultBody, startState.Parameters); + } + + private static MethodInfo GetFetchMethod(LambdaExpression prop, LambdaExpression? prevProp) where TSource : class { if (prevProp == null) { - return new Func, Expression>, IIncludableQueryable>(this.ApplyFetch) + return new Func, Expression>, IIncludableQueryable>(EntityFrameworkQueryableExtensions.Include) .CreateGenericMethod(typeof(TSource), prop.Body.Type); } else @@ -68,35 +94,15 @@ private MethodInfo GetFetchMethod(LambdaExpression prop, LambdaExpressi if (prevPropRealType.IsGenericType && typeof(IEnumerable<>).MakeGenericType(prevElementType).IsAssignableFrom(prevPropRealType)) { return new Func>, Expression>, IIncludableQueryable>( - this.ApplyThenFetch) + EntityFrameworkQueryableExtensions.ThenInclude) .CreateGenericMethod(typeof(TSource), prevElementType, nextPropertyType); } else { return new Func, Expression>, IIncludableQueryable>( - this.ApplyThenFetch) + EntityFrameworkQueryableExtensions.ThenInclude) .CreateGenericMethod(typeof(TSource), prevElementType, nextPropertyType); } } } - - private IIncludableQueryable ApplyFetch(IQueryable source, Expression> prop) - where TSource : class - { - return source.Include(prop); - } - - private IIncludableQueryable ApplyThenFetch( - IIncludableQueryable> source, Expression> prop) - where TSource : class - { - return source.ThenInclude(prop); - } - - private IIncludableQueryable ApplyThenFetch( - IIncludableQueryable source, Expression> prop) - where TSource : class - { - return source.ThenInclude(prop); - } } \ No newline at end of file diff --git a/src/GenericQueryable.Runtime/Fetching/FetchRuleExpanderExtensions.cs b/src/GenericQueryable.Runtime/Fetching/FetchRuleExpanderExtensions.cs new file mode 100644 index 0000000..38b9e78 --- /dev/null +++ b/src/GenericQueryable.Runtime/Fetching/FetchRuleExpanderExtensions.cs @@ -0,0 +1,7 @@ +namespace GenericQueryable.Fetching; + +public static class FetchRuleExpanderExtensions +{ + public static PropertyFetchRule Expand(this IFetchRuleExpander fetchRuleExpander, FetchRule fetchRule) => + fetchRuleExpander.TryExpand(fetchRule) ?? throw new ArgumentOutOfRangeException(nameof(fetchRule)); +} \ No newline at end of file diff --git a/src/__SolutionItems/CommonAssemblyInfo.cs b/src/__SolutionItems/CommonAssemblyInfo.cs index a3d7303..1df93de 100644 --- a/src/__SolutionItems/CommonAssemblyInfo.cs +++ b/src/__SolutionItems/CommonAssemblyInfo.cs @@ -3,7 +3,7 @@ [assembly: AssemblyProduct("GenericQueryable")] [assembly: AssemblyCompany("IvAt")] -[assembly: AssemblyVersion("2.1.9.0")] +[assembly: AssemblyVersion("2.1.11.0")] [assembly: AssemblyInformationalVersion("changes at build")] #if DEBUG