Skip to content

Succinct but flexible data conversions for extended primitive data types

License

Notifications You must be signed in to change notification settings

ockham-net/ockham.net.convert

Repository files navigation

Ockham.Convert

Succinct but flexible data conversions for extended primitive data types. Part of the Ockham.NET project.

The Problem

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 API

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

Customization

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 */ );

Purpose

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 Guids
  • 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:

Differences in System.Convert, Microsoft.VisualBasic.CompilerServices.Conversions

The two BCL converters also have important differences and notable behaviors:

  • System.Convert.ChangeType(object, Type) will automatically use an applicable IConvertible 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 use IConvertible.
  • System.Convert.ToInt32(null) returns 0, while System.Convert.ChangeType(null, typeof(int)) throws an exception
  • Microsoft.VisualBasic.CompilerServices.Conversions.Integer(null) returns 0, and Microsoft.VisualBasic.CompilerServices.Conversions.ChangeType(null, typeof(int)) also returns 0

Controlling subtleties of conversion

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)

Dynamic Value Inspection

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.

Architecture

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.

About

Succinct but flexible data conversions for extended primitive data types

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published