diff --git a/src/Components/Forms/src/FieldIdentifier.cs b/src/Components/Forms/src/FieldIdentifier.cs index 3c147a02841f..3a5eab25b70c 100644 --- a/src/Components/Forms/src/FieldIdentifier.cs +++ b/src/Components/Forms/src/FieldIdentifier.cs @@ -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; @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.Forms; /// public readonly struct FieldIdentifier : IEquatable { - private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), Func> _fieldAccessors = new(); + private static readonly ConcurrentDictionary<(Type ModelType, MemberInfo Member), Func> _fieldAccessors = new(); static FieldIdentifier() { @@ -151,7 +152,7 @@ private static void ParseAccessor(Expression> accessor, out object mo internal static object GetModelFromMemberAccess( MemberExpression member, - ConcurrentDictionary<(Type ModelType, string FieldName), Func>? cache = null) + ConcurrentDictionary<(Type ModelType, MemberInfo Member), Func>? cache = null) { cache ??= _fieldAccessors; Func? accessor = null; @@ -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; @@ -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 CreateAccessor((Type model, string member) arg) + static Func 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>(expression, parameter); diff --git a/src/Components/Forms/test/FieldIdentifierTest.cs b/src/Components/Forms/test/FieldIdentifierTest.cs index 0f4b0c903576..c767f2fa7d85 100644 --- a/src/Components/Forms/test/FieldIdentifierTest.cs +++ b/src/Components/Forms/test/FieldIdentifierTest.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Linq.Expressions; +using System.Reflection; namespace Microsoft.AspNetCore.Components.Forms; @@ -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>(); + var cache = new ConcurrentDictionary<(Type ModelType, MemberInfo FieldName), Func>(); var result = new TestModel[2]; for (var i = 0; i < models.Length; i++) { @@ -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 @@ -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; + } }