Skip to content

Commit 29cbf6e

Browse files
authored
Make PropertySetter a concrete type (#25054)
* Make PropertySetter a concrete type * Use the pattern from PropertyHelpers to set property values * Tweaks to Blazor WebAssembly tests to allow running tests locally * Update src/Components/Components/src/Reflection/IPropertySetter.cs
1 parent db77380 commit 29cbf6e

File tree

8 files changed

+107
-61
lines changed

8 files changed

+107
-61
lines changed

src/Components/Components/src/ComponentFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private Action<IServiceProvider, IComponent> CreateInitializer(Type type)
6363
(
6464
propertyName: property.Name,
6565
propertyType: property.PropertyType,
66-
setter: MemberAssignment.CreatePropertySetter(type, property, cascading: false)
66+
setter: new PropertySetter(type, property)
6767
)).ToArray();
6868

6969
return Initialize;

src/Components/Components/src/Reflection/ComponentProperties.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public static void SetProperties(in ParameterView parameters, object target)
144144
}
145145
}
146146

147-
static void SetProperty(object target, IPropertySetter writer, string parameterName, object value)
147+
static void SetProperty(object target, PropertySetter writer, string parameterName, object value)
148148
{
149149
try
150150
{
@@ -246,13 +246,13 @@ private static void ThrowForInvalidCaptureUnmatchedValuesParameterType(Type targ
246246
private class WritersForType
247247
{
248248
private const int MaxCachedWriterLookups = 100;
249-
private readonly Dictionary<string, IPropertySetter> _underlyingWriters;
250-
private readonly ConcurrentDictionary<string, IPropertySetter?> _referenceEqualityWritersCache;
249+
private readonly Dictionary<string, PropertySetter> _underlyingWriters;
250+
private readonly ConcurrentDictionary<string, PropertySetter?> _referenceEqualityWritersCache;
251251

252252
public WritersForType(Type targetType)
253253
{
254-
_underlyingWriters = new Dictionary<string, IPropertySetter>(StringComparer.OrdinalIgnoreCase);
255-
_referenceEqualityWritersCache = new ConcurrentDictionary<string, IPropertySetter?>(ReferenceEqualityComparer.Instance);
254+
_underlyingWriters = new Dictionary<string, PropertySetter>(StringComparer.OrdinalIgnoreCase);
255+
_referenceEqualityWritersCache = new ConcurrentDictionary<string, PropertySetter?>(ReferenceEqualityComparer.Instance);
256256

257257
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
258258
{
@@ -271,7 +271,10 @@ public WritersForType(Type targetType)
271271
$"The type '{targetType.FullName}' declares a parameter matching the name '{propertyName}' that is not public. Parameters must be public.");
272272
}
273273

274-
var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: cascadingParameterAttribute != null);
274+
var propertySetter = new PropertySetter(targetType, propertyInfo)
275+
{
276+
Cascading = cascadingParameterAttribute != null,
277+
};
275278

276279
if (_underlyingWriters.ContainsKey(propertyName))
277280
{
@@ -298,17 +301,17 @@ public WritersForType(Type targetType)
298301
ThrowForInvalidCaptureUnmatchedValuesParameterType(targetType, propertyInfo);
299302
}
300303

301-
CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: false);
304+
CaptureUnmatchedValuesWriter = new PropertySetter(targetType, propertyInfo);
302305
CaptureUnmatchedValuesPropertyName = propertyInfo.Name;
303306
}
304307
}
305308
}
306309

307-
public IPropertySetter? CaptureUnmatchedValuesWriter { get; }
310+
public PropertySetter? CaptureUnmatchedValuesWriter { get; }
308311

309312
public string? CaptureUnmatchedValuesPropertyName { get; }
310313

