Monday, August 26, 2013

Convert all the (C#) types!

Well, almost all. So here is an incredible handy conversion class that I always carry around in all my projects. It has never failed me, never let me down... you get the idea. Although I cannot take full credit for it, thank Steven for the initial setup, I have done some minor tweaks in the past.

Usage is pretty straight forward. Call the (generic) method, put in a string in the correct format, converted and casted type comes out.

var value = Conversion.AsValue<int>("123"); 
//// Returns 123 as int

var value = Conversion.AsValue<int?>("123"); 
//// Returns 123 as nullable int

var value = Conversion.AsValue<int?>(""); 
//// Returns null (handy for webrequest parameters)

The above example can be used for numerous of types such as decimal, DateTime, enums, anything with a TypeConverter.

The conversion helper can also conver value types to strings. It's done in the current culture of the thread. This comes in handy when you need to pass string values as strings. However passing values in a query string it's necessary to use an invariant culture as shown below. This will ensure the correct transfer without losing data. Converting the value is also done by adding the culture as a parameter.

string value = Conversion.AsString(123); 
//// Returns 123 as a string

string value = Conversion.AsValue(123.56M); 
/// Returned as 123.56 as a culture invariant string representation.

string value = Conversion.AsValue(new DateTime(2015, 4, 3, 11, 22, 33)); 
/// Returns 3-4-2015 11:22.

string value = Conversion.AsValue(new DateTime(2015, 4, 3, 11, 22, 33), CultureInfo.InvariantCulture);
/// Returns 04/03/2015 11:22:33.

I won't explain all the specific details of the conversion process, the code should be clear enough, but the basic outline is as follows. When you convert from a value you can either state the expected result type by a generic method of a boxed type. The generic method overloads the boxed version. With specified type a converter is being selected. If the expected type is an array, a custom array type converter will be created for the array type that can handle a comma separated string.

Here is the code that you can put into your own projects:

/// <summary>Helper methods for converting values.</summary>
public static class Conversion
{
    /// <summary>
    /// Enables conversion values for the given type from it's string representation.
    /// </summary>
    /// <typeparam name="T">The type to convert to. This can be a <see cref="Nullable"/> type.</typeparam>
    /// <param name="value">The string to convert.</param>
    /// <returns>An instance of the given <typeparamref name="T"/>.</returns>
    [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter",
        Justification = "A design with 'object ConvertFromString(Type type, string value)' is " +
        "very suitable in this API.")]
    [DebuggerStepThrough]
    public static T AsValue<T>(string value)
    {
        return AsValue<T>(value, Thread.CurrentThread.CurrentCulture);
    }

    /// <summary>
    /// Enables conversion values for the given type from it's string representation.
    /// </summary>
    /// <typeparam name="T">The type to convert to. This can be a <see cref="Nullable"/> type.</typeparam>
    /// <param name="value">The string to convert.</param>
    /// <returns>An instance of the given <typeparamref name="T"/>.</returns>
    [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter",
        Justification = "A design with 'object ConvertFromString(Type type, string value)' is " +
        "very suitable in this API.")]
    [DebuggerStepThrough]
    public static T AsValue<T>(string value, CultureInfo culture)
    {
        return (T)AsValue(typeof(T), value, culture);
    }

    /// <summary>
    /// Enables conversion values for the given type from it's string representation.
    /// </summary>
    /// <typeparam name="T">The type to convert to. This can be a <see cref="Nullable"/> type.</typeparam>
    /// <param name="value">The string to convert.</param>
    /// <returns>An instance of the given <typeparamref name="T"/>.</returns>
    [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter",
        Justification = "A design with 'object ConvertFromString(Type type, string value)' is " +
        "very suitable in this API.")]
    [DebuggerStepThrough]
    public static object AsValue(Type type, string value)
    {
        return AsValue(type, value, Thread.CurrentThread.CurrentCulture);
    }

    /// <summary>
    /// Enables conversion values for the given type from it's string representation.
    /// </summary>
    /// <typeparam name="T">The type to convert to. This can be a <see cref="Nullable"/> type.</typeparam>
    /// <param name="value">The string to convert.</param>
    /// <returns>An instance of the given <typeparamref name="T"/>.</returns>
    [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter",
        Justification = "A design with 'object ConvertFromString(Type type, string value)' is " +
        "very suitable in this API.")]
    [DebuggerStepThrough]
    public static object AsValue(Type type, string value, CultureInfo culture)
    {
        TypeConverter converter = GetConverterForType(type);

        try
        {
            return converter.ConvertFromString(null, culture, value);
        }
        catch (Exception ex)
        {
            // HACK: There is a bug in the .NET framework (3.5sp1) BaseNumberConverter class. The
            // class throws an Exception base class, and therefore we must catch the 'Exception' base class.
            throw new FormatException(
                "The supplied value '" + value + "' could not be converted. " + ex.Message, ex);
        }
    }

    /// <summary>Converts an value to it's string representation.</summary>
    /// <remarks>Note that there is a corner case in which conversion is not correct in the case of using
    /// <see cref="Nullable{T}"/>. Converting an empty array and converting an array of one element with
    /// a value of null will convert to an empty string, while converting an empty string back will always
    /// result in an empty array.</remarks>
    /// <typeparam name="T">The type of the value.</typeparam>
    /// <param name="value">The value to convert.</param>
    /// <returns>The string representation of the value.</returns>
    [DebuggerStepThrough]
    public static string AsString<T>(T value)
    {
        TypeConverter converter = GetConverterForType(typeof(T));

        return AsString(value, Thread.CurrentThread.CurrentCulture);
    }

    [DebuggerStepThrough]
    public static string AsString<T>(T value, CultureInfo culture)
    {
        TypeConverter converter = GetConverterForType(typeof(T));

        return converter.ConvertToString(null, culture, value);
    }

    [DebuggerStepThrough]
    private static TypeConverter GetConverterForType(Type type)
    {
        if (type.IsArray)
        {
            Type arrayElementType = type.GetElementType();

            if (arrayElementType.IsValueType)
            {
                // For ValueType arrays (like int[], Guid[] en int?[]) we will use our own converter
                // because the .NET ArrayConverter cannot conver comma seperated values.
                Type converterType =
                    typeof(ArrayConverter<>).MakeGenericType(new[] { arrayElementType });

                return (TypeConverter)Activator.CreateInstance(converterType);
            }
        }

        return TypeDescriptor.GetConverter(type);
    }

    /// <summary>Converts arrays of values.</summary>
    /// <typeparam name="T">The type of the elements in the array.</typeparam>
    private sealed class ArrayConverter<T> : TypeConverter
    {
        private const char SplitChar = ',';

        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string) ||
            this.CanConvertFrom(context, sourceType);
        }

        [DebuggerStepThrough]
        public override object ConvertTo(
            ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                if (value == null)
                {
                    return null;
                }

                T[] collection = value as T[];

                if (collection == null)
                {
                    throw new ArgumentException("Invalid type " + value.GetType().FullName, "value");
                }

                return ConvertArrayToString(context, culture, collection);
            }

            return this.ConvertTo(context, culture, value, destinationType);
        }

        [DebuggerStepThrough]
        public override object ConvertFrom(
            ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value == null)
            {
                return null;
            }

            string val = value as string;

            if (val != null)
            {
                return ConvertStringToArray(context, culture, val);
            }

            return this.ConvertFrom(context, culture, value);
        }

        [DebuggerStepThrough]
        private static string ConvertArrayToString(
            ITypeDescriptorContext context, CultureInfo culture, T[] collection)
        {
            StringBuilder builder = new StringBuilder();

            TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));

            bool first = true;

            string splitChar = SplitChar.ToString();

            foreach (T t in collection)
            {
                if (!first)
                {
                    builder.Append(splitChar);
                }

                string convertedValue = converter.ConvertToString(context, culture, t);

                builder.Append(convertedValue);

                first = false;
            }

            return builder.ToString();
        }

        [DebuggerStepThrough]
        private static T[] ConvertStringToArray(
            ITypeDescriptorContext context, CultureInfo culture, string value)
        {
            if (value.Length == 0)
            {
                // WARNING: The conversion is incorrect when converting an array of a nullable type
                // with a single element with a value of null (i.e. new int?[1] { null }). This will
                // convert to an empty string, while converting back would result in an array with no
                // elements. The only way to solve this in this converter is to denote non-empty
                // arrays with for instance brackets, but this would break existing code. We therefore
                // move the responsibility of this to the caller.
                return new T[0];
            }

            string[] elements = value.Split(new char[] { SplitChar });

            TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));

            T[] list = new T[elements.Length];
            int index = 0;

            foreach (string element in elements)
            {
                try
                {
                    list[index] = (T)converter.ConvertFromString(context, culture, element);
                    index++;
                }
                catch (FormatException ex)
                {
                    // Throw a more expressive message.
                    throw new FormatException(ex.Message + " Supplied value: '" + element + "'.", ex);
                }
            }

            return list;
        }
    }
}

1 comment: