DEV Community

Cover image for Value Conversion using String Extensions in C#
Bob Rundle
Bob Rundle

Posted on

Value Conversion using String Extensions in C#

One of the perennial frustrations I have with C# is the poverty of string to value conversions. There are plenty of course, but plenty never seems to be enough as I find myself continually writing new ones. Furthermore what is there seems inelegant. One idea is to use string extension methods to improve this situation.

If you have never used extension methods they can be a valuable tool for strapping new functionality on built-in types. On the downside, it can be disconcerting to see, through IntelliSense, new methods suddenly appear on types that you thought you thoroughly understood.

Let's take a simple string to value conversion and see how it might be done with an extension to the string class.

    string s = "123";
    int i = int.Parse(s);

Enter fullscreen mode Exit fullscreen mode

The extension is defined in a static method using "this" as the first argument.

    public static class StringExtensions
    {
        public static int GetValue(this string s)
        {
            return int.Parse(s);
        } 
    }
Enter fullscreen mode Exit fullscreen mode

We invoke the extension like this…

    String s = "123";
    int i = s.GetValue();
Enter fullscreen mode Exit fullscreen mode

So far, nothing has been improved. But now let's redefine GetValue() as a generic method and use reflection to find the static Parse() method on the type and invoke it. We end up with this…

        public static T GetValue<T>(this string s)
        {
            MethodInfo mi = typeof(T).GetMethod("Parse", new Type[] { typeof(string) });
            if (s == null)
            {
                throw new ArgumentNullException("s");
            }
            else if (mi != null)
            {
                return (T)mi.Invoke(typeof(T), new object[] { s });
            }
            else if (typeof(T).IsEnum)
            {
                if(Enum.TryParse(typeof(T), s, out object ev))
                    return (T)(object)ev;
                else
                    throw new ArgumentException($"{s} is not a valid member of {typeof(T).Name}");
            }
            else
            {
                throw new ArgumentException($"No conversion supported for {typeof(T).Name}");
            }
        }
Enter fullscreen mode Exit fullscreen mode

Note that we are looking for a Parse() method with a single string argument. A lot of types such as float, double, int, long, DateTime have this. Enum is a special case. Enum always seems to be a special case however we really want to support it. There seems to be a fundamental truth revealed as the enumeration type seems to create special cases in every programming language in which it appears.

Now we can do things like…

    String si = "123";
    int i = si.GetValue<int>();
    String sf = "1.234";
    float f = sf.GetValue<float>();
    String sd = "8/7/2021";
    DateTime d = sd.GetValue<DateTime>();

Enter fullscreen mode Exit fullscreen mode

We want the value conversion to work both ways so I've added another static method to the extension class to convert back…

        public static string SetValue(object value)
        {
            if (value == null)
            {
                return null;
            }
            else if (value is float)
            {
                return ((float)value).ToString("R");
            }
            else if (value is double)
            {
                return ((double)value).ToString("R");
            }
            else if (value is DateTime)
            {
                return ((DateTime)value).ToString("O");
            }
            else
            {
                return value.ToString();
            }
        }
Enter fullscreen mode Exit fullscreen mode

Note the use of MakeByRefType() to search for the out parameter for TryParse();

With the additional of the default value for our value conversion we now have something that delivers. But here is the real bonus. GetValue() is not limited to built-in types. It will work for any type of user defined class or struct that we define Parse() and TryParse() on. For example….

    public struct BoardSize
    {
        public int Width { get; set; }
        public int Height { get; set; }
        public override string ToString()
        {
            return $"({Width},{Height})";
        }
        public static bool TryParse(string s, out BoardSize bs)
        {
            string[] ss = s?.Split(new char[] { '(', ',', ')' });
            if (ss.Length == 4)
            {
                bs = new BoardSize()
                {
                    Width = ss[1].GetValue<int>(40),
                    Height = ss[2].GetValue<int>(40)
                };
                return s == bs.ToString();
            }
            else
            {
                bs = new BoardSize() { Width = 40, Height = 40 };
                return false;
            }
        }
        public static BoardSize Parse(string s)
        {
            if (TryParse(s, out BoardSize bs))
                return bs;
            else
                throw new FormatException();
        }
    }
Enter fullscreen mode Exit fullscreen mode

The primary use case for value conversions is serialization. Let's say you are building a game want to load a config file on startup and save it on shutdown

    public class ConfigFile
    {
        public BoardSize BoardSize { get; set; } = new BoardSize() { Width = 40, Height = 40 };
        public int HighScore { get; set; } = 0;
        public DateTime HighScoreDate { get; set; } = DateTime.MinValue;
        public enum Difficulty {  Easy, Medium, Hard, Impossible };
        public Difficulty DifficultySetting { get; set; } = Difficulty.Medium;
        public float ScalingFactor { get; set; } = 1.0f;
        public string GameFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MyGame");
        public const string GameConfigFile = "MyGame.ini";
        public string GameConfigFilePath => Path.Combine(GameFolder, GameConfigFile);
        public ConfigFile()
        {
            Load();               
        }
        public void Load()
        {
            try
            {
                string contents = File.ReadAllText(GameConfigFilePath);
                string[] elements = contents.Split('|');
                BoardSize = elements[0].GetValue<BoardSize>(new BoardSize() { Width = 40, Height = 40 });
                HighScore = elements[1].GetValue<int>(0);
                HighScoreDate = elements[2].GetValue<DateTime>(DateTime.MinValue);
                DifficultySetting = elements[3].GetValue<Difficulty>(Difficulty.Medium);
                ScalingFactor = elements[4].GetValue<float>(1.0f);
            }
            catch { }
        }
        public void Save()
        {
            Directory.CreateDirectory(GameFolder);
            File.WriteAllText(GameConfigFilePath, ToString());
        }
        public override string ToString()
        {
            return StringExtensions.SetValue(BoardSize)
                + "|" + StringExtensions.SetValue(HighScore)
                + "|" + StringExtensions.SetValue(HighScoreDate)
                + "|" + StringExtensions.SetValue(DifficultySetting)
                + "|" + StringExtensions.SetValue(ScalingFactor);
        }
    }

Enter fullscreen mode Exit fullscreen mode

This will produce a config file with contents similar to this…

(100,50)|0|0001-01-01T00:00:00.0000000|Hard|1
Enter fullscreen mode Exit fullscreen mode

Summary and Discussion

In this post I have defined a set of string extensions that assist in value conversion of not only built in types but also user defined structs and classes. The primary use case is wherever serialization of objects is needed. There are, of course, plenty of approaches to serialization of .NET objects, including JSON, XML, ISerializable and many others. The string extensions defined here have the advantage of being very light weight and suitable for many use cases other than serialization.

Performance might be an issue with this code as it relies heavily on reflection. Performance can be improved by explicitly implementing the built-in type conversions. I'll do some performance modeling when I get a chance.

Are these extensions more elegant then the available Parse() and TryParase() methods? I'll admit this is debatable for the built-in types but where the extensions shine is the ability to extend the GetValue/SetValue pattern to user defined types and structs.

All the code for this post can be found at https://github.com/bobrundle/stringextensions

Discussion (0)