311-
public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out IPropertySetter writer)
314+
public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out PropertySetter writer)
312315
{
313316
// In intensive parameter-passing scenarios, one of the most expensive things we do is the
314317
// lookup from parameterName to writer. Pre-5.0 that was because of the string hashing.
Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,55 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.Reflection;
6+
47
namespace Microsoft.AspNetCore.Components.Reflection
58
{
6-
internal interface IPropertySetter
9+
internal sealed class PropertySetter
710
{
8-
bool Cascading { get; }
11+
private static readonly MethodInfo CallPropertySetterOpenGenericMethod =
12+
typeof(PropertySetter).GetMethod(nameof(CallPropertySetter), BindingFlags.NonPublic | BindingFlags.Static)!;
13+
14+
private readonly Action<object, object> _setterDelegate;
15+
16+
public PropertySetter(Type targetType, PropertyInfo property)
17+
{
18+
if (property.SetMethod == null)
19+
{
20+
throw new InvalidOperationException($"Cannot provide a value for property " +
21+
$"'{property.Name}' on type '{targetType.FullName}' because the property " +
22+
$"has no setter.");
23+
}
24+
25+
var setMethod = property.SetMethod;
26+
27+
var propertySetterAsAction =
28+
setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(targetType, property.PropertyType));
29+
var callPropertySetterClosedGenericMethod =
30+
CallPropertySetterOpenGenericMethod.MakeGenericMethod(targetType, property.PropertyType);
31+
_setterDelegate = (Action<object, object>)
32+
callPropertySetterClosedGenericMethod.CreateDelegate(typeof(Action<object, object>), propertySetterAsAction);
33+
}
34+
35+
public bool Cascading { get; init; }
36+
37+
public void SetValue(object target, object value) => _setterDelegate(target, value);
938

10-
void SetValue(object target, object value);
39+
private static void CallPropertySetter<TTarget, TValue>(
40+
Action<TTarget, TValue> setter,
41+
object target,
42+
object value)
43+
where TTarget : notnull
44+
{
45+
if (value == null)
46+
{
47+
setter((TTarget)target, default!);
48+
}
49+
else
50+
{
51+
setter((TTarget)target, (TValue)value);
52+
}
53+
}
1154
}
1255
}

src/Components/Components/src/Reflection/MemberAssignment.cs

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -44,46 +44,5 @@ public static IEnumerable<PropertyInfo> GetPropertiesIncludingInherited(
4444

4545
return dictionary.Values.SelectMany(p => p);
4646
}
47-
48-
public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property, bool cascading)
49-
{
50-
if (property.SetMethod == null)
51-
{
52-
throw new InvalidOperationException($"Cannot provide a value for property " +
53-
$"'{property.Name}' on type '{targetType.FullName}' because the property " +
54-
$"has no setter.");
55-
}
56-
57-
return (IPropertySetter)Activator.CreateInstance(
58-
typeof(PropertySetter<,>).MakeGenericType(targetType, property.PropertyType),
59-
property.SetMethod,
60-
cascading)!;
61-
}
62-
63-
class PropertySetter<TTarget, TValue> : IPropertySetter where TTarget : notnull
64-
{
65-
private readonly Action<TTarget, TValue> _setterDelegate;
66-
67-
public PropertySetter(MethodInfo setMethod, bool cascading)
68-
{
69-
_setterDelegate = (Action<TTarget, TValue>)Delegate.CreateDelegate(
70-
typeof(Action<TTarget, TValue>), setMethod);
71-
Cascading = cascading;
72-
}
73-
74-
public bool Cascading { get; }
75-
76-
public void SetValue(object target, object value)
77-
{
78-
if (value == null)
79-
{
80-
_setterDelegate((TTarget)target, default!);
81-
}
82-
else
83-
{
84-
_setterDelegate((TTarget)target, (TValue)value);
85-
}
86-
}
87-
}
8847
}
8948
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project>
2+
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
3+
4+
<PropertyGroup>
5+
<!-- Makes our docker composition simpler by not redirecting build and publish output to the artifacts dir -->
6+
<BaseIntermediateOutputPath />
7+
<IntermediateOutputPath />
8+
<BaseOutputPath />
9+
<OutputPath />
10+
</PropertyGroup>
11+
</Project>

