dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.97k stars 4.66k forks source link

System.ComponentModel.DataAnnotations.Validator.TryValidateObject does not work with ICustomTypeDescriptor #58867

Open weifenluo opened 3 years ago

weifenluo commented 3 years ago

In the demo project (full source code here: https://github.com/weifenluo/TestCustomTypeDescriptor), there is a DataClass that implements ICustomTypeDescriptor, with one Name property decorated with Required attribute:

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace TestCustomTypeDescriptor
{
    public class DataClass : ICustomTypeDescriptor
    {
        private string _name;
        public string GetName() => _name;

        public string SetName(string value) => _name = value;

        AttributeCollection ICustomTypeDescriptor.GetAttributes() => CustomTypeDescriptor.GetAttributes();

        string ICustomTypeDescriptor.GetClassName() => CustomTypeDescriptor.GetClassName();

        string ICustomTypeDescriptor.GetComponentName() => CustomTypeDescriptor.GetComponentName();

        TypeConverter ICustomTypeDescriptor.GetConverter() => CustomTypeDescriptor.GetConverter();

        EventDescriptor ICustomTypeDescriptor.GetDefaultEvent() => CustomTypeDescriptor.GetDefaultEvent();

        PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty() => CustomTypeDescriptor.GetDefaultProperty();

        object ICustomTypeDescriptor.GetEditor(Type editorBaseType) => CustomTypeDescriptor.GetEditor(editorBaseType);

        EventDescriptorCollection ICustomTypeDescriptor.GetEvents() => CustomTypeDescriptor.GetEvents();

        EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes) => CustomTypeDescriptor.GetEvents(attributes);

        PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() => CustomTypeDescriptor.GetProperties();

        PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes) => CustomTypeDescriptor.GetProperties(attributes);

        object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd) => CustomTypeDescriptor.GetPropertyOwner(pd);

        private sealed class DataClassTypeDescriptor : CustomTypeDescriptor
        {
            private sealed class NameProperty : PropertyDescriptor
            {
                public static readonly NameProperty Singleton = new NameProperty();

                private NameProperty()
                    : base("Name", new Attribute[] { new RequiredAttribute() })
                {
                }

                public override Type PropertyType => typeof(string);

                public override Type ComponentType => typeof(DataClass);

                public override bool IsReadOnly => false;

                public override object GetValue(object component) => ((DataClass)component).GetName();

                public override void SetValue(object component, object value) => ((DataClass)component).SetName((string)value);

                public override bool CanResetValue(object component) => true;

                public override void ResetValue(object component) => ((DataClass)component).SetName(null);

                public override bool ShouldSerializeValue(object component) => false;
            }

            public static readonly DataClassTypeDescriptor Singleton = new DataClassTypeDescriptor();

            private DataClassTypeDescriptor()
            {
                Properties = new PropertyDescriptorCollection(new PropertyDescriptor[] { NameProperty.Singleton });
            }

            private PropertyDescriptorCollection Properties { get; }

            public override PropertyDescriptorCollection GetProperties()
            {
                return GetProperties(null);
            }

            public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
            {
                bool filtering = attributes != null && attributes.Length > 0;
                if (!filtering)
                    return Properties;

                var result = new PropertyDescriptorCollection(null);
                foreach (PropertyDescriptor prop in Properties)
                {
                    if (prop.Attributes.Contains(attributes))
                        result.Add(prop);
                }

                return result;
            }
        }

        private static ICustomTypeDescriptor CustomTypeDescriptor => DataClassTypeDescriptor.Singleton;
    }
}

This DataClass should be equivalent to:

public class DataClass
{
    [Required]
    public string Name { get; set; }
}

The following unit test failed:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Xunit;

namespace TestCustomTypeDescriptor
{
    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            var data = new DataClass();

            Assert.Null(data.GetName());

            Assert.Single(TypeDescriptor.GetProperties(data));
            Assert.Single(TypeDescriptor.GetProperties(data, new Attribute[] { new RequiredAttribute() }));

            var validationContext = new ValidationContext(data, null, null);
            var validationResults = new List<ValidationResult>();
            Validator.TryValidateObject(data, validationContext, validationResults, validateAllProperties: true);
            Assert.Single(validationResults);
        }
    }
}

When targeting .Net Framework 4.6.1, it throws an exception:

System.ArgumentException : The type 'DataClass' does not contain a public property named 'Name'.
Parameter name: propertyName"

When targeting .Net 5, it failed the last assert:

The collection was expected to contain a single element, but it was empty.
dotnet-issue-labeler[bot] commented 3 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

ghost commented 3 years ago

Tagging subscribers to this area: @safern See info in area-owners.md if you want to be subscribed.

Issue Details
In the demo project (full source code here: https://github.com/weifenluo/TestCustomTypeDescriptor), there is a `DataClass` that implements `ICustomTypeDescriptor`, with one `Name` property decorated with `Required` attribute: ``` using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace TestCustomTypeDescriptor { public class DataClass : ICustomTypeDescriptor { private string _name; public string GetName() => _name; public string SetName(string value) => _name = value; AttributeCollection ICustomTypeDescriptor.GetAttributes() => CustomTypeDescriptor.GetAttributes(); string ICustomTypeDescriptor.GetClassName() => CustomTypeDescriptor.GetClassName(); string ICustomTypeDescriptor.GetComponentName() => CustomTypeDescriptor.GetComponentName(); TypeConverter ICustomTypeDescriptor.GetConverter() => CustomTypeDescriptor.GetConverter(); EventDescriptor ICustomTypeDescriptor.GetDefaultEvent() => CustomTypeDescriptor.GetDefaultEvent(); PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty() => CustomTypeDescriptor.GetDefaultProperty(); object ICustomTypeDescriptor.GetEditor(Type editorBaseType) => CustomTypeDescriptor.GetEditor(editorBaseType); EventDescriptorCollection ICustomTypeDescriptor.GetEvents() => CustomTypeDescriptor.GetEvents(); EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes) => CustomTypeDescriptor.GetEvents(attributes); PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() => CustomTypeDescriptor.GetProperties(); PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes) => CustomTypeDescriptor.GetProperties(attributes); object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd) => CustomTypeDescriptor.GetPropertyOwner(pd); private sealed class DataClassTypeDescriptor : CustomTypeDescriptor { private sealed class NameProperty : PropertyDescriptor { public static readonly NameProperty Singleton = new NameProperty(); private NameProperty() : base("Name", new Attribute[] { new RequiredAttribute() }) { } public override Type PropertyType => typeof(string); public override Type ComponentType => typeof(DataClass); public override bool IsReadOnly => false; public override object GetValue(object component) => ((DataClass)component).GetName(); public override void SetValue(object component, object value) => ((DataClass)component).SetName((string)value); public override bool CanResetValue(object component) => true; public override void ResetValue(object component) => ((DataClass)component).SetName(null); public override bool ShouldSerializeValue(object component) => false; } public static readonly DataClassTypeDescriptor Singleton = new DataClassTypeDescriptor(); private DataClassTypeDescriptor() { Properties = new PropertyDescriptorCollection(new PropertyDescriptor[] { NameProperty.Singleton }); } private PropertyDescriptorCollection Properties { get; } public override PropertyDescriptorCollection GetProperties() { return GetProperties(null); } public override PropertyDescriptorCollection GetProperties(Attribute[] attributes) { bool filtering = attributes != null && attributes.Length > 0; if (!filtering) return Properties; var result = new PropertyDescriptorCollection(null); foreach (PropertyDescriptor prop in Properties) { if (prop.Attributes.Contains(attributes)) result.Add(prop); } return result; } } private static ICustomTypeDescriptor CustomTypeDescriptor => DataClassTypeDescriptor.Singleton; } } ``` This `DataClass` should be equivalent to: ``` public class DataClass { [Required] public string Name { get; set; } } ``` The following unit test failed: ``` using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Xunit; namespace TestCustomTypeDescriptor { public class UnitTest1 { [Fact] public void Test1() { var data = new DataClass(); Assert.Null(data.GetName()); Assert.Single(TypeDescriptor.GetProperties(data)); Assert.Single(TypeDescriptor.GetProperties(data, new Attribute[] { new RequiredAttribute() })); var validationContext = new ValidationContext(data, null, null); var validationResults = new List(); Validator.TryValidateObject(data, validationContext, validationResults, validateAllProperties: true); Assert.Single(validationResults); } } } ``` When targeting .Net Framework 4.6.1, it throws an exception: ``` System.ArgumentException : The type 'DataClass' does not contain a public property named 'Name'. Parameter name: propertyName" ``` When targeting .Net 5, it failed the last assert: ``` The collection was expected to contain a single element, but it was empty. ```
Author: weifenluo
Assignees: -
Labels: `area-System.ComponentModel`, `untriaged`
Milestone: -
ghost commented 3 years ago

Tagging subscribers to this area: @ajcvickers, @bricelam, @roji See info in area-owners.md if you want to be subscribed.

Issue Details
In the demo project (full source code here: https://github.com/weifenluo/TestCustomTypeDescriptor), there is a `DataClass` that implements `ICustomTypeDescriptor`, with one `Name` property decorated with `Required` attribute: ``` using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace TestCustomTypeDescriptor { public class DataClass : ICustomTypeDescriptor { private string _name; public string GetName() => _name; public string SetName(string value) => _name = value; AttributeCollection ICustomTypeDescriptor.GetAttributes() => CustomTypeDescriptor.GetAttributes(); string ICustomTypeDescriptor.GetClassName() => CustomTypeDescriptor.GetClassName(); string ICustomTypeDescriptor.GetComponentName() => CustomTypeDescriptor.GetComponentName(); TypeConverter ICustomTypeDescriptor.GetConverter() => CustomTypeDescriptor.GetConverter(); EventDescriptor ICustomTypeDescriptor.GetDefaultEvent() => CustomTypeDescriptor.GetDefaultEvent(); PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty() => CustomTypeDescriptor.GetDefaultProperty(); object ICustomTypeDescriptor.GetEditor(Type editorBaseType) => CustomTypeDescriptor.GetEditor(editorBaseType); EventDescriptorCollection ICustomTypeDescriptor.GetEvents() => CustomTypeDescriptor.GetEvents(); EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes) => CustomTypeDescriptor.GetEvents(attributes); PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() => CustomTypeDescriptor.GetProperties(); PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes) => CustomTypeDescriptor.GetProperties(attributes); object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd) => CustomTypeDescriptor.GetPropertyOwner(pd); private sealed class DataClassTypeDescriptor : CustomTypeDescriptor { private sealed class NameProperty : PropertyDescriptor { public static readonly NameProperty Singleton = new NameProperty(); private NameProperty() : base("Name", new Attribute[] { new RequiredAttribute() }) { } public override Type PropertyType => typeof(string); public override Type ComponentType => typeof(DataClass); public override bool IsReadOnly => false; public override object GetValue(object component) => ((DataClass)component).GetName(); public override void SetValue(object component, object value) => ((DataClass)component).SetName((string)value); public override bool CanResetValue(object component) => true; public override void ResetValue(object component) => ((DataClass)component).SetName(null); public override bool ShouldSerializeValue(object component) => false; } public static readonly DataClassTypeDescriptor Singleton = new DataClassTypeDescriptor(); private DataClassTypeDescriptor() { Properties = new PropertyDescriptorCollection(new PropertyDescriptor[] { NameProperty.Singleton }); } private PropertyDescriptorCollection Properties { get; } public override PropertyDescriptorCollection GetProperties() { return GetProperties(null); } public override PropertyDescriptorCollection GetProperties(Attribute[] attributes) { bool filtering = attributes != null && attributes.Length > 0; if (!filtering) return Properties; var result = new PropertyDescriptorCollection(null); foreach (PropertyDescriptor prop in Properties) { if (prop.Attributes.Contains(attributes)) result.Add(prop); } return result; } } private static ICustomTypeDescriptor CustomTypeDescriptor => DataClassTypeDescriptor.Singleton; } } ``` This `DataClass` should be equivalent to: ``` public class DataClass { [Required] public string Name { get; set; } } ``` The following unit test failed: ``` using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Xunit; namespace TestCustomTypeDescriptor { public class UnitTest1 { [Fact] public void Test1() { var data = new DataClass(); Assert.Null(data.GetName()); Assert.Single(TypeDescriptor.GetProperties(data)); Assert.Single(TypeDescriptor.GetProperties(data, new Attribute[] { new RequiredAttribute() })); var validationContext = new ValidationContext(data, null, null); var validationResults = new List(); Validator.TryValidateObject(data, validationContext, validationResults, validateAllProperties: true); Assert.Single(validationResults); } } } ``` When targeting .Net Framework 4.6.1, it throws an exception: ``` System.ArgumentException : The type 'DataClass' does not contain a public property named 'Name'. Parameter name: propertyName" ``` When targeting .Net 5, it failed the last assert: ``` The collection was expected to contain a single element, but it was empty. ```
Author: weifenluo
Assignees: -
Labels: `area-System.ComponentModel.DataAnnotations`, `untriaged`
Milestone: -