Skip to content

Commit 5540fd1

Browse files
StefHCopilot
andauthored
Update DynamicGetMemberBinder to only add BindingRestrictions for dynamic type and cache the DynamicMetaObject (#922)
* Update DynamicGetMemberBinder to only add BindingRestrictions for dynamic type and cache the DynamicMetaObject * internal * 2 * Update src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs Co-authored-by: Copilot <[email protected]> * . --------- Co-authored-by: Copilot <[email protected]>
1 parent 54660ef commit 5540fd1

File tree

4 files changed

+90
-3
lines changed

4 files changed

+90
-3
lines changed

System.Linq.Dynamic.Core.sln

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.EntityFrameworkCo
159159
EndProject
160160
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Linq.Dynamic.Core.Tests.Net8", "test\System.Linq.Dynamic.Core.Tests.Net8\System.Linq.Dynamic.Core.Tests.Net8.csproj", "{CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C}"
161161
EndProject
162+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppPerformanceTest", "src-console\ConsoleAppPerformanceTest\ConsoleAppPerformanceTest.csproj", "{067C00CF-29FA-4643-814D-3A3C3C84634F}"
163+
EndProject
162164
Global
163165
GlobalSection(SolutionConfigurationPlatforms) = preSolution
164166
Debug|Any CPU = Debug|Any CPU
@@ -1039,6 +1041,22 @@ Global
10391041
{CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C}.Release|x64.Build.0 = Release|Any CPU
10401042
{CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C}.Release|x86.ActiveCfg = Release|Any CPU
10411043
{CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C}.Release|x86.Build.0 = Release|Any CPU
1044+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1045+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|Any CPU.Build.0 = Debug|Any CPU
1046+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|ARM.ActiveCfg = Debug|Any CPU
1047+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|ARM.Build.0 = Debug|Any CPU
1048+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|x64.ActiveCfg = Debug|Any CPU
1049+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|x64.Build.0 = Debug|Any CPU
1050+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|x86.ActiveCfg = Debug|Any CPU
1051+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Debug|x86.Build.0 = Debug|Any CPU
1052+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|Any CPU.ActiveCfg = Release|Any CPU
1053+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|Any CPU.Build.0 = Release|Any CPU
1054+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|ARM.ActiveCfg = Release|Any CPU
1055+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|ARM.Build.0 = Release|Any CPU
1056+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x64.ActiveCfg = Release|Any CPU
1057+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x64.Build.0 = Release|Any CPU
1058+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x86.ActiveCfg = Release|Any CPU
1059+
{067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x86.Build.0 = Release|Any CPU
10421060
EndGlobalSection
10431061
GlobalSection(SolutionProperties) = preSolution
10441062
HideSolutionNode = FALSE
@@ -1098,6 +1116,7 @@ Global
10981116
{7A31366C-DD98-41A3-A0C1-A97068BD9658} = {BCA2A024-9032-4E56-A6C4-17A15D921728}
10991117
{C774DAE7-54A0-4FCD-A3B7-3CB63D7E112D} = {DBD7D9B6-FCC7-4650-91AF-E6457573A68F}
11001118
{CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C} = {8463ED7E-69FB-49AE-85CF-0791AFD98E38}
1119+
{067C00CF-29FA-4643-814D-3A3C3C84634F} = {7971CAEB-B9F2-416B-966D-2D697C4C1E62}
11011120
EndGlobalSection
11021121
GlobalSection(ExtensibilityGlobals) = postSolution
11031122
SolutionGuid = {94C56722-194E-4B8B-BC23-B3F754E89A20}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net48</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<LangVersion>12</LangVersion>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\..\src\System.Linq.Dynamic.Core\System.Linq.Dynamic.Core.csproj" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Linq.Dynamic.Core;
2+
3+
TestDynamic();
4+
return;
5+
6+
static void TestDynamic()
7+
{
8+
var list = new List<Demo>();
9+
for (int i = 0; i < 100000; i++)
10+
{
11+
list.Add(new Demo { ID = i, Name = $"Name {i}", Description = $"Description {i}" });
12+
}
13+
14+
var xTimeAll = System.Diagnostics.Stopwatch.StartNew();
15+
var query = list.AsQueryable().Select(typeof(Demo), "new { ID, Name }").ToDynamicList();
16+
Console.WriteLine($"Total 1st Query: {(int)xTimeAll.Elapsed.TotalMilliseconds}ms");
17+
18+
xTimeAll.Restart();
19+
_ = query.AsQueryable().Select("ID").Cast<int>().ToList();
20+
Console.WriteLine($"Total 2nd Query: {(int)xTimeAll.Elapsed.TotalMilliseconds}ms");
21+
22+
xTimeAll.Restart();
23+
_ = query.AsQueryable().Select("new { it.ID as Idee } ").ToDynamicList();
24+
Console.WriteLine($"Total 3rd Query: {(int)xTimeAll.Elapsed.TotalMilliseconds}ms");
25+
}
26+
27+
class Demo
28+
{
29+
public int ID { get; set; }
30+
public string Name { get; set; }
31+
public string Description { get; set; }
32+
}

src/System.Linq.Dynamic.Core/DynamicGetMemberBinder.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#if !NET35 && !UAP10_0 && !NETSTANDARD1_3
22
using System.Collections;
3+
using System.Collections.Concurrent;
34
using System.Collections.Generic;
45
using System.Dynamic;
56
using System.Linq.Expressions;
@@ -15,7 +16,15 @@ internal class DynamicGetMemberBinder : GetMemberBinder
1516
{
1617
private static readonly MethodInfo DynamicGetMemberMethod = typeof(DynamicGetMemberBinder).GetMethod(nameof(GetDynamicMember))!;
1718

18-
public DynamicGetMemberBinder(string name, ParsingConfig? config) : base(name, config?.IsCaseSensitive != true)
19+
// The _metaObjectCache uses a Tuple<Type, string, bool> as the key to cache DynamicMetaObject instances.
20+
// The key components are:
21+
// - Type: The LimitType of the target object, ensuring type-specific caching.
22+
// - string: The member name being accessed.
23+
// - bool: The IgnoreCase flag, indicating whether the member name comparison is case-insensitive.
24+
// This strategy ensures that the cache correctly handles different types, member names, and case-sensitivity settings.
25+
private readonly ConcurrentDictionary<Tuple<Type, string, bool>, DynamicMetaObject> _metaObjectCache = new();
26+
27+
internal DynamicGetMemberBinder(string name, ParsingConfig? config) : base(name, config?.IsCaseSensitive != true)
1928
{
2029
}
2130

@@ -28,8 +37,20 @@ public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, Dy
2837
Expression.Constant(IgnoreCase));
2938

3039
// Fix #907 and #912: "The result of the dynamic binding produced by the object with type '<>f__AnonymousType1`4' for the binder 'System.Linq.Dynamic.Core.DynamicGetMemberBinder' needs at least one restriction.".
31-
var restrictions = BindingRestrictions.GetInstanceRestriction(target.Expression, target.Value);
32-
return new DynamicMetaObject(methodCallExpression, restrictions, target.Value!);
40+
// Fix #921: "Slow Performance"
41+
// Only add TypeRestriction if it's a Dynamic type and make sure to cache the DynamicMetaObject.
42+
if (target.Value is IDynamicMetaObjectProvider)
43+
{
44+
var key = new Tuple<Type, string, bool>(target.LimitType, Name, IgnoreCase);
45+
46+
return _metaObjectCache.GetOrAdd(key, _ =>
47+
{
48+
var restrictions = BindingRestrictions.GetTypeRestriction(target.Expression, target.LimitType);
49+
return new DynamicMetaObject(methodCallExpression, restrictions, target.Value);
50+
});
51+
}
52+
53+
return DynamicMetaObject.Create(target.Value!, methodCallExpression);
3354
}
3455

3556
public static object? GetDynamicMember(object value, string name, bool ignoreCase)

0 commit comments

Comments
 (0)