Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
// Copyright (c) Umbraco.
2021-02-18 11:06:02 +01:00
// See LICENSE for more details.
2019-05-20 17:57:28 +02:00
using System.Collections ;
using System.Collections.Concurrent ;
using System.ComponentModel ;
using System.Linq.Expressions ;
using System.Reflection ;
using System.Runtime.CompilerServices ;
using System.Xml ;
2022-12-14 11:39:36 +01:00
using Microsoft.Extensions.Primitives ;
2021-02-18 11:06:02 +01:00
using Umbraco.Cms.Core ;
using Umbraco.Cms.Core.Collections ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
namespace Umbraco.Extensions ;
/// <summary>
/// Provides object extension methods.
/// </summary>
public static class ObjectExtensions
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
private static readonly ConcurrentDictionary < Type , Type ? > NullableGenericCache = new ( ) ;
private static readonly ConcurrentDictionary < CompositeTypeTypeKey , TypeConverter ? > InputTypeConverterCache = new ( ) ;
private static readonly ConcurrentDictionary < CompositeTypeTypeKey , TypeConverter ? > DestinationTypeConverterCache =
new ( ) ;
private static readonly ConcurrentDictionary < CompositeTypeTypeKey , bool > AssignableTypeCache = new ( ) ;
private static readonly ConcurrentDictionary < Type , bool > BoolConvertCache = new ( ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
private static readonly char [ ] NumberDecimalSeparatorsToNormalize = { '.' , ',' } ;
private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new ( ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// private static readonly ConcurrentDictionary<Type, Func<object>> ObjectFactoryCache = new ConcurrentDictionary<Type, Func<object>>();
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// </summary>
/// <param name="input"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static IEnumerable < T > AsEnumerableOfOne < T > ( this T input ) = > Enumerable . Repeat ( input , 1 ) ;
/// <summary>
/// </summary>
/// <param name="input"></param>
public static void DisposeIfDisposable ( this object input )
{
if ( input is IDisposable disposable )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
disposable . Dispose ( ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Provides a shortcut way of safely casting an input when you cannot guarantee the <typeparamref name="T" /> is
/// an instance type (i.e., when the C# AS keyword is not applicable).
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="input">The input.</param>
/// <returns></returns>
public static T ? SafeCast < T > ( this object input )
{
if ( ReferenceEquals ( null , input ) | | ReferenceEquals ( default ( T ) , input ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return default ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( input is T variable )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return variable ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return default ;
}
/// <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>
/// <typeparam name="T">The type to convert to</typeparam>
/// <param name="input">The input.</param>
/// <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 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt < T > . Succeed ( ( T ? ) result . Result ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
if ( input = = null )
{
if ( typeof ( T ) . IsValueType )
Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
{
2022-06-07 15:28:38 +02:00
// fail, cannot convert null to a value type
return Attempt < T > . Fail ( ) ;
Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
}
2022-06-07 15:28:38 +02:00
// sure, null can be any object
return Attempt < T > . Succeed ( ( T ) input ! ) ;
}
// just try to cast
try
{
return Attempt < T > . Succeed ( ( T ) input ) ;
}
catch ( Exception e )
{
return Attempt < T > . Fail ( e ) ;
}
}
/// <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="target">The type to convert to</param>
/// <returns>The <see cref="Attempt{Object}" /></returns>
public static Attempt < object? > TryConvertTo ( this object? input , Type target )
{
if ( target = = null )
{
return Attempt < object? > . Fail ( ) ;
}
try
{
Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
if ( input = = null )
{
2022-06-07 15:28:38 +02:00
// Nullable is ok
if ( target . IsGenericType & & GetCachedGenericNullableType ( target ) ! = null )
Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
{
2022-06-07 15:28:38 +02:00
return Attempt < object? > . Succeed ( null ) ;
2021-09-14 22:13:39 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// Reference types are ok
return Attempt < object? > . If ( target . IsValueType = = false , null ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
Type inputType = input . GetType ( ) ;
// Easy
if ( target = = typeof ( object ) | | inputType = = target )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt . Succeed ( input ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// Check for string so that overloaders of ToString() can take advantage of the conversion.
if ( target = = typeof ( string ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt < object? > . Succeed ( input . ToString ( ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// 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 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
Type ? underlying = GetCachedGenericNullableType ( target ) ;
if ( underlying ! = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
// Special case for empty strings for bools/dates which should return null if an empty string.
if ( input is string inputString )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
// 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 ) ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt < object? > . Succeed ( null ) ;
2019-05-20 17:57:28 +02:00
}
}
2022-06-07 15:28:38 +02:00
// Recursively call into this method with the inner (not-nullable) type and handle the outcome
Attempt < object? > inner = input . TryConvertTo ( underlying ) ;
// And if successful, fall on through to rewrap in a nullable; if failed, pass on the exception
if ( inner . Success )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
input = inner . Result ; // Now fall on through...
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
else
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt < object? > . Fail ( inner . Exception ) ;
2019-05-20 17:57:28 +02:00
}
}
2022-06-07 15:28:38 +02:00
}
else
{
// target is not a generic type
2022-12-14 11:39:36 +01:00
var inputString = input as string ? ? ( input is StringValues sv ? sv . ToString ( ) : null ) ;
if ( inputString ! = null )
2019-05-20 17:57:28 +02:00
{
2022-12-14 11:39:36 +01:00
// Try convert from string or StringValues, returns an Attempt if the string could be
2022-06-07 15:28:38 +02:00
// processed (either succeeded or failed), else null if we need to try
// other methods
Attempt < object? > ? result = TryConvertToFromString ( inputString , target ) ;
if ( result . HasValue )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return result . Value ;
2019-05-20 17:57:28 +02:00
}
}
2022-06-07 15:28:38 +02:00
// 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.
if ( GetCachedCanAssign ( input , inputType , target ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt . Succeed ( Convert . ChangeType ( input , target ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
if ( target = = typeof ( bool ) )
{
if ( GetCachedCanConvertToBoolean ( inputType ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt . Succeed ( CustomBooleanTypeConverter . ConvertFrom ( input ! ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
TypeConverter ? inputConverter = GetCachedSourceTypeConverter ( inputType , target ) ;
if ( inputConverter ! = null )
{
return Attempt . Succeed ( inputConverter . ConvertTo ( input , target ) ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
TypeConverter ? outputConverter = GetCachedTargetTypeConverter ( inputType , target ) ;
if ( outputConverter ! = null )
{
return Attempt . Succeed ( outputConverter . ConvertFrom ( input ! ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( target . IsGenericType & & GetCachedGenericNullableType ( target ) ! = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
// 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 ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// 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 < object? > . Fail ( e ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return Attempt < object? > . Fail ( ) ;
}
// public enum PropertyNamesCaseType
// {
// CamelCase,
// CaseInsensitive
// }
///// <summary>
///// Convert an object to a JSON string with camelCase formatting
///// </summary>
///// <param name="obj"></param>
///// <returns></returns>
// public static string ToJsonString(this object obj)
// {
// return obj.ToJsonString(PropertyNamesCaseType.CamelCase);
// }
///// <summary>
///// Convert an object to a JSON string with the specified formatting
///// </summary>
///// <param name="obj">The obj.</param>
///// <param name="propertyNamesCaseType">Type of the property names case.</param>
///// <returns></returns>
// 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);
// }
/// <summary>
/// Converts an object into a dictionary
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TProperty"></typeparam>
/// <typeparam name="TVal"> </typeparam>
/// <param name="o"></param>
/// <param name="ignoreProperties"></param>
/// <returns></returns>
public static IDictionary < string , TVal > ? ToDictionary < T , TProperty , TVal > (
this T o ,
params Expression < Func < T , TProperty > > [ ] ignoreProperties ) = > o ? . ToDictionary < TVal > ( 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 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ObjectDisposedException ( objectname ) ;
}
}
/// <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="target">The type to convert to</param>
/// <returns>The <see cref="Nullable{Attempt}" /></returns>
private static Attempt < object? > ? TryConvertToFromString ( this string input , Type target )
{
// Easy
if ( target = = typeof ( string ) )
{
return Attempt < object? > . Succeed ( input ) ;
}
// Null, empty, whitespaces
if ( string . IsNullOrWhiteSpace ( input ) )
{
if ( target = = typeof ( bool ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
// null/empty = bool false
return Attempt < object? > . Succeed ( false ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( target = = typeof ( DateTime ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
// null/empty = min DateTime value
return Attempt < object? > . Succeed ( DateTime . MinValue ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// 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 ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt < object? > . Succeed ( value ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// 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 < object? > . If ( decimal . TryParse ( input2 , out var value2 ) , Convert . ToInt32 ( value2 ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( target = = typeof ( long ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
if ( long . TryParse ( input , out var value ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt < object? > . Succeed ( value ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// Same as int
var input2 = NormalizeNumberDecimalSeparator ( input ) ;
return Attempt < object? > . If ( decimal . TryParse ( input2 , out var value2 ) , Convert . ToInt64 ( value2 ) ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// TODO: Should we do the decimal trick for short, byte, unsigned?
if ( target = = typeof ( bool ) )
{
if ( bool . TryParse ( input , out var value ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt < object? > . Succeed ( value ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// Don't declare failure so the CustomBooleanTypeConverter can try
return null ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// Calling this method directly is faster than any attempt to cache it.
switch ( Type . GetTypeCode ( target ) )
{
case TypeCode . Int16 :
return Attempt < object? > . If ( short . TryParse ( input , out var value ) , value ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case TypeCode . Double :
var input2 = NormalizeNumberDecimalSeparator ( input ) ;
return Attempt < object? > . If ( double . TryParse ( input2 , out var valueD ) , valueD ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case TypeCode . Single :
var input3 = NormalizeNumberDecimalSeparator ( input ) ;
return Attempt < object? > . If ( float . TryParse ( input3 , out var valueF ) , valueF ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case TypeCode . Char :
return Attempt < object? > . If ( char . TryParse ( input , out var valueC ) , valueC ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case TypeCode . Byte :
return Attempt < object? > . If ( byte . TryParse ( input , out var valueB ) , valueB ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case TypeCode . SByte :
return Attempt < object? > . If ( sbyte . TryParse ( input , out var valueSb ) , valueSb ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case TypeCode . UInt32 :
return Attempt < object? > . If ( uint . TryParse ( input , out var valueU ) , valueU ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case TypeCode . UInt16 :
return Attempt < object? > . If ( ushort . TryParse ( input , out var valueUs ) , valueUs ) ;
case TypeCode . UInt64 :
return Attempt < object? > . If ( ulong . TryParse ( input , out var valueUl ) , valueUl ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
else if ( target = = typeof ( Guid ) )
{
return Attempt < object? > . If ( Guid . TryParse ( input , out Guid value ) , value ) ;
}
else if ( target = = typeof ( DateTime ) )
{
if ( DateTime . TryParse ( input , out DateTime value ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
switch ( value . Kind )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
case DateTimeKind . Unspecified :
case DateTimeKind . Utc :
return Attempt < object? > . Succeed ( value ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case DateTimeKind . Local :
return Attempt < object? > . Succeed ( value . ToUniversalTime ( ) ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
default :
throw new ArgumentOutOfRangeException ( ) ;
2019-05-20 17:57:28 +02:00
}
}
2022-06-07 15:28:38 +02:00
return Attempt < object? > . Fail ( ) ;
}
else if ( target = = typeof ( DateTimeOffset ) )
{
return Attempt < object? > . If ( DateTimeOffset . TryParse ( input , out DateTimeOffset value ) , value ) ;
}
else if ( target = = typeof ( TimeSpan ) )
{
return Attempt < object? > . If ( TimeSpan . TryParse ( input , out TimeSpan value ) , value ) ;
}
else if ( target = = typeof ( decimal ) )
{
var input2 = NormalizeNumberDecimalSeparator ( input ) ;
return Attempt < object? > . If ( decimal . TryParse ( input2 , out var value ) , value ) ;
}
else if ( input ! = null & & target = = typeof ( Version ) )
{
return Attempt < object? > . If ( Version . TryParse ( input , out Version ? value ) , value ) ;
}
// E_NOTIMPL IPAddress, BigInteger
return null ; // we can't decide...
}
/// <summary>
/// Turns object into dictionary
/// </summary>
/// <param name="o"></param>
/// <param name="ignoreProperties">Properties to ignore</param>
/// <returns></returns>
public static IDictionary < string , TVal > ToDictionary < TVal > ( this object o , params string [ ] ignoreProperties )
{
if ( o ! = null )
{
PropertyDescriptorCollection props = TypeDescriptor . GetProperties ( o ) ;
var d = new Dictionary < string , TVal > ( ) ;
foreach ( PropertyDescriptor prop in props . Cast < PropertyDescriptor > ( )
. Where ( x = > ignoreProperties . Contains ( x . Name ) = = false ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
var val = prop . GetValue ( o ) ;
if ( val ! = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
d . Add ( prop . Name , ( TVal ) val ) ;
2019-05-20 17:57:28 +02:00
}
}
2022-06-07 15:28:38 +02:00
return d ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return new Dictionary < string , TVal > ( ) ;
}
2019-10-30 15:28:15 +01:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Returns an XmlSerialized safe string representation for the value
/// </summary>
/// <param name="value"></param>
/// <param name="type">The Type can only be a primitive type or Guid and byte[] otherwise an exception is thrown</param>
/// <returns></returns>
public static string ToXmlString ( this object value , Type type )
{
if ( value = = null )
{
return string . Empty ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
if ( type = = typeof ( string ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return value . ToString ( ) . IsNullOrWhiteSpace ( ) ? string . Empty : value . ToString ( ) ! ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
if ( type = = typeof ( bool ) )
{
return XmlConvert . ToString ( ( bool ) value ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
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 ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( type = = typeof ( ushort ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
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 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return "\"{0}\"" . InvariantFormat ( obj ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
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? )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return "{0}" . InvariantFormat ( obj ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( obj is Enum )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return "[{0}]" . InvariantFormat ( obj ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( obj is IEnumerable enumerable )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
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 ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
PropertyInfo [ ] props = obj . GetType ( ) . GetProperties ( ) ;
if ( props . Length = = 2 & & props [ 0 ] . Name = = "Key" & & props [ 1 ] . Name = = "Value" & & levels > - 2 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
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]" ;
}
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( levels > - 1 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
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 ;
2019-05-20 17:57:28 +02:00
}
}
2022-06-07 15:28:38 +02:00
catch ( Exception ex )
{
return "[Exception:{0}]" . InvariantFormat ( ex . Message ) ;
}
return null ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Attempts to serialize the value to an XmlString using ToXmlString
/// </summary>
/// <param name="value"></param>
/// <param name="type"></param>
/// <returns></returns>
internal static Attempt < string? > TryConvertToXmlString ( this object value , Type type )
{
try
{
var output = value . ToXmlString ( type ) ;
return Attempt . Succeed ( output ) ;
}
catch ( NotSupportedException ex )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Attempt < string? > . Fail ( ex ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
/// <summary>
/// Returns an XmlSerialized safe string representation for the value and type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <returns></returns>
public static string ToXmlString < T > ( this object value ) = > value . ToXmlString ( typeof ( T ) ) ;
public static Guid AsGuid ( this object value ) = > value is Guid guid ? guid : Guid . Empty ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
private static string? GetEnumPropertyDebugString ( object enumItem , int levels )
{
try
{
return enumItem . ToDebugString ( levels - 1 ) ;
}
catch ( Exception )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return "[GetEnumPartException]" ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
private static string? GetPropertyDebugString ( PropertyInfo propertyInfo , object obj , int levels )
{
try
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return propertyInfo . GetValue ( obj , null ) . ToDebugString ( levels - 1 ) ;
}
catch ( Exception )
{
return "[GetPropertyValueException]" ;
}
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string NormalizeNumberDecimalSeparator ( string s )
{
var normalized = Thread . CurrentThread . CurrentCulture . NumberFormat . NumberDecimalSeparator [ 0 ] ;
return s . ReplaceMany ( NumberDecimalSeparatorsToNormalize , normalized ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// 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 ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
if ( InputTypeConverterCache . TryGetValue ( key , out TypeConverter ? typeConverter ) )
{
return typeConverter ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
TypeConverter converter = TypeDescriptor . GetConverter ( source ) ;
if ( converter . CanConvertTo ( target ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return InputTypeConverterCache [ key ] = converter ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
InputTypeConverterCache [ key ] = null ;
return null ;
}
2019-10-30 15:28:15 +01:00
2022-06-07 15:28:38 +02:00
// 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 ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
if ( DestinationTypeConverterCache . TryGetValue ( key , out TypeConverter ? typeConverter ) )
{
return typeConverter ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
TypeConverter converter = TypeDescriptor . GetConverter ( target ) ;
if ( converter . CanConvertFrom ( source ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return DestinationTypeConverterCache [ key ] = converter ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
DestinationTypeConverterCache [ key ] = null ;
return null ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// 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 ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( type . GetGenericTypeDefinition ( ) = = typeof ( Nullable < > ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
Type ? underlying = Nullable . GetUnderlyingType ( type ) ;
return NullableGenericCache [ type ] = underlying ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
NullableGenericCache [ type ] = null ;
return null ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// 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 ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// "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 ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return AssignableTypeCache [ key ] = true ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
return AssignableTypeCache [ key ] = false ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// 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 ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( CustomBooleanTypeConverter . CanConvertFrom ( type ) )
{
return BoolConvertCache [ type ] = true ;
}
2019-10-30 15:28:15 +01:00
2022-06-07 15:28:38 +02:00
return BoolConvertCache [ type ] = false ;
2019-05-20 17:57:28 +02:00
}
}