Click here to Skip to main content
Click here to Skip to main content

Universal Type Converter

, 5 Jun 2013
Rate this:
Please Sign up or sign in to vote.
Converting nearly every type to another type

Table of Contents

Introduction

This article is about the different possibilities of type conversion provided by the .NET Framework. At the end, it offers a combination of all these methods (and more) within the UniversalTypeConverter which converts nearly every type to another type.

I came across this during the requirement of mapping values, coming from a database, to some objects. Of course, a problem which could be done by an O/R mapper. But this time, the query against the database - and the objects to map the result to - were only known at runtime. It was a kind of reporting tool. Not knowing the exact types, I decided to search for a generic conversion solution. But none of those provided a ready-steady-method like convert x to y for all desired types. So the idea of the UniversalTypeConverter was born.

Just to give you an idea about what I was looking for:

int myInt = myGivenValue.ConvertTo<int>();

Regardless of myGivenValue's type.

If you keep on reading, you will get a deep look into conversions. Not only from the programmatic point of view - practical background and the why is described, as well. So get you a cup of coffee, tea, beer or whatever you prefer and read on.

If you are not interested in the whole story, you can jump to the end, looking after Using the code.

A Solution in Mind

During my research, I hit some nice articles about the TypeConverter, at which we will have a closer look, soon. Till then, you can imagine a TypeConverter to be a generic way to tell a type how it could be converted from/to another type. That seemed to be a really elegant solution. So I tried it out with some different types - all working fine - and thought I am prepared for doing the job. How naive...

Setting Up the Project

At first, I built up a matrix of all kinds of combinations of type conversions which seemed useful to me. You can have a look at this matrix - it was done in Excel and could be found within the solution-download (beneath the _Documents-folder within the test-solution). I tried to cover all base types (a.k.a. common language runtime types), their nullable pendants (e.g. int?) and null by itself. This matrix was the base for the unit tests which I wrote before the actual programming. If you look into the tests - don't panic: most of the over 1.500 tests were built by a little program I wrote and were not set up manually. For such structured test, it may be an ease to create them by a code-generation-tool. Not mentioned in the matrix - but useful - and so added to the tests - I was looking forward to support Enums, too. Beside the tests, I set up the UniversalTypeConverter-class consisting of my desired ready-steady generic ConvertTo<T>-method. That way, all the generated tests worked - but failed, of course. So I was ready for the actual programming with the goal to get one test after the other shining green.

About the Code

Just in case you are looking forward to have a look into the source code, I will explain its structure, in short: the main method is the most overloaded version of TryConvert. Most of the other public methods delegate their work to this method. Of course, this method delegates parts of its work to private helpers, too. Because I implemented the Try-pattern, most of these helper-methods had to follow this concept and return a bool while handling the converted value as an out-parameter.

The code makes use of Code Contracts and FluentAssertions for testing. So you will have to install them - both for free - in order to compile and test the source code. But do not worry, the final DLL can be found in the bin-folder - ready to use.

Let's Start Simple

There are a few simple scenarios which are checked at the beginning. You will find the code in the mentioned version of the TryConvert- method. There is nothing to do if the destination type is object - everything is an object - so we can just return it. Simple as that is the solution if the input type is assignable to the requested destination, whether by implementing the requested interface or by deriving from the requested type - or simply being the requested type by itself.

Have a look at the code:

if (destinationType == typeof(object)) {
    result = value;
    return true;
}
if (ValueRepresentsNull(value)) {
    return TryConvertFromNull(destinationType, out result, options);
}
if (destinationType.IsAssignableFrom(value.GetType())) {
    result = value;
    return true;
}

Null

You will have noticed the handling of null in the code above. The ValueRepresentsNull-method checks whether the value is null or DBNull.Value. DBNull represents normal null in the world of ADO.NET when operating with databases.

I think it is something everybody stumbles upon the first - and the second - time dealing with databases. So I treat both nulls the same way.

If something is null, we will have to handle it - like it is done in TryConvertFromNull:

result = GetDefaultValueOfType(destinationType);
if (result == null) {
    return true;
}
return (options & ConversionOptions.AllowDefaultValueIfNull) ==
       ConversionOptions.AllowDefaultValueIfNull;

