Search code examples
c#asp.netasp.net-web-apitypeconverter

Multiple classes sharing 1 type converter


I have multiple DTO class which require type converter. The following is one of the implementations. As you will see, I need ConvertFrom only.

public class EmployeeFilterTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        if (typeof(string) == sourceType)
            return true;
        return base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        var strVal = value as String;
        if (string.IsNullOrEmpty(strVal))
            return new EmployeeFilter();
        EmployeeFilter employeeFilter = new EmployeeFilter();
        string[] filters = strVal.Split(';');

        foreach (var filter in filters)
        {
            var filterSplit = filter.Split(':');
            if (filterSplit.Length == 2)
            {
                var key = filterSplit[0];
                var val = filterSplit[1];
                SetPropertyValue(employeeFilter, key, val);
            }
        }
        return employeeFilter;
    }

    private void SetPropertyValue(EmployeeFilter employeeFilter, string key, string val)
    {
        var t = typeof(EmployeeFilter);
        PropertyInfo[] props = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);
        PropertyInfo prop = props.Where(p => p.Name.Equals(key, StringComparison.CurrentCultureIgnoreCase) == true && p.CanWrite).FirstOrDefault();
        if (prop != null)
            prop.SetValue(employeeFilter, val);
    }
}

I want to make multiple DTOs sharing the same converter in hopes of reducing code duplication as well as tests and after some researches, I have 2 problems at hand

  1. Get the type that I want to convert in ConvertFrom method
  2. Using Type class to initialize new object

For the first one, I don't know how to get from ITypeDescriptorContext.

For the second one, I will use the following according to this post

Type employeeType = typeof(EmployeeFilter);
object objtype = Activator.CreateInstance(employeeType);

So, how to get the type that I want to convert to?


