diff --git a/src/LuYao.Common/Data/Attributes/RecordAttribute.cs b/src/LuYao.Common/Data/Attributes/RecordAttribute.cs deleted file mode 100644 index 3fc9479..0000000 --- a/src/LuYao.Common/Data/Attributes/RecordAttribute.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace LuYao.Data.Attributes; - -/// -/// 用于标记 Record 映射行为的特性基类。 -/// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public abstract class RecordAttribute : Attribute -{ -} diff --git a/src/LuYao.Common/Data/Attributes/RecordColumnNameAttribute.cs b/src/LuYao.Common/Data/Attributes/RecordColumnNameAttribute.cs deleted file mode 100644 index 8d55ce3..0000000 --- a/src/LuYao.Common/Data/Attributes/RecordColumnNameAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace LuYao.Data.Attributes; - -/// -/// 标记属性对应的列名称,优先级高于 。 -/// 对标 。 -/// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class RecordColumnNameAttribute : RecordAttribute -{ - /// - /// 初始化 的新实例。 - /// - /// 列的名称。 - /// 为 null 或空白时抛出。 - public RecordColumnNameAttribute(string name) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - Name = name; - } - - /// - /// 列的名称。 - /// - public string Name { get; } -} diff --git a/src/LuYao.Common/Data/Attributes/RecordColumnStorageAttribute.cs b/src/LuYao.Common/Data/Attributes/RecordColumnStorageAttribute.cs deleted file mode 100644 index c2f2416..0000000 --- a/src/LuYao.Common/Data/Attributes/RecordColumnStorageAttribute.cs +++ /dev/null @@ -1,45 +0,0 @@ -using LuYao.Data.Mapping; -using System; - -namespace LuYao.Data.Attributes; - -/// -/// 声明属性在 中的列存储类型,优先级高于 的全局策略。 -/// -/// -/// -/// 仅在属性类型不是 RecordTable 原生支持的类型时才有意义。对于原生支持的类型,此特性会被忽略。 -/// -/// -/// 使用 或 时, -/// 必须在 中注册该属性类型对应的 ,否则将在映射时抛出异常。 -/// -/// -/// -/// -/// // 将 Guid 属性以字符串形式存储到 RecordTable -/// [RecordColumnStorage(RecordColumnStorageTarget.String)] -/// public Guid Id { get; set; } -/// -/// // 跳过该属性,不创建对应列 -/// [RecordColumnStorage(RecordColumnStorageTarget.Skip)] -/// public MyComplexObject Payload { get; set; } -/// -/// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class RecordColumnStorageAttribute : RecordAttribute -{ - /// - /// 初始化 的新实例。 - /// - /// 指定的存储类型。 - public RecordColumnStorageAttribute(RecordColumnStorageTarget target) - { - Target = target; - } - - /// - /// 属性在 RecordTable 中的列存储类型。 - /// - public RecordColumnStorageTarget Target { get; } -} diff --git a/src/LuYao.Common/Data/Binary/BinaryPayloadHeader.cs b/src/LuYao.Common/Data/Binary/BinaryPayloadHeader.cs deleted file mode 100644 index f24b188..0000000 --- a/src/LuYao.Common/Data/Binary/BinaryPayloadHeader.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.IO; - -namespace LuYao.Data.Binary; - -internal enum BinaryPayloadType : byte -{ - RecordTable = 1, - RecordSet = 2 -} - -internal static class BinaryPayloadHeader -{ - private const byte Marker = 0xFF; - private const byte Signature1 = (byte)'L'; - private const byte Signature2 = (byte)'Y'; - - public static void Write(BinaryWriter writer, BinaryPayloadType payloadType) - { - writer.Write(Marker); - writer.Write(Signature1); - writer.Write(Signature2); - writer.Write((byte)payloadType); - } - - public static byte ReadHeaderAndVersion(BinaryReader reader, BinaryPayloadType expectedType) - { - byte first = reader.ReadByte(); - if (first != Marker) - { - return first; - } - - byte sig1 = reader.ReadByte(); - byte sig2 = reader.ReadByte(); - if (sig1 != Signature1 || sig2 != Signature2) - throw new InvalidOperationException($"无效的二进制文件头签名,期望 'LY',实际 0x{sig1:X2}{sig2:X2}。"); - - var actualType = (BinaryPayloadType)reader.ReadByte(); - if (actualType != expectedType) - throw new InvalidOperationException($"二进制数据类型不匹配,期望 {expectedType},实际 {actualType}。"); - - return reader.ReadByte(); - } - - /// - /// 尝试从二进制数据读取类型头中的 payload 类型。 - /// - /// 二进制数据。 - /// 成功时返回读取到的 payload 类型。 - /// 当数据包含有效类型头时返回 true;否则返回 false。 - public static bool TryGetPayloadType(byte[] data, out BinaryPayloadType payloadType) - { - payloadType = default; - if (data == null) return false; - if (data.Length < 4) return false; - if (data[0] != Marker || data[1] != Signature1 || data[2] != Signature2) return false; - - byte rawType = data[3]; - if (rawType != (byte)BinaryPayloadType.RecordTable && rawType != (byte)BinaryPayloadType.RecordSet) - return false; - - payloadType = (BinaryPayloadType)rawType; - return true; - } -} diff --git a/src/LuYao.Common/Data/Binary/RecordBinaryPayloadCodec.cs b/src/LuYao.Common/Data/Binary/RecordBinaryPayloadCodec.cs deleted file mode 100644 index 858e644..0000000 --- a/src/LuYao.Common/Data/Binary/RecordBinaryPayloadCodec.cs +++ /dev/null @@ -1,212 +0,0 @@ -#if !NET45 && !NET461 -using System; -using System.IO; -using System.IO.Compression; - -namespace LuYao.Data.Binary; - -/// -/// Record 二进制负载编解码器。 -/// -/// -/// 当前格式(版本 1)头部共 6 字节: -/// -/// 0xFE 'L' 'Y' 'Z' <version=1> <algo> -/// -/// -/// 其中 algo 对应 :0 = 不压缩,1 = GZip。 -/// 头部中的 0xFE 与 的 marker (0xFF) 以及遗留格式的版本号字节 (1) 均不冲突。 -/// -/// -/// 为防御 zip bomb / DoS 攻击,可通过 和 -/// 属性配置限制。 -/// -/// -public sealed class RecordBinaryPayloadCodec -{ - // ── Magic ────────────────────────────────────────────────── - private const byte MagicByte0 = 0xFE; - private const byte MagicByte1 = (byte)'L'; - private const byte MagicByte2 = (byte)'Y'; - private const byte MagicByte3 = (byte)'Z'; - private const int MagicLength = 4; - - // 版本 1 头部:magic(4) + version(1) + algo(1) - private const byte FormatVersion = 1; - private const int HeaderLength = MagicLength + 1 + 1; // 6 - - private const int DecodeBufferSize = 16 * 1024; - private const int InitialOutputCapacity = 4 * 1024; - - // ── 默认 DoS 限制 ────────────────────────────────────────── - private const int DefaultMaxDecompressedBytes = 64 * 1024 * 1024; - private const int DefaultMaxCompressedBytes = 16 * 1024 * 1024; - - // ── 单例:使用默认配置 ────────────────────────────────────── - /// 使用默认配置的共享实例(GZip 压缩,默认 DoS 阈值)。 - public static readonly RecordBinaryPayloadCodec Default = new(); - - // ── 属性 ──────────────────────────────────────────────────── - - /// 序列化时使用的压缩算法,默认 。 - public RecordPayloadCompression Compression { get; set; } = RecordPayloadCompression.GZip; - - /// - /// 解压后允许的最大字节数,用于防御 zip bomb 等 DoS 攻击。 - /// 仅在使用压缩算法时生效。默认 64 MB。 - /// - public int MaxDecompressedBytes { get; set; } = DefaultMaxDecompressedBytes; - - /// - /// 压缩负载(Base64 解码后)允许的最大字节数,用于防御过大压缩输入。 - /// 默认 16 MB。 - /// - public int MaxCompressedBytes { get; set; } = DefaultMaxCompressedBytes; - - /// - /// JSON 字符串中 Base64 文本允许的最大字符数。 - /// 取 对应的 Base64 长度上限并向上取整。 - /// - public int MaxBase64Length => ((MaxCompressedBytes + 2) / 3) * 4; - - // ── Encode ────────────────────────────────────────────────── - - /// 按配置的压缩算法编码负载,并写入版本化头部。 - /// 原始负载。 - /// 带头部的编码负载。 - public byte[] Encode(byte[] payload) - { - if (payload == null) throw new ArgumentNullException(nameof(payload)); - return Encode(payload, 0, payload.Length); - } - - /// 按配置的压缩算法编码负载,并写入版本化头部。 - /// 原始负载。 - /// 起始偏移。 - /// 长度。 - /// 带头部的编码负载。 - public byte[] Encode(byte[] payload, int offset, int count) - { - if (payload == null) throw new ArgumentNullException(nameof(payload)); - if (offset < 0 || offset > payload.Length) throw new ArgumentOutOfRangeException(nameof(offset)); - if (count < 0 || payload.Length - offset < count) throw new ArgumentOutOfRangeException(nameof(count)); - - using var output = new MemoryStream(count + HeaderLength + 16); - WriteHeader(output, Compression); - - switch (Compression) - { - case RecordPayloadCompression.None: - output.Write(payload, offset, count); - break; - case RecordPayloadCompression.GZip: - using (var gzip = new GZipStream(output, CompressionLevel.Fastest, leaveOpen: true)) - { - gzip.Write(payload, offset, count); - } - break; - default: - throw new InvalidOperationException($"不支持的压缩算法:{Compression}。"); - } - - return output.ToArray(); - } - - // ── Decode ────────────────────────────────────────────────── - - /// - /// 读取负载并自动识别版本与压缩算法进行解码;若负载不带本格式头则原样返回(向后兼容遗留无压缩格式)。 - /// - /// Base64 解码后的负载。 - /// 解码后的原始负载。 - /// 为 null。 - /// 输入过大、解压结果超出限制或算法不支持。 - public byte[] Decode(byte[] payload) - { - if (payload == null) throw new ArgumentNullException(nameof(payload)); - - if (!TryReadHeader(payload, out byte version, out RecordPayloadCompression algo, out int dataOffset)) - { - // 无头部:遗留格式,原样返回 - return payload; - } - - if (algo == RecordPayloadCompression.None) - { - // 无压缩:直接截取数据部分 - int dataLength = payload.Length - dataOffset; - var result = new byte[dataLength]; - Buffer.BlockCopy(payload, dataOffset, result, 0, dataLength); - return result; - } - - // 有压缩:施加输入上限防御 DoS - if (payload.Length > MaxCompressedBytes) - throw new InvalidDataException($"压缩负载超过限制:{MaxCompressedBytes} bytes。"); - - return Decompress(payload, dataOffset, algo); - } - - // ── Private helpers ───────────────────────────────────────── - - private static void WriteHeader(Stream stream, RecordPayloadCompression algo) - { - stream.WriteByte(MagicByte0); - stream.WriteByte(MagicByte1); - stream.WriteByte(MagicByte2); - stream.WriteByte(MagicByte3); - stream.WriteByte(FormatVersion); - stream.WriteByte((byte)algo); - } - - /// - /// 尝试解析版本 1 头部(6 字节)。 - /// - private static bool TryReadHeader(byte[] data, out byte version, out RecordPayloadCompression algo, out int dataOffset) - { - version = 0; - algo = RecordPayloadCompression.None; - dataOffset = 0; - - if (data.Length < HeaderLength) return false; - if (data[0] != MagicByte0 || data[1] != MagicByte1 || data[2] != MagicByte2 || data[3] != MagicByte3) - return false; - - version = data[MagicLength]; - algo = (RecordPayloadCompression)data[MagicLength + 1]; - dataOffset = HeaderLength; - return true; - } - - private byte[] Decompress(byte[] payload, int dataOffset, RecordPayloadCompression algo) - { - int dataLength = payload.Length - dataOffset; - using var input = new MemoryStream(payload, dataOffset, dataLength, writable: false); - - Stream decompressStream = algo switch - { - RecordPayloadCompression.GZip => new GZipStream(input, CompressionMode.Decompress, leaveOpen: false), - _ => throw new InvalidDataException($"不支持的压缩算法标识:0x{(byte)algo:X2}。") - }; - - using (decompressStream) - using (var output = new MemoryStream(InitialOutputCapacity)) - { - var buffer = new byte[DecodeBufferSize]; - long total = 0; - while (true) - { - int read = decompressStream.Read(buffer, 0, buffer.Length); - if (read <= 0) break; - - if (total + read > MaxDecompressedBytes) - throw new InvalidDataException($"解压后数据超过限制:{MaxDecompressedBytes} bytes。"); - - total += read; - output.Write(buffer, 0, read); - } - return output.ToArray(); - } - } -} -#endif \ No newline at end of file diff --git a/src/LuYao.Common/Data/Binary/RecordPayloadCompression.cs b/src/LuYao.Common/Data/Binary/RecordPayloadCompression.cs deleted file mode 100644 index 48fb5c8..0000000 --- a/src/LuYao.Common/Data/Binary/RecordPayloadCompression.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace LuYao.Data.Binary; - -/// -/// 指定 序列化时使用的压缩算法。 -/// -public enum RecordPayloadCompression : byte -{ - /// 不压缩。 - None = 0, - - /// 使用 GZip 压缩。 - GZip = 1, -} diff --git a/src/LuYao.Common/Data/ConversionFailureHandling.cs b/src/LuYao.Common/Data/ConversionFailureHandling.cs deleted file mode 100644 index db04900..0000000 --- a/src/LuYao.Common/Data/ConversionFailureHandling.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace LuYao.Data; - -/// -/// 在 RecordTable 到 DTO 映射时,列值类型转换失败的处理策略。 -/// -public enum ConversionFailureHandling -{ - /// 跳过转换失败的列,对应属性保持默认值(默认)。 - Skip = 0, - - /// 转换失败时抛出异常。 - Throw = 1, -} diff --git a/src/LuYao.Common/Data/Helpers.cs b/src/LuYao.Common/Data/Helpers.cs index f06fea6..472a89e 100644 --- a/src/LuYao.Common/Data/Helpers.cs +++ b/src/LuYao.Common/Data/Helpers.cs @@ -1,8 +1,5 @@ -using LuYao.Data.Meta; using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq.Expressions; namespace LuYao.Data; @@ -45,7 +42,7 @@ static Helpers() } /// - /// From CLR get the base (excluding Nullable and array info). + /// From CLR get its . /// internal static RecordColumnType GetColumnType(Type type) { @@ -55,25 +52,12 @@ internal static RecordColumnType GetColumnType(Type type) return RecordColumnType.Binary; } - var lookup = NormalizeColumnLookupType(type); - - if (TypeToColumnType.TryGetValue(lookup, out var ct)) + if (TypeToColumnType.TryGetValue(type, out var ct)) return ct; throw new NotSupportedException($"类型 '{type.FullName}' 不是支持的列类型"); } - /// - /// 判断 CLR 是否为 Nullable 值类型,或其数组元素类型为 Nullable。 - /// - internal static bool IsNullableType(Type type) - { - return Nullable.GetUnderlyingType(type) != null; - } - - /// - /// 从基础 、 和 还原 CLR 。 - /// - internal static Type GetClrType(RecordColumnType columnType, bool isNullable) + internal static Type GetClrType(RecordColumnType columnType) { if (columnType == RecordColumnType.Binary) return typeof(byte[]); @@ -81,8 +65,6 @@ internal static Type GetClrType(RecordColumnType columnType, bool isNullable) if (!ColumnTypeToType.TryGetValue(columnType, out var baseType)) throw new NotSupportedException($"未知列类型枚举值: {columnType}"); - if (isNullable && baseType.IsValueType) - return typeof(Nullable<>).MakeGenericType(baseType); return baseType; } @@ -95,75 +77,21 @@ internal static bool IsSupportedColumnType(Type type) // byte[] is directly supported as Binary if (type == typeof(byte[])) return true; - var normalized = NormalizeColumnLookupType(type); - return TypeToColumnType.ContainsKey(normalized); + return TypeToColumnType.ContainsKey(type); } - internal static bool IsSupportedForReading(IXProp p) - { - if (p != null && p.CanRead && IsSupportedColumnType(p.Type)) return true; - return false; - } - internal static bool IsSupportedForWriting(IXProp p) - { - if (p != null && p.CanWrite && IsSupportedColumnType(p.Type)) return true; - return false; - } - - /// /// 验证列类型是否在白名单内,不在则抛出异常。 - /// internal static void ValidateColumnType(Type type) { if (!IsSupportedColumnType(type)) { - throw new NotSupportedException($"类型 '{type.FullName}' 不是支持的列类型。支持的类型包括:bool, 整数类型, 浮点类型, char, string, DateTime, DateTimeOffset, TimeSpan, Guid, byte[]、枚举及其 Nullable 形式,以及上述类型的数组(如 int[], string[,])。"); + throw new NotSupportedException($"类型 '{type.FullName}' 不是支持的列类型。支持类型由 RecordColumnType 定义;byte[] 作为 Binary 支持,其他数组、Nullable 和枚举不受支持。"); } } - /// - /// 规范化列类型查找目标:先展开 Nullable,再将枚举转换为其基础数值类型。 - /// - private static Type NormalizeColumnLookupType(Type type) - { - var effective = Nullable.GetUnderlyingType(type) ?? type; - return effective.IsEnum ? Enum.GetUnderlyingType(effective) : effective; - } - private static readonly ConcurrentDictionary> _cache = new(); - - private static Func GetConstructor(Type type) - { - return _cache.GetOrAdd(type, static t => - { - // 创建泛型类型 - var genericType = typeof(RecordColumn<>).MakeGenericType(t); - - // 获取构造函数 - var ctor = genericType.GetConstructor(new[] { - typeof(RecordTable), - typeof(string), - typeof(Type) - }) ?? throw new InvalidOperationException($"Constructor not found for {genericType}"); - - // 定义参数表达式 - var pRecord = Expression.Parameter(typeof(RecordTable), "table"); - var pName = Expression.Parameter(typeof(string), "name"); - var pType = Expression.Parameter(typeof(Type), "type"); - - // 创建构造函数调用表达式 - var newExpr = Expression.New(ctor, pRecord, pName, pType); - - // 编译表达式树为委托 - return Expression.Lambda>( - newExpr, pRecord, pName, pType).Compile(); - }); - } - public static RecordColumn MakeRecordColumn(RecordTable table, string name) { - // 使用泛型方法优化,避免运行时查找类型 - var ctor = GetConstructor(typeof(T)); - return ctor.Invoke(table, name, typeof(T)); + return MakeRecordColumn(table, name, typeof(T)); } public static RecordColumn MakeRecordColumn(RecordTable table, string name, Type type) @@ -174,7 +102,36 @@ public static RecordColumn MakeRecordColumn(RecordTable table, string name, Type ValidateColumnType(type); - var ctor = GetConstructor(type); - return ctor.Invoke(table, name, type); + return MakeRecordColumn(table, name, GetColumnType(type), type); + } + + internal static RecordColumn MakeRecordColumn(RecordTable table, string name, RecordColumnType columnType, Type? clrType = null) + { + var type = clrType ?? GetClrType(columnType); + return columnType switch + { + RecordColumnType.Boolean => new RecordColumn(table, name, type), + RecordColumnType.SByte => new RecordColumn(table, name, type), + RecordColumnType.Int16 => new RecordColumn(table, name, type), + RecordColumnType.Int32 => new RecordColumn(table, name, type), + RecordColumnType.Int64 => new RecordColumn(table, name, type), + RecordColumnType.Byte => new RecordColumn(table, name, type), + RecordColumnType.UInt16 => new RecordColumn(table, name, type), + RecordColumnType.UInt32 => new RecordColumn(table, name, type), + RecordColumnType.UInt64 => new RecordColumn(table, name, type), + RecordColumnType.Single => new RecordColumn(table, name, type), + RecordColumnType.Double => new RecordColumn(table, name, type), + RecordColumnType.Decimal => new RecordColumn(table, name, type), + RecordColumnType.Char => new RecordColumn(table, name, type), + RecordColumnType.String => new RecordColumn(table, name, type), + RecordColumnType.DateTime => new RecordColumn(table, name, type), + RecordColumnType.DateTimeOffset => new RecordColumn(table, name, type), + RecordColumnType.TimeSpan => new RecordColumn(table, name, type), + RecordColumnType.Guid => new RecordColumn(table, name, type), + RecordColumnType.Binary => new RecordColumn(table, name, type), + _ => throw new NotSupportedException($"未知列类型: {columnType}") + }; } } + + diff --git a/src/LuYao.Common/Data/Json/RecordSetJsonConverter.cs b/src/LuYao.Common/Data/Json/RecordSetJsonConverter.cs deleted file mode 100644 index 574b4a3..0000000 --- a/src/LuYao.Common/Data/Json/RecordSetJsonConverter.cs +++ /dev/null @@ -1,78 +0,0 @@ -#if !NET45 && !NET461 -using LuYao.Data.Binary; -using System; -using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace LuYao.Data.Json; - -/// -/// 将 以 Base64 二进制格式进行 JSON 序列化/反序列化。 -/// -public class RecordSetJsonConverter : JsonConverter -{ - private readonly RecordBinaryPayloadCodec _codec; - - /// 使用默认编解码器(GZip 压缩)创建转换器。 - public RecordSetJsonConverter() : this(RecordBinaryPayloadCodec.Default) { } - - /// 使用指定编解码器创建转换器。 - /// 编解码器实例。 - public RecordSetJsonConverter(RecordBinaryPayloadCodec codec) - { - if (codec == null) throw new ArgumentNullException(nameof(codec)); - _codec = codec; - } - - /// - public override RecordSet? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) return null; - if (reader.TokenType == JsonTokenType.String) - { - // 在分配字符串/字节数组前先检查 Base64 长度上限,防御超大输入引发的 OOM。 - long base64Length = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; - if (base64Length > _codec.MaxBase64Length) - throw new JsonException($"RecordSet Base64 payload exceeds maximum allowed length {_codec.MaxBase64Length}."); - - var base64 = reader.GetString()!; - try - { - var bytes = Convert.FromBase64String(base64); - bytes = _codec.Decode(bytes); - return RecordSet.FromBytes(bytes); - } - catch (FormatException ex) - { - throw new JsonException($"Failed to read RecordSet from Base64 JSON string. TokenType={reader.TokenType}, Base64Length={base64.Length}.", ex); - } - catch (EndOfStreamException ex) - { - throw new JsonException($"Failed to read RecordSet from binary payload. TokenType={reader.TokenType}, Base64Length={base64.Length}.", ex); - } - catch (InvalidDataException ex) - { - throw new JsonException($"Failed to read RecordSet from binary payload. TokenType={reader.TokenType}, Base64Length={base64.Length}.", ex); - } - catch (InvalidOperationException ex) - { - throw new JsonException($"Failed to read RecordSet from binary payload. TokenType={reader.TokenType}, Base64Length={base64.Length}.", ex); - } - } - throw new JsonException($"Unexpected token type {reader.TokenType} for RecordSet."); - } - - /// - public override void Write(Utf8JsonWriter writer, RecordSet value, JsonSerializerOptions options) - { - if (value == null) - { - writer.WriteNullValue(); - return; - } - var bytes = _codec.Encode(value.ToBytes()); - writer.WriteStringValue(Convert.ToBase64String(bytes)); - } -} -#endif diff --git a/src/LuYao.Common/Data/Json/RecordTableJsonConverter.cs b/src/LuYao.Common/Data/Json/RecordTableJsonConverter.cs deleted file mode 100644 index 1f63698..0000000 --- a/src/LuYao.Common/Data/Json/RecordTableJsonConverter.cs +++ /dev/null @@ -1,92 +0,0 @@ -#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.Json; - -/// -/// 将 以 Base64 二进制格式进行 JSON 序列化/反序列化。 -/// -public class RecordTableJsonConverter : JsonConverter -{ - private readonly RecordBinaryPayloadCodec _codec; - - /// 使用默认编解码器(GZip 压缩)创建转换器。 - public RecordTableJsonConverter() : this(RecordBinaryPayloadCodec.Default) { } - - /// 使用指定编解码器创建转换器。 - /// 编解码器实例。 - public RecordTableJsonConverter(RecordBinaryPayloadCodec codec) - { - if (codec == null) throw new ArgumentNullException(nameof(codec)); - _codec = codec; - } - - /// - public override RecordTable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) return null; - if (reader.TokenType == JsonTokenType.String) - { - // 在分配字符串/字节数组前先检查 Base64 长度上限,防御超大输入引发的 OOM。 - long base64Length = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; - if (base64Length > _codec.MaxBase64Length) - throw new JsonException($"RecordTable Base64 payload exceeds maximum allowed length {_codec.MaxBase64Length}."); - - var base64 = reader.GetString()!; - try - { - var bytes = Convert.FromBase64String(base64); - bytes = _codec.Decode(bytes); - using var ms = new MemoryStream(bytes, writable: false); - using var br = new BinaryReader(ms, Encoding.UTF8, leaveOpen: true); - var record = new RecordTable(); - record.ReadFrom(br); - return record; - } - catch (FormatException ex) - { - throw new JsonException($"Failed to read RecordTable from Base64 JSON string. TokenType={reader.TokenType}, Base64Length={base64.Length}.", ex); - } - catch (EndOfStreamException ex) - { - throw new JsonException($"Failed to read RecordTable from binary payload. TokenType={reader.TokenType}, Base64Length={base64.Length}.", ex); - } - catch (InvalidDataException ex) - { - throw new JsonException($"Failed to read RecordTable from binary payload. TokenType={reader.TokenType}, Base64Length={base64.Length}.", ex); - } - catch (InvalidOperationException ex) - { - throw new JsonException($"Failed to read RecordTable from binary payload. TokenType={reader.TokenType}, Base64Length={base64.Length}.", ex); - } - } - throw new JsonException($"Unexpected token type {reader.TokenType} for RecordTable."); - } - - /// - public override void Write(Utf8JsonWriter writer, RecordTable value, JsonSerializerOptions options) - { - if (value == null) - { - writer.WriteNullValue(); - return; - } - using var ms = new MemoryStream(); - using var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true); - value.WriteTo(bw); - bw.Flush(); - - string base64; - if (ms.TryGetBuffer(out ArraySegment buffer) && buffer.Array != null) - base64 = Convert.ToBase64String(_codec.Encode(buffer.Array, buffer.Offset, (int)ms.Length)); - else - base64 = Convert.ToBase64String(_codec.Encode(ms.ToArray())); - writer.WriteStringValue(base64); - } -} -#endif diff --git a/src/LuYao.Common/Data/Mapping/ColumnNameResolver.cs b/src/LuYao.Common/Data/Mapping/ColumnNameResolver.cs deleted file mode 100644 index 64f0a94..0000000 --- a/src/LuYao.Common/Data/Mapping/ColumnNameResolver.cs +++ /dev/null @@ -1,31 +0,0 @@ -using LuYao.Data.Attributes; -using LuYao.Data.Meta; -using System.Reflection; - -namespace LuYao.Data.Mapping; - -/// -/// 根据 解析属性对应的列名。 -/// 优先级: > > 属性原名。 -/// -internal static class ColumnNameResolver -{ - /// - /// 解析 对应的列名。 - /// - /// 属性描述符。 - /// 映射选项;为 时使用属性原名。 - /// 解析后的列名。 - internal static string Resolve(XProp prop, RecordMappingOptions? options) - { - // 优先级1:RecordColumnNameAttribute - var attr = prop.GetCustomAttribute(); - if (attr != null) return attr.Name; - - // 优先级2:NamingPolicy - if (options?.NamingPolicy != null) return options.NamingPolicy.ConvertName(prop.Name); - - // 优先级3:属性原名 - return prop.Name; - } -} diff --git a/src/LuYao.Common/Data/Mapping/RecordMappingContext.cs b/src/LuYao.Common/Data/Mapping/RecordMappingContext.cs deleted file mode 100644 index b34fa5f..0000000 --- a/src/LuYao.Common/Data/Mapping/RecordMappingContext.cs +++ /dev/null @@ -1,211 +0,0 @@ -using LuYao.Data.Internal; -using LuYao.Data.Meta; -using System; - -namespace LuYao.Data.Mapping; - -/// -/// Encapsulates the execution context for a single mapping operation. -/// Holds a instance and centralises all -/// DTO â†? mapping logic. -/// -/// -/// All overloads ultimately delegate to this class, -/// keeping the logic in one place. -/// The options are frozen via -/// at construction time. -/// -internal sealed class RecordMappingContext -{ - private readonly RecordMappingOptions _options; - - /// - /// Creates a mapping context with the specified options and immediately freezes them. - /// - /// Mapping options; must not be null. - internal RecordMappingContext(RecordMappingOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _options.MakeReadOnly(); - } - - // ─── DTO â†?RecordRow (map to existing columns, no auto-create) ─────────────── - - /// - /// Maps readable properties of on - /// into the corresponding columns of . - /// Columns that do not exist are silently skipped; no columns are created. - /// - internal void MapDtoToRow(Type type, object data, RecordRow target) - { - var cols = target.Table.Columns; - foreach (var prop in XProp.GetAll(type)) - { - if (!prop.CanRead) continue; - - var colName = ColumnNameResolver.Resolve(prop, _options); - var col = cols.Find(colName); - if (col == null) continue; - - if (Helpers.IsSupportedForReading(prop)) - { - col.Set(target, prop.GetValue(data)); - continue; - } - - HandleUnsupportedTypeForWrite(prop); - } - } - - // ─── DTO â†?RecordRow (auto-create columns) ─────────────────────────────────── - - /// - /// Writes readable properties of on - /// into the corresponding columns of . - /// Missing columns are created automatically before writing. - /// - internal void WriteDtoToRow(Type type, object data, RecordRow target) - { - var cols = target.Table.Columns; - foreach (var prop in XProp.GetAll(type)) - { - if (!prop.CanRead) continue; - - if (Helpers.IsSupportedForReading(prop)) - { - var colName = ColumnNameResolver.Resolve(prop, _options); - var col = cols.Find(colName) ?? cols.Add(colName, prop.Type); - col.Set(target, prop.GetValue(data)); - continue; - } - - HandleUnsupportedTypeForWrite(prop); - } - } - - // ─── RecordRow â†?DTO ───────────────────────────────────────────────────────── - - /// - /// Maps column values from into the writable properties - /// of . Columns that do not exist are silently skipped. - /// - internal void MapRowToDto(Type type, object data, RecordRow source) - { - var cols = source.Table.Columns; - foreach (var prop in XProp.GetAll(type)) - { - if (!prop.CanWrite) continue; - - var colName = ColumnNameResolver.Resolve(prop, _options); - var col = cols.Find(colName); - if (col == null) continue; - - var rawValue = col.Get(source); - - if (!Helpers.IsSupportedForWriting(prop)) - { - HandleConversionFailure( - new NotSupportedException( - $"Property '{prop.Name}' has type '{prop.Type.FullName}' which is not supported.")); - continue; - } - - if (col.Type == prop.Type) - { - TrySetValue(data, prop, rawValue); - continue; - } - - // Types differ (e.g. column is int, DTO property is decimal). - // Use Convert.ChangeType as a best-effort numeric/primitive coercion, - // mirroring the strategy used by Dapper and similar micro-ORMs. - TrySetValueWithConversion(data, prop, rawValue); - } - } - - // ─── AddFrom (column definition) ───────────────────────────────────────────── - - /// - /// Appends column definitions to based on the readable - /// properties of . - /// - internal void AddColumnsFrom(RecordColumnCollection columns) where T : class - => AddColumnsFrom(typeof(T), columns); - - /// - /// Appends column definitions to based on the readable - /// properties of . - /// - internal void AddColumnsFrom(Type type, RecordColumnCollection columns) - { - foreach (var prop in XProp.GetAll(type)) - { - if (!prop.CanRead) continue; - - if (Helpers.IsSupportedForReading(prop)) - { - var colName = ColumnNameResolver.Resolve(prop, _options); - columns.Add(colName, prop.Type); - continue; - } - - HandleUnsupportedTypeForWrite(prop); - } - } - - private void TrySetValue(object data, XProp prop, object? value) - { - try - { - prop.SetValue(data, value); - } - catch (Exception ex) when (_options.ConversionFailureHandling == ConversionFailureHandling.Skip) - { - _ = ex; - } - } - - /// - /// Attempts to set on after - /// coercing it to the property's declared type via . - /// Falls back to direct assignment when the value is already assignable (e.g. null or subtype). - /// Obeys on failure. - /// - private void TrySetValueWithConversion(object data, XProp prop, object? value) - { - try - { - if (value is null or DBNull) - { - prop.SetValue(data, null); - return; - } - - prop.SetValue(data, DataConvert.ChangeType(value, prop.Type)); - } - catch (Exception ex) - { - HandleConversionFailure( - new InvalidCastException( - $"Cannot convert value '{value}' ({value?.GetType().FullName}) to property '{prop.Name}' of type '{prop.Type.FullName}'.", - ex)); - } - } - - private void HandleUnsupportedTypeForWrite(XProp prop) - { - if (_options.UnsupportedTypeHandling == UnsupportedTypeHandling.Throw) - throw new NotSupportedException( - $"Property '{prop.Name}' has type '{prop.Type.FullName}' which is not supported and cannot be written to a RecordTable."); - // Skip: silently ignore - } - - private void HandleConversionFailure(Exception inner) - { - if (_options.ConversionFailureHandling == ConversionFailureHandling.Throw) - throw inner; - // Skip: silently ignore - } - - -} diff --git a/src/LuYao.Common/Data/Mapping/RecordMappingOptions.cs b/src/LuYao.Common/Data/Mapping/RecordMappingOptions.cs deleted file mode 100644 index 3d3208e..0000000 --- a/src/LuYao.Common/Data/Mapping/RecordMappingOptions.cs +++ /dev/null @@ -1,85 +0,0 @@ -using LuYao.Data.Attributes; -using System; - -namespace LuYao.Data.Mapping; - -/// -/// 控制 与 DTO 之间映射行为的选项类。 -/// -/// -/// -/// 实例首次参与映射任务后将被冻结(只读),之后任何属性赋值或 调用 -/// 均会抛出 。 -/// -/// -/// 可通过 获取全局默认实例;也可自行创建实例并在首次使用前配置。 -/// -/// -public sealed class RecordMappingOptions -{ - // ─── 静态默认实例 ─────────────────────────────────────────────────────────── - - /// 全局默认选项实例。 - public static RecordMappingOptions Default { get; } = new RecordMappingOptions(); - - // ─── 只读控制 ──────────────────────────────────────────────────────────────── - - private bool _isReadOnly; - - /// - /// 将当前实例标记为只读(冻结)。首次映射任务开始时由框架自动调用,调用后属性不可再更改。 - /// - internal void MakeReadOnly() - { - _isReadOnly = true; - } - - /// 是否已被冻结(只读)。 - public bool IsReadOnly => _isReadOnly; - - private void ThrowIfReadOnly() - { - if (_isReadOnly) - throw new InvalidOperationException( - "RecordMappingOptions 已被冻结,首次参与映射后不可再修改。"); - } - - // ─── NamingPolicy ──────────────────────────────────────────────────────────── - - private RecordNamingPolicy? _namingPolicy; - - /// - /// 列名转换策略。 表示使用属性原名(除非有 标记)。 - /// - public RecordNamingPolicy? NamingPolicy - { - get => _namingPolicy; - set { ThrowIfReadOnly(); _namingPolicy = value; } - } - - // ─── UnsupportedTypeHandling ───────────────────────────────────────────────── - - private UnsupportedTypeHandling _unsupportedTypeHandling = UnsupportedTypeHandling.Skip; - - /// - /// DTO → RecordTable 时遇到不支持的属性类型的处理策略,默认为 。 - /// - public UnsupportedTypeHandling UnsupportedTypeHandling - { - get => _unsupportedTypeHandling; - set { ThrowIfReadOnly(); _unsupportedTypeHandling = value; } - } - - // ─── ConversionFailureHandling ─────────────────────────────────────────────── - - private ConversionFailureHandling _conversionFailureHandling = ConversionFailureHandling.Skip; - - /// - /// RecordTable → DTO 时列值类型转换失败的处理策略,默认为 。 - /// - public ConversionFailureHandling ConversionFailureHandling - { - get => _conversionFailureHandling; - set { ThrowIfReadOnly(); _conversionFailureHandling = value; } - } -} diff --git a/src/LuYao.Common/Data/Mapping/XCopy.cs b/src/LuYao.Common/Data/Mapping/XCopy.cs deleted file mode 100644 index 1c3e225..0000000 --- a/src/LuYao.Common/Data/Mapping/XCopy.cs +++ /dev/null @@ -1,284 +0,0 @@ -using LuYao.Data.Meta; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; - -namespace LuYao.Data.Mapping; - -/// -/// Static utility class for bidirectional property mapping between objects and . -/// Property lists are cached keyed by runtime type, so derived-class properties are handled naturally. -/// -/// -/// All mapping logic is delegated to . -/// Overloads without options are equivalent to using . -/// -public static class XCopy -{ - private static readonly ConcurrentDictionary> _readableCache - = new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary> _writableCache - = new ConcurrentDictionary>(); - - internal static IReadOnlyList GetReadableProps(Type runtimeType) - => _readableCache.GetOrAdd(runtimeType, t => - { - var all = XProp.GetAll(t); - var list = new List(all.Count); - foreach (var p in all) - { - if (Helpers.IsSupportedForReading(p)) list.Add(p); - } - return list.AsReadOnly(); - }); - - internal static IReadOnlyList GetWritableProps(Type runtimeType) - => _writableCache.GetOrAdd(runtimeType, t => - { - var all = XProp.GetAll(t); - var list = new List(all.Count); - foreach (var p in all) - { - if (Helpers.IsSupportedForWriting(p)) list.Add(p); - } - return list.AsReadOnly(); - }); - - #region RecordRow - - /// - /// Maps readable properties of into the corresponding columns of - /// . Uses the runtime type to scan properties. - /// Columns that do not exist are silently skipped; no columns are created. - /// - /// Source object; must not be null. - /// Target row. - /// Thrown when is null. - public static void MapTo(object data, RecordRow target) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - MapTo(data.GetType(), data, target, RecordMappingOptions.Default); - } - - /// - /// Maps readable properties of the -declared members on - /// into the corresponding columns of . - /// Columns that do not exist are silently skipped; no columns are created. - /// - /// Declared type used to scan properties; must not be null. - /// Source object; must not be null. - /// Target row. - /// Thrown when or is null. - public static void MapTo(Type type, object data, RecordRow target) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (data == null) throw new ArgumentNullException(nameof(data)); - MapTo(type, data, target, RecordMappingOptions.Default); - } - - /// - /// Maps readable properties of into the corresponding columns of - /// using the specified options. - /// Columns that do not exist are silently skipped; no columns are created. - /// - /// Source object; must not be null. - /// Target row. - /// Mapping options; must not be null. - /// Thrown when any argument is null. - public static void MapTo(object data, RecordRow target, RecordMappingOptions options) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - MapTo(data.GetType(), data, target, options); - } - - /// - /// Maps readable properties of the -declared members on - /// into the corresponding columns of using the specified options. - /// Columns that do not exist are silently skipped; no columns are created. - /// - /// Declared type used to scan properties; must not be null. - /// Source object; must not be null. - /// Target row. - /// Mapping options; must not be null. - /// Thrown when any argument is null. - public static void MapTo(Type type, object data, RecordRow target, RecordMappingOptions options) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (data == null) throw new ArgumentNullException(nameof(data)); - if (options == null) throw new ArgumentNullException(nameof(options)); - new RecordMappingContext(options).MapDtoToRow(type, data, target); - } - - /// - /// Maps column values from that correspond to object properties - /// into . Uses the runtime type to scan properties. - /// - /// Target object; must not be null. - /// Source row. - /// Thrown when is null. - public static void MapFrom(object data, RecordRow source) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - MapFrom(data.GetType(), data, source, RecordMappingOptions.Default); - } - - /// - /// Maps column values from that correspond to - /// -declared properties into . - /// - /// Declared type used to scan properties; must not be null. - /// Target object; must not be null. - /// Source row. - /// Thrown when or is null. - public static void MapFrom(Type type, object data, RecordRow source) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (data == null) throw new ArgumentNullException(nameof(data)); - MapFrom(type, data, source, RecordMappingOptions.Default); - } - - /// - /// Maps column values from into - /// using the specified options. - /// - /// Target object; must not be null. - /// Source row. - /// Mapping options; must not be null. - /// Thrown when any argument is null. - public static void MapFrom(object data, RecordRow source, RecordMappingOptions options) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - MapFrom(data.GetType(), data, source, options); - } - - /// - /// Maps column values from that correspond to - /// -declared properties into - /// using the specified options (supports custom converters and column-name strategies). - /// - /// Declared type used to scan properties; must not be null. - /// Target object; must not be null. - /// Source row. - /// Mapping options; must not be null. - /// Thrown when any argument is null. - public static void MapFrom(Type type, object data, RecordRow source, RecordMappingOptions options) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (data == null) throw new ArgumentNullException(nameof(data)); - if (options == null) throw new ArgumentNullException(nameof(options)); - new RecordMappingContext(options).MapRowToDto(type, data, source); - } - - /// - /// Writes readable properties of into the corresponding columns of - /// , creating missing columns automatically. - /// Uses the runtime type to scan properties. - /// - /// Source object; must not be null. - /// Target row. - /// Thrown when is null. - public static void WriteTo(object data, RecordRow target) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - WriteTo(data.GetType(), data, target, RecordMappingOptions.Default); - } - - /// - /// Writes readable properties of the -declared members on - /// into the corresponding columns of , creating missing columns automatically. - /// - /// Declared type used to scan properties; must not be null. - /// Source object; must not be null. - /// Target row. - /// Thrown when or is null. - public static void WriteTo(Type type, object data, RecordRow target) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (data == null) throw new ArgumentNullException(nameof(data)); - WriteTo(type, data, target, RecordMappingOptions.Default); - } - - /// - /// Writes readable properties of into the corresponding columns of - /// (auto-creating columns), using the specified options. - /// - /// Source object; must not be null. - /// Target row. - /// Mapping options; must not be null. - /// Thrown when any argument is null. - public static void WriteTo(object data, RecordRow target, RecordMappingOptions options) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - WriteTo(data.GetType(), data, target, options); - } - - /// - /// Writes readable properties of the -declared members on - /// into the corresponding columns of (auto-creating columns), - /// using the specified options. - /// - /// Declared type used to scan properties; must not be null. - /// Source object; must not be null. - /// Target row. - /// Mapping options; must not be null. - /// Thrown when any argument is null. - public static void WriteTo(Type type, object data, RecordRow target, RecordMappingOptions options) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (data == null) throw new ArgumentNullException(nameof(data)); - if (options == null) throw new ArgumentNullException(nameof(options)); - new RecordMappingContext(options).WriteDtoToRow(type, data, target); - } - - #endregion -} - -/// -/// Generic thin wrapper over that fixes the scan type to the -/// compile-time type . All logic is delegated to the explicit -/// overloads of . -/// -/// The object type to map; must be a reference type. -public static class XCopy where T : class -{ - #region RecordRow - /// - /// Maps readable properties of into the corresponding columns of - /// . Always scans using the compile-time type ; - /// derived-class properties are not processed even if the runtime type is a subclass. - /// - /// Source object. - /// Target row; columns that do not exist are silently skipped, no columns are created. - public static void MapTo(T data, RecordRow target) => XCopy.MapTo(typeof(T), data, target); - - /// Maps readable properties of into the corresponding columns of using the specified options. - public static void MapTo(T data, RecordRow target, RecordMappingOptions options) => XCopy.MapTo(typeof(T), data, target, options); - - /// - /// Maps column values from that match object properties - /// into . Always scans using the compile-time type ; - /// derived-class properties are not processed even if the runtime type is a subclass. - /// - /// Target object. - /// Source row. - public static void MapFrom(T data, RecordRow source) => XCopy.MapFrom(typeof(T), data, source); - - /// Maps column values from into using the specified options. - public static void MapFrom(T data, RecordRow source, RecordMappingOptions options) => XCopy.MapFrom(typeof(T), data, source, options); - - /// - /// Writes readable properties of into the corresponding columns of - /// , creating missing columns automatically. - /// Always scans using the compile-time type ; - /// derived-class properties are not processed even if the runtime type is a subclass. - /// - /// Source object. - /// Target row. - public static void WriteTo(T data, RecordRow target) => XCopy.WriteTo(typeof(T), data, target); - - /// Writes readable properties of into the corresponding columns of (auto-creating columns) using the specified options. - public static void WriteTo(T data, RecordRow target, RecordMappingOptions options) => XCopy.WriteTo(typeof(T), data, target, options); - - #endregion -} - diff --git a/src/LuYao.Common/Data/Meta/IXProp.cs b/src/LuYao.Common/Data/Meta/IXProp.cs deleted file mode 100644 index 9509425..0000000 --- a/src/LuYao.Common/Data/Meta/IXProp.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace LuYao.Data.Meta; - -/// -/// 表示一个可按对象实例读写的属性描述符。 -/// -/// -/// 由 (基于反射)和 (基于列存储)实现。 -/// GetValue/SetValue 的 instance 参数语义取决于具体实现: -/// 对于 ,instance 为承载属性的 CLR 对象; -/// 对于 ,instance 为 (值类型装箱后传入)。 -/// -public interface IXProp -{ - /// 属性名称。 - string Name { get; } - - /// 属性类型。 - Type Type { get; } - - /// 是否可读。 - bool CanRead { get; } - - /// 是否可写。 - bool CanWrite { get; } - - /// - /// 读取指定实例的属性值。 - /// - /// 属性所属的对象实例。 - /// 属性当前值。 - /// 属性不可读时抛出。 - object? GetValue(object instance); - - /// - /// 设置指定实例的属性值。 - /// - /// 属性所属的对象实例。 - /// 要写入的值。 - /// 属性不可写时抛出。 - void SetValue(object instance, object? value); -} diff --git a/src/LuYao.Common/Data/Meta/XData.cs b/src/LuYao.Common/Data/Meta/XData.cs deleted file mode 100644 index d53d740..0000000 --- a/src/LuYao.Common/Data/Meta/XData.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace LuYao.Data.Meta; - -/// -/// 提供对强类型对象属性的按名称读写操作,底层使用 加速反射访问。 -/// -/// 目标类型,必须具有无参构造函数。 -public static class XData where T : class -{ - private static readonly IReadOnlyList _props = XProp.GetAll(typeof(T)); - - private static XProp Get(string name) - { - foreach (var p in _props) - { - if (string.Equals(p.Name, name, StringComparison.Ordinal)) - { - return p; - } - } - throw new ArgumentException($"类型 {typeof(T).FullName} 上未找到属性 '{name}'。", nameof(name)); - } - - /// - /// 按属性名写入值。 - /// - /// 目标对象实例。 - /// 属性名称(大小写敏感)。 - /// 要写入的值。 - /// 当 为 null 时抛出。 - /// 属性不存在时抛出。 - /// 属性不可写时抛出。 - public static void Set(T data, string name, object? value) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - Get(name).SetValue(data, value); - } - - /// - /// 按属性名读取值。 - /// - /// 目标对象实例。 - /// 属性名称(大小写敏感)。 - /// 属性当前值。 - /// 当 为 null 时抛出。 - /// 属性不存在时抛出。 - /// 属性不可读时抛出。 - public static object? Get(T data, string name) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - return Get(name).GetValue(data); - } -} diff --git a/src/LuYao.Common/Data/Meta/XProp.cs b/src/LuYao.Common/Data/Meta/XProp.cs deleted file mode 100644 index 25ca9f6..0000000 --- a/src/LuYao.Common/Data/Meta/XProp.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Reflection; - -namespace LuYao.Data.Meta; - - -/// -/// 封装某个类型的单个公共属性,提供快速的反射读写访问(通过编译的委托实现)。 -/// -public class XProp : IXProp -{ - private static readonly ConcurrentDictionary> _cache = - new ConcurrentDictionary>(); - - /// - /// 扫描指定类型的所有公共实例属性,并以缓存方式返回对应的 列表。 - /// - /// 要扫描的目标类型。 - /// 该类型所有公共实例属性对应的 只读列表。 - /// 当 为 null 时抛出。 - public static IReadOnlyList GetAll(Type type) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - return _cache.GetOrAdd(type, static t => - { - var props = t.GetProperties(BindingFlags.Public | BindingFlags.Instance); - var list = new List(props.Length); - foreach (var p in props) - { - if (p.GetIndexParameters().Length > 0) continue; // 跳过索引器属性 - list.Add(new XProp(p)); - } - return list.AsReadOnly(); - }); - } - - private readonly Func? _getter; - private readonly Action? _setter; - private readonly string _name; - private readonly Type _type; - - private XProp(PropertyInfo property) - { - _property = property; - _name = property.Name; - _type = property.PropertyType; - - if (property.CanRead && property.GetGetMethod(nonPublic: false) is { } getMethod) - { - // (object instance) => (object)((T)instance).Prop - var instance = System.Linq.Expressions.Expression.Parameter(typeof(object), "instance"); - var castInstance = System.Linq.Expressions.Expression.Convert(instance, property.DeclaringType!); - var propAccess = System.Linq.Expressions.Expression.Property(castInstance, property); - var castResult = System.Linq.Expressions.Expression.Convert(propAccess, typeof(object)); - _getter = System.Linq.Expressions.Expression.Lambda>(castResult, instance).Compile(); - } - - if (property.CanWrite && property.GetSetMethod(nonPublic: false) is { } setMethod) - { - // (object instance, object value) => ((T)instance).Prop = (TProp)value - var instance = System.Linq.Expressions.Expression.Parameter(typeof(object), "instance"); - var value = System.Linq.Expressions.Expression.Parameter(typeof(object), "value"); - var castInstance = System.Linq.Expressions.Expression.Convert(instance, property.DeclaringType!); - var castValue = System.Linq.Expressions.Expression.Convert(value, property.PropertyType); - var assign = System.Linq.Expressions.Expression.Call(castInstance, setMethod, castValue); - _setter = System.Linq.Expressions.Expression.Lambda>(assign, instance, value).Compile(); - } - } - - private readonly PropertyInfo _property; - - /// - /// 属性类型 - /// - public Type Type => _type; - - /// - /// 属性名称 - /// - public string Name => _name; - - /// - /// 可否读取 - /// - public bool CanRead => _getter is not null; - - /// - /// 可否写入 - /// - public bool CanWrite => _setter is not null; - - /// - /// 读取指定对象的属性值。 - /// - /// 属性所属的对象实例。 - /// 属性当前值。 - /// 属性不可读时抛出。 - public object? GetValue(object instance) - { - if (_getter is null) throw new InvalidOperationException($"属性 {Name} 不可读。"); - return _getter(instance); - } - - /// - /// 设置指定对象的属性值。 - /// - /// 属性所属的对象实例。 - /// 要写入的值。 - /// 属性不可写时抛出。 - public void SetValue(object instance, object? value) - { - if (_setter is null) throw new InvalidOperationException($"属性 {Name} 不可写。"); - _setter(instance, value); - } - - /// - /// 获取属性上标记的指定类型特性;若未标记则返回 。 - /// - /// 要查找的特性类型。 - /// 特性实例,或 。 - public T? GetCustomAttribute() where T : Attribute - => _property.GetCustomAttribute(inherit: true); -} diff --git a/src/LuYao.Common/Data/NameFilter.cs b/src/LuYao.Common/Data/NameFilter.cs deleted file mode 100644 index 3e5aae4..0000000 --- a/src/LuYao.Common/Data/NameFilter.cs +++ /dev/null @@ -1,106 +0,0 @@ -using LuYao.Data.Meta; -using System; -using System.Collections.Generic; -using System.Linq.Expressions; - -namespace LuYao.Data; - -/// -/// 针对类型 的属性名称过滤器,支持链式调用, -/// 用于声明式地选取或排除需要参与列映射的属性。 -/// -/// -/// 默认构造时包含类型 中所有可读且受支持的属性,顺序与属性声明顺序一致。 -/// 手动调用 时,列名按调用顺序追加; 返回结果与内部列表顺序完全一致,且不含重复项。 -/// -/// 提供属性来源的对象类型。 -public class NameFilter where T : class -{ - // 使用 List 保持插入顺序(用户手动 Include 的顺序即为最终列顺序),通过 Contains 检查保证无重复。 - private readonly List _names = new List(); - - /// - /// 初始化 的新实例,默认包含 的所有可读属性。 - /// - public NameFilter() - { - this.All(); - } - - /// - /// 重置为包含 中所有可读且受支持的属性,顺序按属性声明顺序。 - /// - /// 当前实例,支持链式调用。 - public NameFilter All() - { - _names.Clear(); - var props = XProp.GetAll(typeof(T)); - foreach (var p in props) - { - if (Helpers.IsSupportedForReading(p)) - _names.Add(p.Name); - } - return this; - } - - /// - /// 清空所有已选取的属性名。 - /// - /// 当前实例,支持链式调用。 - public NameFilter Clear() - { - _names.Clear(); - return this; - } - - /// - /// 将指定属性追加到选取列表末尾。若已存在则保持原有位置,不重复添加。 - /// - /// 指向目标属性的 Lambda 表达式,例如 x => x.Name。 - /// 当前实例,支持链式调用。 - /// 不是成员访问表达式。 - public NameFilter Include(Expression> expre) - { - var name = GetMemberName(expre); - if (!_names.Contains(name)) - _names.Add(name); - return this; - } - - /// - /// 将指定属性从选取列表中移除。若不存在则忽略。 - /// - /// 指向目标属性的 Lambda 表达式,例如 x => x.Email。 - /// 当前实例,支持链式调用。 - /// 不是成员访问表达式。 - public NameFilter Exclude(Expression> expre) - { - var name = GetMemberName(expre); - _names.Remove(name); - return this; - } - - /// - /// 将当前选取列表物化为属性名数组。 - /// 结果顺序与内部列表一致: 按属性声明顺序,手动 按调用顺序。 - /// 结果不含重复项。 - /// - /// 已选中属性名的有序、无重复数组。 - public string[] ToNames() - { - return _names.ToArray(); - } - - /// - /// 从成员访问 Lambda 表达式中提取属性名称。 - /// - private static string GetMemberName(Expression> expre) - { - Expression body = expre.Body; - if (body is UnaryExpression unary) - body = unary.Operand; - if (body is MemberExpression member) - return member.Member.Name; - throw new ArgumentException("Expression must be a member access.", nameof(expre)); - } -} diff --git a/src/LuYao.Common/Data/RecordColumn.cs b/src/LuYao.Common/Data/RecordColumn.cs index 70089ba..6298a78 100644 --- a/src/LuYao.Common/Data/RecordColumn.cs +++ b/src/LuYao.Common/Data/RecordColumn.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; using LuYao.Data.Internal; -using LuYao.Data.Meta; namespace LuYao.Data; /// /// 表示一个数据列。 /// -public abstract class RecordColumn : IXProp +public abstract class RecordColumn { /// /// 获取关联的记录实例。 @@ -30,7 +29,6 @@ internal RecordColumn(RecordTable table, string name, Type type) this.Name = name ?? throw new ArgumentNullException(nameof(name)); this._type = type ?? throw new ArgumentNullException(nameof(type)); this.ColumnType = Helpers.GetColumnType(type); - this.IsNullable = Helpers.IsNullableType(type); } private Type _type; /// @@ -46,14 +44,13 @@ internal RecordColumn(RecordTable table, string name, Type type) public Type Type => this._type; /// - /// 获取列的基础枚举类型标识(不含可空信息和数组维度)。 + /// 获取列的枚举类型标识。 /// public RecordColumnType ColumnType { get; } /// /// 获取列是否为可空类型。 /// - public bool IsNullable { get; } /// @@ -165,24 +162,5 @@ internal void OnGet(int row) } #endregion - #region IXProp 实现 +} - bool IXProp.CanRead => true; - bool IXProp.CanWrite => true; - - object? IXProp.GetValue(object instance) - { - if (instance is RecordRow row) return Get(row.Row); - throw new InvalidCastException(); - } - - void IXProp.SetValue(object instance, object? value) - { - if (instance is RecordRow row) - Set(row.Row, value); - else - throw new InvalidCastException(); - } - - #endregion -} \ No newline at end of file diff --git a/src/LuYao.Common/Data/RecordColumnCollection.cs b/src/LuYao.Common/Data/RecordColumnCollection.cs index 0f5d1ad..b4c088c 100644 --- a/src/LuYao.Common/Data/RecordColumnCollection.cs +++ b/src/LuYao.Common/Data/RecordColumnCollection.cs @@ -1,6 +1,4 @@ using LuYao.Collections.Generic; -using LuYao.Data.Mapping; -using LuYao.Data.Meta; using System; using System.Collections; using System.Collections.Generic; @@ -197,6 +195,22 @@ public RecordColumn Add(string name, Type type) return col; } + /// 根据封闭的 RecordColumnType 类型添加列。 + public RecordColumn Add(string name, RecordColumnType type) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name), "列名不能为空"); + var clrType = Helpers.GetClrType(type); + var existing = Find(name); + if (existing != null) + { + if (existing.ColumnType != type) throw new InvalidOperationException($"列 '{name}' 类型不匹配。"); + return existing; + } + var col = Helpers.MakeRecordColumn(Table, name, type, clrType); + _items.Add(col); + return col; + } + /// /// 添加指定泛型类型的列。 /// @@ -224,83 +238,6 @@ public RecordColumn Add(string name) #endregion - #region AddFrom - - /// - /// 按照类型 的可读属性向当前集合追加对应的列定义。 - /// - /// 提供列定义的对象类型。 - public void AddFrom() where T : class - { - AddFrom(RecordMappingOptions.Default); - } - - /// - /// 按照类型 的可读属性向当前集合追加对应的列定义,使用指定的映射选项。 - /// - /// 提供列定义的对象类型。 - /// 映射选项,不可为 null。 - /// 为 null 时抛出。 - /// - /// 的 为 - /// 且存在不受支持的属性类型时抛出。 - /// - public void AddFrom(RecordMappingOptions options) where T : class - { - if (options == null) throw new ArgumentNullException(nameof(options)); - new RecordMappingContext(options).AddColumnsFrom(this); - } - - /// - /// 根据对象实例(例如匿名类型实例)的可读属性向当前集合追加对应的列定义。 - /// - /// 提供列定义的对象类型。 - /// 用于推断列结构的对象实例;仅使用其编译时类型信息,不读取其属性值。 - /// 为 。 - public void AddFrom(T template) where T : class - { - if (template == null) throw new ArgumentNullException(nameof(template)); - this.AddFrom(); - } - - /// - /// 按照指定的属性名列表向当前集合追加列定义,列的追加顺序与 的顺序一致。 - /// 不在类型 中或不受支持的属性名将被忽略。 - /// 若 为 或空数组,则不追加任何列。 - /// - /// 提供列类型信息的对象类型。 - /// 要追加的属性名数组,列将按此顺序添加。 - public void AddFrom(string[] names) where T : class - { - if (names == null || names.Length == 0) return; - var propMap = new Dictionary(StringComparer.Ordinal); - foreach (var p in XProp.GetAll(typeof(T))) - { - if (Helpers.IsSupportedForReading(p)) - propMap[p.Name] = p; - } - foreach (var name in names) - { - if (propMap.TryGetValue(name, out var prop)) - this.Add(prop.Name, prop.Type); - } - } - - /// - /// 通过 的链式配置向当前集合追加列定义。 - /// 列的追加顺序与 中配置的属性选取顺序一致。 - /// - /// 提供列定义的对象类型。 - /// 用于配置属性过滤规则的委托,接收一个 实例。 - public void AddFrom(Action> filter) where T : class - { - var arg = new NameFilter(); - filter(arg); - this.AddFrom(arg.ToNames()); - } - - #endregion - /// /// 重命名指定列。 /// @@ -334,4 +271,4 @@ internal void ReplaceAt(int index, RecordColumn column) public IEnumerator GetEnumerator() => _items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); -} \ No newline at end of file +} diff --git a/src/LuYao.Common/Data/RecordColumnStorageTarget.cs b/src/LuYao.Common/Data/RecordColumnStorageTarget.cs deleted file mode 100644 index 13eee17..0000000 --- a/src/LuYao.Common/Data/RecordColumnStorageTarget.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace LuYao.Data; - -/// -/// 指定 DTO 属性在 中的列存储类型。 -/// 当属性类型不是 RecordTable 原生支持的类型时,由此决定如何存储。 -/// -public enum RecordColumnStorageTarget -{ - /// - /// 跳过该属性,不在 RecordTable 中创建对应的列。 - /// - Skip = 0 -} diff --git a/src/LuYao.Common/Data/RecordColumnType.cs b/src/LuYao.Common/Data/RecordColumnType.cs index 1544ded..6d85fa7 100644 --- a/src/LuYao.Common/Data/RecordColumnType.cs +++ b/src/LuYao.Common/Data/RecordColumnType.cs @@ -2,7 +2,7 @@ namespace LuYao.Data; /// /// 定义 支持的所有基础列数据类型。 -/// 白名单封闭,不可外部扩展。可空性通过 独立表达。 +/// 白名单封闭,不可外部扩展。除 Binary 对应的 byte[] 外,不支持数组、Nullable 和枚举。 /// public enum RecordColumnType : byte { diff --git a/src/LuYao.Common/Data/RecordRow.cs b/src/LuYao.Common/Data/RecordRow.cs index a8a6b4f..a201235 100644 --- a/src/LuYao.Common/Data/RecordRow.cs +++ b/src/LuYao.Common/Data/RecordRow.cs @@ -8,7 +8,7 @@ namespace LuYao.Data; /// /// 代表一行数据,提供对列存储数据集合中特定行数据的访问。 /// -public partial struct RecordRow : IDynamicMetaObjectProvider +public partial struct RecordRow { /// /// 初始化 结构体的新实例。 @@ -36,12 +36,6 @@ internal RecordRow(RecordTable table, int row) /// 从零开始的行索引。 public int Row { get; } - /// - /// 返回 ,支持动态成员访问。 - /// - public readonly DynamicMetaObject GetMetaObject(System.Linq.Expressions.Expression parameter) - => new RecordRowMetaObject(parameter, this); - /// /// 定义从 到 的隐式转换。 /// 返回行的索引位置。 @@ -139,3 +133,4 @@ public readonly String ToString(string? name) return col.ToString(this.Row); } } + diff --git a/src/LuYao.Common/Data/RecordRow/DynamicMetaObject.cs b/src/LuYao.Common/Data/RecordRow/DynamicMetaObject.cs deleted file mode 100644 index e28a9b6..0000000 --- a/src/LuYao.Common/Data/RecordRow/DynamicMetaObject.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Dynamic; -using System.Linq; -using System.Linq.Expressions; - -namespace LuYao.Data; - -partial struct RecordRow -{ - /// - /// 为 提供动态成员绑定支持。 - /// - private sealed class RecordRowMetaObject : DynamicMetaObject - { - // dynamic 读取/写入走 this[string] 索引器 - private static readonly System.Reflection.PropertyInfo IndexerProperty = - typeof(RecordRow).GetProperty("Item", new[] { typeof(string) })!; - - public RecordRowMetaObject(Expression expression, RecordRow value) - : base(expression, BindingRestrictions.Empty, value) - { - } - - private Expression GetLimitedSelf() - => Expression.Convert(Expression, typeof(RecordRow)); - - /// - public override System.Collections.Generic.IEnumerable GetDynamicMemberNames() - => ((RecordRow)Value!).Table.Columns.Select(c => c.Name); - - /// - public override DynamicMetaObject BindGetMember(GetMemberBinder binder) - { - var restrictions = BindingRestrictions.GetTypeRestriction(Expression, typeof(RecordRow)); - var call = Expression.MakeIndex(GetLimitedSelf(), IndexerProperty, new[] { Expression.Constant(binder.Name) }); - return new DynamicMetaObject(call, restrictions); - } - - /// - public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value) - { - var restrictions = BindingRestrictions.GetTypeRestriction(Expression, typeof(RecordRow)); - var indexAccess = Expression.MakeIndex(GetLimitedSelf(), IndexerProperty, new[] { Expression.Constant(binder.Name) }); - var assign = Expression.Assign(indexAccess, Expression.Convert(value.Expression, typeof(object))); - return new DynamicMetaObject(assign, restrictions); - } - - /// - public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes) - { - if (indexes.Length != 1 || indexes[0].LimitType != typeof(string)) - return base.BindGetIndex(binder, indexes); - var restrictions = BindingRestrictions.GetTypeRestriction(Expression, typeof(RecordRow)); - var key = Expression.Convert(indexes[0].Expression, typeof(string)); - var call = Expression.MakeIndex(GetLimitedSelf(), IndexerProperty, new[] { key }); - return new DynamicMetaObject(call, restrictions); - } - - /// - public override DynamicMetaObject BindSetIndex(SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value) - { - if (indexes.Length != 1 || indexes[0].LimitType != typeof(string)) - return base.BindSetIndex(binder, indexes, value); - var restrictions = BindingRestrictions.GetTypeRestriction(Expression, typeof(RecordRow)); - var key = Expression.Convert(indexes[0].Expression, typeof(string)); - var indexAccess = Expression.MakeIndex(GetLimitedSelf(), IndexerProperty, new[] { key }); - var assign = Expression.Assign(indexAccess, Expression.Convert(value.Expression, typeof(object))); - return new DynamicMetaObject(assign, restrictions); - } - } -} diff --git a/src/LuYao.Common/Data/RecordRow/Mapping.cs b/src/LuYao.Common/Data/RecordRow/Mapping.cs deleted file mode 100644 index a8b11a5..0000000 --- a/src/LuYao.Common/Data/RecordRow/Mapping.cs +++ /dev/null @@ -1,74 +0,0 @@ -using LuYao.Data.Mapping; -using LuYao.Data.Meta; -using System; - -namespace LuYao.Data; - -partial struct RecordRow -{ - /// - /// Maps column values from the current row into the corresponding properties of - /// . Uses the runtime type; derived-class properties are handled correctly. - /// - /// The object instance to populate; must not be null. - /// Thrown when is null. - public void MapTo(object data) - { - XCopy.MapFrom(data, this); - } - - /// - /// Maps column values from the current row into the corresponding properties of - /// using the specified options. - /// - /// The object instance to populate; must not be null. - /// Mapping options; must not be null. - public void MapTo(object data, RecordMappingOptions options) - { - XCopy.MapFrom(data, this, options); - } - - /// - /// Maps readable properties of into the corresponding columns of - /// the current row. Uses the runtime type; derived-class properties are handled correctly. - /// - /// The source object; must not be null. - /// Thrown when is null. - public void MapFrom(object data) => XCopy.MapTo(data, this); - - /// - /// Maps readable properties of into the corresponding columns of - /// the current row using the specified options. - /// - /// The source object; must not be null. - /// Mapping options; must not be null. - public void MapFrom(object data, RecordMappingOptions options) => XCopy.MapTo(data, this, options); - - /// - /// Creates a new instance and populates its properties - /// from the current row's column values. - /// - /// Target object type; must have a parameterless constructor. - /// A new object instance populated with column values. - public T To() where T : class, new() - { - var ret = new T(); - this.MapTo(ret); - return ret; - } - - /// - /// Creates a new instance and populates its properties - /// from the current row's column values using the specified options. - /// - /// Target object type; must have a parameterless constructor. - /// Mapping options; must not be null. - /// A new object instance populated with column values. - public T To(RecordMappingOptions options) where T : class, new() - { - var ret = new T(); - this.MapTo(ret, options); - return ret; - } -} - diff --git a/src/LuYao.Common/Data/RecordRow/Merge.cs b/src/LuYao.Common/Data/RecordRow/Merge.cs deleted file mode 100644 index e6b720e..0000000 --- a/src/LuYao.Common/Data/RecordRow/Merge.cs +++ /dev/null @@ -1,52 +0,0 @@ -using LuYao.Data.Mapping; -using LuYao.Data.Meta; -using System; - -namespace LuYao.Data; - -partial struct RecordRow -{ - /// - /// 将另一行 的数据合并到当前行:同名列直接覆盖,当前行不存在的列则追加。 - /// - /// 数据来源行,可来自不同的 。 - public void Merge(RecordRow other) - { - foreach (var srcCol in other.Table.Columns) - { - var value = srcCol.Get(other); - var dstCol = this.Table.Columns.Find(srcCol.Name); - if (dstCol == null) - { - dstCol = this.Table.Columns.Add(srcCol.Name, srcCol.Type); - } - dstCol.Set(this, value); - } - } - - /// - /// 将对象 的可读属性合并到当前行:同名列直接覆盖,当前行不存在的列则追加。 - /// 使用运行时实际类型,派生类新增属性会被正确处理。 - /// - /// 属性值的来源对象,不可为 null。 - /// 当 为 null 时抛出。 - public void Merge(object model) - { - if (model == null) throw new ArgumentNullException(nameof(model)); - Merge(model, RecordMappingOptions.Default); - } - - /// - /// 将对象 的可读属性合并到当前行:同名列直接覆盖,当前行不存在的列则追加。 - /// 使用运行时实际类型,派生类新增属性会被正确处理。 - /// - /// 属性值的来源对象,不可为 null。 - /// 映射选项,不可为 null。 - /// 任一参数为 null 时抛出。 - public void Merge(object model, RecordMappingOptions options) - { - if (model == null) throw new ArgumentNullException(nameof(model)); - if (options == null) throw new ArgumentNullException(nameof(options)); - new RecordMappingContext(options).WriteDtoToRow(model.GetType(), model, this); - } -} diff --git a/src/LuYao.Common/Data/RecordSchema.cs b/src/LuYao.Common/Data/RecordSchema.cs index 241d039..f1c97ff 100644 --- a/src/LuYao.Common/Data/RecordSchema.cs +++ b/src/LuYao.Common/Data/RecordSchema.cs @@ -4,7 +4,7 @@ namespace LuYao.Data; /// -/// 表示 Record 的列定义信息(列名 + 类型),用于序列化、传输或 Schema 比较。 +/// 表示 Record 的列定义信息(列名 + 类型),用于 Schema 比较。 /// public sealed class RecordSchema { @@ -31,25 +31,19 @@ public sealed class ColumnDef public string Name { get; } /// - /// 列的基础枚举类型标识(不含可空信息和数组维度)。 + /// 列的枚举类型标识。 /// public RecordColumnType ColumnType { get; } - /// - /// 列是否为可空类型。 - /// - public bool IsNullable { get; } - /// /// 列的 CLR 数据类型。 /// - public Type Type => Helpers.GetClrType(ColumnType, IsNullable); + public Type Type => Helpers.GetClrType(ColumnType); - internal ColumnDef(string name, RecordColumnType columnType, bool isNullable) + internal ColumnDef(string name, RecordColumnType columnType) { Name = name; ColumnType = columnType; - IsNullable = isNullable; } } } diff --git a/src/LuYao.Common/Data/RecordSet/Binary.cs b/src/LuYao.Common/Data/RecordSet/Binary.cs deleted file mode 100644 index da239f2..0000000 --- a/src/LuYao.Common/Data/RecordSet/Binary.cs +++ /dev/null @@ -1,118 +0,0 @@ -using LuYao.Data.Binary; -using System; -using System.IO; -using System.Text; - -namespace LuYao.Data; - -public partial class RecordSet -{ - private const byte BinaryFormatVersion = 1; - - /// - /// 将当前 写入二进制流。 - /// - /// 目标流。 - public void WriteTo(Stream stream) - { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); - WriteTo(writer); - } - - /// - /// 将当前 写入 。 - /// - /// 目标写入器。 - public void WriteTo(BinaryWriter writer) - { - if (writer == null) throw new ArgumentNullException(nameof(writer)); - BinaryPayloadHeader.Write(writer, BinaryPayloadType.RecordSet); - writer.Write(BinaryFormatVersion); - writer.Write(_records.Count); - foreach (var kvp in _records) - { - // 以字典键作为权威名称,防止 Record.Name 被外部修改后产生漂移 - kvp.Value.Name = kvp.Key; - kvp.Value.WriteTo(writer); - } - } - - /// - /// 从二进制流读取并填充当前 实例。 - /// - /// 源流。 - public void ReadFrom(Stream stream) - { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); - ReadFrom(reader); - } - - /// - /// 从 读取并填充当前 实例。 - /// - /// 源读取器。 - public void ReadFrom(BinaryReader reader) - { - if (reader == null) throw new ArgumentNullException(nameof(reader)); - byte version = BinaryPayloadHeader.ReadHeaderAndVersion(reader, BinaryPayloadType.RecordSet); - if (version != BinaryFormatVersion) - throw new InvalidOperationException($"不支持的二进制格式版本: {version}"); - - this.Clear(); - int count = reader.ReadInt32(); - for (int i = 0; i < count; i++) - { - var table = new RecordTable(); - table.ReadFrom(reader); - this.Add(table.Name, table); - } - } - - /// - /// 从二进制流创建新的 实例。 - /// - /// 源流。 - /// 反序列化的 实例。 - public static RecordSet FromStream(Stream stream) - { - var set = new RecordSet(); - set.ReadFrom(stream); - return set; - } - - /// - /// 序列化为字节数组。 - /// - public byte[] ToBytes() - { - using var ms = new MemoryStream(); - WriteTo(ms); - return ms.ToArray(); - } - - /// - /// 从字节数组反序列化。 - /// - /// 二进制数据。 - /// 反序列化的 实例。 - public static RecordSet FromBytes(byte[] data) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - using var ms = new MemoryStream(data, writable: false); - return FromStream(ms); - } - - /// - /// 检测二进制数据是否为带类型头的 。 - /// - /// 二进制数据。 - /// 当数据包含 类型头时返回 true;否则返回 false。 - public static bool IsBinaryPayload(byte[] data) - { - if (data == null) return false; - return BinaryPayloadHeader.TryGetPayloadType(data, out var payloadType) - && payloadType == BinaryPayloadType.RecordSet; - } -} diff --git a/src/LuYao.Common/Data/RecordTable.cs b/src/LuYao.Common/Data/RecordTable.cs index 5183489..a63d019 100644 --- a/src/LuYao.Common/Data/RecordTable.cs +++ b/src/LuYao.Common/Data/RecordTable.cs @@ -530,7 +530,7 @@ public RecordSchema GetSchema() var columns = new List(this.Columns.Count); foreach (RecordColumn col in this.Columns) { - columns.Add(new RecordSchema.ColumnDef(col.Name, col.ColumnType, col.IsNullable)); + columns.Add(new RecordSchema.ColumnDef(col.Name, col.ColumnType)); } return new RecordSchema(columns); } diff --git a/src/LuYao.Common/Data/RecordTable/Binary.cs b/src/LuYao.Common/Data/RecordTable/Binary.cs deleted file mode 100644 index 42e7d94..0000000 --- a/src/LuYao.Common/Data/RecordTable/Binary.cs +++ /dev/null @@ -1,295 +0,0 @@ -using LuYao.Data.Binary; -using System; -using System.IO; -using System.Text; - -namespace LuYao.Data; - -public partial class RecordTable -{ - // 二进制格式版本号,用于未来兼容性检查 - private const byte BinaryFormatVersion = 2; - - /// - /// 将当前 写入二进制流。 - /// - /// 目标流。 - /// 当 为 null 时抛出。 - public void WriteTo(Stream stream) - { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); - WriteTo(writer); - } - - /// - /// 将当前 写入 。 - /// - /// 目标写入器。 - public void WriteTo(BinaryWriter writer) - { - if (writer == null) throw new ArgumentNullException(nameof(writer)); - - // Header - BinaryPayloadHeader.Write(writer, BinaryPayloadType.RecordTable); - writer.Write(BinaryFormatVersion); - writer.Write(this.Name ?? string.Empty); - writer.Write(this.Page); - writer.Write(this._pageSize); - writer.Write(this._maxCount); - - // Schema: column count + (name, type, isNullable) per column - writer.Write(this.Columns.Count); - for (int c = 0; c < this.Columns.Count; c++) - { - var col = this.Columns[c]; - writer.Write(col.Name); - writer.Write((byte)col.ColumnType); - writer.Write(col.IsNullable); - } - - // Row count - int rowCount = this.Count; - writer.Write(rowCount); - - // Column data: each column writes its data array sequentially - for (int c = 0; c < this.Columns.Count; c++) - { - WriteColumnData(writer, this.Columns[c], rowCount); - } - } - - /// - /// 从二进制流读取并填充当前 实例。 - /// - /// 源流。 - /// 当 为 null 时抛出。 - public void ReadFrom(Stream stream) - { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); - ReadFrom(reader); - } - - /// - /// 从 读取并填充当前 实例。 - /// - /// 源读取器。 - public void ReadFrom(BinaryReader reader) - { - if (reader == null) throw new ArgumentNullException(nameof(reader)); - - // Header - byte version = BinaryPayloadHeader.ReadHeaderAndVersion(reader, BinaryPayloadType.RecordTable); - if (version != BinaryFormatVersion) - throw new InvalidOperationException($"不支持的二进制格式版本: {version}"); - - this.Name = reader.ReadString(); - this.Page = reader.ReadInt32(); - this._pageSize = reader.ReadInt32(); - this._maxCount = reader.ReadInt32(); - - // Schema - this.Columns.Clear(); - int colCount = reader.ReadInt32(); - for (int c = 0; c < colCount; c++) - { - string name = reader.ReadString(); - var columnType = (RecordColumnType)reader.ReadByte(); - bool isNullable = reader.ReadBoolean(); - Type clrType = Helpers.GetClrType(columnType, isNullable); - this.Columns.Add(name, clrType); - } - - // Row count - int rowCount = reader.ReadInt32(); - - // 预分配行 - for (int r = 0; r < rowCount; r++) - { - this.AddRow(); - } - - // Column data - for (int c = 0; c < colCount; c++) - { - ReadColumnData(reader, this.Columns[c], rowCount); - } - } - - /// - /// 从二进制流创建新的 实例。 - /// - /// 源流。 - /// 反序列化的 实例。 - public static RecordTable FromStream(Stream stream) - { - var table = new RecordTable(); - table.ReadFrom(stream); - return table; - } - - /// - /// 序列化为字节数组。 - /// - /// 二进制表示。 - public byte[] ToBytes() - { - using var ms = new MemoryStream(); - WriteTo(ms); - return ms.ToArray(); - } - - /// - /// 从字节数组反序列化。 - /// - /// 二进制数据。 - /// 反序列化的 实例。 - public static RecordTable FromBytes(byte[] data) - { - if (data == null) throw new ArgumentNullException(nameof(data)); - using var ms = new MemoryStream(data, writable: false); - return FromStream(ms); - } - - /// - /// 检测二进制数据是否为带类型头的 。 - /// - /// 二进制数据。 - /// 当数据包含 类型头时返回 true;否则返回 false。 - public static bool IsBinaryPayload(byte[] data) - { - if (data == null) return false; - return BinaryPayloadHeader.TryGetPayloadType(data, out var payloadType) - && payloadType == BinaryPayloadType.RecordTable; - } - - #region 列数据序列化 - - private static void WriteColumnData(BinaryWriter writer, RecordColumn col, int rowCount) - { - bool isBinary = col.ColumnType == RecordColumnType.Binary; - bool needsNullCheck = col.IsNullable || col.ColumnType == RecordColumnType.String || isBinary; - - for (int r = 0; r < rowCount; r++) - { - var val = col.Get(r); - if (needsNullCheck) - { - if (val is null) - { - writer.Write(false); - continue; - } - writer.Write(true); - } - - if (isBinary) - { - var bytes = (byte[])val!; - writer.Write(bytes.Length); - writer.Write(bytes); - } - else - { - WritePrimitiveValue(writer, val!, col.ColumnType); - } - } - } - - private static void ReadColumnData(BinaryReader reader, RecordColumn col, int rowCount) - { - bool isBinary = col.ColumnType == RecordColumnType.Binary; - bool needsNullCheck = col.IsNullable || col.ColumnType == RecordColumnType.String || isBinary; - - for (int r = 0; r < rowCount; r++) - { - if (needsNullCheck) - { - bool hasValue = reader.ReadBoolean(); - if (!hasValue) continue; - } - - object val; - if (isBinary) - { - int len = reader.ReadInt32(); - val = reader.ReadBytes(len); - } - else - { - val = ReadPrimitiveValue(reader, col.ColumnType); - } - col.Set(r, val); - } - } - - - private static void WritePrimitiveValue(BinaryWriter writer, object value, RecordColumnType columnType) - { - if (value.GetType().IsEnum) - { - value = Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType())); - } - - switch (columnType) - { - case RecordColumnType.Boolean: writer.Write((bool)value); break; - case RecordColumnType.SByte: writer.Write((sbyte)value); break; - case RecordColumnType.Int16: writer.Write((short)value); break; - case RecordColumnType.Int32: writer.Write((int)value); break; - case RecordColumnType.Int64: writer.Write((long)value); break; - case RecordColumnType.Byte: writer.Write((byte)value); break; - case RecordColumnType.UInt16: writer.Write((ushort)value); break; - case RecordColumnType.UInt32: writer.Write((uint)value); break; - case RecordColumnType.UInt64: writer.Write((ulong)value); break; - case RecordColumnType.Single: writer.Write((float)value); break; - case RecordColumnType.Double: writer.Write((double)value); break; - case RecordColumnType.Decimal: writer.Write((decimal)value); break; - case RecordColumnType.Char: writer.Write((char)value); break; - case RecordColumnType.String: writer.Write((string)value); break; - case RecordColumnType.DateTime: writer.Write(((DateTime)value).ToBinary()); break; - case RecordColumnType.DateTimeOffset: - var dto = (DateTimeOffset)value; - writer.Write(dto.Ticks); - writer.Write((short)dto.Offset.TotalMinutes); - break; - case RecordColumnType.TimeSpan: writer.Write(((TimeSpan)value).Ticks); break; - case RecordColumnType.Guid: writer.Write(((Guid)value).ToByteArray()); break; - default: - throw new NotSupportedException($"不支持的列类型: {columnType}"); - } - } - - private static object ReadPrimitiveValue(BinaryReader reader, RecordColumnType columnType) - { - switch (columnType) - { - case RecordColumnType.Boolean: return reader.ReadBoolean(); - case RecordColumnType.SByte: return reader.ReadSByte(); - case RecordColumnType.Int16: return reader.ReadInt16(); - case RecordColumnType.Int32: return reader.ReadInt32(); - case RecordColumnType.Int64: return reader.ReadInt64(); - case RecordColumnType.Byte: return reader.ReadByte(); - case RecordColumnType.UInt16: return reader.ReadUInt16(); - case RecordColumnType.UInt32: return reader.ReadUInt32(); - case RecordColumnType.UInt64: return reader.ReadUInt64(); - case RecordColumnType.Single: return reader.ReadSingle(); - case RecordColumnType.Double: return reader.ReadDouble(); - case RecordColumnType.Decimal: return reader.ReadDecimal(); - case RecordColumnType.Char: return reader.ReadChar(); - case RecordColumnType.String: return reader.ReadString(); - case RecordColumnType.DateTime: return DateTime.FromBinary(reader.ReadInt64()); - case RecordColumnType.DateTimeOffset: - long ticks = reader.ReadInt64(); - short offsetMinutes = reader.ReadInt16(); - return new DateTimeOffset(ticks, TimeSpan.FromMinutes(offsetMinutes)); - case RecordColumnType.TimeSpan: return new TimeSpan(reader.ReadInt64()); - case RecordColumnType.Guid: return new Guid(reader.ReadBytes(16)); - default: - throw new NotSupportedException($"不支持的列类型: {columnType}"); - } - } - - #endregion -} diff --git a/src/LuYao.Common/Data/RecordTable/Mapping.cs b/src/LuYao.Common/Data/RecordTable/Mapping.cs deleted file mode 100644 index f30b583..0000000 --- a/src/LuYao.Common/Data/RecordTable/Mapping.cs +++ /dev/null @@ -1,246 +0,0 @@ -using LuYao.Data.Mapping; -using LuYao.Data.Meta; -using System; -using System.Collections.Generic; - -namespace LuYao.Data; - -partial class RecordTable -{ - /// - /// Creates a from a single object, inferring the column structure - /// automatically and writing one row of data. - /// - /// The source object type. - /// The object used to infer columns and populate the row. - /// A new containing one row. - public static RecordTable From(T data) where T : class - { - var re = new RecordTable(); - re.Columns.AddFrom(); - re.AddRowFrom(data); - return re; - } - - /// - /// Creates a from a single object using the specified mapping options, - /// inferring the column structure automatically and writing one row of data. - /// - public static RecordTable From(T data, RecordMappingOptions options) where T : class - { - var re = new RecordTable(); - re.Columns.AddFrom(options); - re.AddRowFrom(data, options); - return re; - } - - /// - /// Creates a from a single object, inferring the column structure - /// automatically, writing one row, and returning a reference to that row. - /// - /// The source object type. - /// The object used to infer columns and populate the row. - /// Returns a reference to the newly added row. - /// A new containing one row. - public static RecordTable From(T data, out RecordRow row) where T : class - { - var re = new RecordTable(); - re.Columns.AddFrom(); - row = re.AddRowFrom(data); - return re; - } - - /// - /// Creates a from a single object using the specified mapping options, - /// inferring the column structure automatically, writing one row, and returning a reference to that row. - /// - /// The source object type. - /// The object used to infer columns and populate the row. - /// Mapping options; must not be null. - /// Returns a reference to the newly added row. - /// A new containing one row. - public static RecordTable From(T data, RecordMappingOptions options, out RecordRow row) where T : class - { - var re = new RecordTable(); - re.Columns.AddFrom(options); - row = re.AddRowFrom(data, options); - return re; - } - - /// - /// Converts the first row of the current to a object. - /// If the table has no rows, a default instance created by the parameterless constructor is returned. - /// - /// Target object type; must have a parameterless constructor. - /// The converted object instance. - public T To() where T : class, new() - { - var ret = new T(); - if (this.Count > 0) - { - XCopy.MapFrom(ret, this[0]); - } - return ret; - } - - /// - /// Converts the first row of the current to a object - /// using the specified mapping options. - /// - public T To(RecordMappingOptions options) where T : class, new() - { - var ret = new T(); - if (this.Count > 0) - { - XCopy.MapFrom(ret, this[0], options); - } - return ret; - } - - /// - /// Creates a from a collection of objects, inferring the column - /// structure automatically and writing each object as one row. - /// - /// The collection element type. - /// The collection of objects used to populate the rows. - /// A new with the same number of rows as elements in the collection. - public static RecordTable FromList(IEnumerable items) where T : class - { - var re = new RecordTable(); - re.Columns.AddFrom(); - re.AddRowsFromList(items); - return re; - } - - /// - /// Creates a from a collection of objects using the specified mapping options, - /// inferring the column structure automatically and writing each object as one row. - /// - public static RecordTable FromList(IEnumerable items, RecordMappingOptions options) where T : class - { - var re = new RecordTable(); - re.Columns.AddFrom(options); - re.AddRowsFromList(items, options); - return re; - } - - /// - /// Appends a new row to the current and writes the properties of - /// into it. - /// - /// The source object type. - /// The object to append; must not be . - /// Thrown when is . - public RecordRow AddRowFrom(T item) where T : class - { - if (item == null) throw new ArgumentNullException(nameof(item)); - var ret = this.AddRow(); - ret.MapFrom(item); - return ret; - } - - /// - /// Appends a new row to the current and writes the properties of - /// into it using the specified mapping options. - /// - public RecordRow AddRowFrom(T item, RecordMappingOptions options) where T : class - { - if (item == null) throw new ArgumentNullException(nameof(item)); - var ret = this.AddRow(); - ret.MapFrom(item, options); - return ret; - } - - /// - /// Appends one row per element in to the current . - /// - /// The collection element type. - /// The objects to append. - public void AddRowsFromList(IEnumerable items) where T : class - { - foreach (var item in items) this.AddRowFrom(item); - } - - /// - /// Appends one row per element in to the current - /// using the specified mapping options. - /// - public void AddRowsFromList(IEnumerable items, RecordMappingOptions options) where T : class - { - foreach (var item in items) this.AddRowFrom(item, options); - } - - /// - /// Converts all rows in the current to a list of objects. - /// - /// Target object type; must have a parameterless constructor. - /// A list with the same number of elements as rows. - public List ToList() where T : class, new() - { - var list = new List(); - foreach (var row in this) - { - var item = row.To(); - list.Add(item); - } - return list; - } - - /// - /// Converts all rows in the current to a list of objects - /// using the specified mapping options. - /// - public List ToList(RecordMappingOptions options) where T : class, new() - { - var list = new List(); - foreach (var row in this) - { - var item = row.To(options); - list.Add(item); - } - return list; - } - - /// - /// Converts the current to a dictionary keyed by the first column. - /// Returns an empty dictionary when there are no rows or no columns. - /// Duplicate keys are overwritten by the later occurrence. - /// - /// Key type; corresponds to the value type of the first column. - /// Value type; must have a parameterless constructor. - /// A dictionary keyed by the first column's value with row objects as values. - public Dictionary ToDictionary() - where TKey : notnull - where T : class, new() - { - var dict = new Dictionary(); - if (this.Count == 0 || this.Columns.Count == 0) return dict; - var keyColumn = this.Columns[0]; - foreach (var row in this) - { - var key = keyColumn.To(row.Row)!; - dict[key] = row.To(); - } - return dict; - } - - /// - /// Converts the current to a dictionary keyed by the first column, - /// using the specified mapping options. - /// - public Dictionary ToDictionary(RecordMappingOptions options) - where TKey : notnull - where T : class, new() - { - var dict = new Dictionary(); - if (this.Count == 0 || this.Columns.Count == 0) return dict; - var keyColumn = this.Columns[0]; - foreach (var row in this) - { - var key = keyColumn.To(row.Row)!; - dict[key] = row.To(options); - } - return dict; - } -} - diff --git a/src/LuYao.Common/Data/RecordTable/Query.cs b/src/LuYao.Common/Data/RecordTable/Query.cs index ff8a3bc..b724001 100644 --- a/src/LuYao.Common/Data/RecordTable/Query.cs +++ b/src/LuYao.Common/Data/RecordTable/Query.cs @@ -114,40 +114,6 @@ public IEnumerable FindAll(Func filter) } } - /// - /// 使用基于 dynamic 的动态类型筛选器,查找符合要求的第一条行数据。 - /// (依赖 对 的实现) - /// - /// 用于动态筛选数据行的条件委托。 - /// 符合条件的第一条 ,未找到则返回 null。 - /// 当 为 null 时抛出。 - public RecordRow? FindByDynamic(Func filter) - { - if (filter == null) throw new ArgumentNullException(nameof(filter)); - for (int i = 0; i < this.Count; i++) - { - var row = new RecordRow(this, i); - if (filter(row)) return row; - } - return null; - } - - /// - /// 使用基于 dynamic 的动态类型筛选器,查找所有符合要求的行数据。 - /// - /// 用于动态筛选数据行的条件委托。 - /// 所有符合条件的 数据流序列。 - /// 当 为 null 时抛出。 - public IEnumerable FindAllByDynamic(Func filter) - { - if (filter == null) throw new ArgumentNullException(nameof(filter)); - for (int i = 0; i < this.Count; i++) - { - var row = new RecordRow(this, i); - if (filter(row)) yield return row; - } - } - /// /// 将指定列的所有数据转换为 ,顺序与行索引一致。 /// 原值为 null 时对应元素为 的默认值。 diff --git a/src/LuYao.Common/Data/UnsupportedTypeHandling.cs b/src/LuYao.Common/Data/UnsupportedTypeHandling.cs deleted file mode 100644 index bda389f..0000000 --- a/src/LuYao.Common/Data/UnsupportedTypeHandling.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace LuYao.Data; - -/// -/// 在 DTO 到 RecordTable 映射时,遇到不支持的属性类型的处理策略。 -/// -public enum UnsupportedTypeHandling -{ - /// 跳过不支持的属性,不创建对应列(默认)。 - Skip = 0, - - /// 抛出 。 - Throw = 1, -} diff --git a/tests/LuYao.Common.UnitTests/Data/Expression/XPropTests.cs b/tests/LuYao.Common.UnitTests/Data/Expression/XPropTests.cs deleted file mode 100644 index 3d552c6..0000000 --- a/tests/LuYao.Common.UnitTests/Data/Expression/XPropTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -using LuYao.Data.Meta; - -namespace LuYao.Data.Meta; - -[TestClass] -public class XPropTests -{ - // ── 测试用模型 ────────────────────────────────────────────────────────── - private class SampleModel - { - public int Id { get; set; } - public string? Name { get; set; } - public int ReadOnly { get; } = 42; - private string? Hidden { get; set; } - } - - private class NoProps { } - - // ── GetAll ─────────────────────────────────────────────────────────────── - - [TestMethod] - public void GetAll_NullType_ThrowsArgumentNullException() - { - Assert.Throws(() => XProp.GetAll(null!)); - } - - [TestMethod] - public void GetAll_TypeWithNoProps_ReturnsEmptyList() - { - var props = XProp.GetAll(typeof(NoProps)); - Assert.AreEqual(0, props.Count); - } - - [TestMethod] - public void GetAll_SampleModel_ReturnsOnlyPublicInstanceProps() - { - var props = XProp.GetAll(typeof(SampleModel)); - // Id, Name, ReadOnly — Hidden は private なので含まれない - Assert.AreEqual(3, props.Count); - } - - [TestMethod] - public void GetAll_CalledTwice_ReturnsSameInstance() - { - var first = XProp.GetAll(typeof(SampleModel)); - var second = XProp.GetAll(typeof(SampleModel)); - Assert.AreSame(first, second); - } - - // ── Name / Type ────────────────────────────────────────────────────────── - - [TestMethod] - public void Name_ReturnsPropertyName() - { - var prop = FindProp(nameof(SampleModel.Id)); - Assert.AreEqual("Id", prop.Name); - } - - [TestMethod] - public void Type_ReturnsPropertyType() - { - var prop = FindProp(nameof(SampleModel.Id)); - Assert.AreEqual(typeof(int), prop.Type); - } - - // ── CanRead / CanWrite ─────────────────────────────────────────────────── - - [TestMethod] - public void CanRead_ReadWriteProp_IsTrue() - { - Assert.IsTrue(FindProp(nameof(SampleModel.Id)).CanRead); - } - - [TestMethod] - public void CanWrite_ReadWriteProp_IsTrue() - { - Assert.IsTrue(FindProp(nameof(SampleModel.Id)).CanWrite); - } - - [TestMethod] - public void CanRead_ReadOnlyProp_IsTrue() - { - Assert.IsTrue(FindProp(nameof(SampleModel.ReadOnly)).CanRead); - } - - [TestMethod] - public void CanWrite_ReadOnlyProp_IsFalse() - { - Assert.IsFalse(FindProp(nameof(SampleModel.ReadOnly)).CanWrite); - } - - // ── GetValue ───────────────────────────────────────────────────────────── - - [TestMethod] - public void GetValue_IntProp_ReturnsCorrectValue() - { - var model = new SampleModel { Id = 99 }; - var value = FindProp(nameof(SampleModel.Id)).GetValue(model); - Assert.AreEqual(99, value); - } - - [TestMethod] - public void GetValue_StringPropNull_ReturnsNull() - { - var model = new SampleModel { Name = null }; - var value = FindProp(nameof(SampleModel.Name)).GetValue(model); - Assert.IsNull(value); - } - - [TestMethod] - public void GetValue_WriteOnlyProp_ThrowsInvalidOperationException() - { - // 构造一个只有 setter 的属性 —— 使用匿名对象包装器代替; - // 这里直接验证 SetValue 在 CanWrite=false 时的路径, - // GetValue 在 CanRead=false 时抛异常。 - var prop = FindProp(nameof(SampleModel.ReadOnly)); - // ReadOnly 仍可读,所以用 SetValue 测试不可写路径 - var model = new SampleModel(); - Assert.Throws(() => prop.SetValue(model, 1)); - } - - // ── SetValue ───────────────────────────────────────────────────────────── - - [TestMethod] - public void SetValue_IntProp_UpdatesValue() - { - var model = new SampleModel { Id = 0 }; - FindProp(nameof(SampleModel.Id)).SetValue(model, 7); - Assert.AreEqual(7, model.Id); - } - - [TestMethod] - public void SetValue_StringProp_UpdatesValue() - { - var model = new SampleModel(); - FindProp(nameof(SampleModel.Name)).SetValue(model, "hello"); - Assert.AreEqual("hello", model.Name); - } - - // ── XData ───────────────────────────────────────────────────────────── - - [TestMethod] - public void XData_Set_ThenGet_RoundTrip() - { - var model = new SampleModel(); - XData.Set(model, "Id", 55); - var result = XData.Get(model, "Id"); - Assert.AreEqual(55, result); - } - - [TestMethod] - public void XData_Get_NullData_ThrowsArgumentNullException() - { - Assert.Throws(() => XData.Get(null!, "Id")); - } - - [TestMethod] - public void XData_Set_NullData_ThrowsArgumentNullException() - { - Assert.Throws(() => XData.Set(null!, "Id", 1)); - } - - [TestMethod] - public void XData_Get_UnknownProp_ThrowsArgumentException() - { - Assert.Throws(() => XData.Get(new SampleModel(), "NonExistent")); - } - - [TestMethod] - public void XData_Set_UnknownProp_ThrowsArgumentException() - { - Assert.Throws(() => XData.Set(new SampleModel(), "NonExistent", 1)); - } - - // ── helpers ────────────────────────────────────────────────────────────── - - private static XProp FindProp(string name) - { - foreach (var p in XProp.GetAll(typeof(SampleModel))) - if (p.Name == name) return p; - throw new InvalidOperationException($"Property {name} not found."); - } -} diff --git a/tests/LuYao.Common.UnitTests/Data/Meta/XCopyTests.cs b/tests/LuYao.Common.UnitTests/Data/Meta/XCopyTests.cs deleted file mode 100644 index d508208..0000000 --- a/tests/LuYao.Common.UnitTests/Data/Meta/XCopyTests.cs +++ /dev/null @@ -1,437 +0,0 @@ -using LuYao.Data; -using LuYao.Data.Mapping; -using LuYao.Data.Meta; - -namespace LuYao.Data.Meta -{ - [TestClass] - public class XCopyTests - { - #region Test helper types - - private class Base - { - public int Id { get; set; } - public string? Name { get; set; } - // Unsupported type, should be skipped - public object? Ignored { get; set; } - } - - private class Derived : Base - { - public double Score { get; set; } - } - - private static RecordTable BuildTable() - { - var t = new RecordTable(); - t.Columns.Add("Id"); - t.Columns.Add("Name"); - t.Columns.Add("Score"); - return t; - } - - #endregion - - // ── MapTo ─────────────────────────────────────────────────────────── - - [TestMethod] - public void CopyTo_Base_WritesBaseProps() - { - var table = BuildTable(); - var row = table.AddRow(); - var obj = new Base { Id = 1, Name = "Alice" }; - - XCopy.MapTo(obj, row); - - Assert.AreEqual(1, table.Columns.Get("Id").Get(row)); - Assert.AreEqual("Alice", table.Columns.Get("Name").Get(row)); - } - - [TestMethod] - public void CopyTo_Derived_WritesBaseAndDerivedProps() - { - var table = BuildTable(); - var row = table.AddRow(); - var obj = new Derived { Id = 2, Name = "Bob", Score = 9.5 }; - - XCopy.MapTo(obj, row); - - Assert.AreEqual(2, table.Columns.Get("Id").Get(row)); - Assert.AreEqual("Bob", table.Columns.Get("Name").Get(row)); - Assert.AreEqual(9.5, table.Columns.Get("Score").Get(row)); - } - - [TestMethod] - public void CopyTo_DerivedViaBaseRef_WritesDerivedProps() - { - var table = BuildTable(); - var row = table.AddRow(); - Base obj = new Derived { Id = 3, Name = "Carol", Score = 8.0 }; - - // Passed as base reference; runtime type is still Derived - XCopy.MapTo(obj, row); - - Assert.AreEqual(3, table.Columns.Get("Id").Get(row)); - Assert.AreEqual(8.0, table.Columns.Get("Score").Get(row)); - } - - [TestMethod] - public void CopyTo_MissingColumn_SilentlySkips() - { - var table = new RecordTable(); - table.Columns.Add("Id"); - var row = table.AddRow(); - var obj = new Base { Id = 7, Name = "X" }; - - // Name column does not exist; should not throw - XCopy.MapTo(obj, row); - - Assert.AreEqual(7, table.Columns.Get("Id").Get(row)); - } - - [TestMethod] - public void CopyTo_NullData_ThrowsArgumentNullException() - { - var table = BuildTable(); - var row = table.AddRow(); - - try - { - XCopy.MapTo(null!, row); - Assert.Fail("Expected ArgumentNullException"); - } - catch (ArgumentNullException) { } - } - - // ── MapFrom ───────────────────────────────────────────────────────── - - [TestMethod] - public void CopyFrom_Base_ReadsBaseProps() - { - var table = BuildTable(); - var row = table.AddRow(); - table.Columns.Get("Id").Set(row, 10); - table.Columns.Get("Name").Set(row, "Dave"); - - var obj = new Base(); - XCopy.MapFrom(obj, row); - - Assert.AreEqual(10, obj.Id); - Assert.AreEqual("Dave", obj.Name); - } - - [TestMethod] - public void CopyFrom_Derived_ReadsBaseAndDerivedProps() - { - var table = BuildTable(); - var row = table.AddRow(); - table.Columns.Get("Id").Set(row, 20); - table.Columns.Get("Name").Set(row, "Eve"); - table.Columns.Get("Score").Set(row, 7.5); - - var obj = new Derived(); - XCopy.MapFrom(obj, row); - - Assert.AreEqual(20, obj.Id); - Assert.AreEqual("Eve", obj.Name); - Assert.AreEqual(7.5, obj.Score); - } - - [TestMethod] - public void CopyFrom_DerivedViaBaseRef_SetsDerivedProps() - { - var table = BuildTable(); - var row = table.AddRow(); - table.Columns.Get("Id").Set(row, 30); - table.Columns.Get("Score").Set(row, 6.0); - - Base obj = new Derived(); - XCopy.MapFrom(obj, row); - - Assert.AreEqual(30, ((Derived)obj).Id); - Assert.AreEqual(6.0, ((Derived)obj).Score); - } - - [TestMethod] - public void CopyFrom_NullData_ThrowsArgumentNullException() - { - var table = BuildTable(); - var row = table.AddRow(); - - try - { - XCopy.MapFrom(null!, row); - Assert.Fail("Expected ArgumentNullException"); - } - catch (ArgumentNullException) { } - } - - // ── XCopy thin wrapper ────────────────────────────────────────── - - [TestMethod] - public void Generic_CopyTo_DelegatesToNonGeneric() - { - var table = BuildTable(); - var row = table.AddRow(); - var obj = new Derived { Id = 5, Name = "Frank", Score = 3.14 }; - - // XCopy uses compile-time type typeof(Base), so only Base properties are copied - XCopy.MapTo(obj, row); - - Assert.AreEqual(5, table.Columns.Get("Id").Get(row)); - Assert.AreEqual("Frank", table.Columns.Get("Name").Get(row)); - // Score belongs to Derived, outside typeof(Base) scope; remains default - Assert.AreEqual(0.0, table.Columns.Get("Score").Get(row)); - } - - [TestMethod] - public void Generic_CopyFrom_DelegatesToNonGeneric() - { - var table = BuildTable(); - var row = table.AddRow(); - table.Columns.Get("Id").Set(row, 99); - table.Columns.Get("Score").Set(row, 1.23); - - var obj = new Derived(); - // XCopy uses compile-time type typeof(Base), so only Base properties are written back - XCopy.MapFrom(obj, row); - - Assert.AreEqual(99, obj.Id); - // Score belongs to Derived, outside typeof(Base) scope; remains default - Assert.AreEqual(0.0, obj.Score); - } - - // ── WriteTo ──────────────────────────────────────────────────────── - - [TestMethod] - public void WriteTo_Base_CreatesColumnsAndWritesValues() - { - var table = new RecordTable(); - var row = table.AddRow(); - var obj = new Base { Id = 1, Name = "Alice" }; - - XCopy.WriteTo(obj, row); - - Assert.AreEqual(1, table.Columns.Get("Id").Get(row)); - Assert.AreEqual("Alice", table.Columns.Get("Name").Get(row)); - } - - [TestMethod] - public void WriteTo_Derived_CreatesAllColumnsIncludingDerivedProps() - { - var table = new RecordTable(); - var row = table.AddRow(); - var obj = new Derived { Id = 2, Name = "Bob", Score = 9.5 }; - - XCopy.WriteTo(obj, row); - - Assert.AreEqual(2, table.Columns.Get("Id").Get(row)); - Assert.AreEqual("Bob", table.Columns.Get("Name").Get(row)); - Assert.AreEqual(9.5, table.Columns.Get("Score").Get(row)); - } - - [TestMethod] - public void WriteTo_DerivedViaBaseRef_CreatesDerivedColumns() - { - var table = new RecordTable(); - var row = table.AddRow(); - Base obj = new Derived { Id = 3, Name = "Carol", Score = 8.0 }; - - XCopy.WriteTo(obj, row); - - // Score column should be created automatically - Assert.IsNotNull(table.Columns.Find("Score")); - Assert.AreEqual(8.0, table.Columns.Get("Score").Get(row)); - } - - [TestMethod] - public void WriteTo_ExistingColumns_OverwritesValues() - { - var table = BuildTable(); - var row = table.AddRow(); - table.Columns.Get("Id").Set(row, 99); - - var obj = new Base { Id = 42, Name = "New" }; - XCopy.WriteTo(obj, row); - - Assert.AreEqual(42, table.Columns.Get("Id").Get(row)); - Assert.AreEqual("New", table.Columns.Get("Name").Get(row)); - } - - [TestMethod] - public void WriteTo_UnsupportedTypeProperty_IsSkipped() - { - var table = new RecordTable(); - var row = table.AddRow(); - var obj = new Base { Id = 5, Name = "Eve", Ignored = new object() }; - - // Ignored property type object is unsupported; no column should be created and no exception thrown - XCopy.WriteTo(obj, row); - - Assert.IsNull(table.Columns.Find("Ignored")); - Assert.AreEqual(5, table.Columns.Get("Id").Get(row)); - } - - [TestMethod] - public void WriteTo_NullData_ThrowsArgumentNullException() - { - var table = new RecordTable(); - var row = table.AddRow(); - - try - { - XCopy.WriteTo(null!, row); - Assert.Fail("Expected ArgumentNullException"); - } - catch (ArgumentNullException) { } - } - - [TestMethod] - public void Generic_WriteTo_DelegatesToNonGeneric() - { - var table = new RecordTable(); - var row = table.AddRow(); - var obj = new Derived { Id = 7, Name = "Frank", Score = 3.14 }; - - // XCopy uses compile-time type typeof(Base), so only Base property columns are created - XCopy.WriteTo(obj, row); - - Assert.AreEqual(7, table.Columns.Get("Id").Get(row)); - Assert.AreEqual("Frank", table.Columns.Get("Name").Get(row)); - // Score belongs to Derived, outside typeof(Base) scope; column should not be created - Assert.IsNull(table.Columns.Find("Score")); - } - - // ── MapTo(Type, ...) explicit type overloads ───────────────────────────────────── - - [TestMethod] - public void CopyTo_ExplicitBaseType_OnlyWritesBaseProps() - { - var table = BuildTable(); - var row = table.AddRow(); - var obj = new Derived { Id = 10, Name = "Grace", Score = 5.0 }; - - // Explicitly specify Base type; Score property should not be written - XCopy.MapTo(typeof(Base), obj, row); - - Assert.AreEqual(10, table.Columns.Get("Id").Get(row)); - Assert.AreEqual("Grace", table.Columns.Get("Name").Get(row)); - Assert.AreEqual(0.0, table.Columns.Get("Score").Get(row)); - } - - [TestMethod] - public void CopyTo_NullType_ThrowsArgumentNullException() - { - var table = BuildTable(); - var row = table.AddRow(); - - try { XCopy.MapTo(null!, new Base(), row); Assert.Fail("Expected ArgumentNullException"); } - catch (ArgumentNullException) { } - } - - [TestMethod] - public void CopyTo_ExplicitType_NullData_ThrowsArgumentNullException() - { - var table = BuildTable(); - var row = table.AddRow(); - - try { XCopy.MapTo(typeof(Base), null!, row); Assert.Fail("Expected ArgumentNullException"); } - catch (ArgumentNullException) { } - } - - // ── MapFrom(Type, ...) explicit type overloads ─────────────────────────────────── - - [TestMethod] - public void CopyFrom_ExplicitBaseType_OnlyReadsBaseProps() - { - var table = BuildTable(); - var row = table.AddRow(); - table.Columns.Get("Id").Set(row, 20); - table.Columns.Get("Name").Set(row, "Hank"); - table.Columns.Get("Score").Set(row, 4.5); - - var obj = new Derived(); - // Explicitly specify Base type; Score property should not be written back - XCopy.MapFrom(typeof(Base), obj, row); - - Assert.AreEqual(20, obj.Id); - Assert.AreEqual("Hank", obj.Name); - Assert.AreEqual(0.0, obj.Score); - } - - [TestMethod] - public void CopyFrom_NullType_ThrowsArgumentNullException() - { - var table = BuildTable(); - var row = table.AddRow(); - - try { XCopy.MapFrom(null!, new Base(), row); Assert.Fail("Expected ArgumentNullException"); } - catch (ArgumentNullException) { } - } - - [TestMethod] - public void CopyFrom_ExplicitType_NullData_ThrowsArgumentNullException() - { - var table = BuildTable(); - var row = table.AddRow(); - - try { XCopy.MapFrom(typeof(Base), null!, row); Assert.Fail("Expected ArgumentNullException"); } - catch (ArgumentNullException) { } - } - - // ── WriteTo(Type, ...) explicit type overloads ─────────────────────────────────── - - [TestMethod] - public void WriteTo_ExplicitBaseType_OnlyCreatesBaseColumns() - { - var table = new RecordTable(); - var row = table.AddRow(); - var obj = new Derived { Id = 30, Name = "Ivy", Score = 2.0 }; - - // Explicitly specify Base type; Score column should not be created - XCopy.WriteTo(typeof(Base), obj, row); - - Assert.AreEqual(30, table.Columns.Get("Id").Get(row)); - Assert.AreEqual("Ivy", table.Columns.Get("Name").Get(row)); - Assert.IsNull(table.Columns.Find("Score")); - } - - [TestMethod] - public void WriteTo_NullType_ThrowsArgumentNullException() - { - var table = new RecordTable(); - var row = table.AddRow(); - - try { XCopy.WriteTo(null!, new Base(), row); Assert.Fail("Expected ArgumentNullException"); } - catch (ArgumentNullException) { } - } - - [TestMethod] - public void WriteTo_ExplicitType_NullData_ThrowsArgumentNullException() - { - var table = new RecordTable(); - var row = table.AddRow(); - - try { XCopy.WriteTo(typeof(Base), null!, row); Assert.Fail("Expected ArgumentNullException"); } - catch (ArgumentNullException) { } - } - - // ── Cache consistency ──────────────────────────────────────────────────────────── - - [TestMethod] - public void CopyTo_CalledTwice_ProducesConsistentResults() - { - var table = BuildTable(); - var row1 = table.AddRow(); - var row2 = table.AddRow(); - var obj = new Base { Id = 40, Name = "Jack" }; - - XCopy.MapTo(obj, row1); - XCopy.MapTo(obj, row2); - - Assert.AreEqual(40, table.Columns.Get("Id").Get(row1)); - Assert.AreEqual(40, table.Columns.Get("Id").Get(row2)); - } - } -} diff --git a/tests/LuYao.Common.UnitTests/Data/NameFilterTests.cs b/tests/LuYao.Common.UnitTests/Data/NameFilterTests.cs deleted file mode 100644 index 5dbd1d3..0000000 --- a/tests/LuYao.Common.UnitTests/Data/NameFilterTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -namespace LuYao.Data; - -[TestClass] -public class NameFilterTests -{ - private class SampleData - { - public int Id { get; set; } - public string Name { get; set; } - public string Email { get; set; } - public DateTime CreatedAt { get; set; } - } - - [TestMethod] - public void ToNames_Default_ReturnsAllSupportedProperties() - { - var filter = new NameFilter(); - var names = filter.ToNames(); - CollectionAssert.Contains(names, nameof(SampleData.Id)); - CollectionAssert.Contains(names, nameof(SampleData.Name)); - CollectionAssert.Contains(names, nameof(SampleData.Email)); - CollectionAssert.Contains(names, nameof(SampleData.CreatedAt)); - } - - [TestMethod] - public void Exclude_RemovesProperty() - { - var filter = new NameFilter(); - filter.Exclude(x => x.Email); - var names = filter.ToNames(); - CollectionAssert.Contains(names, nameof(SampleData.Id)); - CollectionAssert.Contains(names, nameof(SampleData.Name)); - CollectionAssert.DoesNotContain(names, nameof(SampleData.Email)); - } - - [TestMethod] - public void Include_AddsBackExcludedProperty() - { - var filter = new NameFilter(); - filter.Exclude(x => x.Email); - filter.Include(x => x.Email); - var names = filter.ToNames(); - CollectionAssert.Contains(names, nameof(SampleData.Email)); - } - - [TestMethod] - public void All_ResetsToAllProperties() - { - var filter = new NameFilter(); - filter.Exclude(x => x.Id).Exclude(x => x.Name); - filter.All(); - var names = filter.ToNames(); - CollectionAssert.Contains(names, nameof(SampleData.Id)); - CollectionAssert.Contains(names, nameof(SampleData.Name)); - } - - [TestMethod] - public void ToNames_AfterAll_PreservesDeclarationOrder() - { - var filter = new NameFilter(); - var names = filter.ToNames(); - var expected = new[] { nameof(SampleData.Id), nameof(SampleData.Name), nameof(SampleData.Email), nameof(SampleData.CreatedAt) }; - CollectionAssert.AreEqual(expected, names); - } - - [TestMethod] - public void ToNames_AfterClearAndManualIncludes_RespectsInsertionOrder() - { - // 手动 Include 的顺序决定最终列顺序 - var filter = new NameFilter(); - filter.Clear() - .Include(x => x.Email) - .Include(x => x.Id) - .Include(x => x.CreatedAt); - var names = filter.ToNames(); - CollectionAssert.AreEqual(new[] { nameof(SampleData.Email), nameof(SampleData.Id), nameof(SampleData.CreatedAt) }, names); - } - - [TestMethod] - public void Clear_RemovesAllProperties() - { - var filter = new NameFilter(); - filter.Clear(); - Assert.AreEqual(0, filter.ToNames().Length); - } - - [TestMethod] - public void Clear_ThenInclude_ReturnsOnlyIncludedInOrder() - { - var filter = new NameFilter(); - filter.Clear().Include(x => x.Name).Include(x => x.Id); - CollectionAssert.AreEqual(new[] { nameof(SampleData.Name), nameof(SampleData.Id) }, filter.ToNames()); - } - - [TestMethod] - public void ToNames_IncludeSamePropertyTwice_NoDuplicates() - { - var filter = new NameFilter(); - filter.Include(x => x.Id).Include(x => x.Id); - var names = filter.ToNames(); - var distinct = new HashSet(names, StringComparer.Ordinal); - Assert.AreEqual(distinct.Count, names.Length, "ToNames() 结果不应包含重复项。"); - } - - [TestMethod] - public void AddFrom_WithNames_RespectsInputOrder() - { - // 传入顺序与声明顺序故意相反,验证列顺序与传入顺序一致 - var record = new RecordTable(); - record.Columns.AddFrom(new[] { nameof(SampleData.Email), nameof(SampleData.Id) }); - Assert.AreEqual(nameof(SampleData.Email), record.Columns[0].Name); - Assert.AreEqual(nameof(SampleData.Id), record.Columns[1].Name); - } - - [TestMethod] - public void AddFrom_WithAnonymousTemplate_AddsColumnsFromTemplateType() - { - var record = new RecordTable(); - - record.Columns.AddFrom(new { Id = 1, Name = "Test", IsOk = true }); - - Assert.AreEqual(3, record.Columns.Count); - Assert.AreEqual("Id", record.Columns[0].Name); - Assert.AreEqual(typeof(int), record.Columns[0].Type); - Assert.AreEqual("Name", record.Columns[1].Name); - Assert.AreEqual(typeof(string), record.Columns[1].Type); - Assert.AreEqual("IsOk", record.Columns[2].Name); - Assert.AreEqual(typeof(bool), record.Columns[2].Type); - } - - [TestMethod] - public void AddFrom_WithNullTemplate_ThrowsArgumentNullException() - { - var record = new RecordTable(); - SampleData template = null; - - var ex = Assert.Throws(() => record.Columns.AddFrom(template)); - Assert.AreEqual("template", ex.ParamName); - } - - [TestMethod] - public void AddFrom_WithFilter_RespectsManualIncludeOrder() - { - // 通过 NameFilter 手动 Include 的顺序应正确反映到 Record 列顺序 - var record = new RecordTable(); - record.Columns.AddFrom(f => f.Clear() - .Include(x => x.CreatedAt) - .Include(x => x.Name)); - Assert.AreEqual(nameof(SampleData.CreatedAt), record.Columns[0].Name); - Assert.AreEqual(nameof(SampleData.Name), record.Columns[1].Name); - } - - [TestMethod] - public void AddFrom_WithNames_OnlyAddsSpecifiedColumns() - { - var record = new RecordTable(); - record.Columns.AddFrom(new[] { nameof(SampleData.Id), nameof(SampleData.Name) }); - Assert.AreEqual(2, record.Columns.Count); - Assert.IsNotNull(record.Columns[nameof(SampleData.Id)]); - Assert.IsNotNull(record.Columns[nameof(SampleData.Name)]); - } - - [TestMethod] - public void AddFrom_WithNullOrEmptyNames_AddsNoColumns() - { - var record1 = new RecordTable(); - record1.Columns.AddFrom(new string[0]); - Assert.AreEqual(0, record1.Columns.Count, "空数组不应追加任何列。"); - - var record2 = new RecordTable(); - record2.Columns.AddFrom((string[])null); - Assert.AreEqual(0, record2.Columns.Count, "null 不应追加任何列。"); - } - - [TestMethod] - public void AddFrom_WithFilter_ExcludesSpecifiedColumn() - { - var record = new RecordTable(); - record.Columns.AddFrom(f => f.Exclude(x => x.Email)); - Assert.AreEqual(3, record.Columns.Count); - Assert.IsNull(record.Columns[nameof(SampleData.Email)], "Email column should not exist."); - } -} diff --git a/tests/LuYao.Common.UnitTests/Data/RecordArrayTypeTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordArrayTypeTests.cs deleted file mode 100644 index a9eb95c..0000000 --- a/tests/LuYao.Common.UnitTests/Data/RecordArrayTypeTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; - -namespace LuYao.Data; - -/// -/// Tests for RecordTable Binary (byte[]) column type support. -/// -[TestClass] -public class RecordArrayTypeTests -{ - #region Basic Binary column tests - - [TestMethod] - public void WhenAddBinaryColumnThenColumnTypeIsBinary() - { - var table = new RecordTable("Test"); - var col = table.Columns.Add("Data"); - - Assert.AreEqual(RecordColumnType.Binary, col.ColumnType); - Assert.AreEqual(typeof(byte[]), col.Type); - } - - [TestMethod] - public void WhenAddNonArrayColumnThenColumnTypeIsCorrect() - { - var table = new RecordTable("Test"); - var col = table.Columns.Add("Value"); - - Assert.AreEqual(RecordColumnType.Int32, col.ColumnType); - Assert.AreEqual(typeof(int), col.Type); - } - - #endregion - - #region Binary data operations - - [TestMethod] - public void WhenSetAndGetByteArrayThenValuesPreserved() - { - var table = new RecordTable("Test"); - table.Columns.Add("Data"); - - var row = table.AddRow(); - row["Data"] = new byte[] { 1, 2, 3, 4, 5 }; - - var retrieved = row.To("Data"); - Assert.IsNotNull(retrieved); - CollectionAssert.AreEqual(new byte[] { 1, 2, 3, 4, 5 }, retrieved); - } - - [TestMethod] - public void WhenSetNullByteArrayThenGetReturnsNull() - { - var table = new RecordTable("Test"); - table.Columns.Add("Data"); - - var row = table.AddRow(); - row["Data"] = null; - - var retrieved = row.To("Data"); - Assert.IsNull(retrieved); - } - - [TestMethod] - public void WhenSetEmptyByteArrayThenGetReturnsEmptyArray() - { - var table = new RecordTable("Test"); - table.Columns.Add("Data"); - - var row = table.AddRow(); - row["Data"] = new byte[0]; - - var retrieved = row.To("Data"); - Assert.IsNotNull(retrieved); - Assert.AreEqual(0, retrieved.Length); - } - - #endregion - - #region Binary serialization tests - - [TestMethod] - public void WhenSerializeByteArrayThenRoundTripSucceeds() - { - var original = new RecordTable("Test"); - original.Columns.Add("Data"); - - var row = original.AddRow(); - row["Data"] = new byte[] { 10, 20, 30, 40 }; - - var bytes = original.ToBytes(); - var deserialized = RecordTable.FromBytes(bytes); - - Assert.AreEqual(1, deserialized.Count); - var data = deserialized[0].To("Data"); - CollectionAssert.AreEqual(new byte[] { 10, 20, 30, 40 }, data); - } - - [TestMethod] - public void WhenSerializeNullByteArrayThenRoundTripSucceeds() - { - var original = new RecordTable("Test"); - original.Columns.Add("Data"); - - var row = original.AddRow(); - row["Data"] = null; - - var bytes = original.ToBytes(); - var deserialized = RecordTable.FromBytes(bytes); - - Assert.IsNull(deserialized[0].To("Data")); - } - - [TestMethod] - public void WhenSerializeEmptyByteArrayThenRoundTripSucceeds() - { - var original = new RecordTable("Test"); - original.Columns.Add("Data"); - - var row = original.AddRow(); - row["Data"] = new byte[0]; - - var bytes = original.ToBytes(); - var deserialized = RecordTable.FromBytes(bytes); - - var data = deserialized[0].To("Data"); - Assert.IsNotNull(data); - Assert.AreEqual(0, data.Length); - } - - [TestMethod] - public void WhenSerializeMultipleRowsWithByteArrayThenAllRowsPreserved() - { - var original = new RecordTable("Test"); - original.Columns.Add("Id"); - original.Columns.Add("Data"); - - for (int i = 1; i <= 3; i++) - { - var row = original.AddRow(); - row["Id"] = i; - row["Data"] = new byte[] { (byte)i, (byte)(i * 2) }; - } - - var bytes = original.ToBytes(); - var deserialized = RecordTable.FromBytes(bytes); - - Assert.AreEqual(3, deserialized.Count); - for (int i = 0; i < 3; i++) - { - var id = deserialized[i].To("Id"); - var data = deserialized[i].To("Data"); - Assert.AreEqual(i + 1, id); - CollectionAssert.AreEqual(new byte[] { (byte)(i + 1), (byte)((i + 1) * 2) }, data); - } - } - - #endregion - - #region ToString tests - - [TestMethod] - public void WhenToStringOnByteArrayThenReturnsBase64() - { - var table = new RecordTable("Test"); - table.Columns.Add("Data"); - - var row = table.AddRow(); - row["Data"] = new byte[] { 1, 2, 3 }; - - var str = row.ToString("Data"); - Assert.AreEqual(Convert.ToBase64String(new byte[] { 1, 2, 3 }), str); - } - - [TestMethod] - public void WhenToStringOnNullByteArrayThenReturnsEmptyString() - { - var table = new RecordTable("Test"); - table.Columns.Add("Data"); - - var row = table.AddRow(); - row["Data"] = null; - - var str = row.ToString("Data"); - Assert.AreEqual("", str); - } - - #endregion - - #region RecordSchema tests - - [TestMethod] - public void WhenGetSchemaWithBinaryColumnThenColumnTypeIsBinary() - { - var table = new RecordTable("Test"); - table.Columns.Add("Id"); - table.Columns.Add("Data"); - - var schema = table.GetSchema(); - - Assert.AreEqual(2, schema.Columns.Count); - - var idCol = schema.Columns[0]; - Assert.AreEqual("Id", idCol.Name); - Assert.AreEqual(typeof(int), idCol.Type); - - var dataCol = schema.Columns[1]; - Assert.AreEqual("Data", dataCol.Name); - Assert.AreEqual(RecordColumnType.Binary, dataCol.ColumnType); - Assert.AreEqual(typeof(byte[]), dataCol.Type); - } - - #endregion - - #region Delete operation tests - - [TestMethod] - public void WhenDeleteRowWithBinaryColumnThenDataShiftsCorrectly() - { - var table = new RecordTable("Test"); - table.Columns.Add("Id"); - table.Columns.Add("Data"); - - for (int i = 1; i <= 3; i++) - { - var row = table.AddRow(); - row["Id"] = i; - row["Data"] = new byte[] { (byte)i }; - } - - table.Delete(1); - - Assert.AreEqual(2, table.Count); - Assert.AreEqual(1, table[0].To("Id")); - CollectionAssert.AreEqual(new byte[] { 1 }, table[0].To("Data")); - Assert.AreEqual(3, table[1].To("Id")); - CollectionAssert.AreEqual(new byte[] { 3 }, table[1].To("Data")); - } - - #endregion - - #region Clone operation tests - - [TestMethod] - public void WhenCloneRecordWithBinaryColumnThenDataCopied() - { - var original = new RecordTable("Test"); - original.Columns.Add("Id"); - original.Columns.Add("Data"); - - var row = original.AddRow(); - row["Id"] = 1; - row["Data"] = new byte[] { 1, 2, 3 }; - - var clone = original.Clone(); - - Assert.AreEqual(1, clone.Count); - Assert.AreEqual(2, clone.Columns.Count); - CollectionAssert.AreEqual(new byte[] { 1, 2, 3 }, clone[0].To("Data")); - } - - [TestMethod] - public void WhenCloneSchemaWithBinaryColumnThenStructureCopied() - { - var original = new RecordTable("Test"); - original.Columns.Add("Id"); - original.Columns.Add("Data"); - - var clone = original.CloneSchema(); - - Assert.AreEqual(0, clone.Count); - Assert.AreEqual(2, clone.Columns.Count); - Assert.AreEqual(RecordColumnType.Int32, clone.Columns[0].ColumnType); - Assert.AreEqual(RecordColumnType.Binary, clone.Columns[1].ColumnType); - } - - #endregion -} \ No newline at end of file diff --git a/tests/LuYao.Common.UnitTests/Data/RecordColumnCollectionTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordColumnCollectionTests.cs index afa4a66..10dfb48 100644 --- a/tests/LuYao.Common.UnitTests/Data/RecordColumnCollectionTests.cs +++ b/tests/LuYao.Common.UnitTests/Data/RecordColumnCollectionTests.cs @@ -155,4 +155,32 @@ public void Enumerate_PreservesInsertionOrder() var names = table.Columns.Select(c => c.Name).ToArray(); CollectionAssert.AreEqual(new[] { "A", "B", "C" }, names); } + + [TestMethod] + public void Add_NullableType_ThrowsNotSupportedException() + { + var table = new RecordTable(); + Assert.Throws(() => table.Columns.Add("Value")); + } + + [TestMethod] + public void Add_ArrayType_ThrowsNotSupportedException() + { + var table = new RecordTable(); + Assert.Throws(() => table.Columns.Add("Values")); + } + + [TestMethod] + public void Add_EnumType_ThrowsNotSupportedException() + { + var table = new RecordTable(); + Assert.Throws(() => table.Columns.Add("Day")); + } + + [TestMethod] + public void Add_UnknownRecordColumnType_ThrowsNotSupportedException() + { + var table = new RecordTable(); + Assert.Throws(() => table.Columns.Add("Value", (RecordColumnType)byte.MaxValue)); + } } diff --git a/tests/LuYao.Common.UnitTests/Data/RecordJsonConverterTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordJsonConverterTests.cs deleted file mode 100644 index a4f59c8..0000000 --- a/tests/LuYao.Common.UnitTests/Data/RecordJsonConverterTests.cs +++ /dev/null @@ -1,461 +0,0 @@ -using LuYao.Data.Binary; -using LuYao.Data.Json; -using System; -using System.IO; -using System.IO.Compression; -using System.Text; -using System.Text.Json; - -namespace LuYao.Data; - -[TestClass] -public class RecordJsonConverterTests -{ - #region RecordTableJsonConverter Tests - - [TestMethod] - public void RecordTableJsonConverter_WhenSerializeAndDeserializeThenDataPreserved() - { - var original = CreateTestRecordTable(); - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordTableJsonConverter()); - - var json = JsonSerializer.Serialize(original, options); - var deserialized = JsonSerializer.Deserialize(json, options); - - Assert.IsNotNull(deserialized); - Assert.AreEqual("Orders", deserialized.Name); - Assert.AreEqual(2, deserialized.Count); - Assert.AreEqual(2, deserialized.Columns.Count); - Assert.AreEqual(2, deserialized.Page); - Assert.AreEqual(10, deserialized.PageSize); - Assert.AreEqual(50, deserialized.MaxCount); - - var idCol = deserialized.Columns["Id"] as RecordColumn; - var nameCol = deserialized.Columns["Name"] as RecordColumn; - Assert.IsNotNull(idCol); - Assert.IsNotNull(nameCol); - - Assert.AreEqual(1, idCol.GetValue(0)); - Assert.AreEqual("Order-1", nameCol.GetValue(0)); - Assert.AreEqual(2, idCol.GetValue(1)); - Assert.AreEqual("Order-2", nameCol.GetValue(1)); - } - - [TestMethod] - public void RecordTableJsonConverter_WhenSerializeNullThenReturnsNull() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordTableJsonConverter()); - - var json = JsonSerializer.Serialize(null, options); - Assert.AreEqual("null", json); - } - - [TestMethod] - public void RecordTableJsonConverter_WhenDeserializeNullThenReturnsNull() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordTableJsonConverter()); - - var deserialized = JsonSerializer.Deserialize("null", options); - Assert.IsNull(deserialized); - } - - [TestMethod] - public void RecordTableJsonConverter_WhenDeserializeInvalidBase64ThenThrowsJsonException() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordTableJsonConverter()); - - Assert.Throws(() => - JsonSerializer.Deserialize("\"invalid-base64!!!\"", options)); - } - - [TestMethod] - public void RecordTableJsonConverter_WhenDeserializeInvalidTokenTypeThenThrowsJsonException() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordTableJsonConverter()); - - Assert.Throws(() => - JsonSerializer.Deserialize("123", options)); - } - - [TestMethod] - public void RecordTableJsonConverter_WhenDeserializeInvalidBinaryPayloadThenThrowsJsonException() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordTableJsonConverter()); - - var invalidBytes = new byte[] { 0x00, 0x01, 0x02, 0x03 }; - var base64 = Convert.ToBase64String(invalidBytes); - var json = $"\"{base64}\""; - - Assert.Throws(() => - JsonSerializer.Deserialize(json, options)); - } - - #endregion - - #region RecordSetJsonConverter Tests - - [TestMethod] - public void RecordSetJsonConverter_WhenSerializeAndDeserializeThenDataPreserved() - { - var original = CreateTestRecordSet(); - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - var json = JsonSerializer.Serialize(original, options); - var deserialized = JsonSerializer.Deserialize(json, options); - - Assert.IsNotNull(deserialized); - Assert.AreEqual(2, deserialized.Count); - Assert.IsTrue(deserialized.Contains("Orders")); - Assert.IsTrue(deserialized.Contains("Products")); - - var orders = deserialized["Orders"]; - Assert.IsNotNull(orders); - Assert.AreEqual("Orders", orders.Name); - Assert.AreEqual(1, orders.Count); - - var products = deserialized["Products"]; - Assert.IsNotNull(products); - Assert.AreEqual("Products", products.Name); - Assert.AreEqual(1, products.Count); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenSerializeNullThenReturnsNull() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - var json = JsonSerializer.Serialize(null, options); - Assert.AreEqual("null", json); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenDeserializeNullThenReturnsNull() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - var deserialized = JsonSerializer.Deserialize("null", options); - Assert.IsNull(deserialized); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenDeserializeInvalidBase64ThenThrowsJsonException() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - Assert.Throws(() => - JsonSerializer.Deserialize("\"invalid-base64!!!\"", options)); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenDeserializeInvalidTokenTypeThenThrowsJsonException() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - Assert.Throws(() => - JsonSerializer.Deserialize("123", options)); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenDeserializeInvalidBinaryPayloadThenThrowsJsonException() - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - var invalidBytes = new byte[] { 0x00, 0x01, 0x02, 0x03 }; - var base64 = Convert.ToBase64String(invalidBytes); - var json = $"\"{base64}\""; - - Assert.Throws(() => - JsonSerializer.Deserialize(json, options)); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenRoundTripWithEmptySetThenSucceeds() - { - var original = new RecordSet(); - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - var json = JsonSerializer.Serialize(original, options); - var deserialized = JsonSerializer.Deserialize(json, options); - - Assert.IsNotNull(deserialized); - Assert.AreEqual(0, deserialized.Count); - } - - #endregion - - #region Security / Compatibility Tests - - // 版本 1 头部(6 字节):0xFE 'L' 'Y' 'Z' + version=1 + algo=GZip=1 - private static readonly byte[] CompressedHeader = { 0xFE, (byte)'L', (byte)'Y', (byte)'Z', 0x01, 0x01 }; - - [TestMethod] - public void RecordSetJsonConverter_WhenDeserializeUncompressedLegacyPayloadThenSucceeds() - { - // 旧的无压缩 RecordSet.ToBytes() 直接 Base64,应当走 passthrough 分支。 - var original = CreateTestRecordSet(); - var rawBytes = original.ToBytes(); - var json = "\"" + Convert.ToBase64String(rawBytes) + "\""; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - var deserialized = JsonSerializer.Deserialize(json, options); - - Assert.IsNotNull(deserialized); - Assert.AreEqual(2, deserialized.Count); - Assert.IsTrue(deserialized.Contains("Orders")); - Assert.IsTrue(deserialized.Contains("Products")); - } - - [TestMethod] - public void RecordTableJsonConverter_WhenDeserializeUncompressedLegacyPayloadThenSucceeds() - { - // 旧的无压缩 RecordTable 二进制(直接 WriteTo),应当走 passthrough 分支。 - var original = CreateTestRecordTable(); - byte[] rawBytes; - using (var ms = new MemoryStream()) - using (var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) - { - original.WriteTo(bw); - bw.Flush(); - rawBytes = ms.ToArray(); - } - var json = "\"" + Convert.ToBase64String(rawBytes) + "\""; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordTableJsonConverter()); - - var deserialized = JsonSerializer.Deserialize(json, options); - - Assert.IsNotNull(deserialized); - Assert.AreEqual("Orders", deserialized.Name); - Assert.AreEqual(2, deserialized.Count); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenPayloadHasCompressionMarkerButInvalidGZipThenThrowsJsonException() - { - // 头部正确,但 gzip 主体为乱码,应当被包装为 JsonException。 - var bad = new byte[CompressedHeader.Length + 16]; - Buffer.BlockCopy(CompressedHeader, 0, bad, 0, CompressedHeader.Length); - for (int i = CompressedHeader.Length; i < bad.Length; i++) bad[i] = 0xAB; - var json = "\"" + Convert.ToBase64String(bad) + "\""; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - Assert.Throws(() => - JsonSerializer.Deserialize(json, options)); - } - - [TestMethod] - public void RecordTableJsonConverter_WhenPayloadHasCompressionMarkerButInvalidGZipThenThrowsJsonException() - { - var bad = new byte[CompressedHeader.Length + 16]; - Buffer.BlockCopy(CompressedHeader, 0, bad, 0, CompressedHeader.Length); - for (int i = CompressedHeader.Length; i < bad.Length; i++) bad[i] = 0xAB; - var json = "\"" + Convert.ToBase64String(bad) + "\""; - - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordTableJsonConverter()); - - Assert.Throws(() => - JsonSerializer.Deserialize(json, options)); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenDecompressedSizeExceedsLimitThenThrowsJsonException() - { - // 构造 zip bomb:极小压缩输入,解压后远超 64MB 上限。 - byte[] compressed; - using (var ms = new MemoryStream()) - { - ms.Write(CompressedHeader, 0, CompressedHeader.Length); - using (var gz = new GZipStream(ms, CompressionLevel.Optimal, leaveOpen: true)) - { - var chunk = new byte[64 * 1024]; // 全零块,压缩比极高 - // 写入 ~80MB,超过 64MB 上限 - for (int i = 0; i < 1280; i++) gz.Write(chunk, 0, chunk.Length); - } - compressed = ms.ToArray(); - } - - var json = "\"" + Convert.ToBase64String(compressed) + "\""; - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter()); - - Assert.Throws(() => - JsonSerializer.Deserialize(json, options)); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenBase64PayloadTooLargeThenThrowsJsonException() - { - var json = BuildOversizeBase64Json(); - - var options = new JsonSerializerOptions { MaxDepth = 64 }; - options.Converters.Add(new RecordSetJsonConverter()); - - // System.Text.Json 默认对单个 token 的字符串长度也有上限,但这里我们关心的是 - // 在分配 byte[] 之前就被守卫拦截。无论命中守卫还是底层读取限制,都应是 JsonException。 - Assert.Throws(() => - JsonSerializer.Deserialize(json, options)); - } - - [TestMethod] - public void RecordTableJsonConverter_WhenBase64PayloadTooLargeThenThrowsJsonException() - { - var json = BuildOversizeBase64Json(); - - var options = new JsonSerializerOptions { MaxDepth = 64 }; - options.Converters.Add(new RecordTableJsonConverter()); - - Assert.Throws(() => - JsonSerializer.Deserialize(json, options)); - } - - private static string BuildOversizeBase64Json() - { - // 构造一个超过 MaxBase64Length 的字符串(内容无所谓,长度用 'A' 填充)。 - // MaxBase64Length 来自 16MB 压缩上限,约 22.4MB Base64 字符。 - const int oversize = 25 * 1024 * 1024; - var sb = new StringBuilder(oversize + 2); - sb.Append('"'); - sb.Append('A', oversize); - sb.Append('"'); - return sb.ToString(); - } - - [TestMethod] - public void RecordBinaryPayloadCodec_WhenNoCompressionThenRoundTripSucceeds() - { - var codec = new RecordBinaryPayloadCodec { Compression = RecordPayloadCompression.None }; - var original = new byte[] { 1, 2, 3, 4, 5 }; - var encoded = codec.Encode(original); - var decoded = codec.Decode(encoded); - CollectionAssert.AreEqual(original, decoded); - } - - [TestMethod] - public void RecordBinaryPayloadCodec_WhenGZipThenRoundTripSucceeds() - { - var codec = new RecordBinaryPayloadCodec { Compression = RecordPayloadCompression.GZip }; - var original = new byte[] { 1, 2, 3, 4, 5 }; - var encoded = codec.Encode(original); - var decoded = codec.Decode(encoded); - CollectionAssert.AreEqual(original, decoded); - } - - [TestMethod] - public void RecordTableJsonConverter_WhenCustomNoCompressionCodecThenRoundTripSucceeds() - { - var codec = new RecordBinaryPayloadCodec { Compression = RecordPayloadCompression.None }; - var original = CreateTestRecordTable(); - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordTableJsonConverter(codec)); - - var json = JsonSerializer.Serialize(original, options); - var deserialized = JsonSerializer.Deserialize(json, options); - - Assert.IsNotNull(deserialized); - Assert.AreEqual("Orders", deserialized.Name); - Assert.AreEqual(2, deserialized.Count); - } - - [TestMethod] - public void RecordSetJsonConverter_WhenCustomNoCompressionCodecThenRoundTripSucceeds() - { - var codec = new RecordBinaryPayloadCodec { Compression = RecordPayloadCompression.None }; - var original = CreateTestRecordSet(); - var options = new JsonSerializerOptions(); - options.Converters.Add(new RecordSetJsonConverter(codec)); - - var json = JsonSerializer.Serialize(original, options); - var deserialized = JsonSerializer.Deserialize(json, options); - - Assert.IsNotNull(deserialized); - Assert.AreEqual(2, deserialized.Count); - } - - [TestMethod] - public void RecordBinaryPayloadCodec_WhenCustomDoSLimitExceededThenThrowsInvalidDataException() - { - var codec = new RecordBinaryPayloadCodec - { - Compression = RecordPayloadCompression.GZip, - MaxDecompressedBytes = 100 - }; - - // 构造能解压出 >100 字节的 GZip 数据 - byte[] encoded; - using (var ms = new MemoryStream()) - { - ms.WriteByte(0xFE); ms.WriteByte((byte)'L'); ms.WriteByte((byte)'Y'); ms.WriteByte((byte)'Z'); - ms.WriteByte(0x01); ms.WriteByte(0x01); // version=1, algo=GZip - using (var gz = new GZipStream(ms, CompressionLevel.Fastest, leaveOpen: true)) - { - var chunk = new byte[200]; - gz.Write(chunk, 0, chunk.Length); - } - encoded = ms.ToArray(); - } - - Assert.Throws(() => codec.Decode(encoded)); - } - - - - private static RecordTable CreateTestRecordTable() - { - var table = new RecordTable("Orders", 2); - var idCol = table.Columns.Add("Id"); - var nameCol = table.Columns.Add("Name"); - table.Page = 2; - table.PageSize = 10; - table.MaxCount = 50; - - var row1 = table.AddRow(); - idCol.SetValue(row1.Row, 1); - nameCol.SetValue(row1.Row, "Order-1"); - - var row2 = table.AddRow(); - idCol.SetValue(row2.Row, 2); - nameCol.SetValue(row2.Row, "Order-2"); - - return table; - } - - private static RecordSet CreateTestRecordSet() - { - var set = new RecordSet(); - - var orders = new RecordTable("Orders", 1); - var orderId = orders.Columns.Add("Id"); - var orderRow = orders.AddRow(); - orderId.SetValue(orderRow.Row, 1); - set.Add("Orders", orders); - - var products = new RecordTable("Products", 1); - var productId = products.Columns.Add("Id"); - var productRow = products.AddRow(); - productId.SetValue(productRow.Row, 100); - set.Add("Products", products); - - return set; - } - - #endregion -} diff --git a/tests/LuYao.Common.UnitTests/Data/RecordMappingOptionsTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordMappingOptionsTests.cs deleted file mode 100644 index df0bc5c..0000000 --- a/tests/LuYao.Common.UnitTests/Data/RecordMappingOptionsTests.cs +++ /dev/null @@ -1,294 +0,0 @@ -using LuYao.Data.Attributes; -using LuYao.Data.Mapping; -using System; - -namespace LuYao.Data; - -[TestClass] -public class RecordMappingOptionsTests -{ - // ─── 测试用 DTO ─────────────────────────────────────────────────────────────── - - private class PersonDto - { - public int Id { get; set; } - public string? FullName { get; set; } - public int Age { get; set; } - } - - private class DtoWithColumnName - { - [RecordColumnNameAttribute("full_name")] - public string? FullName { get; set; } - - public int Age { get; set; } - } - - private class DtoWithUnsupportedType - { - public int Id { get; set; } - // object 不是支持的列类型 - public object? Extra { get; set; } - } - - // ─── RecordColumnNameAttribute ──────────────────────────────────────────────── - - [TestMethod] - public void ColumnNameAttribute_DtoToTable_UsesAttributeName() - { - var options = new RecordMappingOptions(); - var table = RecordTable.From(new DtoWithColumnName { FullName = "Alice", Age = 30 }, options); - - // 列名应为 full_name 而非 FullName - Assert.IsNotNull(table.Columns.Find("full_name")); - Assert.IsNull(table.Columns.Find("FullName")); - Assert.AreEqual("Alice", table.Columns.Find("full_name")!.To(0)); - } - - [TestMethod] - public void ColumnNameAttribute_TableToDto_UsesAttributeName() - { - var table = new RecordTable(); - table.Columns.Add("full_name"); - table.Columns.Add("Age"); - var row = table.AddRow(); - table.Columns.Find("full_name")!.Set(row, "Bob"); - table.Columns.Find("Age")!.Set(row, 25); - - var options = new RecordMappingOptions(); - var dto = table.To(options); - - Assert.AreEqual("Bob", dto.FullName); - Assert.AreEqual(25, dto.Age); - } - - // ─── NamingPolicy: CamelCase ────────────────────────────────────────────────── - - [TestMethod] - public void NamingPolicy_CamelCase_DtoToTable() - { - var options = new RecordMappingOptions { NamingPolicy = RecordNamingPolicy.CamelCase }; - var table = RecordTable.From(new PersonDto { Id = 1, FullName = "Alice", Age = 30 }, options); - - Assert.IsNotNull(table.Columns.Find("id")); - Assert.IsNotNull(table.Columns.Find("fullName")); - Assert.IsNotNull(table.Columns.Find("age")); - Assert.AreEqual("Alice", table.Columns.Find("fullName")!.To(0)); - } - - [TestMethod] - public void NamingPolicy_CamelCase_TableToDto() - { - var table = new RecordTable(); - table.Columns.Add("id"); - table.Columns.Add("fullName"); - table.Columns.Add("age"); - var row = table.AddRow(); - table.Columns.Find("id")!.Set(row, 42); - table.Columns.Find("fullName")!.Set(row, "Carol"); - table.Columns.Find("age")!.Set(row, 20); - - var options = new RecordMappingOptions { NamingPolicy = RecordNamingPolicy.CamelCase }; - var dto = table.To(options); - - Assert.AreEqual(42, dto.Id); - Assert.AreEqual("Carol", dto.FullName); - Assert.AreEqual(20, dto.Age); - } - - // ─── NamingPolicy: SnakeCaseLower ───────────────────────────────────────────── - - [TestMethod] - public void NamingPolicy_SnakeCaseLower_DtoToTable() - { - var options = new RecordMappingOptions { NamingPolicy = RecordNamingPolicy.SnakeCaseLower }; - var table = RecordTable.From(new PersonDto { Id = 1, FullName = "Dave", Age = 40 }, options); - - Assert.IsNotNull(table.Columns.Find("full_name")); - } - - // ─── NamingPolicy: SnakeCaseUpper ───────────────────────────────────────────── - - [TestMethod] - public void NamingPolicy_SnakeCaseUpper_DtoToTable() - { - var options = new RecordMappingOptions { NamingPolicy = RecordNamingPolicy.SnakeCaseUpper }; - var table = RecordTable.From(new PersonDto { Id = 1, FullName = "Eve", Age = 50 }, options); - - Assert.IsNotNull(table.Columns.Find("FULL_NAME")); - } - - // ─── NamingPolicy: KebabCaseLower ──────────────────────────────────────────── - - [TestMethod] - public void NamingPolicy_KebabCaseLower_DtoToTable() - { - var options = new RecordMappingOptions { NamingPolicy = RecordNamingPolicy.KebabCaseLower }; - var table = RecordTable.From(new PersonDto { Id = 1, FullName = "Frank", Age = 55 }, options); - - Assert.IsNotNull(table.Columns.Find("full-name")); - } - - // ─── NamingPolicy: KebabCaseUpper ──────────────────────────────────────────── - - [TestMethod] - public void NamingPolicy_KebabCaseUpper_DtoToTable() - { - var options = new RecordMappingOptions { NamingPolicy = RecordNamingPolicy.KebabCaseUpper }; - var table = RecordTable.From(new PersonDto { Id = 1, FullName = "Grace", Age = 60 }, options); - - Assert.IsNotNull(table.Columns.Find("FULL-NAME")); - } - - // ─── ColumnNameAttribute 优先级高于 NamingPolicy ────────────────────────────── - - [TestMethod] - public void ColumnNameAttribute_HasHigherPriorityThan_NamingPolicy() - { - // NamingPolicy 会把 FullName → fullName,但 RecordColumnNameAttribute 指定了 full_name - var options = new RecordMappingOptions { NamingPolicy = RecordNamingPolicy.CamelCase }; - var table = RecordTable.From(new DtoWithColumnName { FullName = "Heidi", Age = 35 }, options); - - // 应优先使用 Attribute 的 full_name,而非 NamingPolicy 的 fullName - Assert.IsNotNull(table.Columns.Find("full_name")); - Assert.IsNull(table.Columns.Find("fullName")); - } - - // ─── UnsupportedTypeHandling.Skip ──────────────────────────────────────────── - - [TestMethod] - public void UnsupportedTypeHandling_Skip_DoesNotThrow() - { - var options = new RecordMappingOptions - { - UnsupportedTypeHandling = UnsupportedTypeHandling.Skip - }; - var table = new RecordTable(); - table.Columns.Add("Id"); - var dto = new DtoWithUnsupportedType { Id = 7, Extra = new object() }; - var row = table.AddRowFrom(dto, options); - // 不抛出异常,只写入了支持的列 - Assert.AreEqual(7, table.Columns.Find("Id")!.To(0)); - } - - // ─── UnsupportedTypeHandling.Throw ─────────────────────────────────────────── - - [TestMethod] - public void UnsupportedTypeHandling_Throw_ThrowsNotSupportedException() - { - var options = new RecordMappingOptions - { - UnsupportedTypeHandling = UnsupportedTypeHandling.Throw - }; - var table = new RecordTable(); - Assert.Throws(() => - table.Columns.AddFrom(options)); - } - - // ─── ConversionFailureHandling.Skip ────────────────────────────────────────── - - [TestMethod] - public void ConversionFailureHandling_Skip_KeepsDefault() - { - var table = new RecordTable(); - table.Columns.Add("Id"); // 列为 string,但属性为 int - table.Columns.Add("FullName"); - table.Columns.Add("Age"); - var row = table.AddRow(); - table.Columns.Find("Id")!.Set(row, "not_a_number"); - table.Columns.Find("FullName")!.Set(row, "Ivan"); - table.Columns.Find("Age")!.Set(row, 28); - - var options = new RecordMappingOptions - { - NamingPolicy = null, - ConversionFailureHandling = ConversionFailureHandling.Skip - }; - var dto = table.To(options); - - // Id 转换失败,保持默认值 0;其他属性正常 - Assert.AreEqual(0, dto.Id); - Assert.AreEqual("Ivan", dto.FullName); - Assert.AreEqual(28, dto.Age); - } - - // ─── ConversionFailureHandling.Throw ───────────────────────────────────────── - - [TestMethod] - public void ConversionFailureHandling_Throw_ThrowsOnFailure() - { - var table = new RecordTable(); - table.Columns.Add("Id"); - table.Columns.Add("FullName"); - table.Columns.Add("Age"); - var row = table.AddRow(); - table.Columns.Find("Id")!.Set(row, "not_a_number"); - table.Columns.Find("FullName")!.Set(row, "Jack"); - table.Columns.Find("Age")!.Set(row, 33); - - var options = new RecordMappingOptions - { - ConversionFailureHandling = ConversionFailureHandling.Throw - }; - Assert.Throws(() => table.To(options)); - } - - // ─── Options 冻结机制 ───────────────────────────────────────────────────────── - - [TestMethod] - public void Options_CanBeModified_BeforeMapping() - { - var options = new RecordMappingOptions(); - options.NamingPolicy = RecordNamingPolicy.CamelCase; - options.UnsupportedTypeHandling = UnsupportedTypeHandling.Throw; - options.ConversionFailureHandling = ConversionFailureHandling.Throw; - // 不抛出异常即为通过 - Assert.IsFalse(options.IsReadOnly); - } - - [TestMethod] - public void Options_CannotBeModified_AfterMapping() - { - var options = new RecordMappingOptions(); - var table = RecordTable.From(new PersonDto { Id = 1, FullName = "Kate", Age = 22 }, options); - Assert.IsTrue(options.IsReadOnly); - Assert.Throws(() => - options.NamingPolicy = RecordNamingPolicy.SnakeCaseLower); - } - - [TestMethod] - public void Default_CannotBeModified_AfterMapping() - { - // 触发一次映射使 Default 被冻结 - var _ = RecordTable.From(new PersonDto { Id = 1 }, RecordMappingOptions.Default); - Assert.IsTrue(RecordMappingOptions.Default.IsReadOnly); - Assert.Throws(() => - RecordMappingOptions.Default.NamingPolicy = RecordNamingPolicy.CamelCase); - } - - // ─── RecordColumnStorageAttribute ────────────────────────────────────────────── - - private class ComplexTag - { - public string Value { get; set; } = "default"; - public override string ToString() => Value; - } - - [TestMethod] - public void StorageAttribute_Skip_SkipsProperty() - { - // 属性上标注 [RecordColumnStorage(Skip)] 时,该列不应被创建 - var dto = new DtoWithSkipTag { Name = "Bob", Secret = new ComplexTag { Value = "hidden" } }; - var table = RecordTable.From(dto); - Assert.IsNull(table.Columns.Find("Secret")); - Assert.IsNotNull(table.Columns.Find("Name")); - } - - private class DtoWithSkipTag - { - public string Name { get; set; } = ""; - - [RecordColumnStorage(RecordColumnStorageTarget.Skip)] - public ComplexTag Secret { get; set; } = new(); - } -} diff --git a/tests/LuYao.Common.UnitTests/Data/RecordMappingTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordMappingTests.cs deleted file mode 100644 index 65a0022..0000000 --- a/tests/LuYao.Common.UnitTests/Data/RecordMappingTests.cs +++ /dev/null @@ -1,215 +0,0 @@ -namespace LuYao.Data; - -[TestClass] -public class RecordMappingTests -{ - private sealed class TestModel - { - public int Id { get; set; } - public string? Name { get; set; } - } - - // ── AddRows ────────────────────────────────────────────────────────────── - - [TestMethod] - public void AddRowsFromList_ShouldAppendAllItemsAsRows() - { - var list = new List - { - new TestModel { Id = 1, Name = "Name1" }, - new TestModel { Id = 2, Name = "Name2" } - }; - - var table = new RecordTable(); - table.Columns.AddFrom(); - table.AddRowsFromList(list); - - Assert.AreEqual(2, table.Count); - Assert.AreEqual(1, table[0]["Id"]); - Assert.AreEqual("Name1", table[0]["Name"]); - Assert.AreEqual(2, table[1]["Id"]); - Assert.AreEqual("Name2", table[1]["Name"]); - } - - // ── From ────────────────────────────────────────────────────────────── - - [TestMethod] - public void From_ShouldCreateRecordWithSingleRow() - { - var model = new TestModel { Id = 42, Name = "Alice" }; - - var table = RecordTable.From(model); - - Assert.AreEqual(1, table.Count); - Assert.AreEqual(42, table[0]["Id"]); - Assert.AreEqual("Alice", table[0]["Name"]); - } - - [TestMethod] - public void From_WithOutParameter_ShouldCreateRecordAndReturnRow() - { - var model = new TestModel { Id = 99, Name = "Bob" }; - - var table = RecordTable.From(model, out var row); - - Assert.AreEqual(1, table.Count); - Assert.IsNotNull(row); - Assert.AreEqual(0, row.Row); - Assert.AreEqual(99, row["Id"]); - Assert.AreEqual("Bob", row["Name"]); - } - - // ── FromList ─────────────────────────────────────────────────────────── - - [TestMethod] - public void FromList_ShouldCreateRecordWithAllRows() - { - var list = new List - { - new TestModel { Id = 1, Name = "Alice" }, - new TestModel { Id = 2, Name = "Bob" }, - new TestModel { Id = 3, Name = "Carol" } - }; - - var table = RecordTable.FromList(list); - - Assert.AreEqual(3, table.Count); - Assert.AreEqual(1, table[0]["Id"]); - Assert.AreEqual("Alice", table[0]["Name"]); - Assert.AreEqual(3, table[2]["Id"]); - Assert.AreEqual("Carol", table[2]["Name"]); - } - - [TestMethod] - public void FromList_WithEmptyCollection_ShouldCreateRecordWithNoRows() - { - var table = RecordTable.FromList(new List()); - - Assert.AreEqual(0, table.Count); - } - - // ── To ───────────────────────────────────────────────────────────────── - - [TestMethod] - public void To_WithRows_ShouldMapFirstRowToModel() - { - var table = new RecordTable("Users", 2); - var idCol = table.Columns.Add("Id"); - var nameCol = table.Columns.Add("Name"); - - var row1 = table.AddRow(); - idCol.SetValue(row1.Row, 1); - nameCol.SetValue(row1.Row, "Alice"); - - var row2 = table.AddRow(); - idCol.SetValue(row2.Row, 2); - nameCol.SetValue(row2.Row, "Bob"); - - var model = table.To(); - - Assert.AreEqual(1, model.Id); - Assert.AreEqual("Alice", model.Name); - } - - [TestMethod] - public void To_WithNoRows_ShouldReturnDefaultInstance() - { - var table = new RecordTable("Users", 0); - table.Columns.Add("Id"); - table.Columns.Add("Name"); - - var model = table.To(); - - Assert.IsNotNull(model); - Assert.AreEqual(0, model.Id); - Assert.IsNull(model.Name); - } - - // ── ToList ───────────────────────────────────────────────────────────── - - [TestMethod] - public void ToList_ShouldMapAllRowsToModels() - { - var source = new List - { - new TestModel { Id = 1, Name = "Alice" }, - new TestModel { Id = 2, Name = "Bob" } - }; - var table = RecordTable.FromList(source); - - var result = table.ToList(); - - Assert.AreEqual(2, result.Count); - Assert.AreEqual(1, result[0].Id); - Assert.AreEqual("Alice", result[0].Name); - Assert.AreEqual(2, result[1].Id); - Assert.AreEqual("Bob", result[1].Name); - } - - [TestMethod] - public void ToList_WithNoRows_ShouldReturnEmptyList() - { - var table = new RecordTable(); - table.Columns.AddFrom(); - - var result = table.ToList(); - - Assert.AreEqual(0, result.Count); - } - - // ── ToDictionary ───────────────────────────────────────────────── - - [TestMethod] - public void ToDictionary_ShouldMapFirstColumnAsKey() - { - var list = new List - { - new TestModel { Id = 1, Name = "Alice" }, - new TestModel { Id = 2, Name = "Bob" } - }; - var table = RecordTable.FromList(list); - - var dict = table.ToDictionary(); - - Assert.AreEqual(2, dict.Count); - Assert.AreEqual("Alice", dict[1].Name); - Assert.AreEqual("Bob", dict[2].Name); - } - - [TestMethod] - public void ToDictionary_WithDuplicateKeys_ShouldUseLastValue() - { - var list = new List - { - new TestModel { Id = 1, Name = "First" }, - new TestModel { Id = 1, Name = "Second" } - }; - var table = RecordTable.FromList(list); - - var dict = table.ToDictionary(); - - Assert.AreEqual(1, dict.Count); - Assert.AreEqual("Second", dict[1].Name); - } - - [TestMethod] - public void ToDictionary_WithNoRows_ShouldReturnEmptyDictionary() - { - var table = new RecordTable(); - table.Columns.AddFrom(); - - var dict = table.ToDictionary(); - - Assert.AreEqual(0, dict.Count); - } - - [TestMethod] - public void ToDictionary_WithNoColumns_ShouldReturnEmptyDictionary() - { - var table = new RecordTable(); - - var dict = table.ToDictionary(); - - Assert.AreEqual(0, dict.Count); - } -} diff --git a/tests/LuYao.Common.UnitTests/Data/RecordQueryTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordQueryTests.cs index ecba534..3009f76 100644 --- a/tests/LuYao.Common.UnitTests/Data/RecordQueryTests.cs +++ b/tests/LuYao.Common.UnitTests/Data/RecordQueryTests.cs @@ -129,48 +129,6 @@ public void FindAll_WithValidPredicate_ReturnsAllMatches() Assert.AreEqual(3, results[1].To("Id")); } - [TestMethod] - public void FindByDynamic_WithValidPredicate_ReturnsFirstMatch() - { - // Arrange - var table = CreateTestRecord(); - - // Act - var result = table.FindByDynamic(d => d.Name == "Charlie"); - - // Assert - Assert.IsNotNull(result); - Assert.AreEqual(3, result.Value.To("Id")); - } - - [TestMethod] - public void FindByDynamic_WithInvalidPredicate_ReturnsNull() - { - // Arrange - var table = CreateTestRecord(); - - // Act - var result = table.FindByDynamic(d => d.Id == 99); - - // Assert - Assert.IsNull(result); - } - - [TestMethod] - public void FindAllByDynamic_WithValidPredicate_ReturnsAllMatches() - { - // Arrange - var table = CreateTestRecord(); - - // Act - var results = table.FindAllByDynamic(d => d.IsActive == true).ToList(); - - // Assert - Assert.AreEqual(2, results.Count); - Assert.AreEqual(1, results[0].To("Id")); - Assert.AreEqual(3, results[1].To("Id")); - } - [TestMethod] public void Find_ObjectOverload_ReturnsFirstMatchingRow() { diff --git a/tests/LuYao.Common.UnitTests/Data/RecordRowMergeTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordRowMergeTests.cs deleted file mode 100644 index 8726b80..0000000 --- a/tests/LuYao.Common.UnitTests/Data/RecordRowMergeTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace LuYao.Data; - -/// -/// 测试 和 方法。 -/// -[TestClass] -public class RecordRowMergeTests -{ - // ------------------------------------------------------------------ - // Merge(RecordRow other) - // ------------------------------------------------------------------ - - /// - /// 同名列的值应被覆盖。 - /// - [TestMethod] - public void Merge_Row_OverwritesExistingColumn() - { - // Arrange - var r1 = new RecordTable(); - r1.Columns.Add("Name"); - var row1 = r1.AddRow(); - row1["Name"] = "Alice"; - - var r2 = new RecordTable(); - r2.Columns.Add("Name"); - var row2 = r2.AddRow(); - row2["Name"] = "Bob"; - - // Act - row1.Merge(row2); - - // Assert - Assert.AreEqual("Bob", row1["Name"]); - } - - /// - /// r2 中存在而 r1 中不存在的列应被追加。 - /// - [TestMethod] - public void Merge_Row_AppendsNewColumn() - { - // Arrange - var r1 = new RecordTable(); - r1.Columns.Add("Name"); - var row1 = r1.AddRow(); - row1["Name"] = "Alice"; - - var r2 = new RecordTable(); - r2.Columns.Add("Age"); - var row2 = r2.AddRow(); - row2["Age"] = 30; - - // Act - row1.Merge(row2); - - // Assert - Assert.AreEqual(30, row1["Age"]); - Assert.AreEqual("Alice", row1["Name"]); - } - - /// - /// r2 既有同名列也有新列时,应同时覆盖和追加。 - /// - [TestMethod] - public void Merge_Row_OverwritesAndAppends() - { - // Arrange - var r1 = new RecordTable(); - r1.Columns.Add("Name"); - r1.Columns.Add("Score"); - var row1 = r1.AddRow(); - row1["Name"] = "Alice"; - row1["Score"] = 80; - - var r2 = new RecordTable(); - r2.Columns.Add("Name"); - r2.Columns.Add("City"); - var row2 = r2.AddRow(); - row2["Name"] = "Bob"; - row2["City"] = "Beijing"; - - // Act - row1.Merge(row2); - - // Assert - Assert.AreEqual("Bob", row1["Name"]); - Assert.AreEqual(80, row1["Score"]); - Assert.AreEqual("Beijing", row1["City"]); - } - - /// - /// r2 为空列时,r1 不应发生任何变化。 - /// - [TestMethod] - public void Merge_Row_EmptySource_NoChange() - { - // Arrange - var r1 = new RecordTable(); - r1.Columns.Add("Name"); - var row1 = r1.AddRow(); - row1["Name"] = "Alice"; - - var r2 = new RecordTable(); - var row2 = r2.AddRow(); - - // Act - row1.Merge(row2); - - // Assert - Assert.AreEqual("Alice", row1["Name"]); - Assert.AreEqual(1, r1.Columns.Count); - } - - // ------------------------------------------------------------------ - // Merge(T model) - // ------------------------------------------------------------------ - - private class PersonModel - { - public string Name { get; set; } = string.Empty; - public int Age { get; set; } - } - - /// - /// 同名列的值应被模型属性值覆盖。 - /// - [TestMethod] - public void Merge_Model_OverwritesExistingColumn() - { - // Arrange - var r1 = new RecordTable(); - r1.Columns.Add("Name"); - var row1 = r1.AddRow(); - row1["Name"] = "Alice"; - - // Act - row1.Merge(new PersonModel { Name = "Bob", Age = 25 }); - - // Assert - Assert.AreEqual("Bob", row1["Name"]); - } - - /// - /// 模型中存在而行中不存在的属性对应列应被追加。 - /// - [TestMethod] - public void Merge_Model_AppendsNewColumn() - { - // Arrange - var r1 = new RecordTable(); - r1.Columns.Add("Name"); - var row1 = r1.AddRow(); - row1["Name"] = "Alice"; - - // Act - row1.Merge(new PersonModel { Name = "Alice", Age = 25 }); - - // Assert - Assert.AreEqual(25, row1["Age"]); - } - - /// - /// 传入 null 时应抛出 。 - /// - [TestMethod] - public void Merge_Model_NullModel_Throws() - { - // Arrange - var r1 = new RecordTable(); - var row1 = r1.AddRow(); - - // Act & Assert - Assert.Throws(() => row1.Merge(null!)); - } -} diff --git a/tests/LuYao.Common.UnitTests/Data/RecordRowSemanticsTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordRowSemanticsTests.cs index 7126bb9..922157d 100644 --- a/tests/LuYao.Common.UnitTests/Data/RecordRowSemanticsTests.cs +++ b/tests/LuYao.Common.UnitTests/Data/RecordRowSemanticsTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using LuYao.Data.Meta; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace LuYao.Data; @@ -58,147 +57,4 @@ public void Set_ColumnExistsSameType_WritesValue() row["Name"] = "abc"; Assert.AreEqual("abc", row.To("Name")); } - - [TestMethod] - public void Dynamic_SetMember_AutoCreatesColumnByRuntimeType() - { - var table = new RecordTable(); - dynamic dto = table.AddRow(); - dto.Id = 100; - dto.Name = "abc"; - - Assert.AreEqual(2, table.Columns.Count); - Assert.AreEqual(typeof(int), table.Columns.Get("Id").Type); - Assert.AreEqual(typeof(string), table.Columns.Get("Name").Type); - - var row = table[0]; - Assert.AreEqual(100, row.To("Id")); - Assert.AreEqual("abc", row.To("Name")); - } - - [TestMethod] - public void Dynamic_SetMember_NullAndColumnMissing_IsSkipped() - { - var table = new RecordTable(); - dynamic dto = table.AddRow(); - dto.Tag = null; - Assert.AreEqual(0, table.Columns.Count); - } - - [TestMethod] - public void Dynamic_SetMember_NullOnExistingColumn_IsWritten() - { - var table = new RecordTable(); - table.Columns.Add("Name"); - dynamic dto = table.AddRow(); - dto.Name = "x"; - dto.Name = null; - Assert.IsNull(((RecordRow)dto).To("Name")); - } - - [TestMethod] - public void Dynamic_SetIndex_AutoCreatesColumn() - { - var table = new RecordTable(); - dynamic dto = table.AddRow(); - dto["Id"] = 7; - Assert.AreEqual(1, table.Columns.Count); - Assert.AreEqual(7, table[0].To("Id")); - } - - // ── MapTo(object) non-generic overload ───────────────────────────────────────── - - private class CopyToBase - { - public int Id { get; set; } - public string? Name { get; set; } - } - - private class CopyToDerived : CopyToBase - { - public double Score { get; set; } - } - - [TestMethod] - public void CopyTo_NonGeneric_FillsBaseProps() - { - var table = new RecordTable(); - table.Columns.Add("Id"); - table.Columns.Add("Name"); - var row = table.AddRow(); - table.Columns.Get("Id").Set(row, 10); - table.Columns.Get("Name").Set(row, "Alice"); - - var obj = new CopyToBase(); - row.MapTo((object)obj); - - Assert.AreEqual(10, obj.Id); - Assert.AreEqual("Alice", obj.Name); - } - - [TestMethod] - public void CopyTo_NonGeneric_FillsDerivedPropsViaBaseRef() - { - var table = new RecordTable(); - table.Columns.Add("Id"); - table.Columns.Add("Score"); - var row = table.AddRow(); - table.Columns.Get("Id").Set(row, 20); - table.Columns.Get("Score").Set(row, 9.9); - - // Base reference holds a derived instance; runtime type should be correctly identified - CopyToBase obj = new CopyToDerived(); - row.MapTo((object)obj); - - Assert.AreEqual(20, obj.Id); - Assert.AreEqual(9.9, ((CopyToDerived)obj).Score); - } - - [TestMethod] - public void CopyTo_NonGeneric_MissingColumn_SilentlySkips() - { - var table = new RecordTable(); - table.Columns.Add("Id"); - var row = table.AddRow(); - table.Columns.Get("Id").Set(row, 5); - - var obj = new CopyToBase { Name = "original" }; - row.MapTo((object)obj); - - Assert.AreEqual(5, obj.Id); - Assert.AreEqual("original", obj.Name); // Name column does not exist; value should remain unchanged - } - - [TestMethod] - public void CopyTo_NonGeneric_NullData_ThrowsArgumentNullException() - { - var table = new RecordTable(); - var row = table.AddRow(); - - try - { - row.MapTo((object)null!); - Assert.Fail("Expected ArgumentNullException"); - } - catch (ArgumentNullException) { } - } - - [TestMethod] - public void Mapping_CopyFromObject_DoesNotAutoCreateColumns() - { - // Expected: missing column is silently skipped; no column is created - var table = new RecordTable(); - table.Columns.Add("Id"); // Only declare Id column; Name is intentionally absent - var row = table.AddRow(); - row.MapFrom(new MappingDto { Id = 9, Name = "ignored" }); - - Assert.AreEqual(1, table.Columns.Count); - Assert.AreEqual(9, row.To("Id")); - } - - private sealed class MappingDto - { - public int Id { get; set; } - public string Name { get; set; } = string.Empty; - } } diff --git a/tests/LuYao.Common.UnitTests/Data/RecordSerializationTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordSerializationTests.cs deleted file mode 100644 index fa4cbf5..0000000 --- a/tests/LuYao.Common.UnitTests/Data/RecordSerializationTests.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System; -using System.IO; - -namespace LuYao.Data; - -[TestClass] -public class RecordSerializationTests -{ - private enum FavoriteType - { - Common = 100, - Chat = 200, - } - - private static RecordTable CreateTestRecord() - { - var table = new RecordTable("Orders", 2); - var idCol = table.Columns.Add("Id"); - var nameCol = table.Columns.Add("Name"); - table.Page = 2; - table.PageSize = 10; - table.MaxCount = 50; - - var row1 = table.AddRow(); - idCol.SetValue(row1.Row, 1); - nameCol.SetValue(row1.Row, "Order-1"); - - var row2 = table.AddRow(); - idCol.SetValue(row2.Row, 2); - nameCol.SetValue(row2.Row, "Order-2"); - - return table; - } - - #region Binary Serialization - - [TestMethod] - public void WhenBinaryRoundTripThenDataPreserved() - { - var original = CreateTestRecord(); - - var bytes = original.ToBytes(); - var deserialized = RecordTable.FromBytes(bytes); - - Assert.AreEqual("Orders", deserialized.Name); - Assert.AreEqual(2, deserialized.Count); - Assert.AreEqual(2, deserialized.Columns.Count); - Assert.AreEqual(2, deserialized.Page); - Assert.AreEqual(10, deserialized.PageSize); - Assert.AreEqual(50, deserialized.MaxCount); - - Assert.AreEqual(1, deserialized.Columns.Find("Id")!.Get(0)); - Assert.AreEqual("Order-1", deserialized.Columns.Find("Name")!.Get(0)); - Assert.AreEqual(2, deserialized.Columns.Find("Id")!.Get(1)); - Assert.AreEqual("Order-2", deserialized.Columns.Find("Name")!.Get(1)); - } - - [TestMethod] - public void WhenBinaryRoundTripEmptyRecordThenSchemaPreserved() - { - var original = new RecordTable("Empty", 0); - original.Columns.Add("Id"); - original.Columns.Add("Name"); - - var bytes = original.ToBytes(); - var deserialized = RecordTable.FromBytes(bytes); - - Assert.AreEqual("Empty", deserialized.Name); - Assert.AreEqual(0, deserialized.Count); - Assert.AreEqual(2, deserialized.Columns.Count); - Assert.AreEqual("Id", deserialized.Columns[0].Name); - Assert.AreEqual(typeof(int), deserialized.Columns[0].Type); - } - - [TestMethod] - public void WhenBinaryRoundTripWithNullValuesThenPreserved() - { - var table = new RecordTable("Test", 1); - table.Columns.Add("Name"); - table.Columns.Add("Value"); - table.AddRow(); - - var bytes = table.ToBytes(); - var deserialized = RecordTable.FromBytes(bytes); - - Assert.AreEqual(1, deserialized.Count); - Assert.IsNull(deserialized.Columns[0].Get(0)); - Assert.IsNull(deserialized.Columns[1].Get(0)); - } - - [TestMethod] - public void WhenBinaryRoundTripWithByteArrayThenPreserved() - { - var table = new RecordTable("Binary", 1); - var col = table.Columns.Add("Data"); - - Assert.AreEqual(RecordColumnType.Binary, col.ColumnType); - Assert.AreEqual(typeof(byte[]), col.Type); - - var row = table.AddRow(); - col.SetValue(row.Row, new byte[] { 1, 2, 3, 4, 5 }); - - var bytes = table.ToBytes(); - var deserialized = RecordTable.FromBytes(bytes); - - var deserializedCol = deserialized.Columns.Find("Data")!; - Assert.AreEqual(RecordColumnType.Binary, deserializedCol.ColumnType); - - var data = deserializedCol.GetValue(0); - CollectionAssert.AreEqual(new byte[] { 1, 2, 3, 4, 5 }, data); - } - - [TestMethod] - public void WhenBinaryRoundTripWithAllTypesThenPreserved() - { - var table = new RecordTable("AllTypes", 1); - table.Columns.Add("Bool"); - table.Columns.Add("SByte"); - table.Columns.Add("Short"); - table.Columns.Add("Int"); - table.Columns.Add("Long"); - table.Columns.Add("Byte"); - table.Columns.Add("UShort"); - table.Columns.Add("UInt"); - table.Columns.Add("ULong"); - table.Columns.Add("Float"); - table.Columns.Add("Double"); - table.Columns.Add("Decimal"); - table.Columns.Add("Char"); - table.Columns.Add("String"); - table.Columns.Add("DateTime"); - table.Columns.Add("DateTimeOffset"); - table.Columns.Add("TimeSpan"); - table.Columns.Add("Guid"); - - var r = table.AddRow(); - table.Columns.Find("Bool")!.SetValue(r.Row, true); - table.Columns.Find("SByte")!.SetValue(r.Row, -1); - table.Columns.Find("Short")!.SetValue(r.Row, short.MaxValue); - table.Columns.Find("Int")!.SetValue(r.Row, 42); - table.Columns.Find("Long")!.SetValue(r.Row, long.MaxValue); - table.Columns.Find("Byte")!.SetValue(r.Row, 255); - table.Columns.Find("UShort")!.SetValue(r.Row, ushort.MaxValue); - table.Columns.Find("UInt")!.SetValue(r.Row, uint.MaxValue); - table.Columns.Find("ULong")!.SetValue(r.Row, ulong.MaxValue); - table.Columns.Find("Float")!.SetValue(r.Row, 3.14f); - table.Columns.Find("Double")!.SetValue(r.Row, 3.14159265); - table.Columns.Find("Decimal")!.SetValue(r.Row, 99.99m); - table.Columns.Find("Char")!.SetValue(r.Row, 'A'); - table.Columns.Find("String")!.SetValue(r.Row, "Hello"); - var dt = new DateTime(2024, 1, 15, 10, 30, 0, DateTimeKind.Utc); - table.Columns.Find("DateTime")!.SetValue(r.Row, dt); - var dto = new DateTimeOffset(2024, 1, 15, 10, 30, 0, TimeSpan.FromHours(8)); - table.Columns.Find("DateTimeOffset")!.SetValue(r.Row, dto); - table.Columns.Find("TimeSpan")!.SetValue(r.Row, TimeSpan.FromHours(2.5)); - var guid = Guid.NewGuid(); - table.Columns.Find("Guid")!.SetValue(r.Row, guid); - - var bytes = table.ToBytes(); - var d = RecordTable.FromBytes(bytes); - - Assert.AreEqual(true, d.Columns.Find("Bool")!.Get(0)); - Assert.AreEqual((sbyte)-1, d.Columns.Find("SByte")!.Get(0)); - Assert.AreEqual(short.MaxValue, d.Columns.Find("Short")!.Get(0)); - Assert.AreEqual(42, d.Columns.Find("Int")!.Get(0)); - Assert.AreEqual(long.MaxValue, d.Columns.Find("Long")!.Get(0)); - Assert.AreEqual((byte)255, d.Columns.Find("Byte")!.Get(0)); - Assert.AreEqual(ushort.MaxValue, d.Columns.Find("UShort")!.Get(0)); - Assert.AreEqual(uint.MaxValue, d.Columns.Find("UInt")!.Get(0)); - Assert.AreEqual(ulong.MaxValue, d.Columns.Find("ULong")!.Get(0)); - Assert.AreEqual(3.14f, d.Columns.Find("Float")!.Get(0)); - Assert.AreEqual(3.14159265, d.Columns.Find("Double")!.Get(0)); - Assert.AreEqual(99.99m, d.Columns.Find("Decimal")!.Get(0)); - Assert.AreEqual('A', d.Columns.Find("Char")!.Get(0)); - Assert.AreEqual("Hello", d.Columns.Find("String")!.Get(0)); - Assert.AreEqual(dt, d.Columns.Find("DateTime")!.Get(0)); - Assert.AreEqual(dto, d.Columns.Find("DateTimeOffset")!.Get(0)); - Assert.AreEqual(TimeSpan.FromHours(2.5), d.Columns.Find("TimeSpan")!.Get(0)); - Assert.AreEqual(guid, d.Columns.Find("Guid")!.Get(0)); - } - - [TestMethod] - public void WhenBinaryRoundTripWithEnumColumnThenPreserved() - { - var table = new RecordTable("EnumRecord", 1); - var favoriteCol = table.Columns.Add("Favorite"); - var nullableFavoriteCol = table.Columns.Add("NullableFavorite"); - var row = table.AddRow(); - favoriteCol.SetValue(row.Row, FavoriteType.Chat); - nullableFavoriteCol.SetValue(row.Row, FavoriteType.Common); - - var bytes = table.ToBytes(); - var deserialized = RecordTable.FromBytes(bytes); - - Assert.AreEqual(200, deserialized.Columns.Find("Favorite")!.Get(0)); - Assert.AreEqual(100, deserialized.Columns.Find("NullableFavorite")!.Get(0)); - } - - [TestMethod] - public void WhenSerializeToBytesThenRecordHeaderContainsRecordType() - { - var table = new RecordTable("Orders", 1); - table.Columns.Add("Id"); - table.AddRow(); - - var bytes = table.ToBytes(); - - Assert.IsTrue(bytes.Length >= 5); - Assert.AreEqual((byte)0xFF, bytes[0]); - Assert.AreEqual((byte)'L', bytes[1]); - Assert.AreEqual((byte)'Y', bytes[2]); - Assert.AreEqual((byte)1, bytes[3]); - } - - [TestMethod] - public void WhenReadRecordFromRecordSetPayloadThenThrowTypeMismatch() - { - var set = new RecordSet(); - set.Add("Orders", new RecordTable("Orders", 0)); - - var bytes = set.ToBytes(); - - Assert.Throws(() => RecordTable.FromBytes(bytes)); - } - - [TestMethod] - public void IsBinaryPayload_DetectsRecordAndRecordSetCorrectly() - { - var table = new RecordTable("Orders", 0); - var set = new RecordSet(); - set.Add("Orders", new RecordTable("Orders", 0)); - - var recordBytes = table.ToBytes(); - var setBytes = set.ToBytes(); - - Assert.IsTrue(RecordTable.IsBinaryPayload(recordBytes)); - Assert.IsFalse(RecordTable.IsBinaryPayload(setBytes)); - Assert.IsFalse(RecordTable.IsBinaryPayload(new byte[] { 1, 2, 3, 4 })); - Assert.IsFalse(RecordTable.IsBinaryPayload((byte[])null)); - } - - #endregion -} - -[TestClass] -public class RecordSetSerializationTests -{ - #region Binary Serialization - - [TestMethod] - public void WhenBinaryRoundTripThenAllRecordsPreserved() - { - var set = new RecordSet(); - - var orders = new RecordTable("Orders", 1); - orders.Columns.Add("Id"); - var row = orders.AddRow(); - orders.Columns[0].Set(row.Row, 1); - set.Add("Orders", orders); - - var customers = new RecordTable("Customers", 1); - customers.Columns.Add("Name"); - var row2 = customers.AddRow(); - customers.Columns[0].Set(row2.Row, "Alice"); - set.Add("Customers", customers); - - var bytes = set.ToBytes(); - var deserialized = RecordSet.FromBytes(bytes); - - Assert.AreEqual(2, deserialized.Count); - Assert.IsTrue(deserialized.Contains("Orders")); - Assert.IsTrue(deserialized.Contains("Customers")); - Assert.AreEqual(1, deserialized["Orders"].Count); - Assert.AreEqual(1, deserialized["Customers"].Count); - } - - [TestMethod] - public void WhenBinaryRoundTripEmptySetThenEmpty() - { - var set = new RecordSet(); - - var bytes = set.ToBytes(); - var deserialized = RecordSet.FromBytes(bytes); - - Assert.AreEqual(0, deserialized.Count); - } - - [TestMethod] - public void WhenSerializeToBytesThenRecordSetHeaderContainsRecordSetType() - { - var set = new RecordSet(); - set.Add("Orders", new RecordTable("Orders", 0)); - - var bytes = set.ToBytes(); - - Assert.IsTrue(bytes.Length >= 5); - Assert.AreEqual((byte)0xFF, bytes[0]); - Assert.AreEqual((byte)'L', bytes[1]); - Assert.AreEqual((byte)'Y', bytes[2]); - Assert.AreEqual((byte)2, bytes[3]); - } - - [TestMethod] - public void WhenReadRecordSetFromRecordPayloadThenThrowTypeMismatch() - { - var table = new RecordTable("Orders", 0); - var bytes = table.ToBytes(); - - Assert.Throws(() => RecordSet.FromBytes(bytes)); - } - - [TestMethod] - public void IsBinaryPayload_DetectsRecordSetAndRecordCorrectly() - { - var table = new RecordTable("Orders", 0); - var set = new RecordSet(); - set.Add("Orders", new RecordTable("Orders", 0)); - - var recordBytes = table.ToBytes(); - var setBytes = set.ToBytes(); - - Assert.IsTrue(RecordSet.IsBinaryPayload(setBytes)); - Assert.IsFalse(RecordSet.IsBinaryPayload(recordBytes)); - Assert.IsFalse(RecordSet.IsBinaryPayload(new byte[] { 0xFF, (byte)'L', (byte)'Y', 9 })); - Assert.IsFalse(RecordSet.IsBinaryPayload((byte[])null)); - } - - #endregion -} diff --git a/tests/LuYao.Common.UnitTests/Data/RecordTableRowTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordTableRowTests.cs index 0f898cb..cd46241 100644 --- a/tests/LuYao.Common.UnitTests/Data/RecordTableRowTests.cs +++ b/tests/LuYao.Common.UnitTests/Data/RecordTableRowTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using LuYao.Data.Meta; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace LuYao.Data; @@ -489,125 +488,6 @@ public void Set_ThenReadViaIndexer_ShouldBeConsistent() Assert.AreEqual(42, recordRow.To("IntColumn")); } - - - /// - /// dynamic 按成员名读取时,应返回正确值。 - /// - [TestMethod] - public void Dynamic_GetMember_ShouldReturnCorrectValue() - { - // Arrange - var (table, _, stringColumn, _) = CreateTestRecord(); - dynamic row = new RecordRow(table, 0); - - // Act - var result = row.StringColumn; - - // Assert - Assert.AreEqual("Test1", result); - } - - /// - /// dynamic 按成员名写入时,应更新底层记录值。 - /// - [TestMethod] - public void Dynamic_SetMember_ShouldUpdateValue() - { - // Arrange - var (table, _, stringColumn, _) = CreateTestRecord(); - dynamic row = new RecordRow(table, 0); - - // Act - row.StringColumn = "DynValue"; - - // Assert - var recordRow = new RecordRow(table, 0); - Assert.AreEqual("DynValue", recordRow.To("StringColumn")); - } - - /// - /// dynamic 按索引器读取时,应返回正确值。 - /// - [TestMethod] - public void Dynamic_GetIndex_ShouldReturnCorrectValue() - { - // Arrange - var (table, intColumn, _, _) = CreateTestRecord(); - dynamic row = new RecordRow(table, 1); - - // Act - dynamic 索引读取 - var result = row["IntColumn"]; - - // Assert - Assert.AreEqual(200, (int)result!); - } - - /// - /// dynamic 按索引器写入时,应更新底层记录值。 - /// - [TestMethod] - public void Dynamic_SetIndex_ShouldUpdateValue() - { - // Arrange - var (table, intColumn, _, _) = CreateTestRecord(); - dynamic row = new RecordRow(table, 0); - - // Act - dynamic 索引写入 - row["IntColumn"] = 777; - - // Assert - var recordRow = new RecordRow(table, 0); - Assert.AreEqual(777, recordRow.To("IntColumn")); - } - - /// - /// dynamic 读取不存在的成员时,应返回 null。 - /// - [TestMethod] - public void Dynamic_GetMember_ColumnNotExists_ShouldReturnNull() - { - // Arrange - var (table, _, _, _) = CreateTestRecord(); - dynamic row = new RecordRow(table, 0); - - // Act - var result = row.NoSuchColumn; - - // Assert - Assert.IsNull(result); - } - - /// - /// dynamic 写入不存在的成员时,不应抛出异常。 - /// - [TestMethod] - public void Dynamic_SetMember_ColumnNotExists_ShouldNotThrow() - { - // Arrange - var (table, _, _, _) = CreateTestRecord(); - dynamic row = new RecordRow(table, 0); - - // Act & Assert - row.NoSuchColumn = "ignored"; // should not throw - } - - /// - /// dynamic 成员读取结果应与显式调用字段读取方法一致。 - /// - [TestMethod] - public void Dynamic_GetMember_ShouldBeConsistentWithGetMethod() - { - // Arrange - var (table, intColumn, _, boolColumn) = CreateTestRecord(); - dynamic row = new RecordRow(table, 0); - var recordRow = new RecordRow(table, 0); - - // Act & Assert - Assert.AreEqual(recordRow.To("IntColumn"), (int)row.IntColumn!); - Assert.AreEqual(recordRow.To("BoolColumn"), (bool)row.BoolColumn!); - } - /// /// 索引器按列名读取已存在列时,应返回对应对象值。 /// @@ -861,3 +741,4 @@ public void ToString_ByName_BoolColumnExists_ShouldReturnStringRepresentation() Assert.AreEqual("True", result); } } + diff --git a/tests/LuYao.Common.UnitTests/Data/RecordTableTests.cs b/tests/LuYao.Common.UnitTests/Data/RecordTableTests.cs index b097f60..2f9e5ee 100644 --- a/tests/LuYao.Common.UnitTests/Data/RecordTableTests.cs +++ b/tests/LuYao.Common.UnitTests/Data/RecordTableTests.cs @@ -1386,141 +1386,4 @@ public void RecordColumn_ToList_TypeMismatch_ThrowsInvalidCastException() Assert.Throws(() => col.ToList()); } - #region Nullable value type column tests - - [TestMethod] - public void NullableColumn_IsNullable_ShouldBeTrue() - { - // Arrange - var table = new RecordTable(); - var col = table.Columns.Add("NullableInt"); - - // Act & Assert - Assert.IsTrue(col.IsNullable); - Assert.AreEqual(typeof(int?), col.Type); - Assert.AreEqual(RecordColumnType.Int32, col.ColumnType); - } - - [TestMethod] - public void NonNullableColumn_IsNullable_ShouldBeFalse() - { - // Arrange - var table = new RecordTable(); - var col = table.Columns.Add("IntCol"); - - // Act & Assert - Assert.IsFalse(col.IsNullable); - } - - [TestMethod] - public void NullableColumn_SetAndGetNullValue_ShouldReturnNull() - { - // Arrange - var table = new RecordTable(); - var col = table.Columns.Add("NullableInt"); - table.AddRow(); - - // Act - col.Set(0, null); - - // Assert - Assert.IsNull(col.Get(0)); - Assert.IsNull(col.To(0)); - } - - [TestMethod] - public void NullableColumn_SetAndGetValue_ShouldReturnValue() - { - // Arrange - var table = new RecordTable(); - var col = table.Columns.Add("NullableInt"); - table.AddRow(); - - // Act - col.Set(0, (int?)42); - - // Assert - int? result = col.To(0); - Assert.AreEqual(42, result); - } - - [TestMethod] - public void NullableColumn_SetNullThenValue_ShouldUpdateCorrectly() - { - // Arrange - var table = new RecordTable(); - var col = table.Columns.Add("NullableInt"); - table.AddRow(); - - // Act - col.Set(0, null); - Assert.IsNull(col.To(0)); - - col.Set(0, (int?)99); - Assert.AreEqual(99, col.To(0)); - } - - [TestMethod] - public void NullableBoolColumn_SetAndGet_ShouldWork() - { - // Arrange - var table = new RecordTable(); - var col = table.Columns.Add("NullableBool"); - table.AddRow(); - table.AddRow(); - - // Act - col.Set(0, (bool?)true); - col.Set(1, null); - - // Assert - Assert.AreEqual(true, col.To(0)); - Assert.IsNull(col.To(1)); - } - - [TestMethod] - public void NullableDateTimeColumn_SetAndGet_ShouldWork() - { - // Arrange - var table = new RecordTable(); - var col = table.Columns.Add("NullableDate"); - table.AddRow(); - table.AddRow(); - - var now = new DateTime(2024, 6, 1, 12, 0, 0); - - // Act - col.Set(0, (DateTime?)now); - col.Set(1, null); - - // Assert - Assert.AreEqual(now, col.To(0)); - Assert.IsNull(col.To(1)); - } - - [TestMethod] - public void NullableColumn_ToList_ShouldContainNullAndValues() - { - // Arrange - var table = new RecordTable(); - var col = table.Columns.Add("NullableInt"); - table.AddRow(); - table.AddRow(); - table.AddRow(); - - col.Set(0, (int?)1); - col.Set(1, null); - col.Set(2, (int?)3); - - // Act - var list = col.ToList(); - - // Assert - Assert.AreEqual(3, list.Count); - Assert.AreEqual(1, list[0]); - Assert.IsNull(list[1]); - Assert.AreEqual(3, list[2]); - } - - #endregion }