This article is about the different possibilities of type conversion provided by the .NET Framework. At the end, it offers a ready to use solution: the UniversalTypeConverter which converts nearly every type to another type.
Table of Contents
Latest release notes!
Version 2.7.1
Version 2.7.1 supports converting Guid
to byte[]
NuGet-package was updated as well. Check out the supported conversions.
Version 2.6.2
Version 2.6.2 supports common conversions of ReadOnlySpan
of char
.
Version 2.6
Version 2.6 supports new framework types DateOnly
and TimeOnly
- and adds some more date time formats.
Notice
This article is somehow outdated - there is a complete new rewrite of the UniversalTypeConverter. For now, have a look at the new project site. But you can still read on - most of the explained concepts also apply to the new version.
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.
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...
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 Enum
s, 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.
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.
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;
}
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 null
s 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 ValueType
s (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.
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 TypeConverter
s 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 TypeConverter
s 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...
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?
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.
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;
}
catch {
}
}
}
}
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
.
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;
}
Null
, again? I know we already talked about null
. But remember the problem with ValueType
s like int
, which cannot be null
? Since version 2.0 of .NET, there is a wrapper-type providing a kind of null
assignment to ValueType
s. 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 null
s - or other null
s - 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);
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);
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
The UniversalTypeConverter
is also available as a NuGet package. Just search after UniversalTypeConverter
within the NuGet Package Manager.
The last NuGet-Package supports .net Standard >= 1.3 so it can be used in .net Core.
Thanks to Stefan Ossendorf to put some effort into this!
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:
- Converting all elements of an
IEnumerable
to a specified type (e.g. an int[]
to string[]
). - Converting all elements of an
IEnumerable
to a string representation (e.g. "1,2,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>()
.IgnoringNullElements()
.IgnoringNonConvertibleElements()
.UsingCulture(CultureInfo.CurrentCulture)
.UsingConversionOptions(ConversionOptions.AllowDefaultValueIfWhitespace)
.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(
CultureInfo.CurrentCulture,
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(";"))
.TrimmingEndOfElements()
.TrimmingStartOfElements()
.IgnoringEmptyElements()
.WithNullBeing(".null.")
.IgnoringNullElements()
.IgnoringNonConvertibleElements()
.UsingConversionOptions(ConversionOptions.AllowDefaultValueIfWhitespace)
.UsingCulture(CultureInfo.CurrentCulture)
.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