diff --git a/src/LuYao.Common/Data/Attributes/RecordAttribute.cs b/src/LuYao.Common/Data/Attributes/RecordAttribute.cs index 0dd8555..3fc9479 100644 --- a/src/LuYao.Common/Data/Attributes/RecordAttribute.cs +++ b/src/LuYao.Common/Data/Attributes/RecordAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace LuYao.Data; +namespace LuYao.Data.Attributes; /// /// 用于标记 Record 映射行为的特性基类。 diff --git a/src/LuYao.Common/Data/Attributes/RecordColumnNameAttribute.cs b/src/LuYao.Common/Data/Attributes/RecordColumnNameAttribute.cs index 1038c44..8d55ce3 100644 --- a/src/LuYao.Common/Data/Attributes/RecordColumnNameAttribute.cs +++ b/src/LuYao.Common/Data/Attributes/RecordColumnNameAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace LuYao.Data; +namespace LuYao.Data.Attributes; /// /// 标记属性对应的列名称,优先级高于 。 diff --git a/src/LuYao.Common/Data/Attributes/RecordColumnStorageAttribute.cs b/src/LuYao.Common/Data/Attributes/RecordColumnStorageAttribute.cs index 84de306..c2f2416 100644 --- a/src/LuYao.Common/Data/Attributes/RecordColumnStorageAttribute.cs +++ b/src/LuYao.Common/Data/Attributes/RecordColumnStorageAttribute.cs @@ -1,6 +1,7 @@ +using LuYao.Data.Mapping; using System; -namespace LuYao.Data; +namespace LuYao.Data.Attributes; /// /// 声明属性在 中的列存储类型,优先级高于 的全局策略。 diff --git a/src/LuYao.Common/Data/Binary/BinaryPayloadHeader.cs b/src/LuYao.Common/Data/Binary/BinaryPayloadHeader.cs index 9ff1b62..baed06a 100644 --- a/src/LuYao.Common/Data/Binary/BinaryPayloadHeader.cs +++ b/src/LuYao.Common/Data/Binary/BinaryPayloadHeader.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace LuYao.Data; +namespace LuYao.Data.Binary; internal enum BinaryPayloadType : byte { diff --git a/src/LuYao.Common/Data/Binary/RecordBinaryPayloadCodec.cs b/src/LuYao.Common/Data/Binary/RecordBinaryPayloadCodec.cs index 08daf89..858e644 100644 --- a/src/LuYao.Common/Data/Binary/RecordBinaryPayloadCodec.cs +++ b/src/LuYao.Common/Data/Binary/RecordBinaryPayloadCodec.cs @@ -3,7 +3,7 @@ using System.IO; using System.IO.Compression; -namespace LuYao.Data; +namespace LuYao.Data.Binary; /// /// Record 二进制负载编解码器。 diff --git a/src/LuYao.Common/Data/Binary/RecordPayloadCompression.cs b/src/LuYao.Common/Data/Binary/RecordPayloadCompression.cs index b306196..48fb5c8 100644 --- a/src/LuYao.Common/Data/Binary/RecordPayloadCompression.cs +++ b/src/LuYao.Common/Data/Binary/RecordPayloadCompression.cs @@ -1,4 +1,4 @@ -namespace LuYao.Data; +namespace LuYao.Data.Binary; /// /// 指定 序列化时使用的压缩算法。 diff --git a/src/LuYao.Common/Data/Json/RecordSetJsonConverter.cs b/src/LuYao.Common/Data/Json/RecordSetJsonConverter.cs index 9aa146f..574b4a3 100644 --- a/src/LuYao.Common/Data/Json/RecordSetJsonConverter.cs +++ b/src/LuYao.Common/Data/Json/RecordSetJsonConverter.cs @@ -1,10 +1,11 @@ #if !NET45 && !NET461 +using LuYao.Data.Binary; using System; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; -namespace LuYao.Data; +namespace LuYao.Data.Json; /// /// 将 以 Base64 二进制格式进行 JSON 序列化/反序列化。 diff --git a/src/LuYao.Common/Data/Json/RecordTableJsonConverter.cs b/src/LuYao.Common/Data/Json/RecordTableJsonConverter.cs index d5ff60b..1f63698 100644 --- a/src/LuYao.Common/Data/Json/RecordTableJsonConverter.cs +++ b/src/LuYao.Common/Data/Json/RecordTableJsonConverter.cs @@ -1,11 +1,12 @@ #if !NET45 && !NET461 +using LuYao.Data.Binary; using System; using System.IO; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -namespace LuYao.Data; +namespace LuYao.Data.Json; /// /// 将 以 Base64 二进制格式进行 JSON 序列化/反序列化。 diff --git a/src/LuYao.Common/Data/Meta/ColumnNameResolver.cs b/src/LuYao.Common/Data/Mapping/ColumnNameResolver.cs similarity index 92% rename from src/LuYao.Common/Data/Meta/ColumnNameResolver.cs rename to src/LuYao.Common/Data/Mapping/ColumnNameResolver.cs index d4dcfe5..64f0a94 100644 --- a/src/LuYao.Common/Data/Meta/ColumnNameResolver.cs +++ b/src/LuYao.Common/Data/Mapping/ColumnNameResolver.cs @@ -1,6 +1,8 @@ +using LuYao.Data.Attributes; +using LuYao.Data.Meta; using System.Reflection; -namespace LuYao.Data.Meta; +namespace LuYao.Data.Mapping; /// /// 根据 解析属性对应的列名。 diff --git a/src/LuYao.Common/Data/Mapping/Converters/StringToInt32Converter.cs b/src/LuYao.Common/Data/Mapping/Converters/StringToInt32Converter.cs deleted file mode 100644 index a713fcf..0000000 --- a/src/LuYao.Common/Data/Mapping/Converters/StringToInt32Converter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace LuYao.Data.Mapping.Converters; - -public class StringToInt32Converter : XConverter -{ - public StringToInt32Converter() : base(typeof(string), typeof(int)) - { - } - - public override Func ConvertToSource => throw new NotImplementedException(); - - public override Func ConvertFromSource => throw new NotImplementedException(); -} diff --git a/src/LuYao.Common/Data/Meta/RecordMappingContext.cs b/src/LuYao.Common/Data/Mapping/RecordMappingContext.cs similarity index 52% rename from src/LuYao.Common/Data/Meta/RecordMappingContext.cs rename to src/LuYao.Common/Data/Mapping/RecordMappingContext.cs index deb6e28..43090d9 100644 --- a/src/LuYao.Common/Data/Meta/RecordMappingContext.cs +++ b/src/LuYao.Common/Data/Mapping/RecordMappingContext.cs @@ -1,6 +1,7 @@ +using LuYao.Data.Meta; using System; -namespace LuYao.Data.Meta; +namespace LuYao.Data.Mapping; /// /// Encapsulates the execution context for a single mapping operation. @@ -51,14 +52,7 @@ internal void MapDtoToRow(Type type, object data, RecordRow target) continue; } - // Not natively supported: a converter (property type → column type) is required. - var converter = ResolveWriteConverter(prop, col.Type); - if (converter == null) - { - HandleUnsupportedTypeForWrite(prop); - continue; - } - col.Set(target, converter.Convert(prop.Type, col.Type, prop.GetValue(data))); + HandleUnsupportedTypeForWrite(prop); } } @@ -84,22 +78,7 @@ internal void WriteDtoToRow(Type type, object data, RecordRow target) continue; } - // Not natively supported: determine the target column type. - var colType = ResolveColumnType(prop); - if (colType == null) - { - HandleUnsupportedTypeForWrite(prop); - continue; - } - var converter = ResolveWriteConverter(prop, colType); - if (converter == null) - { - ThrowMissingConverter(prop, colType); - continue; - } - var name = ColumnNameResolver.Resolve(prop, _options); - var destCol = cols.Find(name) ?? cols.Add(name, colType); - destCol.Set(target, converter.Convert(prop.Type, colType, prop.GetValue(data))); + HandleUnsupportedTypeForWrite(prop); } } @@ -128,21 +107,9 @@ internal void MapRowToDto(Type type, object data, RecordRow source) continue; } - // Column type differs from property type, or property type is not natively - // supported: a converter (column type → property type) is required. - var converter = _options.FindConverter(col.Type, prop.Type) - ?? (DefaultRecordConverter.Instance.CanConvert(col.Type, prop.Type) - ? DefaultRecordConverter.Instance : null); - - if (converter == null) - { - HandleConversionFailure( - new NotSupportedException( - $"Property '{prop.Name}' has type '{prop.Type.FullName}' which is not supported and no custom converter is registered.")); - continue; - } - - TryConvertAndSetValue(data, prop, col.Type, prop.Type, rawValue, converter); + HandleConversionFailure( + new NotSupportedException( + $"Property '{prop.Name}' has type '{prop.Type.FullName}' which is not supported.")); } } @@ -172,64 +139,10 @@ internal void AddColumnsFrom(Type type, RecordColumnCollection columns) continue; } - var colType = ResolveColumnType(prop); - if (colType == null) - { - HandleUnsupportedTypeForWrite(prop); - continue; - } - var converter = ResolveWriteConverter(prop, colType); - if (converter == null) - { - ThrowMissingConverter(prop, colType); - continue; - } - var name = ColumnNameResolver.Resolve(prop, _options); - columns.Add(name, colType); + HandleUnsupportedTypeForWrite(prop); } } - // ─── Private: column-type and converter resolution ─────────────────────────── - - /// - /// Determines the target column type for a property that is not natively supported. - /// Priority: > - /// (ConvertToString / ConvertToBytes). Returns when Skip or Throw applies. - /// - private Type? ResolveColumnType(XProp prop) - { - var attr = prop.GetCustomAttribute(); - if (attr != null) - { - return attr.Target switch - { - RecordColumnStorageTarget.String => typeof(string), - RecordColumnStorageTarget.Bytes => typeof(byte[]), - _ => null, // Skip - }; - } - - return _options.UnsupportedTypeHandling switch - { - UnsupportedTypeHandling.ConvertToString => typeof(string), - UnsupportedTypeHandling.ConvertToBytes => typeof(byte[]), - _ => null, - }; - } - - /// - /// Resolves a write-direction converter (property type → column type). - /// Priority: options-registered converter > . - /// - private RecordConverter? ResolveWriteConverter(XProp prop, Type colType) - { - var converter = _options.FindConverter(prop.Type, colType); - if (converter != null) return converter; - if (DefaultRecordConverter.Instance.CanConvert(prop.Type, colType)) - return DefaultRecordConverter.Instance; - return null; - } - private void TrySetValue(object data, XProp prop, object? value) { try @@ -242,23 +155,6 @@ private void TrySetValue(object data, XProp prop, object? value) } } - /// - /// Invokes the converter then assigns the result; any exception during conversion or - /// assignment is handled according to the policy. - /// - private void TryConvertAndSetValue(object data, XProp prop, Type sourceType, Type targetType, object? value, RecordConverter converter) - { - try - { - var converted = converter.Convert(sourceType, targetType, value); - prop.SetValue(data, converted); - } - catch (Exception ex) when (_options.ConversionFailureHandling == ConversionFailureHandling.Skip) - { - _ = ex; - } - } - private void HandleUnsupportedTypeForWrite(XProp prop) { if (_options.UnsupportedTypeHandling == UnsupportedTypeHandling.Throw) @@ -274,10 +170,5 @@ private void HandleConversionFailure(Exception inner) // Skip: silently ignore } - private static void ThrowMissingConverter(XProp prop, Type colType) - { - throw new InvalidOperationException( - $"Property '{prop.Name}' (type '{prop.Type.FullName}') is declared to be stored as '{colType.Name}', " + - $"but no matching RecordConverter is registered in RecordMappingOptions and it is not covered by the default converter."); - } + } diff --git a/src/LuYao.Common/Data/RecordMappingOptions.cs b/src/LuYao.Common/Data/Mapping/RecordMappingOptions.cs similarity index 66% rename from src/LuYao.Common/Data/RecordMappingOptions.cs rename to src/LuYao.Common/Data/Mapping/RecordMappingOptions.cs index b974da0..3d3208e 100644 --- a/src/LuYao.Common/Data/RecordMappingOptions.cs +++ b/src/LuYao.Common/Data/Mapping/RecordMappingOptions.cs @@ -1,7 +1,7 @@ +using LuYao.Data.Attributes; using System; -using System.Collections.Generic; -namespace LuYao.Data; +namespace LuYao.Data.Mapping; /// /// 控制 与 DTO 之间映射行为的选项类。 @@ -82,40 +82,4 @@ public ConversionFailureHandling ConversionFailureHandling get => _conversionFailureHandling; set { ThrowIfReadOnly(); _conversionFailureHandling = value; } } - - // ─── 自定义类型转换器 ────────────────────────────────────────────────────────── - - private List? _converters; - - /// - /// 注册自定义双向转换器。 - /// 转换器同时用于 DTO → Table(写)和 Table → DTO(读)两个方向。 - /// - /// 转换器实例,不可为 null。 - /// 为 null。 - /// 实例已被冻结时抛出。 - public void AddConverter(RecordConverter converter) - { - if (converter == null) throw new ArgumentNullException(nameof(converter)); - ThrowIfReadOnly(); - if (_converters == null) _converters = new List(); - _converters.Add(converter); - } - - /// - /// 查找支持从 转换的已注册转换器。 - /// 不包含托底转换器 。 - /// - /// 来源类型。 - /// 目标类型。 - /// 匹配的转换器;未找到则返回 - internal RecordConverter? FindConverter(Type sourceType, Type targetType) - { - if (_converters == null) return null; - foreach (var c in _converters) - { - if (c.CanConvert(sourceType, targetType)) return c; - } - return null; - } } diff --git a/src/LuYao.Common/Data/Mapping/XConverter.cs b/src/LuYao.Common/Data/Mapping/XConverter.cs deleted file mode 100644 index 8a6b732..0000000 --- a/src/LuYao.Common/Data/Mapping/XConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace LuYao.Data.Mapping; - -public abstract class XConverter -{ - protected XConverter(Type sourceType, Type targetType) - { - SourceType = sourceType; - TargetType = targetType; - } - public Type SourceType { get; } - public Type TargetType { get; } - public abstract Func ConvertToSource { get; } - public abstract Func ConvertFromSource { get; } -} diff --git a/src/LuYao.Common/Data/Mapping/XCopy.cs b/src/LuYao.Common/Data/Mapping/XCopy.cs index 2cfee47..1c3e225 100644 --- a/src/LuYao.Common/Data/Mapping/XCopy.cs +++ b/src/LuYao.Common/Data/Mapping/XCopy.cs @@ -1,4 +1,3 @@ -using LuYao.Data; using LuYao.Data.Meta; using System; using System.Collections.Concurrent; @@ -283,90 +282,3 @@ public static class XCopy where T : class #endregion } -/// -/// Static utility class for shallow-copying between two strongly-typed objects by property name. -/// Only properties that exist in both types with the same name, the same type, and are supported -/// for reading/writing are copied. -/// -/// The source object type. -/// The target object type; must have a parameterless constructor. -public static class XCopy where TSource : class where TTarget : class, new() -{ - // Pre-build the source->target property-pair map to avoid repeated lookups on every call. - private static readonly IReadOnlyList _map = BuildMap(); - - private readonly struct PropPair - { - internal readonly IXProp Source; - internal readonly IXProp Target; - - internal PropPair(IXProp source, IXProp target) - { - Source = source; - Target = target; - } - - public void Copy(TSource sourceInstance, TTarget targetInstance) - { - Target.SetValue(targetInstance, Source.GetValue(sourceInstance)); - } - } - - private static IReadOnlyList BuildMap() - { - var sourceProps = XProp.GetAll(typeof(TSource)); - var targetProps = XProp.GetAll(typeof(TTarget)); - - // Build a fast-lookup index keyed by target property name. - var targetIndex = new Dictionary(StringComparer.Ordinal); - foreach (var tp in targetProps) - { - if (tp.CanWrite) targetIndex[tp.Name] = tp; - } - - var map = new List(); - foreach (var sp in sourceProps) - { - if (!sp.CanRead) continue; - if (!targetIndex.TryGetValue(sp.Name, out var tp)) continue; - // Only map properties whose types match exactly. - if (sp.Type != tp.Type) continue; - map.Add(new PropPair(sp, tp)); - } - - return map.AsReadOnly(); - } - - /// - /// Creates a new instance and copies the properties from - /// that share the same name, same type, and are supported. - /// - /// Source object; must not be null. - /// A new target object populated with values from the source. - /// Thrown when is null. - public static TTarget Copy(TSource source) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - var target = new TTarget(); - CopyTo(source, target); - return target; - } - - /// - /// Copies supported property values from into an existing - /// instance. - /// - /// Source object; must not be null. - /// Target object; must not be null. - /// Thrown when any argument is null. - public static void CopyTo(TSource source, TTarget target) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (target == null) throw new ArgumentNullException(nameof(target)); - - foreach (var pair in _map) - { - pair.Copy(source, target); - } - } -} diff --git a/src/LuYao.Common/Data/Meta/DefaultRecordConverter.cs b/src/LuYao.Common/Data/Meta/DefaultRecordConverter.cs deleted file mode 100644 index 1babeaa..0000000 --- a/src/LuYao.Common/Data/Meta/DefaultRecordConverter.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; - -namespace LuYao.Data.Meta; - -/// -/// 内置托底转换器,支持常见数值类型、 之间的互转。 -/// 优先级最低,仅在 中无匹配转换器时使用。 -/// -internal sealed class DefaultRecordConverter : RecordConverter -{ - /// 全局单例。 - public static readonly DefaultRecordConverter Instance = new DefaultRecordConverter(); - - private DefaultRecordConverter() { } - - // 支持的类型对(双向):key 为小类型,value 为大类型;查找时双向均可命中 - private static readonly HashSet _supported = BuildSupportedPairs(); - - private static HashSet BuildSupportedPairs() - { - var numericTypes = new[] - { - typeof(byte), typeof(sbyte), - typeof(short), typeof(ushort), - typeof(int), typeof(uint), - typeof(long), typeof(ulong), - typeof(float), typeof(double), - typeof(decimal), - }; - - var set = new HashSet(); - - // 数值类型两两互转 - for (int i = 0; i < numericTypes.Length; i++) - { - for (int j = 0; j < numericTypes.Length; j++) - { - if (i != j) - set.Add(new TypePair(numericTypes[i], numericTypes[j])); - } - } - - // 数值类型 ↔ string - foreach (var t in numericTypes) - { - set.Add(new TypePair(t, typeof(string))); - set.Add(new TypePair(typeof(string), t)); - } - - // bool ↔ string - set.Add(new TypePair(typeof(bool), typeof(string))); - set.Add(new TypePair(typeof(string), typeof(bool))); - - // bool ↔ 数值类型(0/1) - foreach (var t in numericTypes) - { - set.Add(new TypePair(typeof(bool), t)); - set.Add(new TypePair(t, typeof(bool))); - } - - // DateTime ↔ string - set.Add(new TypePair(typeof(DateTime), typeof(string))); - set.Add(new TypePair(typeof(string), typeof(DateTime))); - - // char ↔ string - set.Add(new TypePair(typeof(char), typeof(string))); - set.Add(new TypePair(typeof(string), typeof(char))); - - return set; - } - - /// - public override bool CanConvert(Type sourceType, Type targetType) - { - if (sourceType == null || targetType == null) return false; - if (sourceType == targetType) return true; - return _supported.Contains(new TypePair(sourceType, targetType)); - } - - /// - public override object? Convert(Type sourceType, Type targetType, object? value) - { - if (!CanConvert(sourceType, targetType)) - throw new NotSupportedException( - $"DefaultRecordConverter 不支持从 {sourceType.Name} 到 {targetType.Name} 的转换。"); - - if (value == null) - { - // 值类型返回默认值 - if (targetType.IsValueType) - return Activator.CreateInstance(targetType); - return null; - } - - if (targetType.IsAssignableFrom(value.GetType())) - return value; - - return System.Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture); - } - - private readonly struct TypePair : IEquatable - { - public readonly Type Source; - public readonly Type Target; - - public TypePair(Type source, Type target) - { - Source = source; - Target = target; - } - - public bool Equals(TypePair other) => Source == other.Source && Target == other.Target; - public override bool Equals(object? obj) => obj is TypePair other && Equals(other); - public override int GetHashCode() - { - unchecked - { - return (Source.GetHashCode() * 397) ^ Target.GetHashCode(); - } - } - } -} diff --git a/src/LuYao.Common/Data/RecordColumnCollection.cs b/src/LuYao.Common/Data/RecordColumnCollection.cs index 07d71d6..6b7f453 100644 --- a/src/LuYao.Common/Data/RecordColumnCollection.cs +++ b/src/LuYao.Common/Data/RecordColumnCollection.cs @@ -1,4 +1,5 @@ using LuYao.Collections.Generic; +using LuYao.Data.Mapping; using LuYao.Data.Meta; using System; using System.Collections; diff --git a/src/LuYao.Common/Data/RecordColumnStorageTarget.cs b/src/LuYao.Common/Data/RecordColumnStorageTarget.cs index f5b84df..13eee17 100644 --- a/src/LuYao.Common/Data/RecordColumnStorageTarget.cs +++ b/src/LuYao.Common/Data/RecordColumnStorageTarget.cs @@ -9,17 +9,5 @@ public enum RecordColumnStorageTarget /// /// 跳过该属性,不在 RecordTable 中创建对应的列。 /// - Skip = 0, - - /// - /// 将属性值转换为 后存储。 - /// 必须在 中注册该属性类型对应的 ,否则将抛出异常。 - /// - String = 1, - - /// - /// 将属性值转换为 [] 后存储。 - /// 必须在 中注册该属性类型对应的 ,否则将抛出异常。 - /// - Bytes = 2, + Skip = 0 } diff --git a/src/LuYao.Common/Data/RecordConverter.cs b/src/LuYao.Common/Data/RecordConverter.cs deleted file mode 100644 index afc8839..0000000 --- a/src/LuYao.Common/Data/RecordConverter.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; - -namespace LuYao.Data; - -/// -/// 定义在 DTO 属性类型与 RecordTable 列类型之间进行双向转换的转换器基类。 -/// -/// -/// -/// 方法承担双向转换,方向由 参数决定: -/// -/// DTO → Table:sourceType 为属性类型,targetType 为列类型(受支持类型)。 -/// Table → DTO:sourceType 为列类型,targetType 为属性类型。 -/// -/// -/// -/// 推荐继承泛型子类 ,它提供了类型安全的重写方式。 -/// -/// -public abstract class RecordConverter -{ - /// - /// 判断此转换器是否支持从 的转换。 - /// - /// 来源类型。 - /// 目标类型。 - /// 支持该转换时返回 ,否则返回 - public abstract bool CanConvert(Type sourceType, Type targetType); - - /// - /// 将 转换为 。 - /// - /// 来源类型。 - /// 目标类型。 - /// 待转换的值。 - /// 转换后的值。 - /// - /// 当 返回 时调用此方法应抛出。 - /// - public abstract object? Convert(Type sourceType, Type targetType, object? value); -} - -/// -/// 提供类型安全的双向转换器基类。 -/// -/// 其中一端的类型(通常为 DTO 属性类型或 RecordTable 列类型)。 -/// 另一端的类型。 -/// -/// 此类自动支持 TSource → TTargetTTarget → TSource 两个方向。 -/// -public abstract class RecordConverter : RecordConverter -{ - /// - public override bool CanConvert(Type sourceType, Type targetType) - { - return (sourceType == typeof(TSource) && targetType == typeof(TTarget)) - || (sourceType == typeof(TTarget) && targetType == typeof(TSource)); - } - - /// - public override object? Convert(Type sourceType, Type targetType, object? value) - { - if (sourceType == typeof(TSource) && targetType == typeof(TTarget)) - return ConvertForward(value is TSource v ? v : (TSource?)value); - if (sourceType == typeof(TTarget) && targetType == typeof(TSource)) - return ConvertBackward(value is TTarget v ? v : (TTarget?)value); - throw new NotSupportedException( - $"{GetType().Name} 不支持从 {sourceType.Name} 到 {targetType.Name} 的转换。"); - } - - /// - /// 将 转换为 (DTO → Table 方向)。 - /// - protected abstract TTarget? ConvertForward(TSource? value); - - /// - /// 将 转换回 (Table → DTO 方向)。 - /// - protected abstract TSource? ConvertBackward(TTarget? value); -} diff --git a/src/LuYao.Common/Data/RecordRow/Merge.cs b/src/LuYao.Common/Data/RecordRow/Merge.cs index a1bd52b..e6b720e 100644 --- a/src/LuYao.Common/Data/RecordRow/Merge.cs +++ b/src/LuYao.Common/Data/RecordRow/Merge.cs @@ -1,3 +1,4 @@ +using LuYao.Data.Mapping; using LuYao.Data.Meta; using System; diff --git a/src/LuYao.Common/Data/RecordSet/Binary.cs b/src/LuYao.Common/Data/RecordSet/Binary.cs index 3d77dcd..dfed32e 100644 --- a/src/LuYao.Common/Data/RecordSet/Binary.cs +++ b/src/LuYao.Common/Data/RecordSet/Binary.cs @@ -1,4 +1,5 @@ -using System; +using LuYao.Data.Binary; +using System; using System.IO; using System.Text; diff --git a/src/LuYao.Common/Data/RecordTable/Binary.cs b/src/LuYao.Common/Data/RecordTable/Binary.cs index 2c2b658..087d7bf 100644 --- a/src/LuYao.Common/Data/RecordTable/Binary.cs +++ b/src/LuYao.Common/Data/RecordTable/Binary.cs @@ -1,4 +1,5 @@ -using System; +using LuYao.Data.Binary; +using System; using System.IO; using System.Text; diff --git a/src/LuYao.Common/Data/UnsupportedTypeHandling.cs b/src/LuYao.Common/Data/UnsupportedTypeHandling.cs index df53040..bda389f 100644 --- a/src/LuYao.Common/Data/UnsupportedTypeHandling.cs +++ b/src/LuYao.Common/Data/UnsupportedTypeHandling.cs @@ -10,16 +10,4 @@ public enum UnsupportedTypeHandling /// 抛出 Throw = 1, - - /// - /// 将属性值转换为 后存入列。 - /// 必须在 中注册该属性类型对应的 ,否则将抛出异常。 - /// - ConvertToString = 2, - - /// - /// 将属性值转换为 [] 后存入列。 - /// 必须在 中注册该属性类型对应的 ,否则将抛出异常。 - /// - ConvertToBytes = 3, } diff --git a/tests/LuYao.Common.UnitTests/Data/Meta/XCopyGenericTests.cs b/tests/LuYao.Common.UnitTests/Data/Meta/XCopyGenericTests.cs deleted file mode 100644 index 8e6eace..0000000 --- a/tests/LuYao.Common.UnitTests/Data/Meta/XCopyGenericTests.cs +++ /dev/null @@ -1,207 +0,0 @@ -using LuYao.Data.Mapping; - -namespace LuYao.Data.Meta; - -[TestClass] -public class XCopyGenericTests -{ - // ── 测试用模型 ──────────────────────────────────────────────────────────── - - private class Source - { - public int Id { get; set; } - public string? Name { get; set; } - public double Score { get; set; } - public string? Extra { get; set; } // Target 中不存在 - public int ReadOnly { get; } = 99; // Source 只读,不影响结果 - } - - private class Target - { - public int Id { get; set; } - public string? Name { get; set; } - public double Score { get; set; } - public string? OnlyInTarget { get; set; } // Source 中不存在 - public int WriteOnly { private get; set; } // Source 不可读,跳过 - } - - private class TypeMismatch - { - public int Id { get; set; } // Source.Id 是 int,此处也是 int → 匹配 - public long Score { get; set; } // Source.Score 是 double,此处是 long → 不匹配,应跳过 - } - - // ── Copy ───────────────────────────────────────────────────────────────── - - [TestMethod] - public void Copy_NullSource_ThrowsArgumentNullException() - { - Assert.Throws(() => XCopy.Copy(null!)); - } - - [TestMethod] - public void Copy_MatchingProperties_AreCopied() - { - var source = new Source { Id = 1, Name = "Alice", Score = 9.5 }; - var target = XCopy.Copy(source); - - Assert.AreEqual(1, target.Id); - Assert.AreEqual("Alice", target.Name); - Assert.AreEqual(9.5, target.Score); - } - - [TestMethod] - public void Copy_ExtraSourceProperty_IsIgnored() - { - // Extra 属性在 Target 中不存在,不应引发任何异常 - var source = new Source { Id = 2, Extra = "extra" }; - var target = XCopy.Copy(source); - - Assert.AreEqual(2, target.Id); - Assert.IsNull(target.OnlyInTarget); - } - - [TestMethod] - public void Copy_ExtraTargetProperty_RemainsDefault() - { - var source = new Source { Id = 3 }; - var target = XCopy.Copy(source); - - Assert.IsNull(target.OnlyInTarget); - } - - [TestMethod] - public void Copy_ReturnsNewInstance() - { - var source = new Source { Id = 4 }; - var t1 = XCopy.Copy(source); - var t2 = XCopy.Copy(source); - - Assert.AreNotSame(t1, t2); - } - - [TestMethod] - public void Copy_NullStringValue_IsCopied() - { - var source = new Source { Id = 5, Name = null }; - var target = XCopy.Copy(source); - - Assert.IsNull(target.Name); - } - - // ── Copy ─────────────────────────────────────────────────────────────── - - [TestMethod] - public void CopyTo_NullSource_ThrowsArgumentNullException() - { - Assert.Throws( - () => XCopy.CopyTo(null!, new Target())); - } - - [TestMethod] - public void CopyTo_NullTarget_ThrowsArgumentNullException() - { - Assert.Throws( - () => XCopy.CopyTo(new Source(), null!)); - } - - [TestMethod] - public void CopyTo_OverwritesExistingTargetValues() - { - var source = new Source { Id = 10, Name = "Bob" }; - var target = new Target { Id = 99, Name = "Old" }; - - XCopy.CopyTo(source, target); - - Assert.AreEqual(10, target.Id); - Assert.AreEqual("Bob", target.Name); - } - - // ── 类型不匹配 ──────────────────────────────────────────────────────────── - - [TestMethod] - public void Copy_TypeMismatchProperty_IsSkipped() - { - var source = new Source { Id = 7, Score = 3.14 }; - var result = XCopy.Copy(source); - - // Id 类型匹配,应被复制 - Assert.AreEqual(7, result.Id); - // Score 类型不匹配(double vs long),应保持默认值 0 - Assert.AreEqual(0L, result.Score); - } - - // ── 同类型自复制 ────────────────────────────────────────────────────────── - - [TestMethod] - public void Copy_SameType_CopiesAllSupportedProperties() - { - var source = new Source { Id = 8, Name = "Charlie", Score = 1.0 }; - var copy = XCopy.Copy(source); - - Assert.AreNotSame(source, copy); - Assert.AreEqual(source.Id, copy.Id); - Assert.AreEqual(source.Name, copy.Name); - Assert.AreEqual(source.Score, copy.Score); - } - - // ── record class ────────────────────────────────────────────────────────── - - private record class RecordSource - { - public int Id { get; set; } - public string? Name { get; set; } - public double Score { get; set; } - } - - private record class RecordTarget - { - public int Id { get; set; } - public string? Name { get; set; } - public double Score { get; set; } - } - - [TestMethod] - public void Copy_RecordClassSource_MatchingPropertiesAreCopied() - { - var source = new RecordSource { Id = 10, Name = "Record", Score = 7.7 }; - var target = XCopy.Copy(source); - - Assert.AreEqual(10, target.Id); - Assert.AreEqual("Record", target.Name); - Assert.AreEqual(7.7, target.Score); - } - - [TestMethod] - public void Copy_RecordClassSource_ReturnsNewInstance() - { - var source = new RecordSource { Id = 11 }; - var t1 = XCopy.Copy(source); - var t2 = XCopy.Copy(source); - - Assert.AreNotSame(t1, t2); - } - - [TestMethod] - public void CopyTo_RecordClassTarget_OverwritesExistingValues() - { - var source = new RecordSource { Id = 12, Name = "New" }; - var target = new RecordTarget { Id = 99, Name = "Old" }; - - XCopy.CopyTo(source, target); - - Assert.AreEqual(12, target.Id); - Assert.AreEqual("New", target.Name); - } - - [TestMethod] - public void Copy_ClassSourceToRecordClassTarget_MatchingPropertiesAreCopied() - { - var source = new Source { Id = 13, Name = "Mixed", Score = 3.3 }; - var target = XCopy.Copy(source); - - Assert.AreEqual(13, target.Id); - Assert.AreEqual("Mixed", target.Name); - Assert.AreEqual(3.3, target.Score); - } -} diff --git a/tests/LuYao.Common.UnitTests/Data/RecordJsonConverterTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordJsonConverterTests.cs index b2b8c61..a4f59c8 100644 --- a/tests/LuYao.Common.UnitTests/Data/RecordJsonConverterTests.cs +++ b/tests/LuYao.Common.UnitTests/Data/RecordJsonConverterTests.cs @@ -1,3 +1,5 @@ +using LuYao.Data.Binary; +using LuYao.Data.Json; using System; using System.IO; using System.IO.Compression; diff --git a/tests/LuYao.Common.UnitTests/Data/RecordMappingOptionsTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordMappingOptionsTests.cs index b915703..df0bc5c 100644 --- a/tests/LuYao.Common.UnitTests/Data/RecordMappingOptionsTests.cs +++ b/tests/LuYao.Common.UnitTests/Data/RecordMappingOptionsTests.cs @@ -1,5 +1,6 @@ +using LuYao.Data.Attributes; +using LuYao.Data.Mapping; using System; -using System.Collections.Generic; namespace LuYao.Data; @@ -184,28 +185,6 @@ public void UnsupportedTypeHandling_Throw_ThrowsNotSupportedException() table.Columns.AddFrom(options)); } - // ─── 自定义转换器 ────────────────────────────────────────────────────────────── - - private class DtoWithDecimal - { - public decimal Price { get; set; } - } - - [TestMethod] - public void CustomConverter_UsedWhen_BuiltinCannotHandle() - { - // decimal 不是 RecordTable 内置支持的类型,用自定义转换器处理 - var table = new RecordTable(); - table.Columns.Add("Price"); - var row = table.AddRow(); - table.Columns.Find("Price")!.Set(row, "3.14"); - - var options = new RecordMappingOptions(); - // DefaultRecordConverter 已支持 string → decimal,无需手动注册 - var dto = table.To(options); - Assert.AreEqual(3.14m, dto.Price); - } - // ─── ConversionFailureHandling.Skip ────────────────────────────────────────── [TestMethod] @@ -287,25 +266,7 @@ public void Default_CannotBeModified_AfterMapping() RecordMappingOptions.Default.NamingPolicy = RecordNamingPolicy.CamelCase); } - [TestMethod] - public void Options_AddConverter_Throws_AfterMapping() - { - var options = new RecordMappingOptions(); - RecordTable.From(new PersonDto { Id = 1 }, options); - Assert.IsTrue(options.IsReadOnly); - Assert.Throws(() => - options.AddConverter(new NoopStringConverter())); - } - - // ─── RecordColumnStorageAttribute / UnsupportedTypeHandling 扩展 ───────────── - - private class DtoWithComplexProp - { - public string Name { get; set; } = ""; - - [RecordColumnStorage(RecordColumnStorageTarget.String)] - public ComplexTag Tag { get; set; } = new(); - } + // ─── RecordColumnStorageAttribute ────────────────────────────────────────────── private class ComplexTag { @@ -313,31 +274,6 @@ private class ComplexTag public override string ToString() => Value; } - private sealed class ComplexTagToStringConverter : RecordConverter - { - public override bool CanConvert(Type sourceType, Type targetType) - => sourceType == typeof(ComplexTag) && targetType == typeof(string); - - public override object? Convert(Type sourceType, Type targetType, object? value) - => value?.ToString(); - } - - [TestMethod] - public void StorageAttribute_String_WritesAsString() - { - // 属性上标注 [RecordColumnStorage(String)],注册对应转换器 - var options = new RecordMappingOptions(); - options.AddConverter(new ComplexTagToStringConverter()); - - var dto = new DtoWithComplexProp { Name = "Alice", Tag = new ComplexTag { Value = "vip" } }; - var table = RecordTable.From(dto, options); - - Assert.AreEqual(1, table.Count); - var row = table[0]; - Assert.AreEqual("Alice", table.Columns.Find("Name")!.Get(row)); - Assert.AreEqual("vip", table.Columns.Find("Tag")!.Get(row)); - } - [TestMethod] public void StorageAttribute_Skip_SkipsProperty() { @@ -355,73 +291,4 @@ private class DtoWithSkipTag [RecordColumnStorage(RecordColumnStorageTarget.Skip)] public ComplexTag Secret { get; set; } = new(); } - - [TestMethod] - public void UnsupportedTypeHandling_ConvertToString_WritesAsString() - { - // 全局配置 ConvertToString,需注册转换器 - var options = new RecordMappingOptions - { - UnsupportedTypeHandling = UnsupportedTypeHandling.ConvertToString - }; - options.AddConverter(new ComplexTagToStringConverter()); - - var dto = new DtoNoAttr { Name = "Charlie", Tag = new ComplexTag { Value = "gold" } }; - var table = RecordTable.From(dto, options); - - Assert.AreEqual("gold", table.Columns.Find("Tag")!.Get(table[0])); - } - - [TestMethod] - public void UnsupportedTypeHandling_ConvertToString_ThrowsWhenNoConverter() - { - var options = new RecordMappingOptions - { - UnsupportedTypeHandling = UnsupportedTypeHandling.ConvertToString - // 未注册 ComplexTag → string 的转换器 - }; - var dto = new DtoNoAttr { Name = "Dave", Tag = new ComplexTag { Value = "x" } }; - Assert.Throws(() => RecordTable.From(dto, options)); - } - - [TestMethod] - public void UnsupportedTypeHandling_ConvertToBytes_WritesAsBytes() - { - var options = new RecordMappingOptions - { - UnsupportedTypeHandling = UnsupportedTypeHandling.ConvertToBytes - }; - options.AddConverter(new ComplexTagToBytesConverter()); - - var dto = new DtoNoAttr { Name = "Eve", Tag = new ComplexTag { Value = "ok" } }; - var table = RecordTable.From(dto, options); - - var bytes = table.Columns.Find("Tag")!.Get(table[0]) as byte[]; - Assert.IsNotNull(bytes); - Assert.AreEqual("ok", System.Text.Encoding.UTF8.GetString(bytes!)); - } - - private class DtoNoAttr - { - public string Name { get; set; } = ""; - public ComplexTag Tag { get; set; } = new(); - } - - private sealed class ComplexTagToBytesConverter : RecordConverter - { - public override bool CanConvert(Type sourceType, Type targetType) - => sourceType == typeof(ComplexTag) && targetType == typeof(byte[]); - - public override object? Convert(Type sourceType, Type targetType, object? value) - => value == null ? null : System.Text.Encoding.UTF8.GetBytes(value.ToString()!); - } - - // ─── 辅助类型 ───────────────────────────────────────────────────────────────── - - /// 用于测试冻结行为的最简转换器。 - private sealed class NoopStringConverter : RecordConverter - { - public override bool CanConvert(Type sourceType, Type targetType) => false; - public override object? Convert(Type sourceType, Type targetType, object? value) => value; - } }