File: FieldIdentifierTest.cs
Web Access
Project: src\src\Components\Forms\test\Microsoft.AspNetCore.Components.Forms.Tests.csproj (Microsoft.AspNetCore.Components.Forms.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
 
namespace Microsoft.AspNetCore.Components.Forms;
 
public class FieldIdentifierTest
{
    [Fact]
    public void CannotUseNullModel()
    {
        var ex = Assert.Throws<ArgumentNullException>(() => new FieldIdentifier(null, "somefield"));
        Assert.Equal("model", ex.ParamName);
    }
 
    [Fact]
    public void CannotUseValueTypeModel()
    {
        var ex = Assert.Throws<ArgumentException>(() => new FieldIdentifier(DateTime.Now, "somefield"));
        Assert.Equal("model", ex.ParamName);
        Assert.StartsWith("The model must be a reference-typed object.", ex.Message);
    }
 
    [Fact]
    public void CannotUseNullFieldName()
    {
        var ex = Assert.Throws<ArgumentNullException>(() => new FieldIdentifier(new object(), null));
        Assert.Equal("fieldName", ex.ParamName);
    }
 
    [Fact]
    public void CanUseEmptyFieldName()
    {
        var fieldIdentifier = new FieldIdentifier(new object(), string.Empty);
        Assert.Equal(string.Empty, fieldIdentifier.FieldName);
    }
 
    [Fact]
    public void CanGetModelAndFieldName()
    {
        // Arrange/Act
        var model = new object();
        var fieldIdentifier = new FieldIdentifier(model, "someField");
 
        // Assert
        Assert.Same(model, fieldIdentifier.Model);
        Assert.Equal("someField", fieldIdentifier.FieldName);
    }
 
    [Fact]
    public void DistinctModelsProduceDistinctHashCodesAndNonEquality()
    {
        // Arrange
        var fieldIdentifier1 = new FieldIdentifier(new object(), "field");
        var fieldIdentifier2 = new FieldIdentifier(new object(), "field");
 
        // Act/Assert
        Assert.NotEqual(fieldIdentifier1.GetHashCode(), fieldIdentifier2.GetHashCode());
        Assert.False(fieldIdentifier1.Equals(fieldIdentifier2));
    }
 
    [Fact]
    public void DistinctFieldNamesProduceDistinctHashCodesAndNonEquality()
    {
        // Arrange
        var model = new object();
        var fieldIdentifier1 = new FieldIdentifier(model, "field1");
        var fieldIdentifier2 = new FieldIdentifier(model, "field2");
 
        // Act/Assert
        Assert.NotEqual(fieldIdentifier1.GetHashCode(), fieldIdentifier2.GetHashCode());
        Assert.False(fieldIdentifier1.Equals(fieldIdentifier2));
    }
 
    [Fact]
    public void FieldIdentifier_ForModelWithoutField_ProduceSameHashCodesAndEquality()
    {
        // Arrange
        var model = new object();
        var fieldIdentifier1 = new FieldIdentifier(model, fieldName: string.Empty);
        var fieldIdentifier2 = new FieldIdentifier(model, fieldName: string.Empty);
 
        // Act/Assert
        Assert.Equal(fieldIdentifier1.GetHashCode(), fieldIdentifier2.GetHashCode());
        Assert.True(fieldIdentifier1.Equals(fieldIdentifier2));
    }
 
    [Fact]
    public void SameContentsProduceSameHashCodesAndEquality()
    {
        // Arrange
        var model = new object();
        var fieldIdentifier1 = new FieldIdentifier(model, "field");
        var fieldIdentifier2 = new FieldIdentifier(model, "field");
 
        // Act/Assert
        Assert.Equal(fieldIdentifier1.GetHashCode(), fieldIdentifier2.GetHashCode());
        Assert.True(fieldIdentifier1.Equals(fieldIdentifier2));
    }
 
    [Fact]
    public void SameContents_WithOverridenEqualsAndGetHashCode_ProduceSameHashCodesAndEquality()
    {
        // Arrange
        var model = new EquatableModel();
        var fieldIdentifier1 = new FieldIdentifier(model, nameof(EquatableModel.Property));
        model.Property = "changed value"; // To show it makes no difference if the overridden `GetHashCode` result changes
        var fieldIdentifier2 = new FieldIdentifier(model, nameof(EquatableModel.Property));
 
        // Act/Assert
        Assert.Equal(fieldIdentifier1.GetHashCode(), fieldIdentifier2.GetHashCode());
        Assert.True(fieldIdentifier1.Equals(fieldIdentifier2));
    }
 
    [Fact]
    public void FieldNamesAreCaseSensitive()
    {
        // Arrange
        var model = new object();
        var fieldIdentifierLower = new FieldIdentifier(model, "field");
        var fieldIdentifierPascal = new FieldIdentifier(model, "Field");
 
        // Act/Assert
        Assert.Equal("field", fieldIdentifierLower.FieldName);
        Assert.Equal("Field", fieldIdentifierPascal.FieldName);
        Assert.NotEqual(fieldIdentifierLower.GetHashCode(), fieldIdentifierPascal.GetHashCode());
        Assert.False(fieldIdentifierLower.Equals(fieldIdentifierPascal));
    }
 
    [Fact]
    public void CanCreateFromExpression_Property()
    {
        var model = new TestModel();
        var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
        Assert.Same(model, fieldIdentifier.Model);
        Assert.Equal(nameof(model.StringProperty), fieldIdentifier.FieldName);
    }
 
    [Fact]
    public void CanCreateFromExpression_PropertyUsesCache()
    {
        var models = new TestModel[] { new TestModel(), new TestModel() };
        var cache = new ConcurrentDictionary<(Type ModelType, MemberInfo FieldName), Func<object, object>>();
        var result = new TestModel[2];
        for (var i = 0; i < models.Length; i++)
        {
            var model = models[i];
            LambdaExpression expression = () => model.StringProperty;
            var body = expression.Body as MemberExpression;
            var value = FieldIdentifier.GetModelFromMemberAccess((MemberExpression)body.Expression, cache);
            result[i] = Assert.IsType<TestModel>(value);
        }
 
        Assert.Single(cache);
        Assert.Equal(models, result);
    }
 
    [Fact]
    public void CannotCreateFromExpression_NonMember()
    {
        var ex = Assert.Throws<ArgumentException>(() =>
            FieldIdentifier.Create(() => new TestModel()));
        Assert.Equal($"The provided expression contains a NewExpression which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.", ex.Message);
    }
 
    [Fact]
    public void CanCreateFromExpression_Field()
    {
        var model = new TestModel();
        var fieldIdentifier = FieldIdentifier.Create(() => model.StringField);
        Assert.Same(model, fieldIdentifier.Model);
        Assert.Equal(nameof(model.StringField), fieldIdentifier.FieldName);
    }
 
    [Fact]
    public void CanCreateFromExpression_WithCastToObject()
    {
        // This case is needed because, if a component is declared as receiving
        // an Expression<Func<object>>, then any value types will be implicitly cast
        var model = new TestModel();
        Expression<Func<object>> accessor = () => model.IntProperty;
        var fieldIdentifier = FieldIdentifier.Create(accessor);
        Assert.Same(model, fieldIdentifier.Model);
        Assert.Equal(nameof(model.IntProperty), fieldIdentifier.FieldName);
    }
 
    [Fact]
    public void CanCreateFromExpression_MemberOfConstantExpression()
    {
        var fieldIdentifier = FieldIdentifier.Create(() => StringPropertyOnThisClass);
        Assert.Same(this, fieldIdentifier.Model);
        Assert.Equal(nameof(StringPropertyOnThisClass), fieldIdentifier.FieldName);
    }
 
    [Fact]
    public void CanCreateFromExpression_MemberOfChildObject()
    {
        var parentModel = new ParentModel { Child = new TestModel() };
        var fieldIdentifier = FieldIdentifier.Create(() => parentModel.Child.StringField);
        Assert.Same(parentModel.Child, fieldIdentifier.Model);
        Assert.Equal(nameof(TestModel.StringField), fieldIdentifier.FieldName);
    }
 
    [Fact]
    public void CanCreateFromExpression_MemberOfIndexedCollectionEntry()
    {
        var models = new List<TestModel>() { null, new TestModel() };
        var fieldIdentifier = FieldIdentifier.Create(() => models[1].StringField);
        Assert.Same(models[1], fieldIdentifier.Model);
        Assert.Equal(nameof(TestModel.StringField), fieldIdentifier.FieldName);
    }
 
    [Fact]
    public void CanCreateFromExpression_MemberOfObjectWithCast()
    {
        var model = new TestModel();
        var fieldIdentifier = FieldIdentifier.Create(() => ((TestModel)(object)model).StringField);
        Assert.Same(model, fieldIdentifier.Model);
        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
    {
        public string StringProperty { get; set; }
 
        public int IntProperty { get; set; }
 
#pragma warning disable 649
        public string StringField;
#pragma warning restore 649
    }
 
    class ParentModel
    {
        public TestModel Child { get; set; }
    }
 
    class EquatableModel : IEquatable<EquatableModel>
    {
        public string Property { get; set; } = "";
 
        public bool Equals(EquatableModel other)
        {
            return string.Equals(Property, other?.Property, StringComparison.Ordinal);
        }
 
        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;
    }
}