Skip to content

[release/8.0] [Blazor] Pass the member info directly #53892

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/Components/Forms/src/FieldIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components.HotReload;

Expand All @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.Forms;
/// </summary>
public readonly struct FieldIdentifier : IEquatable<FieldIdentifier>
{
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), Func<object, object>> _fieldAccessors = new();
private static readonly ConcurrentDictionary<(Type ModelType, MemberInfo Member), Func<object, object>> _fieldAccessors = new();

static FieldIdentifier()
{
Expand Down Expand Up @@ -151,7 +152,7 @@ private static void ParseAccessor<T>(Expression<Func<T>> accessor, out object mo

internal static object GetModelFromMemberAccess(
MemberExpression member,
ConcurrentDictionary<(Type ModelType, string FieldName), Func<object, object>>? cache = null)
ConcurrentDictionary<(Type ModelType, MemberInfo Member), Func<object, object>>? cache = null)
{
cache ??= _fieldAccessors;
Func<object, object>? accessor = null;
Expand All @@ -160,7 +161,7 @@ internal static object GetModelFromMemberAccess(
{
case ConstantExpression model:
value = model.Value ?? throw new ArgumentException("The provided expression must evaluate to a non-null value.");
accessor = cache.GetOrAdd((value.GetType(), member.Member.Name), CreateAccessor);
accessor = cache.GetOrAdd((value.GetType(), member.Member), CreateAccessor);
break;
default:
break;
Expand All @@ -183,11 +184,12 @@ internal static object GetModelFromMemberAccess(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Application code does not get trimmed. We expect the members in the expression to not be trimmed.")]
static Func<object, object> CreateAccessor((Type model, string member) arg)
static Func<object, object> CreateAccessor((Type model, MemberInfo member) arg)
{
var parameter = Expression.Parameter(typeof(object), "value");
Expression expression = Expression.Convert(parameter, arg.model);
expression = Expression.PropertyOrField(expression, arg.member);

expression = Expression.MakeMemberAccess(expression, arg.member);
expression = Expression.Convert(expression, typeof(object));
var lambda = Expression.Lambda<Func<object, object>>(expression, parameter);

Expand Down
73 changes: 72 additions & 1 deletion src/Components/Forms/test/FieldIdentifierTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;

namespace Microsoft.AspNetCore.Components.Forms;

Expand Down Expand Up @@ -142,7 +143,7 @@ public void CanCreateFromExpression_Property()
public void CanCreateFromExpression_PropertyUsesCache()
{
var models = new TestModel[] { new TestModel(), new TestModel() };
var cache = new ConcurrentDictionary<(Type ModelType, string FieldName), Func<object, object>>();
var cache = new ConcurrentDictionary<(Type ModelType, MemberInfo FieldName), Func<object, object>>();
var result = new TestModel[2];
for (var i = 0; i < models.Length; i++)
{
Expand Down Expand Up @@ -221,6 +222,53 @@ public void CanCreateFromExpression_MemberOfObjectWithCast()
Assert.Equal(nameof(TestModel.StringField), fieldIdentifier.FieldName);
}

[Fact]
public void CanCreateFromExpression_DifferentCaseField()
{
var fieldIdentifier = FieldIdentifier.Create(() => model.Field);
Assert.Same(model, fieldIdentifier.Model);
Assert.Equal(nameof(model.Field), fieldIdentifier.FieldName);
}

private DifferentCaseFieldModel model = new() { Field = 1 };
#pragma warning disable CA1823 // This is used in the test above
private DifferentCaseFieldModel Model = new() { field = 2 };
#pragma warning restore CA1823 // Avoid unused private fields

[Fact]
public void CanCreateFromExpression_DifferentCaseProperty()
{
var fieldIdentifier = FieldIdentifier.Create(() => Model2.Property);
Assert.Same(Model2, fieldIdentifier.Model);
Assert.Equal(nameof(Model2.Property), fieldIdentifier.FieldName);
}

protected DifferentCasePropertyModel Model2 { get; } = new() { property = 1 };

protected DifferentCasePropertyModel model2 { get; } = new() { Property = 2 };

[Fact]
public void CanCreateFromExpression_DifferentCasePropertyAndField()
{
var fieldIdentifier = FieldIdentifier.Create(() => model3.Value);
Assert.Same(model3, fieldIdentifier.Model);
Assert.Equal(nameof(Model3.Value), fieldIdentifier.FieldName);
}

[Fact]
public void CanCreateFromExpression_NonAsciiCharacters()
{
var fieldIdentifier = FieldIdentifier.Create(() => @ÖvrigAnställning.Ort);
Assert.Same(@ÖvrigAnställning, fieldIdentifier.Model);
Assert.Equal(nameof(@ÖvrigAnställning.Ort), fieldIdentifier.FieldName);
}

public DifferentCasePropertyFieldModel Model3 { get; } = new() { value = 1 };

public DifferentCasePropertyFieldModel model3 = new() { Value = 2 };

public ÖvrigAnställningModel @ÖvrigAnställning { get; set; } = new();

string StringPropertyOnThisClass { get; set; }

class TestModel
Expand Down Expand Up @@ -253,4 +301,27 @@ public override int GetHashCode()
return StringComparer.Ordinal.GetHashCode(Property);
}
}

public class ÖvrigAnställningModel
{
public int Ort { get; set; }
}

private class DifferentCaseFieldModel
{
public int Field;
public int field;
}

protected class DifferentCasePropertyModel
{
public int Property { get; set; }
public int property { get; set; }
}

public class DifferentCasePropertyFieldModel
{
public int Value { get; set; }
public int value;
}
}