The GetDefaultValueOfType-method brings up the default value of the given type:

return type.IsValueType ? Activator.CreateInstance(type) : null;

That works because every ValueType has a parameterless constructor and all other types support null.

If the default value is null, we are done - because null keeps being null after conversion.

Otherwise - that means for all ValueTypes (e.g. the default value of an int is 0) - it is only possible if you accept the non-null default value explicit. That is controllable by the ConversionOption AllowDefaultValueIfNull. Some of the public methods accept further options - AllowDefaultValueIfNull is one of them. The other options are described later on. For now, I think it is good to have an option to allow converting null to a not nullable type.

TypeConverter

Time for the TypeConverter. MSDN describes it like that: "Provides a unified way of converting types of values to other types". Obviously, all we need!

The main idea is that you can define your own TypeConverters and assign them to your own types by setting the TypeConverterAttribute to your classes. That way, everybody can get the proper TypeConverter by simply calling TypeDescriptor.GetConverter. Once knowing the TypeConverter, you can use the methods CanConvertFrom, CanConvertTo, ConvertFrom, and ConvertTo for validation and conversion. That is really a generic way! If your requested type is supported - you are done.

So the code for the private TryConvertByDefaultTypeConverters-method looks very simple:

TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
if (converter != null) {
    if (converter.CanConvertFrom(value.GetType())) {
        try {
            result = converter.ConvertFrom(null, culture, value);
            return true;
        }
        catch {
        }
    }
}
converter = TypeDescriptor.GetConverter(value);
if (converter != null) {
    if (converter.CanConvertTo(destinationType)) {
        try {
            result = converter.ConvertTo(null, culture, value, destinationType);
            return true;
        }
        catch {
        }
    }
}
return false;

You ask yourself why one should use try-catch if the conversion was already checked by the call of CanConvertFrom/To? Because CanConvertFrom/To only checks the possibility of conversion! In general, it is possible to convert a string to a DateTime - but actually it depends on the string's value, e.g. "Hello World" is not really a DateTime and an attempt to convert it will throw an exception. It would have been nice, if the TypeConverter had implemented the Try-Pattern - for example TryConvertFrom - but sadly that is not the fact. So we have to catch this possible error by ourselves.

The given culture is used for globalization - we will come to this, later on.

If you are interested in learning more about the TypeConverter - or even want to build your own - you should have a look at Klaus Salchner's article here on CodeProject.

For now, the .NET Framework comes along with a lot of predefined TypeConverters for different types and this technique is heavily used by serialization - for example putting all kinds of types into the ViewState of an aspx-page, which only consists of a string. With this present in mind, I started my tests, looking forward to see everything shining green...

IConvertible

But some tests still failed. For example, an int was not convertible to decimal. I was a little bit confused because these tests were using base types and I expected the .NET Framework to provide every base type with a suitable TypeConverter - not just some of them! But that was only an expectation...

So I opened a decompiler (e.g. ILSpy) and had a look at the different ConvertTo-methods of the Convert-class within the mscorlib-assembly. Et voilà - I came across the IConvertible-interface. MSDN describes this interface as follows: "Defines methods that convert the value of the implementing reference or value type to a common language runtime type that has an equivalent value". Don't ask me why - but it's obvious that the framework implements more than one way for handling conversions... and it seems obvious that we will have to consider them, as well.

So - as a kind of fallback - the value is passed to the TryConvertByIConvertibleImplementation method if there is no TypeConverter who could do the job. The type of the given value is checked for implementing IConvertible. This interface offers an explicit ToX-method for every supported destination type. So there was only the less elegant way of defining some if-then-blocks. Again, there is no Try pattern. So it had to be done in brutforce-style within try-catch.

All in all, the code looks like this:

