0

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?

Andes Lam
  • 192
  • 2
  • 8
  • Have you tried using generics to determine the types? – gjhommersom Aug 25 '21 at 19:06
  • 1
    @gjhommersom No because eventually all dto classes need to be decorated with attribute which requires passing the type converter class. It seems that generics is not applicable in attribute parameter passing. This post, https://stackoverflow.com/q/14823669, has illustrated it well. – Andes Lam Aug 26 '21 at 05:02
  • 1
    As far as i understand from that post you cannot create an attribute that contains a new generic. Something like `class WrapperConverter : TypeConverter` and `[TypeConverter(typeof(WrapperConverter))]` is allowed. – gjhommersom Aug 26 '21 at 06:43
  • @gjhommersom Yes it worked. Apologies for my oversight in the attached post and thanks for pointing it out for me. Evidently, my c# skills are still below par. My code is typed below for you and future reference. Do let me know if there are any mistakes. I would not tick it till then. – Andes Lam Aug 26 '21 at 13:39

1 Answers1

0

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

Andes Lam
  • 192
  • 2
  • 8