src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Microsoft.AspNetCore.Hosting.Server.Features;
1717
using Microsoft.Extensions.DependencyInjection;
1818
using Microsoft.Extensions.Hosting;
19+
using OpenQA.Selenium;
1920
using DevHostServerProgram = Microsoft.AspNetCore.Components.WebAssembly.DevServer.Server.Program;
2021

2122
namespace Wasm.Performance.Driver
@@ -81,14 +82,32 @@ public static async Task<int> Main(string[] args)
8182
{
8283
BenchmarkResultTask = new TaskCompletionSource<BenchmarkResult>();
8384
using var runCancellationToken = new CancellationTokenSource(timeForEachRun);
84-
using var registration = runCancellationToken.Token.Register(() => BenchmarkResultTask.TrySetException(new TimeoutException($"Timed out after {timeForEachRun}")));
85+
using var registration = runCancellationToken.Token.Register(() =>
86+
{
87+
string exceptionMessage = $"Timed out after {timeForEachRun}.";
88+
try
89+
{
90+
var innerHtml = browser.FindElement(By.CssSelector(":first-child")).GetAttribute("innerHTML");
91+
exceptionMessage += Environment.NewLine + "Browser state: " + Environment.NewLine + innerHtml;
92+
}
93+
catch
94+
{
95+
// Do nothing;
96+
}
97+
BenchmarkResultTask.TrySetException(new TimeoutException(exceptionMessage));
98+
});
8599

86100
var results = await BenchmarkResultTask.Task;
87101

88102
FormatAsBenchmarksOutput(results,
89103
includeMetadata: firstRun,
90104
isStressRun: isStressRun);
91105

106+
if (!isStressRun)
107+
{
108+
PrettyPrint(results);
109+
}
110+
92111
firstRun = false;
93112
} while (isStressRun && !stressRunCancellation.IsCancellationRequested);
94113

@@ -230,6 +249,17 @@ private static void FormatAsBenchmarksOutput(BenchmarkResult benchmarkResult, bo
230249
Console.WriteLine(builder);
231250
}
232251

252+
static void PrettyPrint(BenchmarkResult benchmarkResult)
253+
{
254+
Console.WriteLine();
255+
Console.WriteLine("| Name | Description | Duration | NumExecutions | ");
256+
Console.WriteLine("--------------------------");
257+
foreach (var result in benchmarkResult.ScenarioResults)
258+
{
259+
Console.WriteLine($"| {result.Descriptor.Name} | {result.Name} | {result.Duration} | {result.NumExecutions} |");
260+
}
261+
}
262+
233263
static IHost StartTestApp()
234264
{
235265
var args = new[]

src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,7 @@ class Selenium
1616
const int SeleniumPort = 4444;
1717
static bool RunHeadlessBrowser = true;
1818

19-
static bool PoolForBrowserLogs =
20-
#if DEBUG
21-
true;
22-
#else
23-
false;
24-
#endif
19+
static bool PoolForBrowserLogs = true;
2520

2621
private static async ValueTask<Uri> WaitForServerAsync(int port, CancellationToken cancellationToken)
2722
{

src/Components/benchmarkapps/Wasm.Performance/TestApp/Wasm.Performance.TestApp.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
<PropertyGroup>
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
55
<IsTestAssetProject>true</IsTestAssetProject>
6+
<!--
7+
Chrome in docker appears to run in to cache corruption issues when the cache is read multiple times over.
8+
Clien caching isn't part of our performance measurement, so we'll skip it.
9+
-->
10+
<BlazorCacheBootResources>false</BlazorCacheBootResources>
611
</PropertyGroup>
712

813
<ItemGroup>

0 commit comments

Comments
 (0)