if (value is IConvertible) {
    try {
        if (destinationType == typeof(Boolean)) {
            result = ((IConvertible)value).ToBoolean(formatProvider);
            return true;
        }
        if (destinationType == typeof(Byte)) {
            result = ((IConvertible)value).ToByte(formatProvider);
            return true;
        }
        if (destinationType == typeof(Char)) {
            result = ((IConvertible)value).ToChar(formatProvider);
            return true;
        }
        if (destinationType == typeof(DateTime)) {
            result = ((IConvertible)value).ToDateTime(formatProvider);
            return true;
        }
        if (destinationType == typeof(Decimal)) {
            result = ((IConvertible)value).ToDecimal(formatProvider);
            return true;
        }
        if (destinationType == typeof(Double)) {
            result = ((IConvertible)value).ToDouble(formatProvider);
            return true;
        }
        if (destinationType == typeof(Int16)) {
            result = ((IConvertible)value).ToInt16(formatProvider);
            return true;
        }
        if (destinationType == typeof(Int32)) {
            result = ((IConvertible)value).ToInt32(formatProvider);
            return true;
        }
        if (destinationType == typeof(Int64)) {
            result = ((IConvertible)value).ToInt64(formatProvider);
            return true;
        }
        if (destinationType == typeof(SByte)) {
            result = ((IConvertible)value).ToSByte(formatProvider);
            return true;
        }
        if (destinationType == typeof(Single)) {
            result = ((IConvertible)value).ToSingle(formatProvider);
            return true;
        }
        if (destinationType == typeof(UInt16)) {
            result = ((IConvertible)value).ToUInt16(formatProvider);
            return true;
        }
        if (destinationType == typeof(UInt32)) {
            result = ((IConvertible)value).ToUInt32(formatProvider);
            return true;
        }
        if (destinationType == typeof(UInt64)) {
            result = ((IConvertible)value).ToUInt64(formatProvider);
            return true;
        }
    }
    catch {
        return false;
    }
}
return false;

Don't mind about the formatProvider, yet - again it is used for globalization as described later on.

So this should finish the job, shouldn't it?

No Exception without Exception

Wrong! Even with IConvertible, e.g. a double is not convertible to char. Maybe it sounds obvious, because you really cannot handle 1.23 as char. But you cannot handle 400 (int) as a char either (because out of range); however, the .NET Framework supports the conversion from int to char out of the box. You know what I mean? Why not convert 1.00 to char? So there are some special cases treated in the TryConvertByIntermediateConversion-method by going through an intermediate type:

if (value is char && (destinationType == typeof(double) || destinationType == typeof(float))) {
    return TryConvertCore(System.Convert.ToInt16(value), 
        destinationType, ref result, culture, options);
}
if ((value is double || value is float) && destinationType == typeof(char)) {
    return TryConvertCore(System.Convert.ToInt16(value), 
    destinationType, ref result, culture, options);
}
return false;

Not much to discuss here - it just had to be figured out.

Implicit - but Explicit!

At first, a few more cases were covered by conversion through an intermediate type. But after posting the first version of this article, leppie pointed me to implicit and explicit conversion operators - thanks leppie!

So what are implicit and explicit conversions?

Implicit conversion is done - well, implicit - when you try to convert from a smaller to a larger integral type or from a derived classes to a base class.
That is why you can write something as follows:

int a = 123;
float b = a;

Explicit conversion needs some special syntax from you, just to show that you know what you are doing. That is because information might be lost during conversion. For example, converting a numeric type to another type that has less precision or a smaller range. That special syntax is known as "casting" and you will have to use the casting operator - that is putting the expression in braces. If you look at it you will recognize it, for sure:

float a = 123;
int b = (int)a;

This all works, because there are special static methods defined for each of these conversions. These methods are marked by the operator keyword and therefore there is an implicit and an explicit operator. The compiler will direct your casting - such as shown in the examples above - to these methods. That is why it integrates so smoothly into the syntax. And maybe that is the reason why I did not recognized them as conversion methods.

By the way, these operator methods are responsible for other operations - e.g., addition - as well.

So we have to manage these calls by ourselves if we want to use them in the UniversalTypeConverter. But where to find them? I figured out that these operator methods were compiled as ordinary methods and were added to the affected types. These methods are always named "op_Implicit" or "op_Explicit" by convention. You can have a look at this with an decompiler by inspecting the Decimal type within the mscorlib.dll.

For calling the proper version of these methods, accordingly to the given input and destination types, we use reflection as it is done in the TryConvertXPlicit method:

private static bool TryConvertXPlicit(object value, Type invokerType, 
    Type destinationType, string xPlicitMethodName, ref object result) {
    var methods = invokerType.GetMethods(BindingFlags.Public | BindingFlags.Static);
    foreach (MethodInfo method in methods.Where(m => m.Name == xPlicitMethodName)) {
        if (destinationType.IsAssignableFrom(method.ReturnType)) {
            var parameters = method.GetParameters();
            if (parameters.Count() == 1 && 
                parameters[0].ParameterType == value.GetType()) {
                try {
                    result = method.Invoke
                        (null, new[] { value });
                    return true;
                }
                // ReSharper disable EmptyGeneralCatchClause
                catch {
                    // ReSharper restore EmptyGeneralCatchClause
                }
            }
        }
    }
    return false;
}

This method checks every method having the given operatorMethodName ("op_Explicit" or "op_Implicit") for the needed types and invokes the proper method if found. It is called from TryConvertXPlicit. Once for the value's type and once for the destinationType. That is because we do not know which type defines the proper operator method. Look at the code:

private static bool TryConvertXPlicit(object value, Type destinationType, 
    string operatorMethodName, ref object result) {
    if (TryConvertXPlicit(value, value.GetType(), 
        destinationType, operatorMethodName, ref result)) {
        return true;
    }
    if (TryConvertXPlicit(value, destinationType, 
        destinationType, operatorMethodName, ref result)) {
        return true;
    }
    return false;
}

So this gives us another generic way for conversion covered by the UniversalTypeConverter.

Enums

At this time, I did not believe in a consistent way of conversion any more. And I was not disappointed.  

Try to convert int to an Enum with the methods described so far. It does not work - you will have to use the Enum's static method ToObject as it is done in the TryConvertToEnum-method:

try {
    result = Enum.ToObject(destinationType, value);
    return true;
}
catch {
    return false;
}

Nullable Types

Null, again? I know we already talked about null. But remember the problem with ValueTypes like int, which cannot be null? Since version 2.0 of .NET, there is a wrapper-type providing a kind of null assignment to ValueTypes. Again, I came with a database background in mind - and a database is able to store null within an int-column. If you have to store these nulls - or other nulls - on the side of .NET, you should have a closer look at the so called nullable types. So did I. Reaching this point, I was not surprised that the conversion from/to these type was not handled by any of the methods described so far.

As mentioned above, the nullable types are a wrapper for the actual type. So I had the idea to unwrap the actual type before converting. That way, we can use all described methods the way they are. Gladly, the way back to a nullable type - if requested - is done implicit by the framework - so nothing more to do.

So you can see that the core type for conversion is defined within TryConvert:

Type coreDestinationType = IsGenericNullable(destinationType) ? 
GetUnderlyingType(destinationType) : destinationType;

The helper methods check if a nullable type is used and get the underlying type, if so:

IsGenericNullable
return type.IsGenericType &&
    type.GetGenericTypeDefinition() == typeof(Nullable<>).GetGenericTypeDefinition();
GetUnderlyingType
return Nullable.GetUnderlyingType(type);

From here on, everything goes as if it had been a normal ValueType.

Don't be afraid, if you could not follow this section as easy as the others. It is not that easy if you have not heard about generics and nullables before. If you are interested, you could study these things on your own and read that section again. But for now, keep on reading this article.  

Done

Believe it or not, that way all intended conversions work - hurray! So we can have a look at some general things to be aware of during conversions and how the UniversalTypeConverter supports you at this.

Think Global

Converting a string to another type often depends on the given culture. For example, you can read 1,000 as one thousand. But in Germany, it means one with three digits after the decimal point because the decimal point is a comma here - pretty confusing, isn't it? So the conversion - e.g. to a decimal - will return different results. Therefore every method has an overload where you can specify the culture. That is why we used culture or formatProvider in the helper-methods, as mentioned above. Basically the current culture is used unless otherwise specified. You should really take care of defining the proper culture for numeric and DateTime conversions if you are going international - it is a silent bug because the conversion is done without an exception, but with a wrong result!

Using the Code

Just reference the DLL and distribute it with your final binaries. The UniversalTypeConverter comes as a static type within the namespace TB.ComponentModel. So you can use it without creating an instance of it:

