Succinct but flexible data conversions for extended primitive data types. Part of the Ockham.NET project.
Every Ockham component should solve a clear problem that is not solved in the .NET BCL, or in the particular libraries it is meant to augment.
The utilities in this library provide robust, configurable data inspection and conversion utilities. These are particularly useful when deserializing from databases or other external sources, and when working with dynamic data. For more detail, see Purpose.
The most basic operation is to convert from a value of an unknown type (object
) to a specific type, using either generic syntax (when you know the target type at compile time) or non-generic syntax (when you need to specify the target type at runtime):
using Ockham.Data;
T Convert.To<T>(object);
object Convert.To(object, Type);
This intuitively works for DBNull
, Guid
, Timespan
, bool
, Nullable<T>
, and all enums (even nullables of enums!), and will automatically use IConvertible
and/or user defined conversion operators where applicable.
Subtleties of these conversions can be controlled with the ConvertOptions
class, described in more detail below:
object Convert.To(object, Type, ConvertOptions);
T Convert.To<T>(object, ConvertOptions);
Several type-specific overloads are also defined for convenience, which use the ConvertOptions.Default
settings:
bool Convert.ToBool(object)
Guid Convert.ToGuid(object)
int Convert.ToInt(object)
long Convert.ToLong(object)
string Convert.ToString(object)
... etc
If desired, conversion failures can be ignored (it is left to the user to determine when this is wise), with or without a default fallback value:
object Force.To(object value, Type targetType);
object Force.To(object value, Type targetType, object defaultValue);
T Force.To<T>(object value);
T Force.To<T>(object value, T defaultValue);
Finally, methods are provided to convert to and from DBNull
, or detect a "null" value (with the user specifying which things should be considered null):
object Convert.ToNull(object) // Converts DBNull to null, leaves other values as-is
object Convert.ToDBNull(object) // Converts null to DBNull, leaves other values as-is
// More control over what is considered null:
object Convert.ToNull(object value, ConvertOptions options)
object Convert.ToNull(object value, bool emptyStringAsNull)
object Convert.ToDBNull(object value, ConvertOptions options)
object Convert.ToDBNull(object value, bool emptyStringAsNull)
// Or to simply inspect:
bool Value.IsNull(object value) // True if null or DBNull
bool Value.IsNull(object value, bool emptyStringAsNull)
bool Value.IsNull(object value, ConvertOptions options)
Similar functions are provided on the Converter
class, where they use whatever options have been configured on the converter instance:
// Where 'other empty value' is determined by the converter's ConvertOptions
converter.ToNull(value) // Converts DBNull or other empty value to null
converter.ToDBNull(value) // Converts null or other empty value to DBNull
converter.IsNull(value) // Detects null, DBNull, or other empty value
A specific set of conversion options can be reused more easily with an instance of the Converter
class
var options = ConvertOptionsBuilder.Create()
.WithEnumOptions(undefinedNames: UndefinedValueOption.Ignore)
.WithStringOptions(emptyStringOptions: EmptyStringConvertOptions.WhitespaceAsNull, allowHex: true)
.WithTrueStrings(bool.TrueString, 't', 'x', 'y')
.WithFalseStrings(bool.FalseString, 'f', '', 'x')
.Options;
var converter = new Converter(options);
// All of the following use the ConvertOptions used to construct the converter:
converter.ToInt(value)
converter.To<int?>(value)
converter.To(value, type)
converter.Force<T>(value, T @default)
... etc
Converter
, ConvertOptions
, and ConvertOptionsBuilder
can all be subclassed. In addition, the base Converter
class can be extended by registering converters for specific types by providing a method that can match ConverterDelegate
or ConverterDelegate<T>
:
var extendedConverter = converter
.WithConverter<bool>((value, options) => /* custom logic, replacing default */ );
The Base Class Library and .NET languages themselves provide multiple ways to convert between primitive data types. So why is a separate library like this needed?
The existing basic data conversions available from the System.Convert
and Microsoft.VisualBasic.CompilerServices.Conversions
classes fail in several cases that arise frequently when working with serialization and deserialization:
- Converting to and from
DBNull
- Converting to and from
Nullable<T>
values - Converting to and from enumeration types
- Converting to and from
Guid
s - Converting to and from
TimeSpan
- Converting between
Boolean
and the many string representations used in databases in the wild
They also lack a universal conversion API with generic syntax. I.e. there is no T Convert.To<T>(object value)
in the BCL. The two BCL universal converters are non-generic methods which require a runtime Type
object and return an object
:
System.Convert.ChangeType
Conversions.ChangeType
, which is used by the VB.NET compiler to implement theCType
language function.
The two BCL converters also have important differences and notable behaviors:
System.Convert.ChangeType(object, Type)
will automatically use an applicableIConvertible
interface method, but will not use user-defined conversion operators (operator implicit
in C# /Widening Operator
in VB.NET)Microsoft.VisualBasic.CompilerServices.Conversions.ChangeType(object, Type)
will use user-defined conversion operators but will not useIConvertible
.System.Convert.ToInt32(null)
returns0
, whileSystem.Convert.ChangeType(null, typeof(int))
throws an exceptionMicrosoft.VisualBasic.CompilerServices.Conversions.Integer(null)
returns0
, andMicrosoft.VisualBasic.CompilerServices.Conversions.ChangeType(null, typeof(int))
also returns0
Beyond the awkward IFormatProvider
option which can be provided to System.Convert
, the BCL conversions do not provide means of explicitly controlling conversion behaviors which must be considered when deserializing or converting from an external data source. This library exposes the following options as explicit choices:
- Treating an empty string as null (or not)
- Recognizing hexadecimal strings as numbers (or not)
- Converting null values to the default value of a value type (or not)
- Suppressing errors when attempting to convert, with or without an explicit fallback value (or not)
The BCL also lacks succinct methods for checking if a value can be treated as numeric, if it is the default value of a value type (e.g 0
for any numeric type), or if it is null / empty / DBNull. These basic inspection actions are often required when validating or reasoning about input from external services or data sources.
The core conversion logic is implemented in the internal method Ockham.Data.Convert.To
:
internal static object To(
object value,
Type targetType,
ConvertOptions options,
bool ignoreError,
object defaultValue,
bool customConverters,
Dictionary<Type, ConverterDelegate> converters
)
The various overloads of To*
and Force*
on the static Convert
class and configurable Converter
class instances internally resolve to this single conversion logic, just with different variations of the input arguments, and sometimes with a direct cast to the required output type.
The conversion logic does not replace built-in BCL conversion tools, but rather arranges and combines them into an efficient whole. It falls back to System.Convert.ChangeType(object, Type)
if it detects an IConvertible
input with a valid candidate output type, and falls back to Microsoft.VisualBasic.CompilerServices.Conversions.ChangeType(object, Type)
for other basic conversions, which takes advantage of that implementation's advanced features such as automatic detection and use of user-defined conversion operators.