File: CachedExpressionCompilerTest.cs
Web Access
Project: src\src\Mvc\Mvc.ViewFeatures\test\Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj (Microsoft.AspNetCore.Mvc.ViewFeatures.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Linq.Expressions;
 
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
 
public class CachedExpressionCompilerTest
{
    [Fact]
    public void Process_IdentityExpression()
    {
        // Arrange
        var model = new TestModel();
        var expression = GetTestModelExpression(m => m);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Same(model, result);
    }
 
    [Fact]
    public void Process_CachesIdentityExpression()
    {
        // Arrange
        var expression1 = GetTestModelExpression(m => m);
        var expression2 = GetTestModelExpression(m => m);
 
        // Act
        var func1 = CachedExpressionCompiler.Process(expression1);
        var func2 = CachedExpressionCompiler.Process(expression2);
 
        // Assert
        Assert.NotNull(func1);
        Assert.Same(func1, func2);
    }
 
    [Fact]
    public void Process_ConstLookup()
    {
        // Arrange
        var model = new TestModel();
        var differentModel = new DifferentModel();
        var expression = GetTestModelExpression(m => differentModel);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Same(differentModel, result);
    }
 
    [Fact]
    public void Process_ConstLookup_ReturningNull()
    {
        // Arrange
        var model = new TestModel();
        var expression = GetTestModelExpression(m => (DifferentModel)null);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_ConstLookup_WithNullModel()
    {
        // Arrange
        var differentModel = new DifferentModel();
        var expression = GetTestModelExpression(m => differentModel);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(null);
        Assert.Same(differentModel, result);
    }
 
    [Fact]
    public void Process_ConstLookup_UsingCachedValue()
    {
        // Arrange
        var model = new TestModel();
        var differentModel = new DifferentModel();
        var expression1 = GetTestModelExpression(m => differentModel);
        var expression2 = GetTestModelExpression(m => differentModel);
 
        // Act - 1
        var func1 = CachedExpressionCompiler.Process(expression1);
 
        // Assert - 1
        var result1 = func1(null);
        Assert.Same(differentModel, result1);
 
        // Act - 2
        var func2 = CachedExpressionCompiler.Process(expression2);
 
        // Assert - 2
        var result2 = func1(null);
        Assert.Same(differentModel, result2);
    }
 
    [Fact]
    public void Process_ConstLookup_WhenCapturedLocalChanges()
    {
        // Arrange
        var model = new TestModel();
        var differentModel = new DifferentModel();
        var expression = GetTestModelExpression(m => differentModel);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert - 1
        var result1 = func(null);
        Assert.Same(differentModel, result1);
 
        // Act - 2
        differentModel = new DifferentModel();
 
        // Assert - 2
        var result2 = func(null);
        Assert.NotSame(differentModel, result1);
        Assert.Same(differentModel, result2);
    }
 
    [Fact]
    public void Process_ConstLookup_WithPrimitiveConstant()
    {
        // Arrange
        var model = new TestModel();
        var expression = GetTestModelExpression(m => 10);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(10, result);
    }
 
    [Fact]
    public void Process_StaticFieldAccess()
    {
        // Arrange
        var model = new TestModel();
        var expression = GetTestModelExpression(m => TestModel.StaticField);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal("StaticValue", result);
    }
 
    [Fact]
    public void Process_CachesStaticFieldAccess()
    {
        // Arrange
        var expression1 = GetTestModelExpression(m => TestModel.StaticField);
        var expression2 = GetTestModelExpression(m => TestModel.StaticField);
 
        // Act
        var func1 = CachedExpressionCompiler.Process(expression1);
        var func2 = CachedExpressionCompiler.Process(expression2);
 
        // Assert
        Assert.NotNull(func1);
        Assert.Same(func1, func2);
    }
 
    [Fact]
    public void Process_StaticPropertyAccess()
    {
        // Arrange
        var expected = "TestValue";
        TestModel.StaticProperty = expected;
        var model = new TestModel();
        var expression = GetTestModelExpression(m => TestModel.StaticProperty);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void Process_CachesStaticPropertyAccess()
    {
        // Arrange
        var expression1 = GetTestModelExpression(m => TestModel.StaticProperty);
        var expression2 = GetTestModelExpression(m => TestModel.StaticProperty);
 
        // Act
        var func1 = CachedExpressionCompiler.Process(expression1);
        var func2 = CachedExpressionCompiler.Process(expression2);
 
        // Assert
        Assert.NotNull(func1);
        Assert.Same(func1, func2);
    }
 
    [Fact]
    public void Process_StaticPropertyAccess_WithNullModel()
    {
        // Arrange
        var expected = "TestValue";
        TestModel.StaticProperty = expected;
        var expression = GetTestModelExpression(m => TestModel.StaticProperty);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(null);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void Process_ConstFieldLookup()
    {
        // Arrange
        var model = new TestModel();
        var expression = GetTestModelExpression(m => DifferentModel.Constant);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(10, result);
    }
 
    [Fact]
    public void Process_ConstFieldLookup_WthNullModel()
    {
        // Arrange
        var expression = GetTestModelExpression(m => DifferentModel.Constant);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(null);
        Assert.Equal(10, result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess()
    {
        // Arrange
        var model = new TestModel { Name = "Test" };
        var expression = GetTestModelExpression(m => m.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal("Test", result);
    }
 
    [Fact]
    public void Process_CachesSimpleMemberAccess()
    {
        // Arrange
        var expression1 = GetTestModelExpression(m => m.Name);
        var expression2 = GetTestModelExpression(m => m.Name);
 
        // Act
        var func1 = CachedExpressionCompiler.Process(expression1);
        var func2 = CachedExpressionCompiler.Process(expression2);
 
        // Assert
        Assert.NotNull(func1);
        Assert.Same(func1, func2);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_ToPrimitive()
    {
        // Arrange
        var model = new TestModel { Age = 12 };
        var expression = GetTestModelExpression(m => m.Age);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(12, result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_WithNullModel()
    {
        // Arrange
        var model = (TestModel)null;
        var expression = GetTestModelExpression(m => m.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_ToPrimitive_WithNullModel()
    {
        // Arrange
        var model = (TestModel)null;
        var expression = GetTestModelExpression(m => m.Age);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_OnTypeWithBadEqualityComparer()
    {
        // Arrange
        var model = new BadEqualityModel { Id = 7 };
        var expression = GetExpression<BadEqualityModel, int>(m => m.Id);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(7, result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_OnTypeWithBadEqualityComparer_WithNullModel()
    {
        // Arrange
        var model = (BadEqualityModel)null;
        var expression = GetExpression<BadEqualityModel, int>(m => m.Id);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_OnValueTypeWithBadEqualityComparer()
    {
        // Arrange
        var model = new BadEqualityValueTypeModel { Id = 7 };
        var expression = GetExpression<BadEqualityValueTypeModel, int>(m => m.Id);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(7, result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_OnTypeWithBadEqualityComparer_WithDefaultValue()
    {
        // Arrange
        var model = (BadEqualityValueTypeModel)default;
        var expression = GetExpression<BadEqualityValueTypeModel, int>(m => m.Id);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(model.Id, result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_OnValueType()
    {
        // Arrange
        var model = new DateTime(2000, 1, 1);
        var expression = GetExpression<DateTime, int>(m => m.Year);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(2000, result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_OnValueType_WithDefaultValue()
    {
        // Arrange
        var model = default(DateTime);
        var expression = GetExpression<DateTime, int>(m => m.Year);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(1, result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_OnNullableValueType()
    {
        // Arrange
        var model = new DateTime(2000, 1, 1);
        var nullableModel = (DateTime?)model;
        var expression = GetExpression<DateTime?, DateTime>(m => m.Value);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(nullableModel);
        Assert.Equal(model, result);
    }
 
    [Fact]
    public void Process_SimpleMemberAccess_OnNullableValueType_WithNullValue()
    {
        // Arrange
        var nullableModel = (DateTime?)null;
        var expression = GetExpression<DateTime?, DateTime>(m => m.Value);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(nullableModel);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_ToValueType()
    {
        // Arrange
        var dateTime = new DateTime(2000, 1, 1);
        var model = new TestModel { Date = dateTime };
        var expression = GetTestModelExpression(m => m.Date.Year);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(2000, result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_ToValueType_WithNullModel()
    {
        // Arrange
        var model = (TestModel)null;
        var expression = GetTestModelExpression(m => m.Date.Year);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_ToReferenceType()
    {
        // Arrange
        var expected = "Test1";
        var model = new TestModel { DifferentModel = new DifferentModel { Name = expected } };
        var expression = GetTestModelExpression(m => m.DifferentModel.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void Process_CachesChainedMemberAccess()
    {
        // Arrange
        var expression1 = GetTestModelExpression(m => m.DifferentModel.Name);
        var expression2 = GetTestModelExpression(m => m.DifferentModel.Name);
 
        // Act
        var func1 = CachedExpressionCompiler.Process(expression1);
        var func2 = CachedExpressionCompiler.Process(expression2);
 
        // Assert
        Assert.NotNull(func1);
        Assert.Same(func1, func2);
    }
 
    [Fact]
    public void Process_CachesChainedMemberAccess_ToValueType()
    {
        // Arrange
        var expression1 = GetTestModelExpression(m => m.Date.Year);
        var expression2 = GetTestModelExpression(m => m.Date.Year);
 
        // Act
        var func1 = CachedExpressionCompiler.Process(expression1);
        var func2 = CachedExpressionCompiler.Process(expression2);
 
        // Assert
        Assert.NotNull(func1);
        Assert.Same(func1, func2);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_LongChain_WithReferenceType()
    {
        // Arrange
        var expected = "TestVal";
        var model = new Chain0Model
        {
            Chain1 = new Chain1Model
            {
                ValueTypeModel = new ValueType1
                {
                    TestModel = new TestModel { DifferentModel = new DifferentModel { Name = expected } }
                }
            }
        };
 
        var expression = GetExpression<Chain0Model, string>(m => m.Chain1.ValueTypeModel.TestModel.DifferentModel.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_LongChain_WithNullIntermediary()
    {
        // Arrange
        var model = new Chain0Model
        {
            Chain1 = new Chain1Model
            {
                ValueTypeModel = new ValueType1 { TestModel = null },
            }
        };
 
        var expression = GetExpression<Chain0Model, string>(m => m.Chain1.ValueTypeModel.TestModel.DifferentModel.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_LongChain_WithNullValueTypeAccessor()
    {
        // Arrange
        // Chain2 is a value type
        var model = new Chain0Model
        {
            Chain1 = null
        };
 
        var expression = GetExpression<Chain0Model, string>(m => m.Chain1.ValueTypeModel.TestModel.DifferentModel.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_LongChain_WithNullableValueType()
    {
        // Arrange
        var expected = "TestVal";
        var model = new Chain0Model
        {
            Chain1 = new Chain1Model
            {
                NullableValueTypeModel = new ValueType1
                {
                    TestModel = new TestModel { DifferentModel = new DifferentModel { Name = expected } }
                }
            }
        };
 
        var expression = GetExpression<Chain0Model, string>(m => m.Chain1.NullableValueTypeModel.Value.TestModel.DifferentModel.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_LongChain_WithNullValuedNullableValueType()
    {
        // Arrange
        var model = new Chain0Model
        {
            Chain1 = new Chain1Model
            {
                NullableValueTypeModel = null
            }
        };
 
        var expression = GetExpression<Chain0Model, string>(m => m.Chain1.NullableValueTypeModel.Value.TestModel.DifferentModel.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_ToReferenceType_WithNullIntermediary()
    {
        // Arrange
        var model = new TestModel { DifferentModel = null };
        var expression = GetTestModelExpression(m => m.DifferentModel.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_ToReferenceType_WithNullModel()
    {
        // Arrange
        var model = (TestModel)null;
        var expression = GetTestModelExpression(m => m.DifferentModel.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_OfValueTypes_ReturningReferenceTypeMember()
    {
        // Arrange
        var expected = "TestName";
        var model = new ValueType1
        {
            ValueType2 = new ValueType2 { Name = expected },
        };
        var expression = GetExpression<ValueType1, string>(m => m.ValueType2.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_OfValueTypes_ReturningValueType()
    {
        // Arrange
        var expected = new DateTime(2001, 1, 1);
        var model = new ValueType1
        {
            ValueType2 = new ValueType2 { Date = expected },
        };
        var expression = GetExpression<ValueType1, DateTime>(m => m.ValueType2.Date);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_OfValueTypes_IncludingNullableType()
    {
        // Arrange
        var expected = "TestName";
        var model = new ValueType1
        {
            NullableValueType2 = new ValueType2 { Name = expected },
        };
        var expression = GetExpression<ValueType1, string>(m => m.NullableValueType2.Value.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_OfValueTypes_WithNullValuedNullable()
    {
        // Arrange
        var model = new ValueType1 { NullableValueType2 = null };
        var expression = GetExpression<ValueType1, string>(m => m.NullableValueType2.Value.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_ChainedMemberAccess_OfValueTypes_WithNullValuedNullable_ReturningValueType()
    {
        // Arrange
        var model = new ValueType1 { NullableValueType2 = null };
        var expression = GetExpression<ValueType1, DateTime>(m => m.NullableValueType2.Value.Date);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Null(result);
    }
 
    [Fact]
    public void Process_MemberAccessOnCapturedVariable_ReturnsNull()
    {
        // Arrange
        var differentModel = new DifferentModel { Name = "Test" };
        var expression = GetTestModelExpression(m => differentModel.Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.Null(func);
    }
 
    [Fact]
    public void Process_CapturedVariable()
    {
        // Arrange
        var differentModel = new DifferentModel();
        var model = new TestModel();
        var expression = GetTestModelExpression(m => differentModel);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Same(differentModel, result);
    }
 
    [Fact]
    public void Process_CapturedVariable_WithNullModel()
    {
        // Arrange
        var differentModel = new DifferentModel();
        var model = (TestModel)null;
        var expression = GetTestModelExpression(m => differentModel);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.NotNull(func);
        var result = func(model);
        Assert.Same(differentModel, result);
    }
 
    [Fact]
    public void Process_MemberAccess_OnCapturedVariable_ReturnsNull()
    {
        // Arrange
        var differentModel = "Hello world";
        var expression = GetTestModelExpression(m => differentModel.Length);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.Null(func);
    }
 
    [Fact]
    public void Process_ComplexChainedMemberAccess_ReturnsNull()
    {
        // Arrange
        var expected = "SomeName";
        var model = new TestModel { DifferentModels = new[] { new DifferentModel { Name = expected } } };
        var expression = GetTestModelExpression(m => m.DifferentModels[0].Name);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.Null(func);
    }
 
    [Fact]
    public void Process_ArrayMemberAccess_ReturnsNull()
    {
        // Arrange
        var expression = GetTestModelExpression(m => m.Sizes[1]);
 
        // Act
        var func = CachedExpressionCompiler.Process(expression);
 
        // Assert
        Assert.Null(func);
    }
 
    private static Expression<Func<TModel, TResult>> GetExpression<TModel, TResult>(Expression<Func<TModel, TResult>> expression)
        => expression;
 
    private static Expression<Func<TestModel, TResult>> GetTestModelExpression<TResult>(Expression<Func<TestModel, TResult>> expression)
        => GetExpression(expression);
 
    public class TestModel
    {
        public static readonly string StaticField = "StaticValue";
 
        public static string StaticProperty { get; set; }
 
        public int Age { get; set; }
 
        public string Name { get; set; }
 
        public DateTime Date { get; set; }
 
        public DifferentModel DifferentModel { get; set; }
 
        public int[] Sizes { get; set; }
 
        public DifferentModel[] DifferentModels { get; set; }
    }
 
    public class DifferentModel
    {
        public const int Constant = 10;
 
        public string Name { get; set; }
    }
 
    public class Chain0Model
    {
        public Chain1Model Chain1 { get; set; }
    }
 
    public class Chain1Model
    {
        public ValueType1 ValueTypeModel { get; set; }
 
        public ValueType1? NullableValueTypeModel { get; set; }
    }
 
    public struct ValueType1
    {
        public TestModel TestModel { get; set; }
 
        public ValueType2 ValueType2 { get; set; }
 
        public ValueType2? NullableValueType2 { get; set; }
    }
 
    public struct ValueType2
    {
        public string Name { get; set; }
 
        public DateTime Date { get; set; }
    }
 
    public class BadEqualityModel
    {
        public int Id { get; set; }
 
        public override bool Equals(object obj)
        {
            return this == obj;
        }
 
        public static bool operator ==(BadEqualityModel a, object b)
        {
            if (a is null || b is null)
            {
                throw new TimeZoneNotFoundException();
            }
 
            return true;
        }
 
        public static bool operator !=(BadEqualityModel a, object b)
        {
            return !(a == b);
        }
 
        public override int GetHashCode() => 0;
    }
 
    public struct BadEqualityValueTypeModel
    {
        public int Id { get; set; }
 
        public override bool Equals(object obj)
        {
            return this == obj;
        }
 
        public static bool operator ==(BadEqualityValueTypeModel a, object b)
        {
            if (b is null)
            {
                throw new TimeZoneNotFoundException();
            }
 
            return true;
        }
 
        public static bool operator !=(BadEqualityValueTypeModel a, object b)
        {
            return !(a == b);
        }
 
        public override int GetHashCode() => 0;
    }
}