Fun with Type Conversions

Recently at work we were coding a particular feature that required us to extend properties with some functionality.  We ended up extending type system with ExtendedPreoprty<T>. , where T was property type.  This lead to a lot of ugly code, such as:

Person p = new Person();

p.Name.Value = “John”;

I remembered from a long time ago that there was functionality in .NET called TypeConverters. Here is sample code that illustrates how we implemented the feature

First of all, let’s define a class we want to convert:

    public struct Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

Our goal would be to convert between Person and other types.  Let’s just implement string conversions for now.  We are going to make an assumption about string format.  We are going to assume there are two parts in that string, one for name, the other for age.  We are going to inherit from a base class so that we can write less code

using System;
using System.ComponentModel;
using System.Globalization;

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

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                return true;
            }
            return base.CanConvertTo(context, destinationType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value is string stringValue)
            {
                return ParseFromString(stringValue);
            }
            else if (value is Person)
            {
                var person = (Person)value;
                return $&quot;{person.Name} {person.Age}&quot;;
            }
            return null;
        }

        private static object ParseFromString(string stringValue)
        {
            var parts = stringValue.Split(new[] { &quot; &quot; }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 2)
            {
                if (int.TryParse(parts[0], out int age))
                {
                    return new Person { Age = age, Name = parts[1] };
                }
                else if (int.TryParse(parts[1], out age))
                {
                    return new Person { Age = age, Name = parts[0] };
                }
                else
                {
                    return null;
                }
            }
            else
            {
                return null;
            }
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if(value is string stringValue)
            {
                return ParseFromString(stringValue);
            }
            else if(value is Person)
            {
                var person = (Person)value;
                return $&quot;{person.Name} {person.Age}&quot;;
            }
            return null;
        }
    }
}

The code is pretty simple, and does not warrant much explanation.  We now just need to decorate our person struct with converter attribute

    [TypeConverter(typeof(PersonConverter))]
    public struct Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

Now, let’s write a test to verify everything works as expected..

        [TestMethod]
        public void Should_Convert_to_String()
        {
            var p = new Person { Name = &quot;Sergey&quot;, Age = 23 };
            var converter = TypeDescriptor.GetConverter(typeof(Person));
            var stringValue = converter.ConvertTo(p, typeof(string));
            Assert.AreEqual(&quot;Sergey 23&quot;, stringValue);
        }

        [TestMethod]
        public void Should_Convert_From_String()
        {
            var converter = TypeDescriptor.GetConverter(typeof(Person));
            var p = (Person)converter.ConvertTo(&quot;Sergey 23&quot;, typeof(Person));
            Assert.AreEqual(&quot;Sergey&quot;, p.Name);
            Assert.AreEqual(23, p.Age);
        }

So far so good.  However, it is annoying that we still have to write converter code.  Wouldn’t it be nice if we could just assign a Person to a string variable and vice versa?  We can actually do this with custom operators we could add to our structure.

    [TypeConverter(typeof(PersonConverter))]
    public struct Person
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public static implicit operator Person(string value)
        {
            var converter = TypeDescriptor.GetConverter(typeof(Person));
            return (Person)converter.ConvertTo(value, typeof(Person));
        }

        public static implicit operator String(Person value)
        {
            var converter = TypeDescriptor.GetConverter(typeof(Person));
            return (string)converter.ConvertTo(value, typeof(string));
        }
    }

This seldom use feature allows us to extend assignment operator by telling compiler we could convert between Person and string in both directions.  Our job is to just call converter we already wrote.  Here are some tests to prove our point

        [TestMethod]
        public void Should_Assign_Person_to_String()
        {
            Person p = &quot;Sergey 23&quot;;
            Assert.AreEqual(&quot;Sergey&quot;, p.Name);
            Assert.AreEqual(23, p.Age);
        }

        [TestMethod]
        public void Should_Assign_String_To_Person()
        {
            var p = new Person { Name = &quot;Sergey&quot;, Age = 23 };
            string stringValue = p;
            Assert.AreEqual(&quot;Sergey 23&quot;, stringValue);
        }

Just a little morsel of fun for the week.

Enjoy.

Leave a Reply

Your email address will not be published. Required fields are marked *