2
0
mirror of https://github.com/esiur/esiur-dotnet.git synced 2025-12-13 16:30:24 +00:00
Files
esiur-dotnet/Esiur/Data/RuntimeCaster.cs
2025-11-04 11:47:40 +03:00

371 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Text;
#nullable enable
namespace Esiur.Data;
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq.Expressions;
public enum NaNInfinityPolicy
{
Throw, // Default: throw on NaN/∞ when converting to non-floating types
NullIfNullable, // If target is Nullable<decimal>, return null; otherwise throw
CoerceZero // Replace NaN/∞ with 0
}
public sealed class RuntimeCastOptions
{
public bool CheckedNumeric { get; set; } = true;
// For DateTime/DateTimeOffset parsing
public CultureInfo Culture { get; set; } = CultureInfo.InvariantCulture;
public DateTimeStyles DateTimeStyles { get; set; } = DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeLocal;
// For enums
public bool EnumIgnoreCase { get; set; } = true;
public bool EnumMustBeDefined { get; set; } = false;
// float/double → decimal behavior
public NaNInfinityPolicy NaNInfinityPolicy { get; set; } = NaNInfinityPolicy.Throw;
}
public static class RuntimeCaster
{
public static readonly RuntimeCastOptions Default = new RuntimeCastOptions();
// (fromType, toType) -> converter(value, options) (options captured at call time)
private static readonly ConcurrentDictionary<(Type from, Type to), Func<object, RuntimeCastOptions, object>> _cache = new();
// Numeric-only compiled converters (fast path), keyed by checked/unchecked
private static readonly ConcurrentDictionary<(Type from, Type to, bool @checked), Func<object, object>> _numericCache = new();
public static object Cast(object value, Type toType, RuntimeCastOptions? options = null)
{
options ??= Default;
if (toType is null) throw new ArgumentNullException(nameof(toType));
if (value is null)
{
if (IsNonNullableValueType(toType))
throw new InvalidCastException($"Cannot cast null to non-nullable {toType}.");
return null;
}
var fromType = value.GetType();
if (toType.IsAssignableFrom(fromType)) return value; // already compatible
var fn = _cache.GetOrAdd((fromType, toType), k => BuildConverter(k.from, k.to));
return fn(value, options);
}
// ------------------------ Builder ------------------------
private static Func<object, RuntimeCastOptions, object> BuildConverter(Type fromType, Type toType)
{
// Nullable handling is done inside ConvertCore.
return (value, opts) => ConvertCore(value, fromType, toType, opts);
}
// ------------------------ Core Routing ------------------------
private static object ConvertCore(object value, Type fromType, Type toType, RuntimeCastOptions opts)
{
var toUnderlying = Nullable.GetUnderlyingType(toType) ?? toType;
var fromUnderlying = Nullable.GetUnderlyingType(fromType) ?? fromType;
// If converting nullable source and it's null → result is null if target nullable, else throw
if (!fromType.IsValueType && value is null)
{
if (IsNonNullableValueType(toType))
throw new InvalidCastException($"Cannot cast null to non-nullable {toType}.");
return null;
}
// Special cases first
// 1) Enum targets
if (toUnderlying.IsEnum)
return ConvertToEnum(value, fromUnderlying, toType, toUnderlying, opts);
// 2) Guid targets
if (toUnderlying == typeof(Guid))
return ConvertToGuid(value, fromUnderlying, toType);
// 3) DateTime / DateTimeOffset targets
if (toUnderlying == typeof(DateTime))
return ConvertToDateTime(value, fromUnderlying, toType, opts);
if (toUnderlying == typeof(DateTimeOffset))
return ConvertToDateTimeOffset(value, fromUnderlying, toType, opts);
// 4) decimal from float/double with NaN/∞ policy
if (toUnderlying == typeof(decimal) && (fromUnderlying == typeof(float) || fromUnderlying == typeof(double)))
{
var dec = ConvertFloatDoubleToDecimal(value, fromUnderlying, opts, out bool useNull);
if (toType != toUnderlying) // wrap in Nullable<decimal>
return useNull ? null : (decimal?)dec;
if (useNull) throw new OverflowException("NaN/Infinity cannot be converted to decimal.");
return dec;
}
// 5) General numeric conversions via compiled expression
if (IsNumeric(fromUnderlying) && IsNumeric(toUnderlying))
{
var nc = _numericCache.GetOrAdd((fromUnderlying, toUnderlying, opts.CheckedNumeric),
k => BuildNumericConverter(k.from, k.to, k.@checked));
var result = nc(value);
// Wrap into nullable if needed
if (toType != toUnderlying) return BoxNullable(result, toUnderlying);
return result;
}
// 6) String <-> other basics (Use TypeConverter first; if no path, fall through)
if (toUnderlying == typeof(string))
return value?.ToString();
// 7) Last-resort: TypeConverter or ChangeType once, inside this compiled path
// Try TypeConverter(target)
var tc = System.ComponentModel.TypeDescriptor.GetConverter(toUnderlying);
if (tc.CanConvertFrom(fromUnderlying))
{
var r = tc.ConvertFrom(null, opts.Culture, value);
if (toType != toUnderlying) return BoxNullable(r, toUnderlying);
return r!;
}
// Try TypeConverter(source)
var tc2 = System.ComponentModel.TypeDescriptor.GetConverter(fromUnderlying);
if (tc2.CanConvertTo(toUnderlying))
{
var r = tc2.ConvertTo(null, opts.Culture, value, toUnderlying);
if (toType != toUnderlying) return BoxNullable(r, toUnderlying);
return r!;
}
// Try Convert.ChangeType for IConvertible fallbacks
try
{
var r = Convert.ChangeType(value, toUnderlying, opts.Culture);
if (toType != toUnderlying) return BoxNullable(r, toUnderlying);
return r!;
}
catch
{
// Final attempt: assignable cast via reflection (e.g., interfaces)
if (toUnderlying.IsInstanceOfType(value))
{
if (toType != toUnderlying) return BoxNullable(value, toUnderlying);
return value;
}
throw new InvalidCastException($"Cannot cast {fromType} to {toType}.");
}
}
// ------------------------ Helpers: Numeric ------------------------
private static Func<object, object> BuildNumericConverter(Type from, Type to, bool @checked)
{
var p = Expression.Parameter(typeof(object), "v");
Expression val = from.IsValueType
? Expression.Unbox(p, from)
: Expression.Convert(p, from);
Expression body;
try
{
body = @checked ? Expression.ConvertChecked(val, to) : Expression.Convert(val, to);
}
catch (InvalidOperationException)
{
// Non-legal numeric convert — should be rare given IsNumeric check.
throw new InvalidCastException($"Numeric conversion not supported: {from} -> {to}");
}
// Box result to object
Expression boxed = to.IsValueType ? Expression.Convert(body, typeof(object)) : body;
return Expression.Lambda<Func<object, object>>(boxed, p).Compile();
}
private static bool IsNumeric(Type t)
{
t = Nullable.GetUnderlyingType(t) ?? t;
return t == typeof(byte) || t == typeof(sbyte) ||
t == typeof(short) || t == typeof(ushort) ||
t == typeof(int) || t == typeof(uint) ||
t == typeof(long) || t == typeof(ulong) ||
t == typeof(float) || t == typeof(double) ||
t == typeof(decimal);
}
private static bool IsNonNullableValueType(Type t)
=> t.IsValueType && Nullable.GetUnderlyingType(t) is null;
private static object BoxNullable(object value, Type underlying)
{
// Create Nullable<T>(value) then box
var nt = typeof(Nullable<>).MakeGenericType(underlying);
var ctor = nt.GetConstructor(new[] { underlying })!;
return ctor.Invoke(new[] { value });
}
// ------------------------ Helpers: NaN/∞ to decimal ------------------------
private static decimal ConvertFloatDoubleToDecimal(object value, Type fromUnderlying, RuntimeCastOptions opts, out bool useNull)
{
useNull = false;
if (fromUnderlying == typeof(float))
{
var f = (float)value;
if (float.IsNaN(f) || float.IsInfinity(f))
{
switch (opts.NaNInfinityPolicy)
{
case NaNInfinityPolicy.NullIfNullable: useNull = true; return default;
case NaNInfinityPolicy.CoerceZero: return 0m;
default: throw new OverflowException("Cannot convert NaN/Infinity to decimal.");
}
}
return opts.CheckedNumeric ? checked((decimal)f) : (decimal)f;
}
else // double
{
var d = (double)value;
if (double.IsNaN(d) || double.IsInfinity(d))
{
switch (opts.NaNInfinityPolicy)
{
case NaNInfinityPolicy.NullIfNullable: useNull = true; return default;
case NaNInfinityPolicy.CoerceZero: return 0m;
default: throw new OverflowException("Cannot convert NaN/Infinity to decimal.");
}
}
return opts.CheckedNumeric ? checked((decimal)d) : (decimal)d;
}
}
// ------------------------ Helpers: Enum ------------------------
private static object ConvertToEnum(object value, Type fromUnderlying, Type toType, Type enumType, RuntimeCastOptions opts)
{
// Nullable<Enum> wrapping
bool wrapNullable = toType != enumType;
// String → Enum
if (fromUnderlying == typeof(string))
{
var parsed = Enum.Parse(enumType, (string)value, opts.EnumIgnoreCase);
if (opts.EnumMustBeDefined && !Enum.IsDefined(enumType, parsed!))
throw new InvalidCastException($"Value '{value}' is not a defined member of {enumType.Name}.");
return wrapNullable ? BoxNullable(parsed!, enumType) : parsed!;
}
// Numeric → Enum
if (IsNumeric(fromUnderlying))
{
// Convert numeric to enums underlying integral type first (checked/unchecked handled by compiled numeric path)
var et = Enum.GetUnderlyingType(enumType);
var numConv = _numericCache.GetOrAdd((fromUnderlying, et, true), k => BuildNumericConverter(k.from, k.to, k.@checked));
var integral = numConv(value);
var enumObj = Enum.ToObject(enumType, integral);
if (opts.EnumMustBeDefined && !Enum.IsDefined(enumType, enumObj))
throw new InvalidCastException($"Numeric value {integral} is not a defined member of {enumType.Name}.");
return wrapNullable ? BoxNullable(enumObj, enumType) : enumObj;
}
// Fallback: not supported
throw new InvalidCastException($"Cannot cast {fromUnderlying} to enum {enumType.Name}.");
}
// ------------------------ Helpers: Guid ------------------------
private static object ConvertToGuid(object value, Type fromUnderlying, Type toType)
{
bool wrapNullable = toType != typeof(Guid);
if (fromUnderlying == typeof(string))
{
if (!Guid.TryParse((string)value, out var g))
throw new InvalidCastException($"Cannot parse '{value}' to Guid.");
return wrapNullable ? (Guid?)g : g;
}
if (fromUnderlying == typeof(byte[]))
{
var bytes = (byte[])value;
if (bytes.Length != 16)
throw new InvalidCastException("Guid requires a 16-byte array.");
var g = new Guid(bytes);
return wrapNullable ? (Guid?)g : g;
}
throw new InvalidCastException($"Cannot cast {fromUnderlying} to Guid.");
}
// ------------------------ Helpers: DateTime / DateTimeOffset ------------------------
private static object ConvertToDateTime(object value, Type fromUnderlying, Type toType, RuntimeCastOptions opts)
{
bool wrapNullable = toType != typeof(DateTime);
if (fromUnderlying == typeof(string))
{
if (!DateTime.TryParse((string)value, opts.Culture, opts.DateTimeStyles, out var dt))
throw new InvalidCastException($"Cannot parse '{value}' to DateTime.");
return wrapNullable ? (DateTime?)dt : dt;
}
if (fromUnderlying == typeof(long))
{
// Treat as ticks
var dt = new DateTime((long)value, DateTimeKind.Unspecified);
return wrapNullable ? (DateTime?)dt : dt;
}
if (fromUnderlying == typeof(double))
{
// Treat as OADate if finite
var d = (double)value;
if (double.IsNaN(d) || double.IsInfinity(d))
throw new InvalidCastException("Cannot convert NaN/Infinity to DateTime.");
var dt = DateTime.FromOADate(d);
return wrapNullable ? (DateTime?)dt : dt;
}
throw new InvalidCastException($"Cannot cast {fromUnderlying} to DateTime.");
}
private static object ConvertToDateTimeOffset(object value, Type fromUnderlying, Type toType, RuntimeCastOptions opts)
{
bool wrapNullable = toType != typeof(DateTimeOffset);
if (fromUnderlying == typeof(string))
{
if (!DateTimeOffset.TryParse((string)value, opts.Culture, opts.DateTimeStyles, out var dto))
throw new InvalidCastException($"Cannot parse '{value}' to DateTimeOffset.");
return wrapNullable ? (DateTimeOffset?)dto : dto;
}
if (fromUnderlying == typeof(long))
{
// Treat as ticks since 0001-01-01
var dto = new DateTimeOffset(new DateTime((long)value, DateTimeKind.Unspecified));
return wrapNullable ? (DateTimeOffset?)dto : dto;
}
if (fromUnderlying == typeof(double))
{
var d = (double)value;
if (double.IsNaN(d) || double.IsInfinity(d))
throw new InvalidCastException("Cannot convert NaN/Infinity to DateTimeOffset.");
var dt = DateTime.FromOADate(d);
var dto = new DateTimeOffset(dt);
return wrapNullable ? (DateTimeOffset?)dto : dto;
}
throw new InvalidCastException($"Cannot cast {fromUnderlying} to DateTimeOffset.");
}
}