decimal myValue = UniversalTypeConverter.ConvertTo<decimal>(myStringValue);

If you are not sure if the type is convertible to another type, you can use the TryConvertTo-method:

decimal result;
bool canConvert = UniversalTypeConverter.TryConvertTo<decimal>(myStringValue, out result);

It follows the Try-pattern, so it returns true if the type was converted and you can read the converted value from the out-parameter. It returns false if the type was not converted. Then the out-parameter is something you should ignore.

If you only want to check if the type is convertible - not interested in the result - you can use the CanConvertTo-method without defining an out-parameter:

bool canConvert = UniversalTypeConverter.CanConvertTo<decimal>(myStringValue);

All these generic methods work fine if you know the types at compiling time. If you get the types at runtime, you will have to use the non-generic versions - giving the destination type as a parameter:

... = UniversalTypeConverter.Convert(myStringValue, requestedType);

Extension-Methods

Furthermore, the UniversalTypeConverter comes with a set of extension methods for the object type. That way, you can use it much more comfortable:

decimal myValue = myStringValue.ConvertTo<decimal>(CultureInfo.CurrentCulture);

Options

Practice has shown that there are some options you will have to specify on your own, if needed. In Think global, I already mentioned the culture being an option. Other options are specified by the ConversionOptions enum. This enum is defined as a flag so you can combine these options as needed:

  • AllowDefaultValueIfNull: Returns the default value of the given type of destination if the given value is null and the type of destination does not support null (as described in the Null-section).
  • AllowDefaultValueIfWhitespace: Returns the default value of the given type of destination if the given value is a string containing only whitespace but no conversion from whitespace is supported.
  • EnhancedTypicalValues: Allows the conversion from typical values making sense to me. E.g., converting the string "True" to bool works out of the box. But I decided "Yes", "No", etc. to be convertible, too. You can have a look at these EnhancedTypicalValues by following the TryConvertSpecialValues-method.

Unless otherwise specified, the conversion will be done with these settings:

  • CultureInfo is taken from CultureInfo.CurrentCulture
  • ConversionOptions uses EnhancedTypicalValues by default

NuGet package

The UniversalTypeConverter is also available as a NuGet package. Just search after UniversalTypeConverter within the NuGet Package Manager.

IEnumerable - arrays, lists, collections and so on

Using the UniversalTypeConverter has shown that it could be extended to simplify the work with arrays or lists. To keep on being generic we will speak about IEnumerable in genereal. So for that there are now three new features included:

  1. Converting all elements of an IEnumerable to a specified type (e.g. an int[] to string[]).
  2. Converting all elements of an IEnumerable to a string representation (e.g. "1,2,3").
  3. Splitting a string (e.g. "1,2,3") and converting all substrings to an IEnumerable of a specified type.

Working with IEnumerable and IEnumerable<T> gives you the comfort to interact with Linq. So I implemented a tiny fluent interface to configure the conversion - just to stay tune to Linq.

I will give you a commented example for each of the three new features. Most of the options are - well, optional. I will show you the maximum of configuration. You can cut it down to your needs.

1. ConvertToEnumerable<T>() from IEnumerable

string[] sourceValues = new[] { "12", null, "118", "xyz" };
int[] convertedValues = sourceValues.ConvertToEnumerable<int>()
    // null values in the input are ignored.
    .IgnoringNullElements()
    // Values which are not convertible to the destination type are ignored.
    .IgnoringNonConvertibleElements()
    // Specifies the culture to use - have a look above in this article.
    .UsingCulture(CultureInfo.CurrentCulture)
    // Specifies the options to use - have a look above in this article.
    .UsingConversionOptions(ConversionOptions.AllowDefaultValueIfWhitespace)
    // You can continue with basic Linq:
    .ToArray();

This will result in an int array containing two elements (12 and 118).

2. ConvertToStringRepresentation()

object[] input = new object[] { 2, null, true, "Hello world!", ""};
    string stringRepresentation = input
    .ConvertToStringRepresentation(
        // Specifies the culture to use - have a look above in this article.
        CultureInfo.CurrentCulture,
        // Specified an IStringConcatenator which uses the semicolon
        // as seperator, shows null values as "null" and ignores empty values.
        new GenericStringConcatenator(";", ".null.", ConcatenationOptions.IgnoreEmpty)
    );

