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
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?
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