Solution

  • Test case

    public class testConverter
    {
        [Theory]
        [InlineData(typeof(string), true)]
        [InlineData(typeof(int), false)]
        public void testCanConvertFrom(Type sourceType, bool expected)
        {
            //Arrange
            Type randomType = typeof(Book);
            Type typeGenericConverter = typeof(EmployeeFilterTypeConverter<>);
            Type typeActualConverter = typeGenericConverter.MakeGenericType(randomType);
            /*
                1. The way of creating EmployeeFilterTypeConverter<thattype>
                    https://stackoverflow.com/a/266282
             */
            dynamic testConverter = Activator.CreateInstance(typeActualConverter);
            Mock<ITypeDescriptorContext> mockDescContext = new Mock<ITypeDescriptorContext>();
            //Act
            bool actual = testConverter.CanConvertFrom(mockDescContext.Object, sourceType);
            //Assert
            Assert.Equal(expected, actual);
        }
        [Theory, ClassData(typeof(TestConvertFromType1))]
        /*
            1. All these classdata, propertydata stuff just for passing complex objects to test
                https://stackoverflow.com/a/22093968
         */
        public void testConverFromType1(object value, EmployeeFilter expected)
        {
            //api/employee?filter=firstName:Nikhil;lastName:Doomra
            //Arrange
            EmployeeFilterTypeConverter<EmployeeFilter> testConverter = new EmployeeFilterTypeConverter<EmployeeFilter>();
            Mock<ITypeDescriptorContext> mockDescContext = new Mock<ITypeDescriptorContext>();
            //Act
            EmployeeFilter actual = testConverter.ConvertFrom(mockDescContext.Object, null, value) as EmployeeFilter;
            //Assert
            //public static void Equal<T>(T expected, T actual);
            Assert.Equal(expected, actual);
        }
        [Theory, ClassData(typeof(TestConvertFromType2))]
        public void testConverFromType2(object value, GeoPoint expected)
        {
            //api/employee?filter=firstName:Nikhil;lastName:Doomra
            //Arrange
            EmployeeFilterTypeConverter<GeoPoint> testConverter = new EmployeeFilterTypeConverter<GeoPoint>();
            Mock<ITypeDescriptorContext> mockDescContext = new Mock<ITypeDescriptorContext>();
            //Act
            GeoPoint actual = testConverter.ConvertFrom(mockDescContext.Object, null, value) as GeoPoint;
            //Assert
            //public static void Equal<T>(T expected, T actual);
            Assert.Equal(expected, actual);
        }
    }
    

    Test Data Model

    public class TestConvertFromType1: IEnumerable<object[]>
    {
        private readonly List<object[]> _data = new List<object[]>
        {
            new object[] { "firstName:Nikhil;lastName:Doomra",
                new EmployeeFilter {
                FirstName = "Nikhil", LastName = "Doomra"
                }},
            new object[] { "firstName:Nikhil",
                new EmployeeFilter {
                FirstName = "Nikhil"
                }}
        };
        public IEnumerator<object[]> GetEnumerator()
        { return _data.GetEnumerator(); }
    
        IEnumerator IEnumerable.GetEnumerator()
        { return GetEnumerator(); }
    }
    public class TestConvertFromType2 : IEnumerable<object[]>
    {
        private readonly List<object[]> _data = new List<object[]>
        {
            new object[] { "Latitude:12.345;Longitude:342.12",
                new GeoPoint {
                Latitude = 12.345, Longitude = 342.12
                }},
            new object[] { "Latitude:11.234;Longitude:345.12",
                new GeoPoint {
                Latitude = 11.234, Longitude = 345.12
                }}
        };
        public IEnumerator<object[]> GetEnumerator()
        { return _data.GetEnumerator(); }
    
        IEnumerator IEnumerable.GetEnumerator()
        { return GetEnumerator(); }
    }
    

    Generic Converter

    public class EmployeeFilterTypeConverter<T> : TypeConverter where T: new()
        /*
            1. You can't declare T type = new T() without this constraint
                Evidently it is because compiler can't say what is the type!
                https://stackoverflow.com/a/29345294
         */
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (typeof(string) == sourceType)
                return true;
            return base.CanConvertFrom(context, sourceType);
        }
    
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            var strVal = value as String;
            if (string.IsNullOrEmpty(strVal))
                return new EmployeeFilter();
    
            T converTo = new T();
    
            string[] filters = strVal.Split(';');
    
            foreach (var filter in filters)
            {
                string[] filterSplit = filter.Split(':');
                if (filterSplit.Length == 2)
                {
                    string key = filterSplit[0];
                    string val = filterSplit[1];
                    SetPropertyValue(converTo, key, val);
                }
            }
            return converTo;
        }
    
        private void SetPropertyValue(T converTo, string key, string val)
        {
            Type t = typeof(T);
            PropertyInfo[] props = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);
            PropertyInfo prop = props.Where(p => p.Name.Equals(key, StringComparison.CurrentCultureIgnoreCase) == true && p.CanWrite).FirstOrDefault();
            if (prop is null) return;
            prop.SetValue(converTo, TypeDescriptor.GetConverter(prop.PropertyType).ConvertFrom(val));
            /*
                1. Problem: val is a string and if your target property is non-string there is
                    a contradiction.
                    The following link offers a solution
                    https://stackoverflow.com/a/2380483
             */
        }
    }
    

    EmployeeFilter

    [TypeConverter(typeof(EmployeeFilterTypeConverter<EmployeeFilter>))]
    public class EmployeeFilter: IEquatable<EmployeeFilter>
    {
        /*
            1. As you can see, the DTO omitted the
                a. ID
                b. DOB
         */
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Street { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string ZipCode { get; set; }
        public DateTime? DOJ { get; set; }
    
        public bool Equals(EmployeeFilter other)
        {
            /*
                1. You need to put parenthesis around (this.FirstName == other.FirstName)
                2. https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=net-5.0
             */
            return (this.FirstName == other.FirstName) &&
                (this.LastName == other.LastName) && 
                (this.Street == other.Street) &&
                (this.City == other.City) &&
                (this.State == other.State) &&
                (this.ZipCode == other.ZipCode) &&
                (this.DOJ == other.DOJ);
        }
    }
    

    GeoPoint

    public class GeoPoint: IEquatable<GeoPoint>
    {
        public double Latitude { get; set; }
        public double Longitude { get; set; }
        public static bool TryParse(string s, out GeoPoint result)
        {
            result = null;
    
            var parts = s.Split(',');
            if (parts.Length != 2)
            {
                return false;
            }
    
            double latitude, longitude;
            if (double.TryParse(parts[0], out latitude) &&
                double.TryParse(parts[1], out longitude))
            {
                result = new GeoPoint() { Longitude = longitude, Latitude = latitude };
                return true;
            }
            return false;
        }
    
        public bool Equals(GeoPoint other)
        {
            return (this.Latitude == other.Latitude) && (this.Longitude == other.Longitude);
        }
    

    Edit: Added model classes