// Copyright (c) Umbraco. // See LICENSE for more details. using System.Collections; using System.Collections.Concurrent; using System.ComponentModel; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Xml; using Microsoft.Extensions.Primitives; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Collections; namespace Umbraco.Extensions; /// /// Provides object extension methods. /// public static class ObjectExtensions { private static readonly ConcurrentDictionary NullableGenericCache = new(); private static readonly ConcurrentDictionary InputTypeConverterCache = new(); private static readonly ConcurrentDictionary DestinationTypeConverterCache = new(); private static readonly ConcurrentDictionary AssignableTypeCache = new(); private static readonly ConcurrentDictionary BoolConvertCache = new(); private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' }; private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new(); // private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); /// /// /// /// /// public static IEnumerable AsEnumerableOfOne(this T input) => Enumerable.Repeat(input, 1); /// /// /// public static void DisposeIfDisposable(this object input) { if (input is IDisposable disposable) { disposable.Dispose(); } } /// /// Provides a shortcut way of safely casting an input when you cannot guarantee the is /// an instance type (i.e., when the C# AS keyword is not applicable). /// /// /// The input. /// public static T? SafeCast(this object input) { if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) { return default; } if (input is T variable) { return variable; } return default; } /// /// Attempts to convert the input object to the output type. /// /// This code is an optimized version of the original Umbraco method /// The type to convert to /// The input. /// The public static Attempt TryConvertTo(this object? input) { Attempt result = TryConvertTo(input, typeof(T)); if (result.Success) { return Attempt.Succeed((T?)result.Result); } if (input == null) { if (typeof(T).IsValueType) { // fail, cannot convert null to a value type return Attempt.Fail(); } // sure, null can be any object return Attempt.Succeed((T)input!); } // just try to cast try { return Attempt.Succeed((T)input); } catch (Exception e) { return Attempt.Fail(e); } } /// /// Attempts to convert the input object to the output type. /// /// This code is an optimized version of the original Umbraco method /// The input. /// The type to convert to /// The public static Attempt TryConvertTo(this object? input, Type target) { if (target == null) { return Attempt.Fail(); } try { if (input == null) { // Nullable is ok if (target.IsGenericType && GetCachedGenericNullableType(target) != null) { return Attempt.Succeed(null); } // Reference types are ok return Attempt.If(target.IsValueType == false, null); } Type inputType = input.GetType(); // Easy 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 (target == typeof(string)) { return Attempt.Succeed(input.ToString()); } // 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 (target.IsGenericType) { Type? 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 inputString) { // TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null? if (string.IsNullOrEmpty(inputString) && (underlying == typeof(DateTime) || underlying == typeof(bool))) { return Attempt.Succeed(null); } } // Recursively call into this method with the inner (not-nullable) type and handle the outcome Attempt inner = input.TryConvertTo(underlying); // And if successful, fall on through to rewrap in a nullable; if failed, pass on the exception if (inner.Success) { input = inner.Result; // Now fall on through... } else { return Attempt.Fail(inner.Exception); } } } else { // target is not a generic type var inputString = input as string ?? (input is StringValues sv ? sv.ToString() : null); if (inputString != null) { // Try convert from string or StringValues, returns an Attempt if the string could be // processed (either succeeded or failed), else null if we need to try // other methods Attempt? result = TryConvertToFromString(inputString, target); if (result.HasValue) { return result.Value; } } // TODO: Do a check for destination type being IEnumerable and source type implementing IEnumerable with // the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it. if (GetCachedCanAssign(input, inputType, target)) { return Attempt.Succeed(Convert.ChangeType(input, target)); } } if (target == typeof(bool)) { if (GetCachedCanConvertToBoolean(inputType)) { return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input!)); } } TypeConverter? inputConverter = GetCachedSourceTypeConverter(inputType, target); if (inputConverter != null) { return Attempt.Succeed(inputConverter.ConvertTo(input, target)); } TypeConverter? outputConverter = GetCachedTargetTypeConverter(inputType, target); if (outputConverter != null) { return Attempt.Succeed(outputConverter.ConvertFrom(input!)); } if (target.IsGenericType && GetCachedGenericNullableType(target) != null) { // cannot Convert.ChangeType as that does not work with nullable // input has already been converted to the underlying type - just // return input, there's an implicit conversion from T to T? anyways return Attempt.Succeed(input); } // Re-check convertibles since we altered the input through recursion if (input is IConvertible convertible2) { return Attempt.Succeed(Convert.ChangeType(convertible2, target)); } } catch (Exception e) { return Attempt.Fail(e); } return Attempt.Fail(); } // public enum PropertyNamesCaseType // { // CamelCase, // CaseInsensitive // } ///// ///// Convert an object to a JSON string with camelCase formatting ///// ///// ///// // public static string ToJsonString(this object obj) // { // return obj.ToJsonString(PropertyNamesCaseType.CamelCase); // } ///// ///// Convert an object to a JSON string with the specified formatting ///// ///// The obj. ///// Type of the property names case. ///// // public static string ToJsonString(this object obj, PropertyNamesCaseType propertyNamesCaseType) // { // var type = obj.GetType(); // var dateTimeStyle = "yyyy-MM-dd HH:mm:ss"; // if (type.IsPrimitive || typeof(string).IsAssignableFrom(type)) // { // return obj.ToString(); // } // if (typeof(DateTime).IsAssignableFrom(type) || typeof(DateTimeOffset).IsAssignableFrom(type)) // { // return Convert.ToDateTime(obj).ToString(dateTimeStyle); // } // var serializer = new JsonSerializer(); // switch (propertyNamesCaseType) // { // case PropertyNamesCaseType.CamelCase: // serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); // break; // } // var dateTimeConverter = new IsoDateTimeConverter // { // DateTimeStyles = System.Globalization.DateTimeStyles.None, // DateTimeFormat = dateTimeStyle // }; // if (typeof(IDictionary).IsAssignableFrom(type)) // { // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); // } // if (type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type))) // { // return JArray.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); // } // return JObject.FromObject(obj, serializer).ToString(Formatting.None, dateTimeConverter); // } /// /// Converts an object into a dictionary /// /// /// /// /// /// /// public static IDictionary? ToDictionary( this T o, params Expression>[] ignoreProperties) => o?.ToDictionary(ignoreProperties .Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray()); internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname) { // TODO: Localize this exception if (isDisposed) { throw new ObjectDisposedException(objectname); } } /// /// Attempts to convert the input string to the output type. /// /// This code is an optimized version of the original Umbraco method /// The input. /// The type to convert to /// The private static Attempt? TryConvertToFromString(this string input, Type target) { // Easy if (target == typeof(string)) { return Attempt.Succeed(input); } // Null, empty, whitespaces if (string.IsNullOrWhiteSpace(input)) { if (target == typeof(bool)) { // null/empty = bool false return Attempt.Succeed(false); } if (target == typeof(DateTime)) { // null/empty = min DateTime value return Attempt.Succeed(DateTime.MinValue); } // Cannot decide here, // Any of the types below will fail parsing and will return a failed attempt // but anything else will not be processed and will return null // so even though the string is null/empty we have to proceed. } // Look for type conversions in the expected order of frequency of use. // // 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 (target.IsPrimitive) { if (target == typeof(int)) { if (int.TryParse(input, out var value)) { return Attempt.Succeed(value); } // Because decimal 100.01m will happily convert to integer 100, it // makes sense that string "100.01" *also* converts to integer 100. var input2 = NormalizeNumberDecimalSeparator(input); return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2)); } if (target == typeof(long)) { if (long.TryParse(input, out var value)) { return Attempt.Succeed(value); } // Same as int var input2 = NormalizeNumberDecimalSeparator(input); return Attempt.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2)); } // TODO: Should we do the decimal trick for short, byte, unsigned? if (target == typeof(bool)) { if (bool.TryParse(input, out var value)) { return Attempt.Succeed(value); } // Don't declare failure so the CustomBooleanTypeConverter can try return null; } // Calling this method directly is faster than any attempt to cache it. switch (Type.GetTypeCode(target)) { case TypeCode.Int16: return Attempt.If(short.TryParse(input, out var value), value); case TypeCode.Double: var input2 = NormalizeNumberDecimalSeparator(input); return Attempt.If(double.TryParse(input2, out var valueD), valueD); case TypeCode.Single: var input3 = NormalizeNumberDecimalSeparator(input); return Attempt.If(float.TryParse(input3, out var valueF), valueF); case TypeCode.Char: return Attempt.If(char.TryParse(input, out var valueC), valueC); case TypeCode.Byte: return Attempt.If(byte.TryParse(input, out var valueB), valueB); case TypeCode.SByte: return Attempt.If(sbyte.TryParse(input, out var valueSb), valueSb); case TypeCode.UInt32: return Attempt.If(uint.TryParse(input, out var valueU), valueU); case TypeCode.UInt16: return Attempt.If(ushort.TryParse(input, out var valueUs), valueUs); case TypeCode.UInt64: return Attempt.If(ulong.TryParse(input, out var valueUl), valueUl); } } else if (target == typeof(Guid)) { return Attempt.If(Guid.TryParse(input, out Guid value), value); } else if (target == typeof(DateTime)) { if (DateTime.TryParse(input, out DateTime value)) { switch (value.Kind) { case DateTimeKind.Unspecified: case DateTimeKind.Utc: return Attempt.Succeed(value); case DateTimeKind.Local: return Attempt.Succeed(value.ToUniversalTime()); default: throw new ArgumentOutOfRangeException(); } } return Attempt.Fail(); } else if (target == typeof(DateTimeOffset)) { return Attempt.If(DateTimeOffset.TryParse(input, out DateTimeOffset value), value); } else if (target == typeof(TimeSpan)) { return Attempt.If(TimeSpan.TryParse(input, out TimeSpan value), value); } else if (target == typeof(decimal)) { var input2 = NormalizeNumberDecimalSeparator(input); return Attempt.If(decimal.TryParse(input2, out var value), value); } else if (input != null && target == typeof(Version)) { return Attempt.If(Version.TryParse(input, out Version? value), value); } // E_NOTIMPL IPAddress, BigInteger return null; // we can't decide... } /// /// Turns object into dictionary /// /// /// Properties to ignore /// public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) { if (o != null) { PropertyDescriptorCollection props = TypeDescriptor.GetProperties(o); var d = new Dictionary(); foreach (PropertyDescriptor prop in props.Cast() .Where(x => ignoreProperties.Contains(x.Name) == false)) { var val = prop.GetValue(o); if (val != null) { d.Add(prop.Name, (TVal)val); } } return d; } return new Dictionary(); } /// /// Returns an XmlSerialized safe string representation for the value /// /// /// The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown /// public static string ToXmlString(this object value, Type type) { if (value == null) { return string.Empty; } if (type == typeof(string)) { return value.ToString().IsNullOrWhiteSpace() ? string.Empty : value.ToString()!; } if (type == typeof(bool)) { return XmlConvert.ToString((bool)value); } if (type == typeof(byte)) { return XmlConvert.ToString((byte)value); } if (type == typeof(char)) { return XmlConvert.ToString((char)value); } if (type == typeof(DateTime)) { return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.Unspecified); } if (type == typeof(DateTimeOffset)) { return XmlConvert.ToString((DateTimeOffset)value); } if (type == typeof(decimal)) { return XmlConvert.ToString((decimal)value); } if (type == typeof(double)) { return XmlConvert.ToString((double)value); } if (type == typeof(float)) { return XmlConvert.ToString((float)value); } if (type == typeof(Guid)) { return XmlConvert.ToString((Guid)value); } if (type == typeof(int)) { return XmlConvert.ToString((int)value); } if (type == typeof(long)) { return XmlConvert.ToString((long)value); } if (type == typeof(sbyte)) { return XmlConvert.ToString((sbyte)value); } if (type == typeof(short)) { return XmlConvert.ToString((short)value); } if (type == typeof(TimeSpan)) { return XmlConvert.ToString((TimeSpan)value); } if (type == typeof(uint)) { return XmlConvert.ToString((uint)value); } if (type == typeof(ulong)) { return XmlConvert.ToString((ulong)value); } if (type == typeof(ushort)) { return XmlConvert.ToString((ushort)value); } throw new NotSupportedException("Cannot convert type " + type.FullName + " to a string using ToXmlString as it is not supported by XmlConvert"); } internal static string? ToDebugString(this object? obj, int levels = 0) { if (obj == null) { return "{null}"; } try { if (obj is string) { return "\"{0}\"".InvariantFormat(obj); } if (obj is int || obj is short || obj is long || obj is float || obj is double || obj is bool || obj is int? || obj is short? || obj is long? || obj is float? || obj is double? || obj is bool?) { return "{0}".InvariantFormat(obj); } if (obj is Enum) { return "[{0}]".InvariantFormat(obj); } if (obj is IEnumerable enumerable) { var items = (from object enumItem in enumerable let value = GetEnumPropertyDebugString(enumItem, levels) where value != null select value).Take(10).ToList(); return items.Any() ? "{{ {0} }}".InvariantFormat(string.Join(", ", items)) : null; } PropertyInfo[] props = obj.GetType().GetProperties(); if (props.Length == 2 && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) { try { var key = props[0].GetValue(obj, null) as string; var value = props[1].GetValue(obj, null).ToDebugString(levels - 1); return "{0}={1}".InvariantFormat(key, value); } catch (Exception) { return "[KeyValuePropertyException]"; } } if (levels > -1) { var items = (from propertyInfo in props let value = GetPropertyDebugString(propertyInfo, obj, levels) where value != null select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); return items.Any() ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, string.Join(", ", items)) : null; } } catch (Exception ex) { return "[Exception:{0}]".InvariantFormat(ex.Message); } return null; } /// /// Attempts to serialize the value to an XmlString using ToXmlString /// /// /// /// internal static Attempt TryConvertToXmlString(this object value, Type type) { try { var output = value.ToXmlString(type); return Attempt.Succeed(output); } catch (NotSupportedException ex) { return Attempt.Fail(ex); } } /// /// Returns an XmlSerialized safe string representation for the value and type /// /// /// /// public static string ToXmlString(this object value) => value.ToXmlString(typeof(T)); public static Guid AsGuid(this object value) => value is Guid guid ? guid : Guid.Empty; private static string? GetEnumPropertyDebugString(object enumItem, int levels) { try { return enumItem.ToDebugString(levels - 1); } catch (Exception) { return "[GetEnumPartException]"; } } private static string? GetPropertyDebugString(PropertyInfo propertyInfo, object obj, int levels) { try { return propertyInfo.GetValue(obj, null).ToDebugString(levels - 1); } catch (Exception) { return "[GetPropertyValueException]"; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string NormalizeNumberDecimalSeparator(string s) { var normalized = Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; 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? GetCachedSourceTypeConverter(Type source, Type target) { var key = new CompositeTypeTypeKey(source, target); if (InputTypeConverterCache.TryGetValue(key, out TypeConverter? typeConverter)) { return typeConverter; } TypeConverter converter = TypeDescriptor.GetConverter(source); if (converter.CanConvertTo(target)) { return InputTypeConverterCache[key] = converter; } 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? GetCachedTargetTypeConverter(Type source, Type target) { var key = new CompositeTypeTypeKey(source, target); if (DestinationTypeConverterCache.TryGetValue(key, out TypeConverter? typeConverter)) { return typeConverter; } TypeConverter converter = TypeDescriptor.GetConverter(target); if (converter.CanConvertFrom(source)) { return DestinationTypeConverterCache[key] = converter; } 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 type) { if (NullableGenericCache.TryGetValue(type, out Type? underlyingType)) { return underlyingType; } if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) { Type? underlying = Nullable.GetUnderlyingType(type); return NullableGenericCache[type] = underlying; } NullableGenericCache[type] = null; return null; } // gets an IConvertible from source to target type, or null if none exists [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool GetCachedCanAssign(object input, Type source, Type target) { var key = new CompositeTypeTypeKey(source, target); if (AssignableTypeCache.TryGetValue(key, out var canConvert)) { return canConvert; } // "object is" is faster than "Type.IsAssignableFrom. // We can use it to very quickly determine whether true/false if (input is IConvertible && target.IsAssignableFrom(source)) { return AssignableTypeCache[key] = true; } return AssignableTypeCache[key] = false; } // determines whether a type can be converted to boolean [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool GetCachedCanConvertToBoolean(Type type) { if (BoolConvertCache.TryGetValue(type, out var result)) { return result; } if (CustomBooleanTypeConverter.CanConvertFrom(type)) { return BoolConvertCache[type] = true; } return BoolConvertCache[type] = false; } }