This returns the string "2;.null.;True;Hello world!".

You can ignore the IStringConcatenator by default. There are overloads for simplifying the definitions of the separator and null values. By default it uses the semicolon and ".null.". But it may come the situation where you want to specify even more details on how to build the result string. Then you can create your own IStringConcatenator passing to the convert method. You can study the code for that if you want.

3. ConvertToEnumerable<T>() from string

This is the buddy of ConvertToStringRepresentation() and looks like this:

string input = "1;;.null.;3;xyz";
int[] result = input.ConvertToEnumerable<int>(new GenericStringSplitter(";"))
// Specifies an IStringSplitter which splits the input by semicolon.

    // Trims the end of each splitted element before conversion.
    .TrimmingEndOfElements()
    // Trims the start of each splitted element before conversion.
    .TrimmingStartOfElements()
    // Values which are empty are ignored.
    .IgnoringEmptyElements()
    // Specifies how null values are represented within the unput string.
    .WithNullBeing(".null.")
    // Values which are null are ignored. 
    .IgnoringNullElements()
    // Values which are not convertible to the destination type are ignored. 
    .IgnoringNonConvertibleElements()
    // Specifies the options to use - have a look above in this article.
    .UsingConversionOptions(ConversionOptions.AllowDefaultValueIfWhitespace)
    // Specifies the culture to use - have a look above in this article.
    .UsingCulture(CultureInfo.CurrentCulture)
    // You can continue with basic Linq:
    .ToArray();

This will result in an int array containing two elements (1 and 3).

Again, the IStringSplitter is for more complex scenarios. You can create your own class in order to support the needed format (e.g. splitting a csv line).

Working on IEnumerable you can call Try() instead of ToArray(). This returns true if conversion works and handles the result as an out parameter.

That's it - thank you for your keeping up reading. And by the way - the NuGet-Package was updated as well. So do not forget to update, too!

Limitations

Conversion will only work if the used types provide the mentioned interfaces - so inherit from TypeConverter, implement IConvertible or provide the proper operator methods. But the concept behind TypeConverter should be suitable for all your own types. Not even worse to mention that the types should be compatible in some way - converting a TextBox to bool would not work, of course.

I often mentioned that I implemented the Try-Pattern. If you dig into that, you will read that it should be implemented without using try-catch. That is because of performance. But you will see that I wrapped the conversions within try-catch. That is because of a lack of a proper TryConvert of the TypeConverter and IConvertible.

Conclusion

.NET does not provide a generic way for conversions across all types. Even the base types are not handled the same way. And not all possible conversions across the base types are supported. We had a look at the different techniques and ended up with the UniversalTypeConverter as a generic solution filling the gaps and with some useful options on top. May be, it is a historical thing that TypeConverter is not used on all types. But this is definitely the way to go if you want to provide your own types being convertible.

All in all, I hope you find this article useful. If so, it would be nice if you vote for it.  

And don't hesitate to drop me a line if you stumble across a conversion not being supported by the UniversalTypeConverter - beside TextBox to bool or something like this, of course...

History

  • 5 Jun 2013: Link to NuGet updated
  • 20 Sep 2012: Fixed broken links 
  • 15 Jul 2012: Help file updated
  • 8 Jul 2012: Support for IEnumerable added, source updated
  • 26 Feb 2012: Help file added to the download section
  • 25 Sep 2011: Section NuGet package added; some minor text changes; source updated
  • 10 Sep 2011: Section Implicit - but explicit! added; some minor text changes; source updated
  • 1 Sep 2011: First version

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Thorsten Bruning
Software Developer
Germany Germany
No Biography provided

Comments and Discussions

 
QuestionVote of 5 PinmemberGanesanSenthilvel8-Jul-12 17:47 
AnswerRe: Vote of 5 PinmemberThorsten Bruning8-Jul-12 20:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140821.2 | Last Updated 5 Jun 2013
Article Copyright 2011 by Thorsten Bruning
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid