This commit is contained in:
Stephan
2018-01-20 15:10:22 +01:00
parent 8d4fd9d1cf
commit 363c8fab2c
12 changed files with 358 additions and 327 deletions

View File

@@ -5,8 +5,9 @@ using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
using System.Xml;
using Umbraco.Core.Collections;
namespace Umbraco.Core
{
@@ -14,19 +15,19 @@ namespace Umbraco.Core
/// Provides object extension methods.
/// </summary>
public static class ObjectExtensions
{
// Used for caching the various type lookups
{
// Cache the various type lookups
private static readonly Dictionary<Type, Type> NullableGenericCache = new Dictionary<Type, Type>();
private static readonly Dictionary<TypePair, TypeConverter> InputTypeConverterCache = new Dictionary<TypePair, TypeConverter>();
private static readonly Dictionary<TypePair, TypeConverter> DestinationTypeConverterCache = new Dictionary<TypePair, TypeConverter>();
private static readonly Dictionary<TypePair, IConvertible> AssignableTypeCache = new Dictionary<TypePair, IConvertible>();
private static readonly Dictionary<CompositeTypeTypeKey, TypeConverter> InputTypeConverterCache = new Dictionary<CompositeTypeTypeKey, TypeConverter>();
private static readonly Dictionary<CompositeTypeTypeKey, TypeConverter> DestinationTypeConverterCache = new Dictionary<CompositeTypeTypeKey, TypeConverter>();
private static readonly Dictionary<CompositeTypeTypeKey, IConvertible> AssignableTypeCache = new Dictionary<CompositeTypeTypeKey, IConvertible>();
private static readonly Dictionary<Type, bool> BoolConvertCache = new Dictionary<Type, bool>();
private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' };
private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter();
//private static readonly ConcurrentDictionary<Type, Func<object>> ObjectFactoryCache = new ConcurrentDictionary<Type, Func<object>>();
private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter();
//private static readonly ConcurrentDictionary<Type, Func<object>> ObjectFactoryCache = new ConcurrentDictionary<Type, Func<object>>();
/// <summary>
///
/// </summary>
@@ -71,62 +72,60 @@ namespace Umbraco.Core
/// <returns>The <see cref="Attempt{T}"/></returns>
public static Attempt<T> TryConvertTo<T>(this object input)
{
Attempt<object> result = TryConvertTo(input, typeof(T));
if (!result.Success)
{
// Just try a straight up conversion
try
{
var converted = (T)input;
return Attempt<T>.Succeed(converted);
}
catch (Exception e)
{
return Attempt<T>.Fail(e);
}
}
var result = TryConvertTo(input, typeof(T));
if (result.Success)
return Attempt<T>.Succeed((T) result.Result);
// just try to cast
try
{
return Attempt<T>.Succeed((T) input);
}
catch (Exception e)
{
return Attempt<T>.Fail(e);
}
}
return Attempt<T>.Succeed((T)result.Result);
}
/// <summary>
/// Attempts to convert the input object to the output type.
/// </summary>
/// <remarks>This code is an optimized version of the original Umbraco method</remarks>
/// <param name="input">The input.</param>
/// <param name="destinationType">The type to convert to</param>
/// <param name="target">The type to convert to</param>
/// <returns>The <see cref="Attempt{Object}"/></returns>
public static Attempt<object> TryConvertTo(this object input, Type destinationType)
public static Attempt<object> TryConvertTo(this object input, Type target)
{
if (target == null)
{
return Attempt<object>.Fail();
}
try
{
if (destinationType == null)
{
return Attempt<object>.Fail();
}
if (input == null)
{
// Nullable is ok
if (destinationType.IsGenericType && GetCachedGenericNullableType(destinationType) != null)
if (target.IsGenericType && GetCachedGenericNullableType(target) != null)
{
return Attempt<object>.Succeed(null);
}
// Reference types are ok
return Attempt<object>.SucceedIf(!destinationType.IsValueType, null);
return Attempt<object>.SucceedIf(target.IsValueType == false, null);
}
Type inputType = input.GetType();
var inputType = input.GetType();
// Easy
if (destinationType == typeof(object) || inputType == destinationType)
if (target == typeof(object) || inputType == target)
{
return Attempt.Succeed(input);
}
// Check for string so that overloaders of ToString() can take advantage of the conversion.
if (destinationType == typeof(string))
if (target == typeof(string))
{
return Attempt<object>.Succeed(input.ToString());
}
@@ -134,42 +133,44 @@ namespace Umbraco.Core
// If we've got a nullable of something, we try to convert directly to that thing.
// We cache the destination type and underlying nullable types
// Any other generic types need to fall through
if (destinationType.IsGenericType)
if (target.IsGenericType)
{
Type underlyingGenericType = GetCachedGenericNullableType(destinationType);
if (underlyingGenericType != null)
var underlying = GetCachedGenericNullableType(target);
if (underlying != null)
{
// Special case for empty strings for bools/dates which should return null if an empty string.
if (input is string asString)
if (input is string inputString)
{
if (string.IsNullOrEmpty(asString) && (underlyingGenericType == typeof(DateTime) || underlyingGenericType == typeof(bool)))
if (string.IsNullOrEmpty(inputString) && (underlying == typeof(DateTime) || underlying == typeof(bool)))
{
return Attempt<object>.Succeed(null);
}
}
// Recursively call into this method with the inner (not-nullable) type and handle the outcome
Attempt<object> nonNullable = input.TryConvertTo(underlyingGenericType);
var inner = input.TryConvertTo(underlying);
// And if sucessful, fall on through to rewrap in a nullable; if failed, pass on the exception
if (nonNullable.Success)
if (inner.Success)
{
input = nonNullable.Result; // Now fall on through...
input = inner.Result; // Now fall on through...
}
else
{
return Attempt<object>.Fail(nonNullable.Exception);
return Attempt<object>.Fail(inner.Exception);
}
}
}
else
{
if (input is string asString)
{
// target is not a generic type
if (input is string inputString)
{
// Try convert from string, returns an Attempt if the string could be
// processed (either succeeded or failed), else null if we need to try
// other methods
Attempt<object>? result = TryConvertToFromString(asString, destinationType);
var result = TryConvertToFromString(inputString, target);
if (result.HasValue)
{
return result.Value;
@@ -178,14 +179,14 @@ namespace Umbraco.Core
// TODO: Do a check for destination type being IEnumerable<T> and source type implementing IEnumerable<T> with
// the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it.
IConvertible convertible = GetCachedAssignableConvertibleResult(input, inputType, destinationType);
var convertible = GetCachedAssignableConvertibleResult(input, inputType, target);
if (convertible != null)
{
return Attempt.Succeed(Convert.ChangeType(convertible, destinationType));
return Attempt.Succeed(Convert.ChangeType(convertible, target));
}
}
if (destinationType == typeof(bool))
if (target == typeof(bool))
{
if (GetCanConvertToBooleanResult(inputType))
{
@@ -193,13 +194,13 @@ namespace Umbraco.Core
}
}
TypeConverter inputConverter = GetCachedInputTypeConverter(inputType, destinationType);
var inputConverter = GetCachedSourceTypeConverter(inputType, target);
if (inputConverter != null)
{
return Attempt.Succeed(inputConverter.ConvertTo(input, destinationType));
return Attempt.Succeed(inputConverter.ConvertTo(input, target));
}
TypeConverter outputConverter = GetCachedDestinationTypeConverter(inputType, destinationType);
var outputConverter = GetCachedTargetTypeConverter(inputType, target);
if (outputConverter != null)
{
return Attempt.Succeed(outputConverter.ConvertFrom(input));
@@ -208,7 +209,7 @@ namespace Umbraco.Core
// Re-check convertables since we altered the input through recursion
if (input is IConvertible convertible2)
{
return Attempt.Succeed(Convert.ChangeType(convertible2, destinationType));
return Attempt.Succeed(Convert.ChangeType(convertible2, target));
}
}
catch (Exception e)
@@ -217,19 +218,19 @@ namespace Umbraco.Core
}
return Attempt<object>.Fail();
}
}
/// <summary>
/// Attempts to convert the input string to the output type.
/// </summary>
/// <remarks>This code is an optimized version of the original Umbraco method</remarks>
/// <param name="input">The input.</param>
/// <param name="destinationType">The type to convert to</param>
/// <param name="target">The type to convert to</param>
/// <returns>The <see cref="Nullable{Attempt}"/></returns>
private static Attempt<object>? TryConvertToFromString(this string input, Type destinationType)
private static Attempt<object>? TryConvertToFromString(this string input, Type target)
{
// Easy
if (destinationType == typeof(string))
if (target == typeof(string))
{
return Attempt<object>.Succeed(input);
}
@@ -237,13 +238,13 @@ namespace Umbraco.Core
// Null, empty, whitespaces
if (string.IsNullOrWhiteSpace(input))
{
if (destinationType == typeof(bool))
if (target == typeof(bool))
{
// null/empty = bool false
return Attempt<object>.Succeed(false);
}
if (destinationType == typeof(DateTime))
if (target == typeof(DateTime))
{
// null/empty = min DateTime value
return Attempt<object>.Succeed(DateTime.MinValue);
@@ -260,37 +261,38 @@ namespace Umbraco.Core
// By using a mixture of ordered if statements and switches we can optimize both for
// fast conditional checking for most frequently used types and the branching
// that does not depend on previous values available to switch statements.
if (destinationType.IsPrimitive)
if (target.IsPrimitive)
{
if (destinationType == typeof(int))
if (target == typeof(int))
{
if (int.TryParse(input, out int value))
if (int.TryParse(input, out var value))
{
return Attempt<object>.Succeed(value);
}
// Because decimal 100.01m will happily convert to integer 100, it
// makes sense that string "100.01" *also* converts to integer 100.
string input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(decimal.TryParse(input2, out decimal value2), Convert.ToInt32(value2));
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2));
}
if (destinationType == typeof(long))
if (target == typeof(long))
{
if (long.TryParse(input, out long value))
if (long.TryParse(input, out var value))
{
return Attempt<object>.Succeed(value);
}
// Same as int
string input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(decimal.TryParse(input2, out decimal value2), Convert.ToInt64(value2));
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2));
}
// TODO: Should we do the decimal trick for short, byte, unsigned?
if (destinationType == typeof(bool))
// TODO: Should we do the decimal trick for short, byte, unsigned?
if (target == typeof(bool))
{
if (bool.TryParse(input, out bool value))
if (bool.TryParse(input, out var value))
{
return Attempt<object>.Succeed(value);
}
@@ -300,45 +302,45 @@ namespace Umbraco.Core
}
// Calling this method directly is faster than any attempt to cache it.
switch (Type.GetTypeCode(destinationType))
switch (Type.GetTypeCode(target))
{
case TypeCode.Int16:
return Attempt<object>.SucceedIf(short.TryParse(input, out short value), value);
return Attempt<object>.SucceedIf(short.TryParse(input, out var value), value);
case TypeCode.Double:
string input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(double.TryParse(input2, out double valueD), valueD);
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(double.TryParse(input2, out var valueD), valueD);
case TypeCode.Single:
string input3 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(float.TryParse(input3, out float valueF), valueF);
var input3 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(float.TryParse(input3, out var valueF), valueF);
case TypeCode.Char:
return Attempt<object>.SucceedIf(char.TryParse(input, out char valueC), valueC);
return Attempt<object>.SucceedIf(char.TryParse(input, out var valueC), valueC);
case TypeCode.Byte:
return Attempt<object>.SucceedIf(byte.TryParse(input, out byte valueB), valueB);
return Attempt<object>.SucceedIf(byte.TryParse(input, out var valueB), valueB);
case TypeCode.SByte:
return Attempt<object>.SucceedIf(sbyte.TryParse(input, out sbyte valueSb), valueSb);
return Attempt<object>.SucceedIf(sbyte.TryParse(input, out var valueSb), valueSb);
case TypeCode.UInt32:
return Attempt<object>.SucceedIf(uint.TryParse(input, out uint valueU), valueU);
return Attempt<object>.SucceedIf(uint.TryParse(input, out var valueU), valueU);
case TypeCode.UInt16:
return Attempt<object>.SucceedIf(ushort.TryParse(input, out ushort valueUs), valueUs);
return Attempt<object>.SucceedIf(ushort.TryParse(input, out var valueUs), valueUs);
case TypeCode.UInt64:
return Attempt<object>.SucceedIf(ulong.TryParse(input, out ulong valueUl), valueUl);
return Attempt<object>.SucceedIf(ulong.TryParse(input, out var valueUl), valueUl);
}
}
else if (destinationType == typeof(Guid))
else if (target == typeof(Guid))
{
return Attempt<object>.SucceedIf(Guid.TryParse(input, out Guid value), value);
return Attempt<object>.SucceedIf(Guid.TryParse(input, out var value), value);
}
else if (destinationType == typeof(DateTime))
else if (target == typeof(DateTime))
{
if (DateTime.TryParse(input, out DateTime value))
if (DateTime.TryParse(input, out var value))
{
switch (value.Kind)
{
@@ -356,27 +358,27 @@ namespace Umbraco.Core
return Attempt<object>.Fail();
}
else if (destinationType == typeof(DateTimeOffset))
else if (target == typeof(DateTimeOffset))
{
return Attempt<object>.SucceedIf(DateTimeOffset.TryParse(input, out DateTimeOffset value), value);
return Attempt<object>.SucceedIf(DateTimeOffset.TryParse(input, out var value), value);
}
else if (destinationType == typeof(TimeSpan))
else if (target == typeof(TimeSpan))
{
return Attempt<object>.SucceedIf(TimeSpan.TryParse(input, out TimeSpan value), value);
return Attempt<object>.SucceedIf(TimeSpan.TryParse(input, out var value), value);
}
else if (destinationType == typeof(decimal))
else if (target == typeof(decimal))
{
string input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(decimal.TryParse(input2, out decimal value), value);
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.SucceedIf(decimal.TryParse(input2, out var value), value);
}
else if (input != null && destinationType == typeof(Version))
else if (input != null && target == typeof(Version))
{
return Attempt<object>.SucceedIf(Version.TryParse(input, out Version value), value);
return Attempt<object>.SucceedIf(Version.TryParse(input, out var value), value);
}
// E_NOTIMPL IPAddress, BigInteger
return null; // We can't decide...
}
}
internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname)
{
//TODO: Localise this exception
@@ -641,115 +643,100 @@ namespace Umbraco.Core
internal static Guid AsGuid(this object value)
{
return value is Guid ? (Guid) value : Guid.Empty;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string NormalizeNumberDecimalSeparator(string s)
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string NormalizeNumberDecimalSeparator(string s)
{
var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0];
return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized);
}
return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized);
}
// gets a converter for source, that can convert to target, or null if none exists
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeConverter GetCachedInputTypeConverter(Type inputType, Type destinationType)
private static TypeConverter GetCachedSourceTypeConverter(Type source, Type target)
{
var key = new TypePair(inputType, destinationType);
if (!InputTypeConverterCache.ContainsKey(key))
var key = new CompositeTypeTypeKey(source, target);
if (InputTypeConverterCache.TryGetValue(key, out var cached))
return cached;
var converter = TypeDescriptor.GetConverter(source);
if (converter.CanConvertTo(target))
{
TypeConverter converter = TypeDescriptor.GetConverter(inputType);
if (converter.CanConvertTo(destinationType))
{
InputTypeConverterCache[key] = converter;
return converter;
}
else
{
InputTypeConverterCache[key] = null;
return null;
}
InputTypeConverterCache[key] = converter;
return converter;
}
return InputTypeConverterCache[key];
InputTypeConverterCache[key] = null;
return null;
}
// gets a converter for target, that can convert from source, or null if none exists
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeConverter GetCachedDestinationTypeConverter(Type inputType, Type destinationType)
private static TypeConverter GetCachedTargetTypeConverter(Type source, Type target)
{
var key = new TypePair(inputType, destinationType);
if (!DestinationTypeConverterCache.ContainsKey(key))
var key = new CompositeTypeTypeKey(source, target);
if (DestinationTypeConverterCache.TryGetValue(key, out var cached))
return cached;
var converter = TypeDescriptor.GetConverter(target);
if (converter.CanConvertFrom(source))
{
TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
if (converter.CanConvertFrom(inputType))
{
DestinationTypeConverterCache[key] = converter;
return converter;
}
else
{
DestinationTypeConverterCache[key] = null;
return null;
}
DestinationTypeConverterCache[key] = converter;
return converter;
}
return DestinationTypeConverterCache[key];
DestinationTypeConverterCache[key] = null;
return null;
}
// gets the underlying type of a nullable type, or null if the type is not nullable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Type GetCachedGenericNullableType(Type destinationType)
private static Type GetCachedGenericNullableType(Type type)
{
if (!NullableGenericCache.ContainsKey(destinationType))
if (NullableGenericCache.TryGetValue(type, out var cached))
return cached;
if (type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
if (destinationType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Type underlying = Nullable.GetUnderlyingType(destinationType);
NullableGenericCache[destinationType] = underlying;
return underlying;
}
else
{
NullableGenericCache[destinationType] = null;
return null;
}
var underlying = Nullable.GetUnderlyingType(type);
NullableGenericCache[type] = underlying;
return underlying;
}
return NullableGenericCache[destinationType];
NullableGenericCache[type] = null;
return null;
}
// gets an IConvertible from source to target type, or null if none exists
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static IConvertible GetCachedAssignableConvertibleResult(object input, Type inputType, Type destinationType)
private static IConvertible GetCachedAssignableConvertibleResult(object input, Type source, Type target)
{
var key = new TypePair(inputType, destinationType);
if (!AssignableTypeCache.ContainsKey(key))
var key = new CompositeTypeTypeKey(source, target);
if (AssignableTypeCache.TryGetValue(key, out var cached))
return cached;
if (target.IsAssignableFrom(source) && input is IConvertible convertible)
{
if (destinationType.IsAssignableFrom(inputType))
{
if (input is IConvertible convertable)
{
AssignableTypeCache[key] = convertable;
return convertable;
}
}
else
{
AssignableTypeCache[key] = null;
return null;
}
AssignableTypeCache[key] = convertible;
return convertible;
}
return AssignableTypeCache[key];
AssignableTypeCache[key] = null;
return null;
}
// determines whether a type can be converted to boolean
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool GetCanConvertToBooleanResult(Type inputType)
private static bool GetCanConvertToBooleanResult(Type type)
{
if (!BoolConvertCache.ContainsKey(inputType))
{
bool canConvert = CustomBooleanTypeConverter.CanConvertFrom(inputType);
BoolConvertCache[inputType] = canConvert;
return canConvert;
}
if (BoolConvertCache.TryGetValue(type, out var cached))
return cached;
return BoolConvertCache[inputType];
}
var canConvert = CustomBooleanTypeConverter.CanConvertFrom(type);
BoolConvertCache[type] = canConvert;
return canConvert;
}
}
}