Skip to main content

29. 流变对象

📝 模块更新日志
  • 新特性
    •   流变对象支持 string 等类型隐式转换为 Clay 类型 4.9.8.12 ⏱️2026.02.05 55ccc46 35eed37
    •   流变对象支持通过路径检查标识符是否定义 ContainsByPath(path) 4.9.7.246 ⏱️2026.01.14 004b928
    •   流变对象支持 application/x-www-form-urlencoded 表单数据进行转换 4.9.7.212 ⏱️2025.11.26 91a8859
    •   流变对象路径标识符支持 JSON Path 简单语法 4.9.7.137 ⏱️2025.11.07 3bea051
    •   流变对象支持一键处理双重序列化 JSON 字符串方法:ParseJson([maxDepth]) 4.9.7.137 ⏱️2025.11.07 3bea051
    •   流变对象一键配置 JSON 序列化转换器扩展方法(含 dynamic 类型自动转换) 4.9.7.135 ⏱️2025.10.30 0fc8b1b bb6f02c
    •   流变对象根据路径查找 JsonNode 节点的 FindNodeByPath 方法 4.9.7.112 ⏱️2025.08.23 1da56a0
    •   流变对象可通过路径标识符设置值 SetPathValue(path, value) 方法 4.9.7.112 ⏱️2025.08.23 1da56a0
    •   流变对象支持二级解析 ParseJson 方法 4.9.7.112 ⏱️2025.08.23 1da56a0
    •   流变对象 Remove(identifier,isPath)Delete(identifier,isPath) 重载方法 4.9.7.97 ⏱️2025.07.13 f7ad536
    •   流变对象 RemovePathValue/DeletePathValue 根据路径删除值功能 4.9.7.96 ⏱️2025.07.12 ee3ca3c
    •   流变对象支持 Unix epoch 日期格式 4.9.7.77 ⏱️2025.05.31 ca9c94e
    •   流变对象支持自动转换为 string 类型 4.9.7.77 ⏱️2025.05.31 ca9c94e
    •   流变对象路径索引支持 4.9.7.60 ⏱️2025.05.05 9d71584
    •   流变对象管道转换方法异步版本 4.9.7.55 ⏱️2025.04.30 3615c5a
    •   流变对象管道转换方法功能支持 4.9.7.54 ⏱️2025.04.29 f47b2e6
    •   流变对象解构函数(析构表达式)功能支持 4.9.7.53 ⏱️2025.04.28 e4dcc10
    •   流变对象支持 ==!= 操作符比较 4.9.7.47 ⏱️2025.04.20 b40ad5b
    •   流变对象可通过任意对象的 ToClay() 扩展方法进行转换 4.9.7.43 ⏱️2025.04.16 535ff66
    •   流变对象 IndexOf(value) 获取集合或数组中指定项(元素)的索引 4.9.7.43 ⏱️2025.04.16 535ff66
    •   流变对象 HasProperty(properyName) 检查单一对象是否定义属性方法 4.9.7.43 ⏱️2025.04.16 535ff66
    •   流变对象检查是否是 JSON 字符串 IsJsonString(input) 静态方法 4.9.7.38 ⏱️2025.04.06 4be20c7
    •   流变对象可通过 PathValue(path) 方法实现路径语法查找 4.9.7.36 ⏱️2025.04.02 9014096
    •   流变对象支持 MVC 应用 URL 表单内容(application/x-www-form-urlencoded)转流变对象 4.9.7.35 ⏱️2025.03.29 350d39a
    •   流变对象用于从文件中读取内容并转换为流变对象的 Clay.ParseFromFile(path) 静态方法 4.9.7.34 ⏱️2025.03.25 a31abc1
    •   流变对象用于获取单一对象属性名列表的 MemberNames 属性 4.9.7.34 ⏱️2025.03.25 a31abc1
    •   流变对象支持通过 Extend 方法扩展数据 4.9.7.30 ⏱️2025.03.24 b87384e
    •   流变对象反序列化时支持 NumberBoolean 类型转 String 类型 4.9.7.29 ⏱️2025.03.23 4dcd67f
    •   流变对象支持非 ISO 8601-1:2019 标准的时间类型转换 4.9.7.25 ⏱️2025.03.14 3f3d619
    •   流变对象 AddEvent 方法,支持动态订阅数据变更事件 4.9.7.20 ⏱️2025.03.02 5fac30d
    •   流变对象为 Controller 类型添加 ViewClay 扩展方法 4.9.7.17 ⏱️2025.02.28 8133f55
    •   流变对象 ClayOptions.Flexible 静态属性 4.9.7.14 ⏱️2025.02.26 af0d0d8
    •   流变对象的集合或数组支持自动转换为 IEnumerable<dynamic?> 4.9.7.12 ⏱️2025.02.25 f3ca0cd
    •   流变对象支持自动转换为 IActionResult 类型 4.9.7.9 ⏱️2025.02.20 d8366a2
    •   流变对象 Clay.Parse(Object, Action<ClayOptions>) 静态重载方法 4.9.7.8 ⏱️2025.02.18 dbc95fe
    •   流变对象实例支持 MapFilter 映射和筛选方法 4.9.7.5 ⏱️2025.02.09 e499ec3
    •   流变对象实例支持动态合并多个流变对象语法 4.9.7.3 ⏱️2025.02.02 463f038
  • 突破性变化
    •   流变对象 ClayOptions.ScalarValueKey 属性默认值:data -> value 4.9.7.220 ⏱️2025.12.05 b185713
    •   流变对象方法命名:IsJsonString -> IsJsonObjectOrArray 4.9.7.60 ⏱️2025.05.05 9d71584
    •   流变对象 Clay 实现接口,由 IEnumerable<KeyValuePair<object, object?>> -> IEnumerable<object?> 4.9.7.19 ⏱️2025.03.02 ed4159e
    •   流变对象 GetEnumerator() 方法返回值,由 IEnumerable<KeyValuePair<object, dynamic?>> -> IEnumerable<dynamic?> 4.9.7.19 ⏱️2025.03.02 ed4159e
    •   流变对象 AsEnumerateArray 返回值类型,由 IEnumerable<KeyValuePair<int, dynamic?>> -> IEnumerable<dynamic?> 4.9.7.12 ⏱️2025.02.25 f3ca0cd
    •   流变对象方法命名:AsEnumerableObject -> AsEnumerateObjectAsEnumerableArray -> AsEnumerateArray 4.9.7.4 ⏱️2025.02.08 9af844f
  • 问题修复
    •   流变对象根据路径设置值出现无法设置问题 4.9.8.30 ⏱️2026.03.30 0e2fc29
    •   流变对象转换 object 对象时丢失 ClayOptions 配置问题 4.9.7.86 ⏱️2025.06.13 0f9f541
    •   流变对象序列化 new object() 属性值时出现无限递归情况 4.9.7.85 ⏱️2025.06.12 5bbd194
    •   流变对象序列化 object 类型时可能出现无限递归情况 4.9.7.84 ⏱️2025.06.12 52fcdf8
    •   流变对象转换为 object 或作为泛型类型时,实际转换成了 JsonElement 问题 4.9.7.49 ⏱️2025.04.24 406ff44
    •   流变对象转换 application/x-www-form-urlencoded 表单数据时可能存在 + 字符 4.9.7.37 ⏱️2025.04.03 4bbf53a
    •   流变对象将包含委托属性的 ExpandoObject 对象转换为流变对象时出现异常 4.9.7.32 ⏱️2025.03.24 50ce498
  • 其他更改
    •   流变对象在调试时显示的内容 4.9.7.110 ⏱️2025.08.18 964222f
    •   流变对象转换为 Dicitionary<TKey, TValue> 字典类型操作 4.9.7.46 ⏱️2025.04.18 d3f3264
    •   流变对象模型绑定设计 4.9.7.38 ⏱️2025.04.06 b559576
    •   流变对象 ClayOptions.Flexible 属性对象配置,添加 PropertyNameCaseInsensitive = true 4.9.7.35 ⏱️2025.03.29 1a25186
4.9.7+ 版本说明

Furion 4.9.7+ 版本采用 Shapeless 流变对象替换原有的粘土对象查看旧文档

重要说明

以下内容仅适用于 Furion 4.9.7+ 版本,且不支持 .NET8 以下版本。

29.1 流变对象概述

流变对象是 Furion 框架基于 DynamicObject 类型创建的运行时动态派生对象。它提供了类似 JavaScriptJSON 对象的灵活操作能力,支持动态增删改查,并兼容 LinqLambda 表达式查询。流变对象不仅简化了运行时对象的构建与操作,还保持了代码的简洁性和高效性能。

29.1.1 应用场景

流变对象在互联网应用系统中具有广泛的应用场景,主要包括:

  • 动态数据操作:基于 JSON 字符串的动态增删改查。
  • 第三方 API 集成:无缝集成第三方 API,无需预定义数据模型。
  • CMS 系统优化:增强内容管理系统(CMS)的页面布局和内容管理灵活性。
  • 工作流与表单管理:动态构建复杂工作流和自定义表单数据。
  • 微服务数据整合:整合多个微服务数据,提供统一的数据视图。
  • 快速原型开发:加速原型开发,减少类型定义的需求。
  • 数据分析与报表:高效进行数据分析和报表生成。
  • 事件驱动架构:作为事件负载,增强系统的解耦合性。
  • 配置管理:管理配置项,适应频繁的更新操作。
  • 模板引擎支持:作为模板引擎的数据源,动态生成内容。
  • 电商平台应用:用于电商平台商品的自定义参数管理。
  • 其他场景:适用于多种其他需要动态对象管理的场景。

通过流变对象,开发者能够更灵活地处理动态数据需求,提升开发效率和系统可维护性。

29.2 快速入门

安装包说明

Furion 框架已内置该功能,无需额外安装 NuGet 包。若使用非 Furion 框架,可通过以下命令安装 ShapelessShapeless.AspNetCore 包:

  • 适用于任何 .NET/C# 应用:
dotnet add package Shapeless
  • 适用于 Web 应用(包含 Shapeless 且提供模型绑定功能):
dotnet add package Shapeless.AspNetCore

流变对象支持两种类型:单一对象集合或数组形式

29.2.1 创建单一对象

单一对象是指一个独立的 C# 类型实例(未实现 IEnumerable 接口),其 JSON 表示形式为 {}。以下是创建单一对象的几种方法:

  • 使用 new 关键字
    dynamic clay = new Clay();
  • 使用嵌套类型 Objectnew 关键字
    dynamic clay = new Clay.Object();
  • 使用 EmptyObject 静态方法
    dynamic clay = Clay.EmptyObject();
  • 使用 Parse 静态方法解析 {} 字符串
    dynamic clay = Clay.Parse("{}"); // 或使用 Clay.Parse(对象);
dynamic 关键字声明说明

推荐使用 dynamic 关键字来接收流变对象实例,因为流变对象在编译时类型不固定,需在运行时确定。

dynamic 的优势在于可以灵活访问数据:既可以通过属性方式(如 clay.Name),也可以通过索引方式(如 clay["Name"])。这样即使 Name 属性未定义,代码也能通过编译。

相比之下,使用 var 或流变对象实际类型 Clay 接收时,仅能通过索引(如 clay["Name"])或其他方法访问。

创建单一对象的流变对象实例后,可以对该实例执行一系列动态操作。以下示例详细展示了这些操作:

// 通过属性方式(添加)设置值
clay.Id = 1;
clay.Name = "Shapeless";

// 通过索引方式(添加)设置值
clay["IsDynamic"] = true;
clay["IsArray"] = false;

// 设置对象或匿名对象
clay.Author = new
{
Nickname = "MonkSoul",
HomePage = new[] { "https://furion.net", "https://baiqian.com" }
};

// 通过属性或索引或组合方式(修改)设置值
clay.Author.Nickname = "百小僧";
clay["Author"].Age = 30;
clay["Author"]["Gender"] = "男";
clay.Author["E-Mail"] = "monksoul@outlook.com";

// 使用索引或 Add/Push 方法添加数组项
clay.Author.HomePage[2] = "https://baiqian.ltd"; // 使用索引方式

var homePage = clay.Author.HomePage; // 简化 clay.Author.HomePage[2] 操作
homePage[homePage.Length] = "https://chinadot.net"; // 使用数组长度作为索引
homePage.Add("https://百签.com"); // 或使用 homePage.Push("https://百签.com");

// 设置嵌套流变对象
clay.extend = new Clay();
clay.extend.username = "MonkSoul";
clay.extend.gitee = "https://gitee.com/monksoul";

// 删除(移除)属性
clay.Remove("IsArray"); // 或使用 clay.Delete("IsArray")

// 访问并修改属性
clay.Id += 1;

// 输出字符串
Console.WriteLine(clay); // 或使用 clay.ToString();
便捷初始化方式

除了先创建 Clay 对象再逐个添加属性的方式,您还可以采用更简洁的初始化技巧,直接在对象创建时设置初始值:

dynamic clay = new Clay
{
["id"] = 1,
["name"] = "Shapeless"
};

上述代码示例展示了流变对象的基础用法,最终将结果打印到控制台。控制台输出如下(JSON 格式):

{
"Id": 2,
"Name": "Shapeless",
"IsDynamic": true,
"Author": {
"Nickname": "\u767E\u5C0F\u50E7",
"HomePage": [
"https://furion.net",
"https://baiqian.com",
"https://baiqian.ltd",
"https://chinadot.net",
"https://\u767E\u7B7E.com"
],
"Age": 30,
"Gender": "\u7537",
"E-Mail": "monksoul@outlook.com"
},
"extend": {
"username": "MonkSoul",
"gitee": "https://gitee.com/monksoul"
}
}

默认情况下,流变对象输出为 JSON 格式字符串,且中文字符会被 Unicode 编码,如之前示例所示。若需取消中文 Unicode 编码,可采用以下方法:

// 输出字符串(U:取消中文 Unicode 编码)
Console.WriteLine($"{clay:U}"); // 或使用 clay.ToString("U");

// 调用 ToJsonString 方法并设置 JsonSerializerOptions,指定 Encoder 属性
Console.WriteLine(clay.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}));

控制台输出将呈现为:

{
"Id": 2,
"Name": "Shapeless",
"IsDynamic": true,
"Author": {
"Nickname": "百小僧",
"HomePage": [
"https://furion.net",
"https://baiqian.com",
"https://baiqian.ltd",
"https://chinadot.net",
"https://百签.com"
],
"Age": 30,
"Gender": "男",
"E-Mail": "monksoul@outlook.com"
},
"extend": {
"username": "MonkSoul",
"gitee": "https://gitee.com/monksoul"
}
}

如需控制 JSON 字符串的键命名策略,可采用以下方法:

// 输出字符串(C:输出小驼峰键命名;P:输出帕斯卡(大驼峰)键命名)
Console.WriteLine($"{clay:UC}"); // 或使用 clay.ToString("UC");
Console.WriteLine($"{clay:UP}"); // 或使用 clay.ToString("UP");

// 调用 ToJsonString 方法并设置 JsonSerializerOptions,指定 WriteIndented 属性
Console.WriteLine(clay.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // 小驼峰键命名
}));

// 调用 ToJsonString 方法并设置 JsonSerializerOptions,指定 WriteIndented 属性
Console.WriteLine(clay.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNamingPolicy = new PascalCaseNamingPolicy() // 帕斯卡(大驼峰)键命名
}));

不同键命名策略下的控制台输出将呈现为:

{
"id": 2,
"name": "Shapeless",
"isDynamic": true,
"author": {
"nickname": "百小僧",
"homePage": [
"https://furion.net",
"https://baiqian.com",
"https://baiqian.ltd",
"https://chinadot.net",
"https://百签.com"
],
"age": 30,
"gender": "男",
"e-Mail": "monksoul@outlook.com"
},
"extend": {
"username": "MonkSoul",
"gitee": "https://gitee.com/monksoul"
}
}
自定义键命名策略

框架内置了多种键命名策略,以满足不同的需求:

  • 小驼峰命名法JsonNamingPolicy.CamelCase,例如将 TempCelsius 转换为 tempCelsius
  • 小写蛇形命名法JsonNamingPolicy.SnakeCaseLower,例如将 TempCelsius 转换为 temp_celsius
  • 大写蛇形命名法JsonNamingPolicy.SnakeCaseUpper,例如将 TempCelsius 转换为 TEMP_CELSIUS
  • 小写短横线命名法JsonNamingPolicy.KebabCaseLower,例如将 TempCelsius 转换为 temp-celsius
  • 大写短横线命名法JsonNamingPolicy.KebabCaseUpper,例如将 TempCelsius 转换为 TEMP-CELSIUS
  • 帕斯卡命名法:通过实例化 new PascalCaseNamingPolicy() 创建,例如将 tempCelsius 转换为 TempCelsius

若需自定义键命名策略,您只需创建一个继承自 JsonNamingPolicy 的新类型,并重写其 ConvertName(string name) 方法即可。这将允许您根据特定规则自定义 JSON 键的命名方式。

若要压缩 JSON 字符串,可采用以下方法:

// 输出字符串(Z:压缩(取消格式化))
Console.WriteLine($"{clay:Z}"); // 或使用 clay.ToString("Z");

// 调用 ToJsonString 方法并设置 JsonSerializerOptions,指定 WriteIndented 属性
Console.WriteLine(clay.ToJsonString(new JsonSerializerOptions
{
WriteIndented = false
}));

控制台输出将呈现为:

{"Id":2,"Name":"Shapeless","IsDynamic":true,"Author":{"Nickname":"\u767E\u5C0F\u50E7","HomePage":["https://furion.net","https://baiqian.com","https://baiqian.ltd","https://chinadot.net","https://\u767E\u7B7E.com"],"Age":30,"Gender":"\u7537","E-Mail":"monksoul@outlook.com"},"extend":{"username":"MonkSoul","gitee":"https://gitee.com/monksoul"}}
组合使用格式化符

格式化符可以组合使用。例如,Console.WriteLine($"{clay:ZUC}"); 可以同时压缩 JSON 字符串、取消中文的 Unicode 编码,并应用小驼峰命名策略。

控制台输出将呈现为:

{"id":2,"name":"Shapeless","isDynamic":true,"author":{"nickname":"百小僧","homePage":["https://furion.net","https://baiqian.com","https://baiqian.ltd","https://chinadot.net","https://百签.com"],"age":30,"gender":"男","e-Mail":"monksoul@outlook.com"},"extend":{"username":"MonkSoul","gitee":"https://gitee.com/monksoul"}}

29.2.2 创建集合或数组

集合或数组是包含多个对象实例或基本类型字面量的容器(通常实现 IEnumerable 接口),其 JSON 表示形式为 []。以下是创建集合或数组的几种常见方法:

  • 使用 new 关键字
    dynamic clay = new Clay(ClayType.Array);
  • 使用嵌套类型 Arraynew 关键字
    dynamic clay = new Clay.Array();
  • 使用 EmptyArray 静态方法
    dynamic clay = Clay.EmptyArray();
  • 使用 Parse 静态方法解析 [] 字符串
    dynamic clay = Clay.Parse("[]"); // 或使用 Clay.Parse(集合对象);

创建集合或数组的流变对象实例后,可以对该实例执行一系列动态操作。以下示例详细展示了这些操作:

// 追加项
clay.Add(1); // 或使用 clay.Push(1);
clay.Add(true);
clay.Add("Furion");
clay.Add(false);

// 追加对象或匿名对象
clay.Add(new { id = 1, name = "Furion" });

// 追加流变对象
clay.Add(Clay.Parse("{\"id\":2,\"name\":\"shapeless\"}"));

// 批量追加项
clay.AddRange(new object[] { 2, 3, "will be deleted" });

// 修改指定索引项
clay[0] += 1; // 或使用 clay.Set(0, 2);
// clay[^1] = "Last"; // 或使用 clay.Set(^1, 2);

// 在索引为 1 处插入
clay.Insert(1, "Insert");

// 在索引为 2 处批量插入
clay.InsertRange(2, new object[] { "Furion", "Sundial", "Jaina", "TimeCrontab", "HttpAgent" });

// 删除项
clay.Remove(4); // 或使用 clay.Delete(4)

// 删除指定索引范围项
// clay.Remove(1, 4); // 或使用 clay.Delete(1, 4);
// clay.Remove(1..^4); // clay.Delete(1..^4);

// 删除末项
clay.Pop();

// 输出字符串
Console.WriteLine(clay); // 或使用 clay.ToString();
便捷初始化方式

除了先创建 Clay 对象再逐个添加数组项的方式,您还可以采用更简洁的初始化技巧,直接在对象创建时设置初始值:

dynamic clay = new Clay.Array
{
[0] = 1,
[1] = "Shapeless",
[2] = true
};

上述代码示例展示了流变对象的基础用法,最终将结果打印到控制台。控制台输出如下(JSON 格式):

[
2,
"Insert",
"Furion",
"Sundial",
"TimeCrontab",
"HttpAgent",
true,
"Furion",
false,
{
"id": 1,
"name": "Furion"
},
{
"id": 2,
"name": "shapeless"
},
2,
3
]

在某些特定的应用场景中,我们可能需要反转集合或数组。可采用以下方法:

// 反转集合或数组
var array = clay.Reverse();

// 输出字符串
Console.WriteLine(array); // 或使用 array.ToString();

控制台输出将呈现为:

[
3,
2,
{
"id": 2,
"name": "shapeless"
},
{
"id": 1,
"name": "Furion"
},
false,
"Furion",
true,
"HttpAgent",
"TimeCrontab",
"Sundial",
"Furion",
"Insert",
2
]
单一对象反转

反转操作也适用于单一对象。

我们可以利用范围运算符 RangeSlice 实例方法来截取集合或数组的一部分,并返回一个新的流变对象。具体示例如下:

// 截取数组
var parts = array[2..^4]; // 或使用 array.Slice(2, 4);

// 输出字符串
Console.WriteLine(parts); // 或使用 parts.ToString();

控制台输出将呈现为:

[
{
"id": 2,
"name": "shapeless"
},
{
"id": 1,
"name": "Furion"
},
false,
"Furion",
true,
"HttpAgent",
"TimeCrontab"
]

29.2.3 从 JSON 字符串创建

在实际项目开发中,我们经常需要对 JSON 字符串进行增删改查操作。流变对象提供了 Parse 静态方法,能够将 JSON 字符串转换为流变对象实例。该方法支持所有标准的 JSON 字符串(键和字符串值需使用双引号)以及 JavaScript 基础类型字面量。

1. 从 JSON 对象字符串创建

JSON 对象字符串是指用 {} 包裹的键值对集合。

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""");

// 添加新属性
clay.age = 30;

// 访问属性
var id = clay.id; // 1
var name = clay["name"]; // "Furion"
var age = clay.age; // 30

// 输出字符串
Console.WriteLine(clay);

控制台输出将呈现为:

{
"id": 1,
"name": "Furion",
"age": 30
}

2. 从 JSON 数组字符串创建

JSON 数组字符串是由 [] 包裹的值集合。

dynamic array = Clay.Parse("[1,2,3,true,\"Furion\"]");

// 追加新项
array.Add(false);
array.Add(clay);

// 访问项
var first = array[0]; // 1
var second = array[1]; // 2
var last = array[^1]; // "{\"id\":1,\"name\":\"Furion\",\"age\":30}"

// 输出字符串
Console.WriteLine(array);

控制台输出将呈现为:

[
1,
2,
3,
true,
"Furion",
false,
{
"id": 1,
"name": "Furion",
"age": 30
}
]

3. 从任意 JSON 字面量字符串创建

除了支持对象或数组格式的 JSON 字符串外,框架还允许使用任意字面量。

dynamic scalar = Clay.Parse("true");

// 访问值
var value = scalar.value;

// 输出字符串
Console.WriteLine(scalar);

控制台输出将呈现为:

{
"value": true
}
自定义非对象/数组字面量的键名

对于数值、布尔值、字符串等非对象或数组的字面量,框架默认将其转换为一个包含键 value 的对象,并将字面量值赋给 value 键。如果需要更改默认的 value 键名,可通过 ClayOptions 进行配置:

dynamic wrapper = Clay.Parse("true", new ClayOptions
{
ScalarValueKey = "data"
});

// 访问值
var data = wrapper.data;

// 输出字符串
Console.WriteLine(wrapper);

控制台输出将呈现为:

{
"data": true
}

4. 从键值对格式的 JSON 字符串创建

键值对格式的 JSON 字符串(即由 key/value 对组成的数组)在默认情况下会被解析为集合或数组类型的流变对象。若您期望将其解析为单一对象的流变对象,只需传入配置好的 ClayOptions 即可:

dynamic dicObject = Clay.Parse("""
[
{
"key": "id",
"value": 1
},
{
"key": "name",
"value": "Furion"
}
]
""", new ClayOptions { KeyValueJsonToObject = true });

// 访问值
var id = dicObject.id; // 1
var name = dicObject.name; // "Furion"

// 输出字符串
Console.WriteLine(dicObject);

控制台输出将呈现为:

{
"id": 1,
"name": "Furion"
}
key/value 键的大小写不敏感

在解析键值对格式的 JSON 字符串时,keyvalue 的大小写是被忽略的,即 Key/Valuekey/value 同样有效。这意味着你可以根据需要灵活地使用不同的大小写形式,而不会影响解析结果。

29.2.4 从 C# 对象实例创建(文件)

除了从 JSON 字符串创建流变对象外,还可以基于多种 C# 数据对象进行创建。这些数据对象需支持序列化,或通过自定义的 JsonConverter 实现序列化。此外,框架还支持从 StreamUtf8JsonReaderJsonElement 等特殊类型中创建流变对象。

1. 从具体类型对象创建

dynamic clay = Clay.Parse(new YourModel { Id = 1, Name = "Shapeless" });

2. 从匿名对象创建

dynamic clay = Clay.Parse(new { id = 1, name = "Furion" });

3. 从字典对象创建

dynamic clay = Clay.Parse(new Dictionary<string, object>
{
{ "id", 1 },
{ "name", "Furion" }
});

4. 从键值对集合或数组创建

dynamic clay = Clay.Parse(new[]
{
new KeyValuePair<string, object?>("id", 1),
new KeyValuePair<string, object?>("name", "furion")
}.ToDictionary()); // 调用 ToDictionary() 方法可以避免解析为 Key/Value 数组

5. 从集合或数组(具体类型或匿名类型)创建

dynamic clay = Clay.Parse(new List<YourModel>
{
new() { Id = 1, Name = "Furion" },
new() { Id = 2, Name = "Shapeless" }
});

6. 从字节数组(JSON 字符串)创建

var byteArray = "{\"id\":1,\"name\":\"furion\"}"u8.ToArray();
dynamic clay = Clay.Parse(byteArray);

7. 从 Stream 流(JSON 字符串)创建

using var memoryStream = new MemoryStream("{\"id\":1,\"name\":\"furion\"}"u8.ToArray());
dynamic clay = Clay.Parse(memoryStream);

8. 从 Utf8JsonReader 创建

var utf8JsonReader = new Utf8JsonReader("{\"id\":1,\"name\":\"furion\"}"u8.ToArray(), true, default);
dynamic clay = Clay.Parse(ref utf8JsonReader); // 需使用 ref 进行引用传递

9. 从 JsonElement 创建

using var jsonDocument = JsonDocument.Parse("{\"id\":1,\"name\":\"Furion\"}");
dynamic clay = Clay.Parse(jsonDocument.RootElement);

10. 从流变对象自身创建

dynamic clay = Clay.Parse(new Clay
{
["id"] = 1,
["name"] = "Shapeless"
});

11. 从任意字面量创建

dynamic clay = Clay.Parse(true);
var value = clay.value; // true
自定义字面量键名

对于数值、布尔值、字符串等字面量,框架默认将其转换为含 value 键的对象。如需更改默认键名,可通过 ClayOptions 配置:

dynamic clay = Clay.Parse(true, new ClayOptions
{
ScalarValueKey = "data"
});
var data = clay.data; // true

12. 从 struct 结构体创建

默认情况下,如果结构体中使用属性(Property)声明内部数据,无需额外配置即可将其转换为流变对象。如果使用的是字段(Field)声明内部数据,则可以通过以下两种方式进行配置:

  1. 使用 [JsonInclude] 特性标记字段:
public struct Point
{
[JsonInclude] public int X;
[JsonInclude] public int Y;
}

dynamic clay = Clay.Parse(new Point { X = 1, Y = 1 });
  1. 通过配置 JsonSerializerOptionsIncludeFields 属性为 true
public struct Point
{
public int X;
public int Y;
}

dynamic clay = Clay.Parse(new Point { X = 1, Y = 1 },
options => options.JsonSerializerOptions.IncludeFields = true);

这两种方式都可以确保结构体中的字段被正确序列化并转换为流变对象。

13. 从文件中读取数据创建

dynamic clay = Clay.ParseFromFile("C:\Workspaces\test.json");

14. 从 application/x-www-form-urlencoded 表单数据转换

dynamic clay = Clay.Parse("IsDeviceEnable=0&Thresholdvalue=1&DeviceState=0&SurplusParams=&DeviceName=2%E5%B1%82%E8%8C%B6%E6%A5%BC%E7%83%AD%E6%B0%B4%E7%94%A8%E6%B0%B4%E9%87%8F6789&Concentrator=476103385&ProtocolId=888085307&BuildId=524328523&DeviceId=489414407&DeviceCode=6789");

15. 使用 ToClay() 扩展方法

您还可以通过 ToClay() 扩展方法(位于 Shapeless.Extensions 命名空间下)将任意对象转换为流变对象。示例如下:

dynamic clay = new { Id = 1, Name = "Furion" }.ToClay();

16. 从自定义 JsonConverter 序列化类型对象创建

对于不能直接序列化为 JSON 的类型(如 DataTable),可以创建一个自定义的 JsonConverter 派生类来处理(如 JsonConverter<DataTable>)。

public class DataTableJsonConverter : JsonConverter<DataTable>
{
/// <inheritdoc />
public override DataTable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DataTable value, JsonSerializerOptions options)
{
// 将 DataTable 转换为字典集合
var dictList = value.AsEnumerable().Select(row =>
row.Table.Columns.Cast<DataColumn>()
.ToDictionary(col => col.ColumnName, col => row[col] != DBNull.Value ? row[col] : null)).ToList();

// 序列化字典列表
JsonSerializer.Serialize(writer, dictList, options);
}
}

随后,配置 ClayOptionsJsonSerializerOptions 属性,并向其中添加自定义转换器。

var dataTable = new DataTable();
dataTable.Columns.Add("id", typeof(int));
dataTable.Columns.Add("name", typeof(string));
dataTable.Rows.Add(1, "Furion");
dataTable.Rows.Add(2, "百小僧");

dynamic clay = Clay.Parse(dataTable,
options => options.JsonSerializerOptions.Converters.Add(new DataTableJsonConverter()));
更多支持的类型

除了上述类型,还支持将以下类型转换为流变对象:ExpandoObjectJsonElementJsonDocumentJsonObjectJsonArray 以及 JsonNode

29.2.5 遍历流变对象(ForEach

流变对象 Clay 实现了 IEnumerable<object?> 接口,因此支持通过 forforeach 循环进行迭代,或者使用枚举器进行遍历。

  • 单一对象遍历
dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");

// 遍历键值
foreach (KeyValuePair<string, dynamic?> item in clay) // 或使用 clay.AsEnumerateObject()
{
Console.WriteLine($"Key: {item.Key} Value: {item.Value}");
}

// 遍历键
foreach (var key in clay.Keys)
{
Console.WriteLine($"Key: {key}");
}

// 遍历值
foreach (var value in clay.Values)
{
Console.WriteLine($"Value: {value}");
}

// 使用枚举器方式遍历
using IEnumerator<dynamic?> objectEnumerator = clay.GetEnumerator();

var listObject = new List<KeyValuePair<string, dynamic?>>();
while (objectEnumerator.MoveNext())
{
listObject.Add(objectEnumerator.Current); // Current 实际类型为:KeyValuePair<string, dynamic?>
}

Debug.Assert(listObject.Count == 2);

// 使用 ForEach 方法遍历
clay.ForEach(new Action<dynamic?>(item =>
{
Console.WriteLine($"Key: {item?.Key} Value: {item?.Value}");
}));
  • 集合或数组遍历
dynamic array = Clay.Parse("""[1,2,true,false,"Furion",{"id":1,"name":"shapeless"},null]""");

// 遍历项
foreach (var item in array) // 或使用 array.AsEnumerateArray()
{
Console.WriteLine($"Value: {item}");
}

// 遍历索引
foreach (var index in array.Keys)
{
Console.WriteLine($"Index: {index}");
}

// 遍历值
foreach (var value in array.Values)
{
Console.WriteLine($"Value: {value}");
}

// 使用枚举器方式遍历
using IEnumerator<dynamic?> arrayEnumerator = array.GetEnumerator();

var listArray = new List<dynamic?>();
while (arrayEnumerator.MoveNext())
{
listArray.Add(objectEnumerator.Current);
}

Debug.Assert(listArray.Count == 7);

// 使用 ForEach 方法遍历
array.ForEach(new Action<dynamic?>(value =>
{
Console.WriteLine($"Value: {value}");
}));

29.2.6 LambdaLinq 表达式查询

流变对象 Clay 实现了 IEnumerable<object?> 接口,因此支持通过 Lambda 表达式和 Linq 查询进行操作。然而,由于流变对象通常使用 dynamic 关键字接收,这会导致在进行 LambdaLinq 查询时无法自动推断表达式的委托类型。

为了解决这一问题,我们需要先将 dynamic 对象转换回 Clay 类型或 IEnumerable<dynamic?> 类型,以启用 IDE 的智能代码完成和类型推断功能。

单一对象 LambdaLinq 查询

首先将 dynamic 类型的流变对象转换回 Clay 类型或 IEnumerable<dynamic?> 类型:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
Clay clayObject = clay;
// IEnumerable<dynamic?> clayObject = clay;

使用 LambdaLinq 表达式操作示例如下:

// Lambda 操作
var list1 = clayObject.Where((dynamic? u) => u?.Key == "id").OrderBy(u => u?.Key).ToList();
var list2 = clayObject.Select((dynamic? u) => u?.Value).ToList();
var dictionary = clayObject.ToDictionary((dynamic? u) => u!.Key, u => u?.Value);

// 或使用 clayObject.AsEnumerateObject()
var list3 = clayObject.AsEnumerateObject().Where(u => u.Key == "id").OrderBy(u => u.Key).ToList();
var list4 = clayObject.AsEnumerateObject().Select(u => u.Value).ToList();
var dictionary2 = clayObject.AsEnumerateObject().ToDictionary(u => u.Key, u => u.Value);

// Linq 操作
var query = from item in clayObject.AsEnumerateObject()
where item.Key == "id"
orderby item.Key
select item;
var list5 = query.ToList();
推荐使用解构函数进行 Lambda 和 Linq 操作 ✅✅✅

在最新版本中,框架为流变对象引入了对解构函数的支持(详情参见第 29.3.4 节解构函数),从而减少了类型转换的操作。以下是示例代码:

var (clay, enumerable) = Clay.Parse("""{"id":1,"name":"furion"}""");

// Lambda
var list = enumerable.Where(u => u?.Key == "id").OrderBy(u => u?.Key).ToList();

// Linq
var query = from item in enumerable
where item.Key == "id"
orderby item.Key
select item;

如果只是对数据进行筛选,可以直接使用 Lambda 表达式配合链式方法完成操作。例如:

var list = Clay.Parse("""{"id":1,"name":"furion"}""")
.Where((dynamic? u) => u?.Key == "id")
.OrderBy(u => u?.Key)
.ToList();
关于 clayObject 和 clayObject.AsEnumerateObject() 说明

由于 Clay 实现了 IEnumerable<object?> 接口,进行 LambdaLinq 操作时,表达式的参数为 object? 类型,因此需要显式声明表达式参数为 dynamic? 类型,在运行时会自动转换为 KeyValuePair<string, dynamic?> 类型

若使用 AsEnumerateObject() 方法,则无需显式声明表达式类型。此外,在 Linq 表达式中,推荐使用 clayObject.AsEnumerateObject() 方法。

集合或数组 LambdaLinq 查询

首先将 dynamic 类型的流变对象转换回 Clay 类型或 IEnumerable<dynamic?> 类型:

dynamic clay = Clay.Parse("""[1,2,true,false,"Furion",{"id":1,"name":"shapeless"},null]""");
Clay clayArray = clay;
// IEnumerable<dynamic?> clayArray = clay;

使用 LambdaLinq 表达式操作示例如下:

// Lambda 操作
var list1 = clayArray.Where((dynamic? u) => u?.Equals(2) == false).ToList();
var list2 = clayArray.Select((dynamic? u) => new { data = u }).ToList();

// 或使用 clayArray.AsEnumerateArray()
var list3 = clayArray.AsEnumerateArray().Where(u => u?.Equals(2) == false).ToList();
var list4 = clayArray.AsEnumerateArray().Select(u => new { data = u }).ToList();

// Linq 操作
var query = from item in clayArray.AsEnumerateArray()
where item?.Equals(2) == false
select new { data = item };
var list5 = query.ToList();
推荐使用解构函数进行 Lambda 和 Linq 操作 ✅✅✅

在最新版本中,框架为流变对象引入了对解构函数的支持(详情参见第 29.3.4 节解构函数),从而减少了类型转换的操作。以下是示例代码:

var (clay, enumerable) = Clay.Parse("""[1,2,true,false,"Furion",{"id":1,"name":"shapeless"},null]""");

// Lambda
var list = enumerable.Where(u => u?.Equals(2) == false).ToList();

// Linq
var query = from item in enumerable
where item?.Equals(2) == false
select new { data = item };
关于 clayArray 和 clayArray.AsEnumerateArray() 说明

由于 Clay 实现了 IEnumerable<object?> 接口,进行 LambdaLinq 操作时,表达式的参数为 object? 类型,因此需要显式声明表达式参数为 dynamic? 类型。

若使用 AsEnumerateArray() 方法,则无需显式声明表达式类型。此外,在 Linq 表达式中,推荐使用 clayArray.AsEnumerateArray() 方法。

29.2.7 流变对象的类型转换

在获取流变对象的属性或集合/数组元素时,框架会自动通过 JSON 反序列化进行类型转换。然而,自动转换可能无法精确匹配所需类型,例如数值可能需要转换为 intlongdoubledecimal,时间格式字符串可能需要转换为 DateTime 或保持为字符串。这类转换需求非常常见。

流变对象的类型转换不仅包括单一对象属性的转换,还涵盖集合/数组元素的转换以及流变对象自身的类型转换。以下是一个示例流变对象:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless","date":"2025-01-14T00:00:00","isTrue":true}""");

🔶 单一对象属性及集合/数组元素的类型转换

  • 使用属性名作为方法名,并指定目标类型为泛型参数:
var id = clay.id; // id 为 int 类型
var name = clay.name; // name 为 string 类型

var date = clay.date<DateTime>();
var isTrue = clay.isTrue<bool>();

// 支持传入 JSON 序列化选项
var date2 = clay.date<DateTime>(new JsonSerializerOptions());
  • 使用属性名作为方法名,并指定目标类型为方法参数:
var date = clay.date(typeof(DateTime)) as DateTime?;
var isTrue = clay.isTrue(typeof(bool)) as bool?;

// 支持传入 JSON 序列化选项
var date2 = clay.date(typeof(DateTime), new JsonSerializerOptions()) as DateTime?;
  • 使用流变对象的 Get<T>(identifier) 方法:
var date = clay.Get<DateTime>("date");
var isTrue = clay.Get<bool>("isTrue");

// 支持传入 JSON 序列化选项
var date = clay.Get<DateTime>("date", new JsonSerializerOptions());
  • 使用流变对象的 Get(identifier, type) 方法:
var date = clay.Get("date", typeof(DateTime)) as DateTime?;
var isTrue = clay.Get("isTrue", typeof(bool)) as bool?;

// 支持传入 JSON 序列化选项
var date = clay.Get("date", typeof(DateTime), new JsonSerializerOptions()) as DateTime?;
集合/数组元素的类型转换

对于集合或数组的流变对象,由于其不包含类似单一对象的属性键值对功能,因此当需要为集合或数组的元素进行类型转换时,可通过流变对象的 Get<T>(index)Get(index, type) 方式实现,如:

dynamic array = Clay.Parse("[1,\"2025-01-14T00:00:00\",true]");

var date = array.Get<DateTime>(1); // 索引为 1
var isTrue = array.Get<bool>(2); // 索引为 2
  • 使用属性名作为方法名,并传递 Func<string?, object?> 委托为方法参数:
var date = clay.date(new Func<string?, object?>(u => Convert.ToDateTime(u)));

这种方法允许您通过自定义逻辑将属性值转换为所需类型。委托接受一个字符串类型的属性值作为输入,并返回转换后的对象。

  • 配置 ClayOptions 以自动解析时间格式字符串为 DateTime 类型:

为满足时间格式化字符串到 DateTime 类型的常见转换需求,框架内置了自动转换功能。在初始化流变对象时,只需传入配置好的 ClayOptions 即可:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless","date":"2025-01-14T00:00:00","isTrue":true}""",
new ClayOptions
{
DateJsonToDateTime = true
});

// 直接访问 date 属性,它已被自动转换为 DateTime 类型
var date = clay.date;

🔶 流变对象的类型转换

流变对象的类型转换指的是将流变对象本身转换为具体的类型。例如,将 clay 流变对象转换为 ClayModel 类型,其中 ClayModel 类的定义如下:

public class ClayModel
{
public int Id { get; set; }
public string? Name { get; set; }
public DateTime? Date { get; set; }
public bool IsTrue { get; set; }
}
  • 使用声明方式(隐式转换)自动转换:
ClayModel clayModel = clay;
  • 使用强制转换(显式转换):
var clayModel2 = (ClayModel)clay;
显式转换的局限性

显式转换(强制转换)不支持转换为非 IEnumerable<dynamic?> 类型。建议使用隐式转换、As<T>() 方法或其他替代方案来实现转换。

  • 使用流变对象的 As<T>() 方法:
var clayModel = clay.As<ClayModel>();

// 支持传入 JSON 序列化选项
var clayModel2 = clay.As<ClayModel>(new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
特殊类型转换

此外,框架还支持将流变对象转换为以下类型:

  • IEnumerable<KeyValuePair<object, dynamic?>>
  • IEnumerable<KeyValuePair<string, dynamic?>>(仅适用于单一对象)
  • IEnumerable<KeyValuePair<int, dynamic?>>(仅适用于集合或数组)
  • IActionResult
  • 使用流变对象的 As(Type) 方法:
var clayModel = clay.As(typeof(ClayModel)) as ClayModel;

// 支持传入 JSON 序列化选项
var clayModel2 = clay.As(typeof(ClayModel), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) as ClayModel;
  • 使用流变对象实例作为方法调用:

框架借助 DynamicObject 的特性,允许将流变对象实例作为方法调用,并支持将目标类型作为参数进行类型转换操作。

var clayModel = clay(typeof(ClayModel)) as ClayModel;

// 支持传入 JSON 序列化选项
var clayModel2 = clay(typeof(ClayModel), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) as ClayModel;
  • 使用 JsonSerializer 反序列化方法:
var clayModel = JsonSerializer.Deserialize<ClayModel>(clay.ToString()
, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
  • 转换为 XElement 类型对象:

框架不仅支持将流变对象转换为常见的具体类型,还特别提供了对 XElement 类型的支持,使得流变对象能够轻松转换为 XML 格式的字符串。以下是几种转换方法:

// 隐式转换
XElement xElement = clay;
// 显式转换
var xElement2 = (XElement)clay;
// As<T>() 方法
var xElement3 = clay.As<XElement>();
  • 转换为 IActionResult 类型对象:

框架还支持将对象自动转换为 ASP.NET MVC 中的 IActionResult 类型,以下是几种转换方法:

// 隐式转换
IActionResult jsonResult = clay;
// As<T> 方法
var jsonResult2 = clay.As<IActionResult>();
  • 支持转换后进行模型验证

在将流变对象转换为具体类型时,您可以配置执行模型验证。只需在初始化时设置验证选项,或使用 Rebuilt 方法进行重建时配置,如下所示:

// 初始化时配置模型验证
dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""",
new ClayOptions
{
ValidateAfterConversion = true
});

// 或使用 Rebuilt 方法重建并配置模型验证
clay.Rebuilt(new ClayOptions
{
ValidateAfterConversion = true
})

当流变对象转换为 ClayModel 或其他具体类型时,则会触发模型验证:

public class ClayModel
{
[Range(2, 10)]
public int Id { get; set; }

[Required]
[MinLength(3)]
public string? Name { get; set; }
}

// 转换
ClayModel model = clay;

此时,若数据不符合验证规则,将抛出异常,例如:

System.ComponentModel.DataAnnotations.ValidationException: The field Id must be between 2 and 10.
集合或数组类型转换

以上操作同样适用于集合或数组流变对象的类型转换。

29.2.8 数据变更事件监听

流变对象支持对数据变更事件的监听,包括数据设置/修改前后以及数据删除前后的监听。由于流变对象通常使用 dynamic 关键字接收,这会导致在订阅事件时无法自动推断事件的委托类型。

为了解决这一问题,我们需要先将 dynamic 对象转换回 Clay 类型,以启用 IDE 的智能代码补全和类型推断功能。

单一对象数据变更事件监听

首先将 dynamic 类型的流变对象转换回 Clay 类型:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
Clay clayObject = clay;

以下代码展示了如何为单一对象设置数据变更事件监听,包括数据变更前后以及数据移除前后的处理逻辑:

// 数据变更之前
clayObject.Changing += (sender, args) =>
{
Console.WriteLine(args.IsFound
? $"变更之前 (键:{args.Identifier},值:{sender[args.Identifier]})"
: $"变更之前 (键: {args.Identifier}) 不存在");
};

// 数据变更之后
clayObject.Changed += (sender, args) =>
{
Console.WriteLine($"变更之后 (键:{args.Identifier},值:{sender[args.Identifier]})");
};

// 数据移除之前
clayObject.Removing += (sender, args) =>
{
Console.WriteLine(args.IsFound
? $"移除之前 (键:{args.Identifier},值:{sender[args.Identifier]})"
: $"移除之前 (键: {args.Identifier}) 不存在");
};

// 数据移除之后
clayObject.Removed += (sender, args) =>
{
Console.WriteLine($"移除之后 (键: {args.Identifier}) 不存在");
};

// 触发数据变更和移除事件
clay.id = 2;
clay.name = "Shapeless";
clay.author = "百小僧";

clay.Remove("author");

控制台输出结果如下:

变更之前 (键:id,值:1)
变更之后 (键:id,值:2)
变更之前 (键:name,值:shapeless)
变更之后 (键:name,值:Shapeless)
变更之前 (键: author) 不存在
变更之后 (键:author,值:百小僧)
移除之前 (键:author,值:百小僧)
移除之后 (键: author) 不存在

通过上述代码,可以清晰地监听流变对象的数据变更和移除事件,并实时获取变更前后的状态信息。

集合或数组数据变更事件监听

首先将 dynamic 类型的流变对象转换回 Clay 类型:

dynamic array = Clay.Parse("[1,2,10.3,true,false]");
Clay clayArray = array;

以下代码展示了如何为集合或数组设置数据变更事件监听,包括数据变更前后以及数据移除前后的处理逻辑:

// 数据变更之前
clayArray.Changing += (sender, args) =>
{
Console.WriteLine(args.IsFound
? $"变更之前 (索引:{args.Identifier},值:{sender[args.Identifier]})"
: $"变更之前 (索引: {args.Identifier}) 不存在");
};

// 数据变更之后
clayArray.Changed += (sender, args) =>
{
Console.WriteLine($"变更之后 (索引:{args.Identifier},值:{sender[args.Identifier]})");
};

// 移除数据之前
clayArray.Removing += (sender, args) =>
{
Console.WriteLine(args.IsFound
? $"移除之前 (索引:{args.Identifier},值:{sender[args.Identifier]})"
: $"移除之前 (索引: {args.Identifier}) 不存在");
};

// 移除数据之后
clayArray.Removed += (sender, args) =>
{
Console.WriteLine($"移除之后 (索引: {args.Identifier}) 不存在");
};

array.Add("Furion");
array.Insert(0, "One");

array.Remove(3);

控制台输出将呈现为:

变更之前 (索引: 5) 不存在
变更之后 (索引:5,值:Furion)
变更之前 (索引:0,值:1)
变更之后 (索引:0,值:One)
移除之前 (索引:3,值:10.3)
移除之后 (索引: 3) 不存在

通过上述代码,可以清晰地监听流变对象的数据变更和移除事件,并实时获取变更前后的状态信息。

AddEvent 方法

除了将 dynamic 类型转换为 Clay 类型以进行事件监听外,还可以通过 AddEvent 方法动态添加事件监听。这种方式无需类型转换,直接操作 dynamic 对象即可。更多详细用法可参考第 29.5.3 章节

29.2.9 输出多种格式

框架为流变对象提供了多种输出方法和格式,包括 JSONXML 字符串。为了演示输出多种格式操作,我们准备了一个样本流变对象。

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless","author":"百小僧"}""");

1. 使用 ToString() 输出 JSON 字符串

  • 默认输出

使用 ToString() 方法或直接输出对象时,默认以 JSON 格式显示对象的内容。在此过程中,中文字符会被自动进行 Unicode 编码处理。

Console.WriteLine(clay);
Console.WriteLine(clay.ToString());

控制台输出将呈现为:

{
"id": 1,
"name": "shapeless",
"author": "\u767E\u5C0F\u50E7"
}
  • 取消中文 Unicode 编码

若需取消中文的 Unicode 编码,可使用格式化符 U

Console.WriteLine($"{clay:U}");
Console.WriteLine(clay.ToString("U"));

控制台输出将呈现为:

{
"id": 1,
"name": "shapeless",
"author": "百小僧"
}
  • 压缩 JSON

使用格式化符 Z 可压缩 JSON 输出,去除格式化空格。

Console.WriteLine($"{clay:Z}");
Console.WriteLine(clay.ToString("Z"));

控制台输出将呈现为:

{"id":1,"name":"shapeless","author":"\u767E\u5C0F\u50E7"}
  • 组合格式化符

格式化符可组合使用,如同时取消中文 Unicode 编码并压缩 JSON

Console.WriteLine($"{clay:UZ}");
Console.WriteLine(clay.ToString("UZ"));

控制台输出将呈现为:

{"id":1,"name":"shapeless","author":"百小僧"}
  • 键名策略设置

使用格式化符 C 输出小驼峰命名,P 输出帕斯卡(大驼峰)命名。

Console.WriteLine($"{clay:C}");
Console.WriteLine(clay.ToString("C"));

Console.WriteLine($"{clay:P}");
Console.WriteLine(clay.ToString("P"));

控制台输出将呈现为:

{
"id": 1,
"name": "shapeless",
"author": "\u767E\u5C0F\u50E7"
}

{
"Id": 1,
"Name": "shapeless",
"Author": "\u767E\u5C0F\u50E7"
}

多个格式化符可组合使用,如:

Console.WriteLine($"{clay:ZUC}");
Console.WriteLine(clay.ToString("ZUC"));

Console.WriteLine($"{clay:ZUP}");
Console.WriteLine(clay.ToString("ZUP"));

控制台输出将呈现为:

{"id":1,"name":"shapeless","author":"百小僧"}

{"Id":1,"Name":"shapeless","Author":"百小僧"}

2. 使用 ToJsonString() 输出 JSON 字符串

除了使用 ToString() 方法输出 JSON 字符串,框架还提供了 ToJsonString() 方法,用于更灵活地输出 JSON 字符串。ToString() 方法提供的 JSON 输出配置较为有限,而 ToJsonString() 方法则通过接收 JsonSerializerOptions 参数,支持更多的自定义配置。

Console.WriteLine(clay.ToJsonString());

// 支持传入 json 序列化选项
Console.WriteLine(clay.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true, // 格式化 JSON
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 取消中文 Unicode 编码
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // 小驼峰键命名策略
}));

控制台输出将呈现为:

{"id":1,"name":"shapeless","author":"百小僧"}

{
"id": 1,
"name": "shapeless",
"author": "百小僧"
}
自定义键命名策略

框架内置了多种键命名策略,以满足不同的需求:

  • 小驼峰命名法JsonNamingPolicy.CamelCase,例如将 TempCelsius 转换为 tempCelsius
  • 小写蛇形命名法JsonNamingPolicy.SnakeCaseLower,例如将 TempCelsius 转换为 temp_celsius
  • 大写蛇形命名法JsonNamingPolicy.SnakeCaseUpper,例如将 TempCelsius 转换为 TEMP_CELSIUS
  • 小写短横线命名法JsonNamingPolicy.KebabCaseLower,例如将 TempCelsius 转换为 temp-celsius
  • 大写短横线命名法JsonNamingPolicy.KebabCaseUpper,例如将 TempCelsius 转换为 TEMP-CELSIUS
  • 帕斯卡命名法:通过实例化 new PascalCaseNamingPolicy() 创建,例如将 tempCelsius 转换为 TempCelsius

若需自定义键命名策略,您只需创建一个继承自 JsonNamingPolicy 的新类型,并重写其 ConvertName(string name) 方法即可。这将允许您根据特定规则自定义 JSON 键的命名方式。

3. 使用流变对象实例作为方法调用输出 JSON 字符串

框架借助 DynamicObject 的特性,允许将流变对象实例作为方法调用,并实现 JSON 字符串输出操作。

Console.WriteLine(clay());

// 支持传入 JSON 序列化选项
Console.WriteLine(clay(new JsonSerializerOptions
{
WriteIndented = true, // 格式化 JSON
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 取消中文 Unicode 编码
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // 小驼峰键命名策略
}));

控制台输出将呈现为:

{"id":1,"name":"shapeless","author":"百小僧"}

{
"id": 1,
"name": "shapeless",
"author": "百小僧"
}
小提示

值得注意的是,上述示例中的 clay() 调用与 clay.ToJsonString() 具有相同效果,同样地,clay(new JsonSerializerOptions()) 也与 clay.ToJsonString(new JsonSerializerOptions()) 功能一致,这为用户提供了更为简洁和直观的调用方式。

4. 使用 ToXmlString() 输出 XML 字符串

框架不仅支持将对象转换为 JSON 字符串,还提供了 ToXmlString() 方法,用于生成 XML 格式的字符串。此外,该方法还接收 XmlWriterSettings 参数,以便用户进行更细致的自定义配置。

Console.WriteLine(clay.ToXmlString());

// 支持传入 XML 写入选项
Console.WriteLine(clay.ToXmlString(new XmlWriterSettings
{
Indent = true
}));

控制台输出将呈现为:

<?xml version="1.0" encoding="utf-8"?><root type="object"><id type="number">1</id><name type="string">shapeless</name><author type="string">百小僧</author></root>

<?xml version="1.0" encoding="utf-8"?>
<root type="object">
<id type="number">1</id>
<name type="string">shapeless</name>
<author type="string">百小僧</author>
</root>

29.2.10 动态添加委托方法

动态添加委托方法的限制

请注意,当前仅支持为单一对象的流变实例动态添加委托方法,而不支持为集合或数组类型的流变对象执行此操作。

框架支持为单一对象的流变对象动态添加 ActionFuncDelegate 类型的委托方法。

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");

// 添加 Func 委托
clay.sayHello = (Func<string>)(() => $"Hello, {clay.name}!");

// 添加 Action 委托
clay.addProp = new Action<string, object?>((key, value) =>
{
clay[key] = value;
});
clay.Increment = new Action(() => clay.id++);

// 调用 sayHello 方法
Console.WriteLine(clay.sayHello());

Console.WriteLine($"调用 Increment 方法之前的 id 值:{clay.id}");
// 调用 Increment 方法
clay.Increment();
Console.WriteLine($"调用 Increment 方法之后的 id 值:{clay.id}");

// 调用 addProp 方法
clay.addProp("age", 30);
clay["addProp"]("address", "广东省中山市"); // 同样支持索引方式

Console.WriteLine($"{clay:U}");

控制台输出将呈现为:

Hello, shapeless!
调用 Increment 方法之前的 id 值:1
调用 Increment 方法之后的 id 值:2
{
"id": 2,
"name": "shapeless",
"age": 30,
"address": "广东省中山市"
}
委托方法名区分大小写

调用委托方法时,请注意方法名是区分大小写的。

在委托内部正确访问流变对象实例

之前的示例中,sayHello 方法直接访问了外部的 clay 对象(如 clay.name),这种做法可能引发闭包相关的问题。为了避免这些问题,应使用 this 关键字来引用当前流变对象实例,但关键在于如何正确传递 this

为解决这一难题,框架引入了 ClayContext 类型,专门用于动态添加委托的方法。只需将 ClayContext 作为委托的第一个参数,并通过访问其 Current 属性,即可轻松获取当前上下文中的流变对象(即 this)。

以下是如何实现的示例:

clay.sayHello2 = (Func<ClayContext, string>)(ctx => $"Hello, {ctx.Current.name}!"); // 或使用 new Func<ClayContext, string>(...)

// 调用 sayHello2 方法,无需显式传入 ClayContext 实例
Console.WriteLine(clay.sayHello2());

控制台输出将呈现为:

Hello, shapeless!

若委托方法需要额外的参数,只需确保 ClayContext 始终作为第一个参数传入即可:

clay.sayHello3 = (Func<ClayContext, string, string>)((ctx, arg) => $"Hello, {ctx.Current.name} {arg}!"); // 或使用 new Func<ClayContext, string>(...)

// 调用 sayHello3 方法,同样无需显式传入 ClayContext 实例
Console.WriteLine(clay.sayHello3("新年快乐"));

控制台输出将呈现为:

Hello, shapeless 新年快乐!

这种方式不仅解决了闭包问题,还确保了代码的简洁性和可读性。

动态添加委托方法的限制

请注意,当前仅支持为单一对象的流变实例动态添加委托方法,而不支持为集合或数组类型的流变对象执行此操作。

29.2.11 在 ASP.NET 应用中使用

流变对象可与 ASP.NET 应用无缝集成,无论是作为视图模型、API 接口参数、自定义类型的属性,还是 API 返回值,Clay 类型均能轻松胜任。

要在 ASP.NET 应用中使用流变对象,请确保完成以下配置:

独立库说明

Furion 框架已内置该功能,无需额外安装 NuGet 包。若您使用 Shapeless 独立库,请安装 Shapeless.AspNetCore 以替代 Shapeless

Startup.csProgram.cs 文件中配置 ClayOptions 选项服务。

services.AddControllers()
.AddClayOptions(options => // 或使用 .AddClayOptions();
{
// 控制是否将键值对格式的 JSON 转换为单一对象
options.KeyValueJsonToObject = true;
});

1. 在 ASP.NET MVC 中的应用

ASP.NET MVC 是流变对象的理想应用场景,它能充分利用流变对象的灵活性。以下是一个简单的示例,展示了如何在 ASP.NET MVC 中使用 Clay 流变对象:

控制器代码:

public class HomeController : Controller
{
public IActionResult Index()
{
return View(Clay.Parse("""{"id":1,"name":"Furion"}""")); // 将流变对象作为视图模型
// return this.ViewClay("""{"id":1,"name":"Furion"}"""); // 或使用 ViewClay 扩展方法
}
}

视图模板代码:

@model dynamic
@{
ViewData["Title"] = "Home Page";
}

<div>@Model.id @Model.name</div>
关于 @model dynamic

ASP.NET MVC 中,@Model 默认是强类型的。由于流变对象是动态类型,因此将 @model 声明为 dynamic 即可。

上述示例展示了如何将流变对象用作视图模型,充分利用了其动态特性。

2. 在 ASP.NET WebAPI 中的应用

ASP.NET WebAPI.NET 提供的快速构建 RESTful API 的工具,框架支持使用流变对象接收请求数据或返回响应。以下示例演示了如何在 ASP.NET WebAPI 中使用 Clay 流变对象:

[ApiController]
[Route("[controller]/[action]")]
public class GetStartController
{
[HttpPost]
public Clay PostClay(Clay clay) // 作为参数接收(也可以作为返回值类型)
{
return clay;
}

[HttpPost]
public dynamic PostClay2([Clay] dynamic clay) // 作为参数接收(推荐 ✅✅✅)
{
return clay;
}

[HttpPost]
public YourModel PostData() // 支持流变对象自动转换为目标类型
{
dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
return clay; // 自动转换为目标类型
}

[HttpPost]
public IActionResult PostData2() // 支持流变对象自动转换为 IActionResult
{
dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
return clay; // 自动转换为 IActionResult 类型
}
}

若需配置流变对象的序列化选项(如键命名策略),可通过 AddJsonOptions 进行设置:

services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
})
.AddClayOptions(options => // 或使用 .AddClayOptions();
{
// 控制是否将键值对格式的 JSON 转换为单一对象
options.KeyValueJsonToObject = true;
});
序列化选项区别说明
  • .AddClayOptions 配置的序列化选项用于控制如何将 HTTP 请求内容解析为流变对象。
  • .AddJsonOptions 配置的序列化选项用于控制如何将流变对象序列化为 JSON 并返回给客户端。

当流变对象作为某个类型的属性时,若不能直接应用全局的 AddClayOptions 配置选项,可以通过调用 Rebuilt 方法来进行重建:

public class ClayModel
{
public int Id { get; set; }
public Clay? Clay { get; set; }
}
[ApiController]
[Route("[controller]/[action]")]
public class GetStartController
{
[HttpPost]
public ClayModel PostClay([FromServices] IOptions<ClayOptions> options, ClayModel model)
{
model.Clay?.Rebuilt(options.Value); // 应用全局配置进行重建
return model;
}
}

3. 在最小 API 中的应用

在最小 API 中使用流变对象前,也需完成相应的配置:

独立库说明

Furion 框架已内置该功能,无需额外安装 NuGet 包。若您使用 Shapeless 独立库,请安装 Shapeless.AspNetCore 以替代 Shapeless

Startup.csProgram.cs 文件中配置 ClayOptions 选项服务。

services.AddControllers()
.AddClayOptions(options => // 或使用 .AddClayOptions();
{
// 控制是否将键值对格式的 JSON 转换为单一对象
options.KeyValueJsonToObject = true;
});

以下是一个简单的示例,展示了如何在最小 API 中使用 Clay 流变对象:

app.MapPost("/clay", async (HttpContext context, Clay clay) =>
{
await context.Response.WriteAsJsonAsync(clay.ToJsonString());
});

29.2.12 隐式与显式转换

默认情况下,将 JSON 字符串转换为 Clay 类型需调用 Clay.Parse(object) 静态方法:

var clay = Clay.Parse("""{"id":1,"name":"furion"}""");

该方法清晰直观,符合常见编程习惯。
在最新版本中,我们进一步简化了这一过程,无需显式调用 Parse 即可自动完成转换:

Clay clay = """{"id":1,"name":"furion"}""";

您也可以使用显式转换语法:

var clay = (Clay)"""{"id":1,"name":"furion"}""";

除了 string 类型,Clay 还支持从以下类型进行隐式或显式转换:

// byte[]
Clay clay = """{"id":1,"name":"furion"}"""u8.ToArray();

// Stream
Clay clay = new MemoryStream("""{"id":1,"name":"furion"}"""u8.ToArray());

// JsonNode
Clay clay = JsonNode.Parse("""{"id":1,"name":"furion"}""");

// Dictionary<string, object?>
Clay clay = new Dictionary<string, object?> { { "id", 1 }, { "name", "furion" } };

// ExpandoObject
dynamic expandoObject = new ExpandoObject();
expandoObject.id = 1;
expandoObject.name = "furion";
Clay clay = expandoObject;

同时,Clay 也支持隐式或显式转换为 stringDictionary<string, object?>

// 转换为 string
string json = Clay.Parse("""{"id":1,"name":"furion"}""");

// 转换为 Dictionary<string, object?>
Dictionary<string, object?> dic = Clay.Parse("""{"id":1,"name":"furion"}""");

借助隐式与显式转换机制,Clay 类型的使用变得更加灵活与简洁,提升了开发效率与代码可读性。

29.3 Clay 类型

Clay 类型是 DynamicObject 的具体派生类型,它实现了 IEnumerable<object?>IFormattable 接口。通过继承 DynamicObjectClay 允许像 JavaScript 那样在 C# 中灵活操作对象数据。

29.3.1 初始化实例

框架提供了多种初始化 Clay 类型实例的方式:

1. 使用 new 关键字

// 创建单一对象
dynamic clay = new Clay();
dynamic clay = new Clay(ClayType.Object);

// 创建集合或数组对象
dynamic array = new Clay(ClayType.Array);

// 支持传入 ClayOptions
dynamic clay = new Clay(new ClayOptions());
dynamic array = new Clay(ClayType.Array, new ClayOptions());

2. 使用嵌套类型 ObjectArraynew 关键字

// 创建单一对象
dynamic clay = new Clay.Object();

// 创建集合或数组对象
dynamic array = new Clay.Array();

// 支持传入 ClayOptions
dynamic clay = new Clay.Object(new ClayOptions());
dynamic array = new Clay.Array(new ClayOptions());

3. 使用 EmptyObjectEmptyArray 静态方法

// 创建单一对象
dynamic clay = Clay.EmptyObject();

// 创建集合或数组对象
dynamic array = Clay.EmptyArray();

// 支持传入 ClayOptions
dynamic clay = Clay.EmptyObject(new ClayOptions());
dynamic array = Clay.EmptyArray(new ClayOptions());

4. 使用 Parse 静态方法

dynamic clay = Clay.Parse("{}");
dynamic array = Clay.Parse("[]");

dynamic clay = Clay.Parse(new {});

// 支持传入 ClayOptions
dynamic clay = Clay.Parse("{}", new ClayOptions());

// ...
dynamic 关键字声明说明

推荐使用 dynamic 关键字来接收流变对象实例,因为流变对象在编译时类型不固定,需运行时确定。

dynamic 的优势在于可灵活访问数据:既可通过属性方式如 clay.Name,也可通过索引方式 clay["Name"]。这样即使 Name 属性未定义,代码也能通过编译。

相比之下,使用 var 或流变对象实际类型 Clay 接收时,仅能通过索引如 clay["Name"] 或其他方法访问。

便捷初始化方式

除了先创建 Clay 对象再逐个添加属性的方式,您还可以采用更简洁的初始化技巧,直接在对象创建时设置初始值:

// 创建单一对象
dynamic clay = new Clay
{
["id"] = 1,
["name"] = "Shapeless"
};

// 创建集合或数组
dynamic array = new Clay.Array
{
[0] = 1,
[1] = "Shapeless",
[2] = true
};

29.3.2 标识符(运算符) ✨

在流变对象中,标识符可以是以下形式:

  • 键(字符串):用于访问或设置对象的属性。
  • 索引(整数):用于访问或设置数组或集合中的元素。
  • 索引运算符(Index:用于从数组或集合的末尾开始访问元素。
  • 范围运算符(Range:用于访问或截取数组或集合中的一段数据范围。

以下是一些使用标识符的示例:

clay.Get("Name"); // 使用字符串标识符
clay.Get(0); // 使用索引标识符
clay.Get(^1); // 使用索引运算符
clay.Get(1..^2); // 使用范围运算符

clay.Name; // 使用属性(字符串)标识符
clay[0]; // 使用索引标识符
clay[^1]; // 使用索引运算符
clay[1..^]; // 使用范围运算符

// 除上述标识符外,还支持路径标识符与 JSON Path 表达式标识符。
clay.PathValue("AppInfo:Company:Address:City");
clay.PathValue("$.AppInfo.Company.Telephones[0]")
关于 Index 和 Range 运算符
  • Index 运算符
    • 仅适用于集合或数组的流变对象
    • 若在单一对象上使用,将引发 NotSupportedException 异常,提示:“Accessing or setting properties using System.Index '^1' is not supported in the Clay.
  • Range 运算符
    • 仅适用于集合或数组的流变对象
    • 仅支持访问和删除操作
    • 若在单一对象上使用,将引发 NotSupportedException 异常,提示:“Accessing or setting properties using System.Range '1..^2' is not supported in the Clay.
    • 若尝试检查集合或数组的索引是否定义时,将引发 NotSupportedException 异常,提示:“Checking containment using a System.Range '1..^2' is not supported in the Clay.

29.3.3 索引器访问与设置

Clay 类型作为框架中的流变对象,提供了索引器功能,允许通过索引器方便地访问、添加或设置数据:

// 单一对象
dynamic clay = new Clay();
clay["id"] = 1; // 支持 clay.id 访问或设置
clay["name"] = "Shapeless"; // 支持 clay.name 访问或设置

// 集合或数组
dynamic array = new Clay.Array();
array[0] = 1;
array[1] = 2;
array[2] = 3;

对于集合或数组的流变对象,框架还支持 IndexRange 运算符:

// 使用 Index 运算符设置和访问元素
array[^1] = "last"; // 设置最后一项
var last = array[^1]; //访问最后一项

// 使用 Range 运算符截取数据范围
var newClay = array[1..^2]; // 截取指定范围的数据并返回新的流变对象

框架还支持通过路径表达式访问嵌套结构的数据。此时需要将索引器的第二个参数设为 true,表示启用路径解析模式:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
}
}
""");

var name = clay["AppInfo:Name", true]; // "Furion"

29.3.4 解构函数(析构表达式)

流变对象提供多种解构函数重载,支持通过析构表达式简化对象解析。该特性特别适用于 LambdaLinq 场景,可减少不必要的类型转换:

// 常规用法:在使用 Lambda 或 Linq 之前,需将对象转换为 IEnumerable<dynamic?> 或 Clay 类型才能操作
dynamic clay = Clay.Parse("""{"id":1,"name":"furion"}""");

// 解构函数:可以直接获取 IEnumerable<dynamic?> 或者额外的 Clay 实例,无需手动转换
var (clay, enumerable) = Clay.Parse("""{"id":1,"name":"furion"}""");
var (clay, enumerable, raw) = Clay.Parse("""{"id":1,"name":"furion"}"""); // 支持获取原始 Clay 类型实例

在上面例子中,claydynamic 类型,enumerableIEnumerable<dynamic?> 类型,而 raw 则是 Clay 类型。

利用解构函数的支持,在 LambdaLinq 应用中可有效减少类型转换的操作。以下是一些示例代码:

var (clay, enumerable) = Clay.Parse("""{"id":1,"name":"furion"}""");

// Lambda
var list = enumerable.Where(u => u?.Key == "id").OrderBy(u => u?.Key).ToList();

// Linq
var query = from item in enumerable
where item.Key == "id"
orderby item.Key
select item;

这样,通过直接使用 enumerableraw 变量,我们可以更加便捷地执行 LambdaLinq 操作。这种做法不仅提高了代码的可读性,也提升了开发效率。

此外,如果流变对象是通过另一个流变对象的属性或元素获取的,则可以通过隐式或显式转换为 IEnumerable<dynamic?>Clay 类型。这使得即使在处理嵌套属性时,也能方便地进行 LambdaLinq 操作。示例如下:

dynamic clay = Clay.Parse("""{"id":1,"name":"furion","children":{"cid":"fur","age":5}}""");

var children = clay.children;
// 隐式转换
IEnumerable<dynamic?> children = clay.children; // 转换为 IEnumerable<dynamic?>
Clay children = clay.children; // 转换为 Clay

// 显式转换
var children = (IEnumerable<dynamic?>)clay.children; // 强制转换为 IEnumerable<dynamic?>
var children = (Clay)clay.children; // 强制转换为 Clay
var (childrenClay, childrenEnumerable) = (Clay)clay.children; // 使用析构表达式

// Lambda 示例
var list = children.Where(u => u?.Key == "id").OrderBy(u => u?.Key).ToList(); // 或使用 children.AsEnumerableObject()

// Linq 示例
var query = from item in children // 或使用 children.AsEnumerableObject()
where item.Key == "id"
orderby item.Key
select item;

通过这种方式,即使是嵌套在其他对象中的属性,也能够轻易地转换为适合 LambdaLinq 操作的类型,从而简化数据处理流程,提高代码的可读性和效率。

29.3.5 内置属性

Clay 类型提供了多个便捷属性,具体如下表所示:

属性名称类型描述
IsObjectbool判断是否为单一对象。
IsArraybool判断是否为集合或数组。
TypeClayType 枚举类型获取流变对象的基本类型。
Countint获取键或元素的数量。
Lengthint获取键或元素的数量。
IsEmptybool判断是否未定义键、为空集合或为空数组。
KeysIEnumerable<object>获取键或索引的列表。
MemberNamesIEnumerable<string>获取单一对象键(属性名)的列表。
ValuesIEnumerable<dynamic?>获取值或元素的列表。
IsReadOnlybool判断是否为只读模式。
内置属性冲突解决方案

在使用单一对象的流变对象时,可能会遇到自定义键与 Clay 类型内置属性名称冲突的情况。例如:

dynamic clay = new Clay();

clay.Count = 10;
clay.IsObject = "True";

在上述代码中,尽管尝试为 CountIsObject 设置自定义值,但访问这些属性时,框架会优先返回 Clay 类型内置属性的值。因此,clay.Count 将返回 Clay 对象默认的键/元素数量,而 clay.IsObject 将返回 Clay 对象的类型判断)。

为解决此类冲突,您可以通过以下方式访问自定义值:

// 索引方式
var count = clay["Count"]; // 10

// 流变对象作为方法调用方式
var count2 = clay("Count");

// 使用 Get 方法
var count3 = clay.Get("Count");

29.3.6 判断是否为单一对象

通过 IsObject 属性可以判断一个对象是否为单一对象:

dynamic clay = new Clay();
var isObject = clay.IsObject; // true

dynamic array = new Clay.Array();
var isObject2 = array.IsObject; // false

29.3.7 判断是否为集合或数组

通过 IsArray 属性可以判断一个对象是否为集合或数组:

dynamic clay = new Clay();
var isArray = clay.IsArray; // false

dynamic array = new Clay.Array();
var isArray2 = array.IsArray; // true

29.3.8 获取流变对象的基本类型

通过 Type 属性可以获取流变对象的基本类型:

dynamic clay = new Clay();
var clayType = clay.Type; // ClayType.Object

dynamic array = new Clay.Array();
var clayType2 = array.Type; // ClayType.Array

Type 属性返回一个 ClayType 枚举值,包含以下选项:

  • ClayType.Object(默认值):表示单一对象。
  • ClayType.Array:表示集合或数组。

29.3.9 获取键或元素的数量

通过 LengthCount 属性可以获取键或元素的数量:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
var length = clay.Length; // 2
var count = clay.Count; // 2

dynamic array = Clay.Parse("[1,2,3,4]");
var length2 = array.Length; // 4
var count2 = array.Count; // 4
选择 Length 或 Count 的说明

在某些编程习惯中,Length 属性通常用于数组,表示其元素数量,而 Count 属性则更常用于集合。

29.3.10 判断是否未定义键、为空集合或为空数组

通过 IsEmpty 属性可以判断是否未定义键、为空集合或为空数组:

dynamic clay = new Clay();
var isEmpty = clay.IsEmpty; // true

clay.id = 1;
var isEmpty2 = clay.IsEmpty; // false

dynamic array = new Clay.Array();
var isEmpty3 = array.IsEmpty; // true

array.Add(1);
var isEmpty4 = array.IsEmpty; // false

29.3.11 获取键或索引的列表

通过 Keys 属性可以获取键或索引的列表:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
var keys = clay.Keys; // ["id", "name"]

dynamic array = Clay.Parse("[1,true,false,\"furion\"]");
var keys2 = array.Keys; // [0,1,2,3]

注意Keys 属性返回的是一个 IEnumerable<object> 类型的集合。

29.3.12 获取单一对象键(属性名)的列表

通过 MemberNames 属性可以获取单一对象的属性名列表:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
var keys = clay.MemberNames; // ["id", "name"]

注意MemberNames 属性与 Keys 属性的区别在于,MemberNames 属性仅适用于单一对象的流变对象。

29.3.13 获取值或元素的列表

通过 Values 属性可以获取值或元素的列表:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
var values = clay.Values; // [1,"shapeless"]

dynamic array = Clay.Parse("[1,true,false,\"furion\"]");
var values2 = array.Values; // [1,true,false,"furion"]

注意Values 属性返回的是一个 IEnumerable<dynamic?> 类型的集合。

29.3.14 判断是否为只读模式

通过 IsReadOnly 属性可以判断是否为只读模式:

dynamic clay = new Clay();
var isReadOnly = clay.IsReadOnly; // false

clay.AsReadOnly();
var isReadOnly2 = clay.IsReadOnly; // true

29.3.15 从 JSON 字符串或 C# 对象或文件创建流变对象

1. 从 JSON 字符串创建

在实际开发中,我们经常需要对 JSON 字符串进行增删改查操作。通过流变对象提供的 Parse 静态方法,可以将 JSON 字符串转换为流变对象实例。该方法支持所有标准的 JSON 字符串(键和字符串值需使用双引号)以及 JavaScript 基础类型字面量。

  • JSON 对象字符串创建

JSON 对象字符串是指用 {} 包裹的键值对集合。

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""");

// 添加新属性
clay.age = 30;

// 访问属性
var id = clay.id; // 1
var name = clay["name"]; // "Furion"
var age = clay.age; // 30

// 输出字符串
Console.WriteLine(clay);

控制台输出:

{
"id": 1,
"name": "Furion",
"age": 30
}
  • JSON 数组字符串创建

JSON 数组字符串是由 [] 包裹的值集合。

dynamic array = Clay.Parse("[1,2,3,true,\"Furion\"]");

// 追加新项
array.Add(false);
array.Add(clay);

// 访问项
var first = array[0]; // 1
var second = array[1]; // 2
var last = array[^1]; // "{\"id\":1,\"name\":\"Furion\",\"age\":30}"

// 输出字符串
Console.WriteLine(array);

控制台输出:

[
1,
2,
3,
true,
"Furion",
false,
{
"id": 1,
"name": "Furion",
"age": 30
}
]
  • 从任意 JSON 字面量字符串创建

除了支持对象或数组格式的 JSON 字符串外,框架还允许使用任意字面量。

dynamic scalar = Clay.Parse("true");

// 访问值
var value = scalar.value;

// 输出字符串
Console.WriteLine(scalar);

控制台输出:

{
"value": true
}
自定义非对象/数组字面量的键名

对于数值、布尔值、字符串等非对象或数组的字面量,框架默认将其转换为一个包含键 value 的对象,并将字面量值赋给 value 键。如果需要更改默认的 value 键名,可通过 ClayOptions 进行配置:

dynamic wrapper = Clay.Parse("true", new ClayOptions
{
ScalarValueKey = "data"
});

// 访问值
var data = wrapper.data;

// 输出字符串
Console.WriteLine(wrapper);

控制台输出:

{
"data": true
}
  • 从键值对格式的 JSON 字符串创建

键值对格式的 JSON 字符串(即由 key/value 对组成的数组)在默认情况下会被解析为集合或数组类型的流变对象。若您期望将其解析为单一对象的流变对象,只需传入配置好的 ClayOptions 即可:

dynamic dicObject = Clay.Parse("""
[
{
"key": "id",
"value": 1
},
{
"key": "name",
"value": "Furion"
}
]
""", new ClayOptions { KeyValueJsonToObject = true });

// 访问值
var id = dicObject.id; // 1
var name = dicObject.name; // "Furion"

// 输出字符串
Console.WriteLine(dicObject);

控制台输出:

{
"id": 1,
"name": "Furion"
}
key/value 键的大小写不敏感

在解析键值对格式的 JSON 字符串时,keyvalue 的大小写是被忽略的,即 Key/Valuekey/value 同样有效。这意味着你可以根据需要灵活地使用不同的大小写形式,而不会影响解析结果。

2. 从 C# 对象实例创建

不仅可以从 JSON 字符串创建流变对象,还能基于多种 C# 数据对象进行创建。这些数据对象需支持序列化,或通过自定义的 JsonConverter 实现序列化。此外,框架支持从 StreamUtf8JsonReaderJsonElement 等特殊类型中创建流变对象。

  • 从具体类型对象创建
dynamic clay = Clay.Parse(new YourModel { Id = 1, Name = "Shapeless" });
  • 从匿名对象创建
dynamic clay = Clay.Parse(new { id = 1, name = "Furion" });
  • 从字典对象创建
dynamic clay = Clay.Parse(new Dictionary<string, object>
{
{ "id", 1 },
{ "name", "Furion" }
});
  • 从键值对集合或数组创建
dynamic clay = Clay.Parse(new[]
{
new KeyValuePair<string, object?>("id", 1),
new KeyValuePair<string, object?>("name", "furion")
}.ToDictionary()); // 调用 ToDictionary() 方法可以避免解析为 Key/Value 数组
  • 从集合或数组(具体类型或匿名类型)创建
dynamic clay = Clay.Parse(new List<YourModel>
{
new() { Id = 1, Name = "Furion" },
new() { Id = 2, Name = "Shapeless" }
});
  • 从字节数组(JSON 字符串)创建
var byteArray = "{\"id\":1,\"name\":\"furion\"}"u8.ToArray();
dynamic clay = Clay.Parse(byteArray);
  • Stream 流(JSON 字符串)创建
using var memoryStream = new MemoryStream("{\"id\":1,\"name\":\"furion\"}"u8.ToArray());
dynamic clay = Clay.Parse(memoryStream);
  • Utf8JsonReader 创建
var utf8JsonReader = new Utf8JsonReader("{\"id\":1,\"name\":\"furion\"}"u8.ToArray(), true, default);
dynamic clay = Clay.Parse(ref utf8JsonReader); // 需使用 ref 进行引用传递
  • JsonElement 创建
using var jsonDocument = JsonDocument.Parse("{\"id\":1,\"name\":\"Furion\"}");
dynamic clay = Clay.Parse(jsonDocument.RootElement);
  • 从流变对象自身创建
dynamic clay = Clay.Parse(new Clay
{
["id"] = 1,
["name"] = "Shapeless"
});
  • 从任意字面量创建
dynamic clay = Clay.Parse(true);
var value = clay.value; // true
自定义字面量键名

对于数值、布尔值、字符串等字面量,框架默认将其转换为含 value 键的对象。如需更改默认键名,可通过 ClayOptions 配置:

dynamic clay = Clay.Parse(true, new ClayOptions
{
ScalarValueKey = "data"
});
var data = clay.data; // true
  • struct 结构体创建

默认情况下,如果结构体中使用属性(Property)声明内部数据,无需额外配置即可将其转换为流变对象。如果使用的是字段(Field)声明内部数据,则可以通过以下两种方式进行配置:

  1. 使用 [JsonInclude] 特性标记字段:
public struct Point
{
[JsonInclude] public int X;
[JsonInclude] public int Y;
}

dynamic clay = Clay.Parse(new Point { X = 1, Y = 1 });
  1. 通过配置 JsonSerializerOptionsIncludeFields 属性为 true
public struct Point
{
public int X;
public int Y;
}

dynamic clay = Clay.Parse(new Point { X = 1, Y = 1 },
options => options.JsonSerializerOptions.IncludeFields = true);
  • 从文件中读取数据创建
dynamic clay = Clay.ParseFromFile("C:\Workspaces\test.json");
  • application/x-www-form-urlencoded 表单数据转换
dynamic clay = Clay.Parse("IsDeviceEnable=0&Thresholdvalue=1&DeviceState=0&SurplusParams=&DeviceName=2%E5%B1%82%E8%8C%B6%E6%A5%BC%E7%83%AD%E6%B0%B4%E7%94%A8%E6%B0%B4%E9%87%8F6789&Concentrator=476103385&ProtocolId=888085307&BuildId=524328523&DeviceId=489414407&DeviceCode=6789");
  • 使用 ToClay() 扩展方法

您还可以通过 ToClay() 扩展方法(位于 Shapeless.Extensions 命名空间下)将任意对象转换为流变对象。示例如下:

dynamic clay = new { Id = 1, Name = "Furion" }.ToClay();
  • 从自定义 JsonConverter 序列化类型对象创建

对于不能直接序列化为 JSON 的类型(如 DataTable),可以创建一个自定义的 JsonConverter 派生类来处理(如 JsonConverter<DataTable>)。

public class DataTableJsonConverter : JsonConverter<DataTable>
{
/// <inheritdoc />
public override DataTable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DataTable value, JsonSerializerOptions options)
{
// 将 DataTable 转换为字典集合
var dictList = value.AsEnumerable().Select(row =>
row.Table.Columns.Cast<DataColumn>()
.ToDictionary(col => col.ColumnName, col => row[col] != DBNull.Value ? row[col] : null)).ToList();

// 序列化字典列表
JsonSerializer.Serialize(writer, dictList, options);
}
}

随后,配置 ClayOptionsJsonSerializerOptions 属性,并向其中添加自定义转换器。

var dataTable = new DataTable();
dataTable.Columns.Add("id", typeof(int));
dataTable.Columns.Add("name", typeof(string));
dataTable.Rows.Add(1, "Furion");
dataTable.Rows.Add(2, "百小僧");

dynamic clay = Clay.Parse(dataTable,
options => options.JsonSerializerOptions.Converters.Add(new DataTableJsonConverter()));
更多支持的类型

除了上述类型,还支持将以下类型转换为流变对象:ExpandoObjectJsonElementJsonDocumentJsonObjectJsonArray 以及 JsonNode

29.3.16 检查标识符是否定义(索引)

使用 Contains 方法可以检查标识符是否已定义:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
var idDefined = clay.Contains("id"); // true
var nameDefined = clay.Contains("name"); // true
var ageDefined = clay.Contains("age"); // false

dynamic array = Clay.Parse("[true,false,30,\"furion\"]");
var index0Defined = array.Contains(0); // true
var index1Defined = array.Contains(1); // true
var index10Defined = array.Contains(10); // false
var index20Defined = 1 < array.Length; // 或通过 Length/Count 属性判断

对于单一对象的流变对象,您还可以通过将属性名作为方法名进行无参调用或使用 HasProperty 方法来检查标识符是否已定义:

var idDefined = clay.id(); // true
var nameDefined = clay.name(); // true
var ageDefined = clay.age(); // false

var idDefined = clay.HasProperty("id"); // true
var nameDefined = clay.HasProperty("name"); // true
var ageDefined = clay.HasProperty("age"); // false

Contains 方法同样适用于检查委托方法是否已定义:

clay.sayHello = (Func<string>)(() => $"Hello, {clay.name}!");
var sayHelloDefined = clay.Contains("sayHello"); // true
委托方法名区分大小写

检查委托方法是否已定义时,请注意方法名是区分大小写的。

使用 IsDefined 方法检查标识符定义

您也可以使用 IsDefined 方法来检查标识符是否已定义,其用法与 Contains 方法一致:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
var idDefined = clay.IsDefined("id"); // true
支持路径检查

除了使用 Contains(path)IsDefined(path) 方法外,框架还支持通过路径索引的方式来检查是否定义。例如:

clay.Contains("AppInfo:Name", true); // true
clay.Contains("AppInfo:Company:Address:City", true); // true
clay.Contains("AppInfo:Company:Telephones:0", true); // true

在使用路径检查时,需要传入第二个布尔参数,用于指示当前访问方式是否为路径访问:

  • 当设置为 true 时,底层将使用 ContainsByPath(path) 方法进行查找,按路径检查定义;
  • 若设置为 false,则会调用 Contains(identifier) 方法,仅进行单层键的查找定义。

29.3.17 根据标识符获取值

Clay 提供了多种方式根据标识符获取值,包括属性访问、索引访问以及 Get 方法。

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
// 使用属性方式
var id = clay.id;
// 使用索引方式
var name = clay["name"];

// 使用 Get 方法
var id2 = clay.Get("id");
var name2 = clay.Get("name");

Get 方法同样适用于集合或数组:

dynamic array = Clay.Parse("[1,true,false,\"furion\"]");
var one = array.Get(0);
var last = array.Get(^1); // "furion"
Get 方法与 Range 运算符的结合

使用 Get 方法时,传入 Range 运算符可以从集合或数组中截取指定范围的元素,并返回一个新的流变对象:

dynamic newArray = array.Get(1..^1); // [true,false]

Get 方法还支持将值转换为目标类型:

var intId = clay.Get<int>("id");
var intId2 = clay.Get("id", typeof(int)) as int?;

// 支持传入 JSON 序列化选项
var stringName = clay.Get<string>("name", new JsonSerializerOptions());
var stringName2 = clay.Get("name", typeof(string), new JsonSerializerOptions());

对于单一对象的流变对象,您还可以通过将属性名作为方法名并指定目标类型来获取值:

var intId = clay.id<int>();
var intId2 = clay.id(typeof(int)) as int?;

// 支持传入 JSON 序列化选项
var stringName = clay.name<string>(new JsonSerializerOptions());
var stringName2 = clay.name(typeof(string), new JsonSerializerOptions());
处理不存在的标识符

当使用 Get 方法访问不存在的标识符时,默认会抛出 KeyNotFoundException 异常。您可以通过设置 ClayOptionsAllowMissingProperty(单一对象)和 AllowIndexOutOfRange(集合或数组)属性为 true,避免抛出异常并返回 null

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""", new ClayOptions
{
AllowMissingProperty = true,
AllowIndexOutOfRange = true
});

var age = clay.Get("age"); // 返回 null,不抛出异常

29.3.18 根据标识符查找 JsonNode 节点

Clay 中,所有数据均以 JsonNode 形式存储。您可以使用 FindNode 方法直接获取 JsonNode 节点:

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"shapeless\"}");
JsonNode idNode = clay.FindNode("id");
JsonNode nameNode = clay.FindNode("name");
处理不存在的标识符

当使用 FindNode 方法访问不存在的标识符时,默认会抛出 KeyNotFoundException 异常。您可以通过设置 ClayOptionsAllowMissingPropertyAllowIndexOutOfRange 属性为 true,避免抛出异常并返回 null

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""", new ClayOptions
{
AllowMissingProperty = true,
AllowIndexOutOfRange = true
});

var ageNode = clay.FindNode("age"); // 返回 null,不抛出异常

除了支持通过单个标识符查找 JsonNode 节点外,框架还提供了基于路径索引的节点查找方式,便于快速定位嵌套层级较深的节点。例如:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"],
"Date":"2024-12-26T00:00:00"
}
}
}
""");

JsonNode comNameNode = clay.FindNodeByPath("AppInfo:Company:Name"); // "Baiqian"
JsonNode telpSecondNode = clay.FindNodeByPath("AppInfo:Company:Telephones:1"); // "0760-88888881"

此外,FindNodeByPath 提供了重载版本,可用于安全地检查节点是否存在,并同时获取解析后的路径片段:

bool isExist = clay.FindNodeByPath("AppInfo:Company:Name", out JsonNode comNameNode, out string[] identifiers);

该重载方法返回一个 bool 值表示节点是否存在,并通过 out 参数返回对应的 JsonNode 实例和按分隔符拆分后的路径数组,便于进一步处理或调试。

29.3.19 根据标识符设置值

Clay 类型提供了多种方法来根据标识符设置值,包括属性方式、索引方式以及专门的 Set 方法。

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
// 使用属性方式
clay.id = 2;
// 使用索引方式
clay["name"] = "Furion";

// 使用 Set 方法
clay.Set("id", 3);
clay.Set("name", "Shapeless");

Set 方法不仅适用于单一对象,同样适用于集合或数组场景。

dynamic array = Clay.Parse("[1,true,false,\"furion\"]");
array.Set(0, 2);
array.Set(^1, "Shapeless");
支持路径设置值

除了使用 Set(identifier, value) 方法外,框架还支持通过路径索引的方式来设置嵌套数据。例如:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"],
"Date":"2024-12-26T00:00:00"
}
}
}
""");

clay.SetPathValue("AppInfo:Name", "百小僧");
Assert.Equal("百小僧", clay["AppInfo"]!["Name"]);

clay.SetPathValue("AppInfo:Company:Address:City", "中国中山市");
Assert.Equal("中国中山市", clay["AppInfo"]!["Company"]["Address"]["City"]);

clay.SetPathValue("AppInfo:Company:Telephones:0", "0760-88888882");
Assert.Equal("0760-88888882", clay["AppInfo"]!["Company"]["Telephones"][0]);

注意SetPathValue 仅支持更新已存在的路径,不支持自动创建中间层级或新增节点。若路径不存在,操作将无效或抛出异常。

29.3.20 在指定索引处插入项

对于集合或数组的流变对象,框架支持在指定索引处插入项:

dynamic array = Clay.Parse("[1,true,false]");
// 在索引 0 处插入 "furion"
array.Insert(0, "furion");
// 支持使用反向索引运算符
array.Insert(^1, 10); // 在倒数第二个位置插入 10

此外,还支持批量插入多个值到指定索引处:

// 在索引 1 处批量插入 "furion", 10, true
array.InsertRange(1, "furion", 10, true);
// 同样支持使用反向索引运算符
array.InsertRange(^2, "furion", 10, true); // 在倒数第二个位置开始批量插入
适用说明
  • InsertInsertRange 方法仅适用于集合或数组的流变对象。
  • 若在单一对象上调用这些方法,将触发 NotSupportedException 异常,提示:“'Insert' method can only be used for array or collection operations.

29.3.21 在末尾处添加项

对于集合或数组的流变对象,框架支持通过 AddAppendPush 以及 AddRange 方法在末尾处添加项。

dynamic array = Clay.Parse("[1,true,false]");
array.Add("furion");
array.Add(true);

// 使用 Append 方法
array.Append(false);

// 使用 Push 方法
array.Push(false);

此外,还支持批量添加多个值到末尾处:

array.AddRange(1, "furion", 10, true);
适用说明
  • AddAppendPushAddRange 方法仅适用于集合或数组的流变对象。
  • 若在单一对象上调用这些方法,将触发 NotSupportedException 异常,提示:“'Add' method can only be used for array or collection operations.

29.3.22 移除末尾处的项

对于集合或数组的流变对象,框架支持通过 Pop 方法移除末尾处的项。

dynamic array = Clay.Parse("[1,true,false]");
array.Pop(); // true
array.Pop(); // true
array.Pop(); // true
array.Pop(); // false

Pop 方法会移除集合或数组的末尾项,并在还有项可移除时返回 true,否则返回 false

适用说明
  • Pop 方法仅适用于集合或数组的流变对象。
  • 若在单一对象上调用该方法,将触发 NotSupportedException 异常,提示:“'Pop' method can only be used for array or collection operations.

29.3.23 反转流变对象

Clay 类型支持使用 Reverse 方法来反转流变对象,并生成一个新的流变对象。

dynamic clayObject = Clay.Parse("{\"id\":1,\"name\":\"shapeless\"}");
dynamic reversedClay = clayObject.Reverse(); // 结果:{"name":"shapeless","id":1}

dynamic array = Clay.Parse("[1,2,3,4]");
dynamic reversedArray = array.Reverse(); // 结果:[4,3,2,1]

无论是单一对象还是集合或数组,Reverse 方法都能有效地将其元素顺序颠倒,并返回一个新的流变对象。

29.3.24 截取集合或数组

对于集合或数组的流变对象,框架提供了多种方式来截取部分数据并生成新的流变对象,包括使用 this[Range] 索引器、Get(Range) 方法以及多个 Slice 方法重载。

dynamic array = Clay.Parse("[1,2,3,4]");

// 使用索引器方式
var newArray = array[1..^1]; // 结果:[2,3]

// 使用 Get 方法
var newArray2 = array.Get(1..^1); // 结果:[2,3]

// 使用 Slice 方法
var newArray3 = array.Slice(1..^1); // 结果:[2,3]
var newArray4 = array.Slice(1, 1); // 结果:[2,3]
适用说明
  • Slice 方法仅适用于集合或数组的流变对象。
  • 若在单一对象上调用该方法,将触发 NotSupportedException 异常,提示:“'Slice' method can only be used for array or collection operations.

29.3.25 合并多个流变对象

Clay 类型支持使用 Combine 方法合并多个流变对象,生成一个新的流变对象。

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"shapeless\"}");
dynamic clay2 = Clay.Parse("{\"name\":\"Furion\",\"age\":30}");
dynamic clay3 = Clay.Parse("{\"author\":\"MonkSoul\"}");
dynamic newClay = clay.Combine(clay2, clay3); // 结果:{"id":1,"name":"Furion","age":30,"author":"MonkSoul"}

dynamic array = Clay.Parse("[1,2,3,4]");
dynamic array2 = Clay.Parse("[4,5,6]");
dynamic array3 = Clay.Parse("[7,8]");
dynamic newArray = array.Combine(array2, array3); // 结果:[1,2,3,4,4,5,6,7,8]

借助 DynamicObject 的特性,您还可以将流变对象的实例作为方法调用,并实现多个流变对象的合并操作:

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"shapeless\"}");
dynamic clay2 = Clay.Parse("{\"name\":\"Furion\",\"age\":30}");
dynamic clay3 = Clay.Parse("{\"author\":\"MonkSoul\"}");
dynamic newClay = clay(clay2, clay3); // 结果:{"id":1,"name":"Furion","age":30,"author":"MonkSoul"}

dynamic array = Clay.Parse("[1,2,3,4]");
dynamic array2 = Clay.Parse("[4,5,6]");
dynamic array3 = Clay.Parse("[7,8]");
dynamic newArray = array(array2, array3); // 结果:[1,2,3,4,4,5,6,7,8]

合并规则:

  • 单一对象
    • 若键相同,后面的流变对象会覆盖前面的相同键。
    • 若键不同,则直接在结果对象中添加新的键值对。
  • 集合或数组
    • 合并操作采用追加方式。
合并流变对象的类型要求
  • 合并操作仅支持相同类型的流变对象。
  • 若尝试将不同类型的流变对象(例如,单一对象与集合或数组)进行合并,系统将抛出 InvalidOperationException 异常,异常信息为:“All Clay objects must be of the same type.

无论是单一对象还是集合或数组,Combine 方法都能有效地合并元素,并返回一个新的流变对象。

29.3.26 根据标识符删除数据

Clay 类型支持使用 RemoveDelete 方法,根据指定的标识符来删除数据。

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
clay.Remove("id"); // true
clay.Delete("name"); // true

RemoveDelete 方法不仅适用于单一对象,同样适用于集合或数组场景。

dynamic array = Clay.Parse("[1,true,false,\"furion\"]");
array.Remove(0); // true
array.Delete(0); // true
Remove 和 Delete 方法与 Range 运算符的结合使用

在使用 RemoveDelete 方法时,如果传入 Range 运算符作为参数,该操作会从集合或数组中删除指定范围内的元素。具体示例如下:

array.Remove(1..^1);
array.Delete(1..^1);

// 根据范围删除数据
array.Remove(1, 1); // 同 Remove(Range)
array.Delete(1, 1); // 同 Remove(Range)

当使用 RemoveDelete 方法根据标识符删除数据时,如果指定的标识符不存在且未配置允许访问不存在的标识符,Clay 将抛出 KeyNotFoundException 异常,异常信息为 "The property 'xxx' was not found in the Clay."

为了处理这种情况,您可以通过设置 ClayOptionsAllowMissingProperty(单一对象) 和 AllowIndexOutOfRange(集合或数组)属性为 true,可以在尝试删除不存在的属性时避免抛出异常,而是返回 false

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""", new ClayOptions
{
AllowMissingProperty = true,
AllowIndexOutOfRange = true
});

clay.Remove("age"); // 返回 false,不抛出异常
支持路径删除

除了使用 Remove(path)Delete(path) 方法外,框架还支持通过路径索引的方式来删除嵌套数据。例如:

clay.Remove("AppInfo:Name", true); // true
clay.Remove("AppInfo:Company:Address:City", true); // true
clay.Remove("AppInfo:Company:Telephones:0", true); // true

在使用路径删除时,需要传入第二个布尔参数,用于指示当前访问方式是否为路径访问:

  • 当设置为 true 时,底层将使用 RemovePathValue(path) 方法进行查找,按路径删除键值;
  • 若设置为 false,则会调用 Remove(identifier) 方法,仅进行单层键的查找删除。

29.3.27 将流变对象转换为具体类型

流变对象支持转换为具体的类型。例如,将 clay 流变对象转换为 ClayModel 类型,其中 ClayModel 类的定义如下:

public class ClayModel
{
public int Id { get; set; }
public string? Name { get; set; }
public DateTime? Date { get; set; }
public bool IsTrue { get; set; }
}

为了演示类型转换操作,我们准备了一个样本流变对象。

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless","date":"2025-01-14T00:00:00","isTrue":true}""");
  • 使用声明方式(隐式转换)自动转换
ClayModel clayModel = clay;
  • 使用强制转换(显式转换)
var clayModel2 = (ClayModel)clay;
显式转换的局限性

显式转换(强制转换)不支持转换为非 IEnumerable<dynamic?> 类型。建议使用隐式转换、As<T>() 方法或其他替代方案来实现转换。

  • 使用流变对象的 As<T>() 方法
var clayModel = clay.As<ClayModel>();

// 支持传入 JSON 序列化选项
var clayModel2 = clay.As<ClayModel>(new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
特殊类型转换

此外,框架还支持将流变对象转换为以下类型:

  • IEnumerable<KeyValuePair<object, dynamic?>>
  • IEnumerable<KeyValuePair<string, dynamic?>>(仅适用于单一对象)
  • IEnumerable<KeyValuePair<int, dynamic?>>(仅适用于集合或数组)
  • IActionResult
  • 使用流变对象的 As(Type) 方法
var clayModel = clay.As(typeof(ClayModel)) as ClayModel;

// 支持传入 JSON 序列化选项
var clayModel2 = clay.As(typeof(ClayModel), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) as ClayModel;
  • 使用流变对象实例作为方法调用

框架借助 DynamicObject 的特性,允许将流变对象实例作为方法调用,并支持将目标类型作为参数进行类型转换操作。

var clayModel = clay(typeof(ClayModel)) as ClayModel;

// 支持传入 JSON 序列化选项
var clayModel2 = clay(typeof(ClayModel), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) as ClayModel;
  • 使用 JsonSerializer 反序列化方法
var clayModel = JsonSerializer.Deserialize<ClayModel>(clay.ToString()
, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
  • 转换为 XElement 类型对象

框架不仅支持将流变对象转换为常见的具体类型,还特别提供了对 XElement 类型的支持,使得流变对象能够轻松转换为 XML 格式的字符串。以下是几种转换方法:

// 隐式转换
XElement xElement = clay;
// 显式转换
var xElement2 = (XElement)clay;
// As<T>() 方法
var xElement3 = clay.As<XElement>();
  • 转换为 IActionResult 类型对象:

框架还支持将对象自动转换为 ASP.NET MVC 中的 IActionResult 类型,以下是几种转换方法:

// 隐式转换
IActionResult jsonResult = clay;
// As<T> 方法
var jsonResult2 = clay.As<IActionResult>();
  • 支持转换后进行模型验证

在将流变对象转换为具体类型时,您可以配置执行模型验证。只需在初始化时设置验证选项,或使用 Rebuilt 方法进行重建时配置,如下所示:

// 初始化时配置模型验证
dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""",
new ClayOptions
{
ValidateAfterConversion = true
});

// 或使用 Rebuilt 方法重建并配置模型验证
clay.Rebuilt(new ClayOptions
{
ValidateAfterConversion = true
})

当流变对象转换为 ClayModel 或其他具体类型时,则会触发模型验证:

public class ClayModel
{
[Range(2, 10)]
public int Id { get; set; }

[Required]
[MinLength(3)]
public string? Name { get; set; }
}

// 转换
ClayModel model = clay;

此时,若数据不符合验证规则,将抛出异常,例如:

System.ComponentModel.DataAnnotations.ValidationException: The field Id must be between 2 and 10.
集合或数组类型转换

以上操作同样适用于集合或数组流变对象的类型转换。

29.3.28 深度克隆流变对象

流变对象支持深度克隆功能,能够生成并返回一个新的、完全独立的流变对象。

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""");

var newClay = clay.DeepClone();
var newClay = clay.DeepClone(new ClayOptions()); // 支持传入新的配置选项

注意:深度克隆仅复制流变对象的结构和数据,不会克隆动态委托方法。

29.3.29 清空流变对象数据

流变对象提供了清空其内部数据的功能。示例如下:

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""");
clay.Clear(); // clay 结果为:{}

dynamic array = Clay.Parse("[1,2,3,4]");
array.Clear(); // array 结果为:[]

29.3.30 支持将流变对象数据写入 Utf8JsonWriter

流变对象提供了便捷的 WriteTo 方法,允许用户将流变对象的数据直接写入 Utf8JsonWriter 实例中。以下是具体示例:

var clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");

using var memoryStream = new MemoryStream();
using (var jsonWriter = new Utf8JsonWriter(memoryStream))
{
clay.WriteTo(jsonWriter); // 支持传递 JsonSerializerOptions 参数
}

29.3.31 只读与可读写模式

流变对象默认支持读写操作,但框架提供了灵活的模式转换功能。通过 AsReadOnly() 方法,可以将流变对象转换为只读模式,防止数据被意外修改。若需恢复可读写模式,则可使用 AsMutable() 方法。

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""");
// 只读模式
clay.AsReadOnly();
clay.id = 2; // 抛出 InvalidOperationException 异常:Operation cannot be performed because the Clay is in read-only mode.

// 可读写模式
clay.AsMutable();
clay.id = 2; // 正常执行

此外,还可在创建流变对象时,通过 ClayOptions 指定其初始模式:

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""", new ClayOptions
{
ReadOnly = true
});

29.3.32 输出 JSONXML 字符串

框架为流变对象提供了多种输出方法和格式,包括 JSONXML 字符串。为了演示输出多种格式操作,我们准备了一个样本流变对象。

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless","author":"百小僧"}""");

1. 使用 ToString() 输出 JSON 字符串

  • 默认输出

使用 ToString() 方法或直接输出对象时,默认以 JSON 格式显示对象的内容。在此过程中,中文字符会被自动进行 Unicode 编码处理。

Console.WriteLine(clay);
Console.WriteLine(clay.ToString());

控制台输出:

{
"id": 1,
"name": "shapeless",
"author": "\u767E\u5C0F\u50E7"
}
  • 取消中文 Unicode 编码

若需取消中文的 Unicode 编码,可使用格式化符 U

Console.WriteLine($"{clay:U}");
Console.WriteLine(clay.ToString("U"));

控制台输出:

{
"id": 1,
"name": "shapeless",
"author": "百小僧"
}
  • 压缩 JSON

使用格式化符 Z 可压缩 JSON 输出,去除格式化空格。

Console.WriteLine($"{clay:Z}");
Console.WriteLine(clay.ToString("Z"));

控制台输出:

{"id":1,"name":"shapeless","author":"\u767E\u5C0F\u50E7"}
  • 组合格式化符

格式化符可组合使用,如同时取消中文 Unicode 编码并压缩 JSON

Console.WriteLine($"{clay:UZ}");
Console.WriteLine(clay.ToString("UZ"));

控制台输出:

{"id":1,"name":"shapeless","author":"百小僧"}
  • 键名策略设置

使用格式化符 C 输出小驼峰命名,P 输出帕斯卡(大驼峰)命名。

Console.WriteLine($"{clay:C}");
Console.WriteLine(clay.ToString("C"));

Console.WriteLine($"{clay:P}");
Console.WriteLine(clay.ToString("P"));

控制台输出:

{
"id": 1,
"name": "shapeless",
"author": "\u767E\u5C0F\u50E7"
}

{
"Id": 1,
"Name": "shapeless",
"Author": "\u767E\u5C0F\u50E7"
}

多个格式化符可组合使用,如:

Console.WriteLine($"{clay:ZUC}");
Console.WriteLine(clay.ToString("ZUC"));

Console.WriteLine($"{clay:ZUP}");
Console.WriteLine(clay.ToString("ZUP"));

控制台输出:

{"id":1,"name":"shapeless","author":"百小僧"}

{"Id":1,"Name":"shapeless","Author":"百小僧"}

2. 使用 ToJsonString() 输出 JSON 字符串

除了使用 ToString() 方法输出 JSON 字符串,框架还提供了 ToJsonString() 方法,用于更灵活地输出 JSON 字符串。ToString() 方法提供的 JSON 输出配置较为有限,而 ToJsonString() 方法则通过接收 JsonSerializerOptions 参数,支持更多的自定义配置。

Console.WriteLine(clay.ToJsonString());

// 支持传入 json 序列化选项
Console.WriteLine(clay.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true, // 格式化 JSON
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 取消中文 Unicode 编码
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // 小驼峰键命名策略
}));

控制台输出:

{"id":1,"name":"shapeless","author":"百小僧"}

{
"id": 1,
"name": "shapeless",
"author": "百小僧"
}
自定义键命名策略

框架内置了多种键命名策略,以满足不同的需求:

  • 小驼峰命名法JsonNamingPolicy.CamelCase,例如将 TempCelsius 转换为 tempCelsius
  • 小写蛇形命名法JsonNamingPolicy.SnakeCaseLower,例如将 TempCelsius 转换为 temp_celsius
  • 大写蛇形命名法JsonNamingPolicy.SnakeCaseUpper,例如将 TempCelsius 转换为 TEMP_CELSIUS
  • 小写短横线命名法JsonNamingPolicy.KebabCaseLower,例如将 TempCelsius 转换为 temp-celsius
  • 大写短横线命名法JsonNamingPolicy.KebabCaseUpper,例如将 TempCelsius 转换为 TEMP-CELSIUS
  • 帕斯卡命名法:通过实例化 new PascalCaseNamingPolicy() 创建,例如将 tempCelsius 转换为 TempCelsius

若需自定义键命名策略,您只需创建一个继承自 JsonNamingPolicy 的新类型,并重写其 ConvertName(string name) 方法即可。这将允许您根据特定规则自定义 JSON 键的命名方式。

3. 使用流变对象实例作为方法调用输出 JSON 字符串

框架借助 DynamicObject 的特性,允许将流变对象实例作为方法调用,并实现 JSON 字符串输出操作。

Console.WriteLine(clay());

// 支持传入 JSON 序列化选项
Console.WriteLine(clay(new JsonSerializerOptions
{
WriteIndented = true, // 格式化 JSON
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 取消中文 Unicode 编码
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // 小驼峰键命名策略
}));

控制台输出:

{"id":1,"name":"shapeless","author":"百小僧"}

{
"id": 1,
"name": "shapeless",
"author": "百小僧"
}
小提示

值得注意的是,上述示例中的 clay() 调用与 clay.ToJsonString() 具有相同效果,同样地,clay(new JsonSerializerOptions()) 也与 clay.ToJsonString(new JsonSerializerOptions()) 功能一致,这为用户提供了更为简洁和直观的调用方式。

4. 使用 ToXmlString() 输出 XML 字符串

框架不仅支持将对象转换为 JSON 字符串,还提供了 ToXmlString() 方法,用于生成 XML 格式的字符串。此外,该方法还接收 XmlWriterSettings 参数,以便用户进行更细致的自定义配置。

Console.WriteLine(clay.ToXmlString());

// 支持传入 XML 写入选项
Console.WriteLine(clay.ToXmlString(new XmlWriterSettings
{
Indent = true
}));

控制台输出:

<?xml version="1.0" encoding="utf-8"?><root type="object"><id type="number">1</id><name type="string">shapeless</name><author type="string">百小僧</author></root>

<?xml version="1.0" encoding="utf-8"?>
<root type="object">
<id type="number">1</id>
<name type="string">shapeless</name>
<author type="string">百小僧</author>
</root>

29.3.33 检查类型或对象是否为流变对象

在一些应用开发场景,我们可能需要判断某个类型或对象是否为流变对象(即 Clay 类型或其子类)。这时,可以使用 Clay.IsClay 静态方法:

Clay.IsClay(typeof(int)); // false
Clay.IsClay(typeof(Clay)); // true

Clay.IsClay(new Clay()); // true
Clay.IsClay(new Clay.Object()); // true
Clay.IsClay(new Clay.Array()); // true

29.3.34 按照键进行排序并返回新的流变对象

在需要对用户提交的数据进行 API 签名时,通常会按照键的升序或降序进行排序,以确保签名的一致性和安全性。为此,流变对象提供了 KSort(升序)和 KRSort(降序)方法:

dynamic clay = Clay.Parse("{\"name\":\"furion\",\"id\":1}");
var newClay = clay.KSort(); // 结果:{"id":1,"name":"furion"}

dynamic clay2 = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");
var newClay2 = clay2.KRSort(); // 结果:{"name":"furion","id":1}

注意KSortKRSort 方法都会返回一个新的流变对象,该对象包含按键排序后的键值对。原始流变对象保持不变。

适用说明
  • KSortKRSort 方法仅适用于单一对象的流变对象。
  • 若在集合或数组上调用这些方法,将触发 NotSupportedException 异常,提示:“'KSort' method can only be used for single object operations.

29.3.35 使用新选项重建流变对象

流变对象支持通过提供新的 ClayOptions 实例来重新构建自身,从而允许在对象初始化后修改其配置选项。例如:

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");
clay.Rebuilt(new ClayOptions
{
AllowMissingProperty = true
});

// 或使用委托方式
clay.Rebuilt(new Action<ClayOptions>(u => u.AllowMissingProperty = true));

框架借助 DynamicObject 的特性,允许将流变对象实例作为方法调用,并支持将 ClayOptionsAction<ClayOptions> 作为参数进行重建操作。

clay(new ClayOptions
{
AllowMissingProperty = true
});

// 或使用委托方式
clay(new Action<ClayOptions>(u => u.AllowMissingProperty = true));

注意Rebuilt 方法会直接修改当前的流变对象,应用新的 ClayOptions 配置。这意味着原始对象的状态将被更新。

29.3.36 获取循环访问元素的枚举数

框架为流变对象提供了 GetEnumerator() 方法,用于获取其元素的枚举器:

单一对象

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");
using IEnumerator<dynamic?> objectEnumerator = clay.GetEnumerator();

var listObject = new List<KeyValuePair<string, dynamic?>>();
while (objectEnumerator.MoveNext())
{
listObject.Add(objectEnumerator.Current); // // Current 实际类型为:KeyValuePair<string, dynamic?>
}

集合或数组

dynamic array = Clay.Parse("""[1,2,true,false,"Furion",{"id":1,"name":"shapeless"},null]""");
using IEnumerator<dynamic?> arrayEnumerator = array.GetEnumerator();

var listArray = new List<dynamic?>();
while (arrayEnumerator.MoveNext())
{
listArray.Add(objectEnumerator.Current);
}

29.3.37 获取单一对象或集合或数组的迭代器

框架为流变对象提供了 AsEnumerable() 方法,该方法能够返回一个迭代器,适用于单一对象、集合或数组。返回的迭代器以 IEnumerable<KeyValuePair<object, dynamic?>> 形式表示。

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");
IEnumerable<KeyValuePair<object, dynamic?>> keyValuePairs = clay.AsEnumerable();

29.3.38 获取单一对象的迭代器

对于仅包含单一对象的流变数据,AsEnumerateObject() 方法提供了一种更具体的迭代器。此方法返回一个 IEnumerable<KeyValuePair<string, dynamic?>> 类型的迭代器,确保键始终为字符串类型,便于直接作为属性名访问。

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");
IEnumerable<KeyValuePair<string, dynamic?>> keyValuePairs = clay.AsEnumerateObject();

29.3.39 获取集合或数组的迭代器

对于集合或数组的流变数据,AsEnumerateArray() 方法提供了一种更具体的迭代器。此方法返回一个 IEnumerable<dynamic?> 类型的迭代器,便于直接访问集合或数组的项集合。

dynamic clay = Clay.Parse("[1,2,3,4]");
IEnumerable<dynamic?> items = clay.AsEnumerateArray();

29.3.40 ForEach 遍历操作

框架为流变对象提供了 ForEach 方法,使用户能够以简洁高效的方式遍历单一对象的属性和集合或数组的元素。该方法接收一个 Action<dynamic?> 委托。

// 单一对象
dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");
clay.ForEach(new Action<dynamic?>(item => // item 实际类型为:KeyValuePair<string, dynamic?>
{
Console.WriteLine($"Key: {item?.Key} Value: {item?.Value}");
}));

// 集合或数组
dynamic array = Clay.Parse("""[1,2,true,false,"Furion",{"id":1,"name":"shapeless"},null]""");
array.ForEach(new Action<dynamic?>(value =>
{
Console.WriteLine($"Value: {value}");
}));

29.3.41 Map 映射操作(Select

框架为流变对象提供了 Map 方法,使用户能够以简洁高效的方式遍历单一对象的属性和集合或数组的元素,该方法接收一个 Func<dynamic?, T> 委托,并返回目标类型的对象集合。

// 单一对象
dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");
IEnumerable<object> models = clay.Map(new Func<dynamic?, object?>(item => new // item 实际类型为:KeyValuePair<string, dynamic?>
{
data = item?.Value
}));

// 集合或数组
dynamic array = Clay.Parse("[{\"id\":1,\"name\":\"furion\"},{\"id\":2,\"name\":\"百小僧\"}]");
IEnumerable<ClayModel> list = array.Map(new Func<dynamic?, ClayModel?>(value => new ClayModel
{
Id = value?.id,
Name = value?.name
}));

29.3.42 Filter 筛选操作(Where

框架为流变对象提供了 Filter 方法,使用户能够以简洁高效的方式遍历单一对象的属性和集合或数组的元素,该方法接收一个 Func<dynamic?, T> 委托,并返回新的流变对象。

// 单一对象
dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\",\"age\":30}");
var newClay = clay.Filter(new Func<dynamic?, bool>(item => item?.Key != "id")); // item 实际类型为:KeyValuePair<string, dynamic?>

// 集合或数组
dynamic array = Clay.Parse("[1,2,3]");
var newArray = array.Filter(new Func<dynamic?, bool>(value => value != 1));

newClaynewArray 最终输出的 JSON 字符串为:

{"name":"furion","age":30}

[2,3]

29.3.43 扩展流变对象

Clay 类型支持使用 Extend 方法进行扩展。以下是具体用法示例:

// 单一对象
dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");
clay.Extend(new { age = 30, name = "shapeless" }); // 基于单个值扩展
clay.Extend(new { address = "广东省中山市"}, new Dictionary<string, object> { { "city", "中山市" } }); // 基于多个值扩展
clay.Extend(new { gender = "男" }).Extend(new { nickname = "百小僧" }); // 支持链式操作

// 集合或数组
dynamic array = Clay.Parse("[1,2,3]");
array.Extend(null, "10", clay).Extend(true, new object[] { 1, 2, 3 });
扩展单一对象注意事项
  • 若为单一对象的流变对象扩展数据,数据不能为 null 或基础(基元)类型,并且必须能够转换为 Dictionary<string, object> 字典类型。
  • 否则,将抛出 InvalidOperationException 异常,异常消息为:Cannot extend a single object with null or basic type values.

29.3.44 根据路径标识符获取值

Clay 类型支持通过路径标识符使用 PathValue 方法获取值,默认情况下,路径标识符使用 :(冒号)作为层级分隔符。使用示例如下:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"],
"Date":"2024-12-26T00:00:00"
}
}
}
""");

var name = clay.PathValue("AppInfo:Name"); // "Furion"
var city = clay.PathValue("AppInfo:Company:Address:City"); // "中国"
var telephone = clay.PathValue("AppInfo:Company:Telephones:0"); // "0760-88888888"
语义一致性说明

为提升代码可读性与语义一致性,框架还提供了 GetPathValue 方法,功能与 PathValue 完全相同。

支持路径索引

除了使用 PathValue(path) 方法外,框架还支持通过路径索引的方式来访问嵌套数据。例如:

var name = clay["AppInfo:Name", true]; // "Furion"
var city = clay["AppInfo:Company:Address:City", true]; // "中国"
var telephone = clay["AppInfo:Company:Telephones:0", true]; // "0760-88888888"

在使用路径索引时,需要传入第二个布尔参数,用于指示当前访问方式是否为路径访问:

  • 当设置为 true 时,底层将使用 PathValue(path) 方法进行查找,按路径解析键值;
  • 若设置为 false,则会调用 Get(identifier) 方法,仅进行单层键的查找。

PathValue 方法还支持类型转换,并可选择传入 JsonSerializerOptions

var date = clay.PathValue("AppInfo:Company:Date", typeof(DateTime));
var date2 = clay.PathValue<DateTime>("AppInfo:Company:Date");

// 支持传入 JSON 序列化选项
var date3 = clay.PathValue<DateTime>("AppInfo:Company:Date", new JsonSerializerOptions());
var date4 = clay.PathValue("AppInfo:Company:Date", typeof(DateTime), new JsonSerializerOptions());

此外,您还可以通过自定义 PathSeparator 指定其他层级分隔符。例如:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"]
}
}
}
""", new ClayOptions
{
PathSeparator = [":", "/"] // 同时支持 : 和 /
});

var name = clay.PathValue("AppInfo:Name"); // "Furion"
var city = clay.PathValue("AppInfo:Company/Address:City"); // "中国"
var telephone = clay.PathValue("AppInfo/Company/Telephones/0"); // "0760-88888888"

在这个例子中,PathSeparator 被配置为同时接受 :/ 作为层级分隔符,从而提供了更灵活的路径访问方式。

处理不存在的标识符

当使用 PathValue 方法访问不存在的标识符时,默认会抛出 KeyNotFoundException 异常。您可以通过设置 ClayOptionsAllowMissingProperty(单一对象)和 AllowIndexOutOfRange(集合或数组)属性为 true,避免抛出异常并返回 null

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""", new ClayOptions
{
AllowMissingProperty = true,
AllowIndexOutOfRange = true
});

var age = clay.PathValue("age"); // 返回 null,不抛出异常
无效路径标识符

若当前路径值非单一对象、集合或数组,继续查找将抛出 InvalidOperationException 异常,异常消息为:“The identifier 'Name' at path 'Name:None' does not support further lookup.”。例如,AppInfo:Name:None 无效,因为 "Furion" 不是可进一步查找的对象。

获取委托方法说明

请注意,通过路径标识符不支持获取委托方法。请使用 Get 方法或其他方式来获取。

29.3.45 检查字符串是否是 JSON 对象({})或数组([]

流变对象提供了静态方法 IsJsonObjectOrArray(input),用于检查字符串是否为有效的 JSON 对象({})或数组([])。以下为示例代码:

Clay.IsJsonObjectOrArray(null); // false
Clay.IsJsonObjectOrArray("true"); // false
Clay.IsJsonObjectOrArray("1"); // false
Clay.IsJsonObjectOrArray("""{"id":1,"name":"shapeless"}"""); // true
Clay.IsJsonObjectOrArray("""[1,2,3]"""); // true

// 支持配置是否允许末尾多余逗号(默认值为 false)
Clay.IsJsonObjectOrArray("""{"id":1,"name":"shapeless",}""", allowTrailingCommas: true); // true
Clay.IsJsonObjectOrArray("""[1,2,3,]""", allowTrailingCommas: true); // true

29.3.46 获取元素(项)的索引

对于集合或数组的流变对象,框架支持通过 IndexOf 方法获取元素(项)的索引。IndexOf 方法会按照元素在流变对象中的顺序返回其索引。如果元素不存在,则返回 -1

var clay = Clay.Parse("[1,2,3,true,false,\"furion\",13.2,{\"id\":1,\"name\":\"furion\"},null,[1,2,3],{\"one\":\"one\",\"some\":null},[1,2,3,{},{\"id\":1}]]");

clay.IndexOf(1); // 输出: 0
clay.IndexOf(2); // 输出: 1
clay.IndexOf(3); // 输出: 2
clay.IndexOf(true); // 输出: 3
clay.IndexOf(false); // 输出: 4
clay.IndexOf("furion"); // 输出: 5
clay.IndexOf(13.2); // 输出: 6
clay.IndexOf(Clay.Parse("{\"id\":1,\"name\":\"furion\"}")); // 输出: 7
clay.IndexOf(Clay.Parse("{\"name\":\"furion\",\"id\":1}")); // 输出: 7
clay.IndexOf(Clay.Parse("{\"name\":null,\"id\":1}")); // 输出: -1
clay.IndexOf(null); // 输出: 8
clay.IndexOf(Clay.Parse("[1,2,3]")); // 输出: 9
clay.IndexOf(Clay.Parse("[1,3,2]")); // 输出: -1
clay.IndexOf(Clay.Parse("{\"one\":\"one\",\"some\":null}")); // 输出: 10
clay.IndexOf(Clay.Parse("{\"some\":null,\"one\":\"one\",}")); // 输出: 10
clay.IndexOf(Clay.Parse("[1,2,3,{},{\"id\":1}]")); // 输出: 11
clay.IndexOf(Clay.Parse("[1,2,3,{\"id\":1},{}]")); // 输出: -1
注意事项
  1. 对象属性顺序与匹配
    • 对于对象类型,IndexOf 方法在比较时会忽略属性的顺序。例如,{"id":1,"name":"furion"}{"name":"furion","id":1} 被视为相同的对象,匹配结果相同。
    • 但如果对象的属性不匹配(如 {"name":null,"id":1}),则匹配失败,返回 -1
  2. 数组顺序与匹配
    • 对于数组类型,IndexOf 方法会严格比较数组中元素的顺序。例如,[1,2,3][1,3,2] 被视为不同的数组,匹配失败,返回 -1
    • 嵌套数组的顺序同样会影响匹配结果。

29.3.47 检查两个流变对象是否相等

在某些场景中,可能需要比较两个流变对象是否相等,或者检查两个 JSON 结构是否一致,此时可通过 Equals 方法进行比较:

var clay1 = Clay.Parse("""{"id":1,"name":"furion"}""");
var clay2 = Clay.Parse("""{"name":"furion","id":1,}""");
var clay3 = Clay.Parse("""{"name":"furion","id":1,"age":30}""");
var clay4 = Clay.Parse("[1,2,3]");

clay1.Equals(clay1); // 输出:true
clay1.Equals(clay2); // 输出:true
clay1.Equals(clay3); // 输出:false
clay1.Equals(clay4); // 输出:false

除了使用 Equals 方法,还可以使用 ==!= 操作符进行比较,例如:

Assert.True(clay1 == clay2);
Assert.False(clay1 == clay3);
Assert.True(clay1 != clay3);
注意事项
  1. 对象属性顺序与匹配
    • 对于对象类型,Equals 方法在比较时会忽略属性的顺序。例如:
      • {"id":1,"name":"furion"}{"name":"furion","id":1} 被视为相同的对象,匹配结果为 true
      • 但如果对象的属性不匹配(如 {"name":null,"id":1}),则匹配失败,返回 false
  2. 数组顺序与匹配
    • 对于数组类型,Equals 方法会严格比较数组中元素的顺序。例如:
      • [1,2,3][1,3,2] 被视为不同的数组,匹配结果为 false
      • 嵌套数组的顺序同样会影响匹配结果。
  3. GetHashCode() 方法的行为
    • 如果两个流变对象相等(即 Equals 返回 true),则它们的 GetHashCode() 值必定相等。
    • 反之,GetHashCode() 值相等并不保证两个对象一定相等(哈希冲突的可能性)。

29.3.48 将流变对象转换为字典类型(Dictionary

流变对象 Clay 实现了 IEnumerable<object?> 接口,因此支持使用 Enumerable.ToDictionary 方法将其转换为 Dictionary 类型。框架提供了多种方式实现这种转换:

dynamic clay = Clay.Parse("""{"id":1,"name":"furion"}""");

// 1. 使用隐式转换
Dictionary<string, dynamic?> dic1 = clay; // 或使用 Dictionary<string, object?> dic1 = clay;

// 2. 使用 ToDictionary() 方法
var dic2 = clay.ToDictionary(); // dic2 类型为 Dictionary<string, dynamic?>

// 3. 使用 As<Dictionary<string, dynamic?>>() 方法
var dic3 = clay.As<Dictionary<string, dynamic?>>();

此外,还可以通过 Enumerable.ToDictionary 方法,显式指定键和值的类型,从而自定义字典的键和值类型:

// 将 dynamic 类型的流变对象转换回 Clay 类型
Clay clayObject = clay;

// 4. 使用 ToDictionary 方法,通过 Lambda 表达式指定键和值
var dic4 = clayObject.ToDictionary((dynamic? u) => u!.Key, u => u?.Value);

// 5. 使用 AsEnumerateObject().ToDictionary 方法
var dic5 = clayObject.AsEnumerateObject().ToDictionary(u => u.Key, u => u.Value);

在最新版本中,框架为流变对象引入了对解构函数的支持(详情参见第 29.3.4 节解构函数),从而减少了类型转换的操作:

var (clay, enumerable) = Clay.Parse("""{"id":1,"name":"furion"}""");
var dic6 = enumerable.ToDictionary(u => u!.Key, u => u?.Value);

29.3.49 管道转换方法

在与第三方 API 交互时,通常会接收到包含完整响应信息的数据结构。然而,应用程序往往仅需关注其中的某些部分,特别是那些类型为对象、集合或数组的属性。此时,可以使用 Pipe 方法对数据进行筛选与转换,提取出符合条件的目标子结构,并返回一个新的流变对象。以下为示例代码:

dynamic data = Clay.Parse("""{
"code": 200,
"link": "https://www.toutiao.com/",
"total": 50,
"data": [
{
"id": "7478865230039121961",
"title": "二手小米SU7 Ultra卖到65万"
},
{
"id": "7478393047366025257",
"title": "加拿大省长模仿特朗普签令下架美国酒"
}
]
}""").Pipe(u => u.data);

var title0 = data[0].title; // "二手小米SU7 Ultra卖到65万"

在上述示例中,通过调用 .Pipe(u => u.data),应用程序可以直接提取所需的 data 属性内容,跳过外层冗余数据结构,从而简化后续的数据访问操作。

此外,还可以将管道转换方法与解构函数(析构表达式)结合使用,进一步提升数据处理的灵活性和效率。例如,配合 Lambda 表达式或 Linq 实现更复杂的数据提取逻辑:

var (data, enumerable) = Clay.Parse("""{
"code": 200,
"link": "https://www.toutiao.com/",
"total": 50,
"data": [
{
"id": "7478865230039121961",
"title": "二手小米SU7 Ultra卖到65万"
},
{
"id": "7478393047366025257",
"title": "加拿大省长模仿特朗普签令下架美国酒"
}
]
}""").Pipe(u => u.data);

var titles = enumerable.Where(u => u.title).ToList(); // ["二手小米SU7 Ultra卖到65万", "加拿大省长模仿特朗普签令下架美国酒"]

在这个例子中,我们不仅提取了 data 数组,还利用了解构语法将结果分离成两个变量:动态对象 data 和可枚举对象 enumerable。随后通过 LambdaSelect 方法高效地提取所有文章标题。

Pipe 方法支持链式调用,允许连续执行多个数据处理步骤。例如:

dynamic first = Clay.Parse("""{
"code": 200,
"link": "https://www.toutiao.com/",
"total": 50,
"data": [
{
"id": "7478865230039121961",
"title": "二手小米SU7 Ultra卖到65万"
},
{
"id": "7478393047366025257",
"title": "加拿大省长模仿特朗普签令下架美国酒"
}
]
}""").Pipe(u => u.data).Pipe(u => u[0]);

var title = first.title; // "二手小米SU7 Ultra卖到65万"

注意: 使用 Pipe 方法时,如果转换结果为 null、非对象或不兼容的数据类型,则会抛出 InvalidOperationException 异常,提示信息为:“Transformation must return a non-null Clay object. The provided function either returned null or an incompatible type.”

为了避免因异常导致程序中断,可以使用 PipeTry 方法。当转换失败时,PipeTry 不会抛出异常,而是返回原始流变对象实例,从而增强程序的容错能力。例如:

dynamic data = Clay.Parse("""{
"code": 200,
"link": "https://www.toutiao.com/",
"total": 50,
"data": [
{
"id": "7478865230039121961",
"title": "二手小米SU7 Ultra卖到65万"
},
{
"id": "7478393047366025257",
"title": "加拿大省长模仿特朗普签令下架美国酒"
}
]
}""").PipeTry(u => u.data2).Pipe(u => u.data);

var title0 = data[0].title; // "二手小米SU7 Ultra卖到65万"

在上述示例中,尝试通过 .PipeTry(u => u.data2) 访问一个不存在的 data2 属性时,由于该属性不存在,转换将失败。但是,与 Pipe 不同,PipeTry 不会抛出异常,而是静默地返回原始流变对象实例,允许后续的 .Pipe(u => u.data) 调用继续正确执行,从而实现预期的数据转换。

这种设计使框架在处理不确定或可能出错的数据结构时,具有更强的容错性和灵活性,特别适用于需要链式操作且数据结构可能会发生变化的场景。

异步版本

除了同步的管道转换方法外,框架还为 Task<Clay?> 类型提供了异步扩展方法 PipeAsyncPipeTryAsync。这些方法非常适合与返回异步结果的流式对象结合使用,例如通过 HTTP 发起的远程请求:

dynamic data = await httpRemoteService.GetAsAsync<Clay>("https://furion.net/").PipeAsync(u => u.data);

29.3.50 根据路径标识符删除值

Clay 类型支持通过路径标识符使用 RemovePathValueDeletePathValue 方法删除值,默认情况下,路径标识符使用 :(冒号)作为层级分隔符。使用示例如下:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"],
"Date":"2024-12-26T00:00:00"
}
}
}
""");

clay.RemovePathValue("AppInfo:Name"); // true
clay.RemovePathValue("AppInfo:Company:Address:City"); // true
clay.RemovePathValue("AppInfo:Company:Telephones:0"); // true

此外,您还可以通过自定义 PathSeparator 指定其他层级分隔符。例如:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"]
}
}
}
""", new ClayOptions
{
PathSeparator = [":", "/"] // 同时支持 : 和 /
});

clay.RemovePathValue("AppInfo:Name"); // true
clay.RemovePathValue("AppInfo:Company/Address:City"); // true
clay.RemovePathValue("AppInfo/Company/Telephones/0"); // true

在这个例子中,PathSeparator 被配置为同时接受 :/ 作为层级分隔符,从而提供了更灵活的路径访问方式。

处理不存在的标识符

当使用 RemovePathValueDeletePathValue 方法访问不存在的标识符时,默认会抛出 KeyNotFoundException 异常。您可以通过设置 ClayOptionsAllowMissingProperty(单一对象)和 AllowIndexOutOfRange(集合或数组)属性为 true,避免抛出异常并返回 false

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""", new ClayOptions
{
AllowMissingProperty = true,
AllowIndexOutOfRange = true
});

clay.RemovePathValue("age"); // 返回 false,不抛出异常
无效路径标识符

若当前路径值非单一对象、集合或数组,继续查找将抛出 InvalidOperationException 异常,异常消息为:“The identifier 'Name' at path 'Name:None' does not support further lookup.”。例如,AppInfo:Name:None 无效,因为 "Furion" 不是可进一步查找的对象。

29.3.51 根据路径标识符设置值

Clay 类型支持通过路径标识符使用 SetPathValue 方法设置值,默认情况下,路径标识符使用 :(冒号)作为层级分隔符。使用示例如下:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"],
"Date":"2024-12-26T00:00:00"
}
}
}
""");

clay.SetPathValue("AppInfo:Name", "百小僧");
clay.SetPathValue("AppInfo:Company:Address:City", "中国中山市");
clay.SetPathValue("AppInfo:Company:Telephones:0", "0760-88888882");

此外,您还可以通过自定义 PathSeparator 指定其他层级分隔符。例如:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"]
}
}
}
""", new ClayOptions
{
PathSeparator = [":", "/"] // 同时支持 : 和 /
});

clay.SetPathValue("AppInfo:Name", "百小僧");
clay.SetPathValue("AppInfo:Company/Address:City", "中国中山市");
clay.SetPathValue("AppInfo/Company/Telephones/0", "0760-88888882");

在这个例子中,PathSeparator 被配置为同时接受 :/ 作为层级分隔符,从而提供了更灵活的路径访问方式。

处理不存在的标识符

当使用 SetPathValue 方法访问不存在的标识符时,默认会抛出 KeyNotFoundException 异常。您可以通过设置 ClayOptionsAllowMissingProperty(单一对象)和 AllowIndexOutOfRange(集合或数组)属性为 true,避免抛出异常:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""", new ClayOptions
{
AllowMissingProperty = true,
AllowIndexOutOfRange = true
});

clay.SetPathValue("age", 30); // 不抛出异常

简单来说:SetPathValue 仅支持更新已存在的路径,不支持自动创建中间层级或新增节点。若路径不存在,操作将无效或抛出异常。

无效路径标识符

若当前路径值非单一对象、集合或数组,继续查找将抛出 InvalidOperationException 异常,异常消息为:“The identifier 'Name' at path 'Name:None' does not support further lookup.”。例如,AppInfo:Name:None 无效,因为 "Furion" 不是可进一步查找的对象。

29.3.52 根据路径检查是否定义

Clay 类型支持通过路径标识符使用 ContainsByPath 方法检查是否定义,默认情况下,路径标识符使用 :(冒号)作为层级分隔符。使用示例如下:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"],
"Date":"2024-12-26T00:00:00"
}
}
}
""");

clay.ContainsByPath("AppInfo:Name"); // true
clay.ContainsByPath("AppInfo:Company:Address:City"); // true
clay.ContainsByPath("AppInfo:Company:Telephones:0"); // true

此外,您还可以通过自定义 PathSeparator 指定其他层级分隔符。例如:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"]
}
}
}
""", new ClayOptions
{
PathSeparator = [":", "/"] // 同时支持 : 和 /
});

clay.ContainsByPath("AppInfo:Name"); // true
clay.ContainsByPath("AppInfo:Company/Address:City"); // true
clay.ContainsByPath("AppInfo/Company/Telephones/0"); // true

在这个例子中,PathSeparator 被配置为同时接受 :/ 作为层级分隔符,从而提供了更灵活的路径访问方式。

29.3.53 处理 JSON 双重序列化

在调用第三方接口或处理网络数据时,常会遇到 JSON 数据双重序列化 的情况。所谓双重序列化,是指整个响应体是标准 JSON 格式,但其中某些字段的值本身是已被序列化的 JSON 字符串。

例如,以下 JSON 数据中,ReferCredOpersData 字段的值实际上是字符串形式的 JSON

{
"EntityNumber": 207053412,
"FullName": "TestDataEntity",
"EntityType": "Corporation",
"ReferCredOpers": "[{\"Did\":144362906,\"CredOperNumber\":200709397004,\"CredOperStep\":3,\"IntervType\":\"Co-Borrower\"},{\"Did\":144362906,\"CredOperNumber\":200709397004,\"CredOperStep\":3,\"IntervType\":\"Vehicle Owner\"}]",
"Data": "{\"Id\":1,\"Name\":\"Furion\"}"
}

然而,在实际开发中,我们通常希望这些字段是真正的 JSON 对象或数组结构,以便直接访问其属性。理想的数据结构应如下所示:

{
"EntityNumber": 207053412,
"FullName": "TestDataEntity",
"EntityType": "Corporation",
"ReferCredOpers": [
{
"Did": 144362906,
"CredOperNumber": 200709397004,
"CredOperStep": 3,
"IntervType": "Co-Borrower"
},
{
"Did": 144362906,
"CredOperNumber": 200709397004,
"CredOperStep": 3,
"IntervType": "Vehicle Owner"
}
],
"Data": {
"Id": 1,
"Name": "Furion"
}
}

为解决此类问题,框架提供了 ParseJson 方法,支持对指定路径的字段进行二次反序列化,自动将 JSON 字符串解析为对应的对象或数组,并替换原始值。

dynamic clay = Clay.Parse("""
{
"EntityNumber": 207053412,
"FullName": "TestDataEntity",
"EntityType": "Corporation",
"ReferCredOpers": "[{\"Did\":144362906,\"CredOperNumber\":200709397004,\"CredOperStep\":3,\"IntervType\":\"Co-Borrower\"},{\"Did\":144362906,\"CredOperNumber\":200709397004,\"CredOperStep\":3,\"IntervType\":\"Vehicle Owner\"}]",
"Data":"{\"Id\":1,\"Name\":\"Furion\"}"
}
""").ParseJson("ReferCredOpers").ParseJson("Data");

var referCredOpers = clay["ReferCredOpers"];
Assert.NotNull(referCredOpers);

Assert.Equal(144362906, referCredOpers![0].Did);
Assert.Equal(200709397004, referCredOpers[0].CredOperNumber);
Assert.Equal(3, referCredOpers[0].CredOperStep);
Assert.Equal("Co-Borrower", referCredOpers[0].IntervType);

Assert.Equal(144362906, referCredOpers[1].Did);
Assert.Equal(200709397004, referCredOpers[1].CredOperNumber);
Assert.Equal(3, referCredOpers[1].CredOperStep);
Assert.Equal("Vehicle Owner", referCredOpers[1].IntervType);

var data = clay["Data"];
Assert.NotNull(data);
Assert.Equal(1, data!.Id);
Assert.Equal("Furion", data.Name);

除了指定特定路径,还可以使用 ParseJson([maxDepth]) 重载方法进行自动遍历检查并解析:

dynamic clay = Clay.Parse("""
{
"EntityNumber": 207053412,
"FullName": "TestDataEntity",
"EntityType": "Corporation",
"ReferCredOpers": "[{\"Did\":144362906,\"CredOperNumber\":200709397004,\"CredOperStep\":3,\"IntervType\":\"Co-Borrower\"},{\"Did\":144362906,\"CredOperNumber\":200709397004,\"CredOperStep\":3,\"IntervType\":\"Vehicle Owner\"}]",
"Data":"{\"Id\":1,\"Name\":\"Furion\"}"
}
""").ParseJson();

注意:自动遍历方式的性能略低于指定路径的方式,因为需要递归检查每个键值对。您还可以设置最大递归解析深度,如 ParseJson(64),默认深度为 3

ParseJson 解析机制说明
  • ParseJson(path) 支持链式调用,接收路径参数(支持嵌套路径,如 "A:B",路径分隔符可自定义)或最大递归深度参数。
  • 框架会自动判断该节点值是否为合法的 JSON 字符串(对象 {} 或数组 [] 形式),若是,则解析并替换为对应结构,否则跳过。
  • 若希望强制对所有类型的值(而不仅限于字符串)执行解析尝试,可将第二个参数 requireJsonObjectOrArrayString 设置为 false(仅适用于路径解析方式):
var clay = Clay.Parse("""{"id":1,"name":"\"Furion\""}""")
.ParseJson("name", requireJsonObjectOrArrayString: false);

Assert.Equal("""{"id":1,"name":{"data":"Furion"}}""", clay.ToJsonString());

注意:当 requireJsonObjectOrArrayStringfalse 时,即使字符串本身不是 {}[] 开头,也会尝试将其作为 JSON 解析。例如,"\"Furion\"" 会被解析为一个包含 data 属性的对象。

29.4 ClayOptions

ClayOptions 是流变对象的配置选项,用于在构建或重建流变对象时提供必要的配置参数。通过配置 ClayOptions 实例,用户可以精确控制流变对象的数据访问模式、序列化与反序列化行为等关键功能。

29.4.1 初始化实例

ClayOptions 类型提供了两种便捷的方式来初始化其实例:

  1. 使用 new 关键字创建新实例
var options = new ClayOptions();
  1. 使用 Default 静态属性获取默认实例
var options = ClayOptions.Default;
  1. 使用 Flexible 静态属性获取预配置实例

其中 AllowMissingPropertyAllowIndexOutOfRangePropertyNameCaseInsensitive 属性均设置为 true

var options = ClayOptions.Flexible;

在初始化 ClayOptions 后,推荐使用 Configure(Action<ClayOptions>) 方法对其进行进一步的定制:

new ClayOptions().Configure(options =>
{
options.JsonSerializerOptions.Converters.Add(new DataTableJsonConverter());
});

这样,您可以在创建 ClayOptions 实例的同时,通过传递一个配置动作(Action<ClayOptions>),直接在该实例上应用所需的配置更改。

29.4.2 内置属性

ClayOptions 类型提供了丰富的属性用于配置流变对象的创建行为,具体如下:

属性名称类型默认值描述
ScalarValueKeystring"value"配置用于包裹非对象和非数组类型的键名。
AllowMissingPropertyboolfalse是否允许访问不存在的属性。
AllowIndexOutOfRangeboolfalse是否允许访问越界的数组索引。
AutoCreateNestedObjectsboolfalse是否自动创建嵌套的对象实例。
AutoCreateNestedArraysboolfalse是否自动创建嵌套的数组实例。
AutoExpandArrayWithNullsboolfalse是否在超出数组长度时自动补位 null
ValidateAfterConversionboolfalse是否在转换后执行数据校验。
DateJsonToDateTimeboolfalse是否将日期格式的 JSON 转换为 DateTime
KeyValueJsonToObjectboolfalse是否将键值对格式的 JSON 转换为单一对象。
PropertyNameCaseInsensitiveboolfalse是否属性名称不区分大小写。
PathSeparatorstring[][":"]路径分隔符。
ReadOnlyboolfalse是否是只读模式。
JsonSerializerOptionsJsonSerializerOptions默认配置JSON 序列化配置。

29.4.3 ScalarValueKey 属性

ScalarValueKey 属性用于自定义非对象和非数组类型值的键名,例如数值、布尔值、字符串等字面量。默认情况下,框架会将这些字面量转换为一个包含键 value 的对象,并将字面量值赋给 value 键。若需更改默认的 value 键名,可通过 ScalarValueKey 属性进行配置。

dynamic clay = Clay.Parse(true);
var value = clay.value; // 默认使用 'value' 键

若希望将键名改为 data,可如下配置:

dynamic clay = Clay.Parse("true", new ClayOptions
{
ScalarValueKey = "data"
});
var data = clay.data; // 使用自定义的 'data' 键

29.4.4 AllowMissingProperty 属性

在访问流变对象的未定义键时,若对象为单一对象类型,通常会抛出 KeyNotFoundException 异常,并显示消息“The property 'xxx' was not found in the Clay.”。为避免此类异常,可将 AllowMissingProperty 属性设置为 true,使系统在遇到未定义键时返回 null 而不是抛出异常。

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");
var age = clay.age; // 此处将抛出异常

为避免上述异常,可配置 AllowMissingProperty 属性:

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}", new ClayOptions
{
AllowMissingProperty = true
});

var age = clay.age; // 返回 null

注意:在 ClayOptions 中设置 AllowMissingProperty = true 后,访问未定义键时将不会抛出异常,而是返回 null

小知识

如果需要允许属性名不区分大小写、访问不存在的属性和越界的数组索引,可以将 options 参数设置为 ClayOptions.Flexible,从而使代码更加简洁:

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}", ClayOptions.Flexible);

29.4.5 AllowIndexOutOfRange 属性

当访问集合或数组的流变对象且索引超出其范围时,系统会抛出 ArgumentOutOfRangeException 异常。为了处理这种情况,您可以将 AllowIndexOutOfRange 属性设置为 true,从而避免异常抛出,并允许返回 null 值。

dynamic clay = Clay.Parse("[1,2,3]");
clay[-1]; // 将抛出异常,因为索引超出范围
clay[0]; // 返回 1,索引在范围内
clay[3]; // 将抛出异常,因为最大有效索引为 2

为了避免上述异常,您可以这样配置 AllowIndexOutOfRange 属性:

dynamic clay = Clay.Parse("[1,2,3]", new ClayOptions
{
AllowIndexOutOfRange = true
});
clay[-1]; // 返回 null,因为索引超出范围但已允许返回 null
clay[0]; // 返回 1,索引在范围内
clay[3]; // 返回 null,因为索引超出范围但已允许返回 null

注意:通过这种方式,即使访问的索引超出集合或数组的实际范围,系统也不会抛出异常,而是返回 null

小知识

若需允许属性名不区分大小写、访问不存在的属性和允许访问越界的数组索引,那么直接将 options 参数设置为 ClayOptions.Flexible,这样可以让代码更加精简:

dynamic clay = Clay.Parse("[1,2,3]", ClayOptions.Flexible);

29.4.6 AutoCreateNestedObjects 属性

在链式访问单一对象的流变对象的嵌套属性时,若某些属性未定义,可以使用空值传播 ? 语法来安全访问,避免异常:

dynamic clay = new Clay(new ClayOptions { AllowMissingProperty = true });
var nested = clay.nested?.nested?.nested; // 返回 null 若属性未定义

但请注意,空值传播(空条件指派)语法不支持设置嵌套属性:

clay.nested?.nested?.nested = "nested"; // 无效语法

为了设置嵌套属性,可以启用 AutoCreateNestedObjects 属性,并使用带有 ? 的索引键名语法:

dynamic clay = new Clay(new ClayOptions
{
AllowMissingProperty = true,
AutoCreateNestedObjects = true
});

clay["nested?"]["nested?"]["nested"] = "nested";
clay["nested?"]["nested?"].nested = "nested"; // 同时支持目标键属性方式设置

这样,框架会自动创建缺失的嵌套对象并赋值,最终 clay 对象的 JSON 表示为:

{ "nested": { "nested": { "nested": "nested" } } }

优点:启用 AutoCreateNestedObjects 后,无需手动检查上级键是否存在,框架会自动创建缺失的嵌套对象。此属性需与 AllowMissingProperty 搭配使用。

29.4.7 AutoCreateNestedArrays 属性

在处理集合或数组流变对象的嵌套索引值时,若采用链式访问且索引超出范围,可利用空值传播 ? 语法安全访问,避免异常:

dynamic clay = new Clay.Array(new ClayOptions { AllowIndexOutOfRange = true });
var nested = clay[0]?[0]?[0]; // 若索引超出范围,则返回 null

但请注意,空值传播(空条件指派)语法不支持设置嵌套索引值:

clay[0]?[0]?[0] = "nested"; // 无效操作

为设置嵌套索引,需启用 AutoCreateNestedArrays 属性,并使用 ? 索引键名语法:

dynamic clay = new Clay.Array(new ClayOptions
{
AllowIndexOutOfRange = true,
AutoCreateNestedArrays = true
});

clay["0?"]["0?"][0] = "nested";

此时,框架将自动创建缺失的嵌套数组并赋值,clay 对象的 JSON 结构如下:

[[["nested"]]]

优点:启用 AutoCreateNestedArrays 后,无需手动检查上级数组是否存在,框架会自动创建缺失的嵌套数组。此属性需与 AllowIndexOutOfRange 搭配使用。

若同时启用 AutoExpandArrayWithNulls 属性,超出数组长度时将自动填充 null

dynamic clay = new Clay.Array(new ClayOptions
{
AllowIndexOutOfRange = true,
AutoCreateNestedArrays = true,
AutoExpandArrayWithNulls = true
});

clay["0?"]["0?"][4] = "nested";

此时,框架同样会自动创建缺失的嵌套数组并赋值,clay 对象的 JSON 结构变为:

[[[null, null, null, null, "nested"]]]

注意:直接设置索引 4 时,会自动填充 4 个 null 值。

29.4.8 AutoExpandArrayWithNulls 属性

AutoExpandArrayWithNulls 属性被设置为 true 时,框架将在为集合或数组的流变对象设置项且索引超出当前范围(即大于最大索引)时,自动使用 null 进行补位。例如:

dynamic clay = Clay.Parse("[1,2]", new ClayOptions
{
AllowIndexOutOfRange = true,
AutoExpandArrayWithNulls = true
});

clay[5] = 5;

执行上述代码后,clay 对象的 JSON 结构将变为:

[1, 2, null, null, null, 5]

注意:自动补位操作仅在执行设置项操作时生效,且此属性需与 AllowIndexOutOfRange 属性配合使用。

29.4.9 ValidateAfterConversion 属性

ValidateAfterConversion 属性决定了在流变对象转换为具体类型后,是否自动执行模型验证。以 ClayModel 类型为例,其定义如下:

public class ClayModel
{
[Range(2, 10)]
public int Id { get; set; }

[Required]
[MinLength(3)]
public string? Name { get; set; }
}

通常情况下,将 clay 流变对象转换为 ClayModel 类型不会触发验证,例如:

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}");

ClayModel model = clay; // 不触发验证
var model2 = (ClayModel)clay; // 不触发验证
ClayModel model3 = clay.As<ClayModel>(); // 不触发验证

但请注意,当 ValidateAfterConversion 设置为 true 时,转换操作将自动触发验证:

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\"}", new ClayOptions
{
ValidateAfterConversion = true
});

ClayModel model = clay; // 触发验证
var model2 = (ClayModel)clay; // 触发验证
ClayModel model3 = clay.As<ClayModel>(); // 触发验证

此时,若数据不符合验证规则,将抛出异常,例如:

System.ComponentModel.DataAnnotations.ValidationException: The field Id must be between 2 and 10.

这表明 Id 字段的值必须在 2 到 10 之间。

29.4.10 DateJsonToDateTime 属性

DateJsonToDateTime 属性用于控制是否将 JSON 中的日期字符串自动转换为 DateTime 类型。

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless","date":"2025-01-14T00:00:00","isTrue":true}""");
var date = clay.date; // 此时 date 是字符串类型

启用 DateJsonToDateTime 属性后:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless","date":"2025-01-14T00:00:00","isTrue":true}""", new ClayOptions
{
DateJsonToDateTime = true
});

// date 属性已被自动转换为 DateTime 类型
var date = clay.date;

此外,该功能不仅支持 ISO 8601 格式(如 "2025-01-14T00:00:00"),还支持 MicrosoftUnix epoch 时间格式(如 "\/Date(1590863400000)\/"),例如:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless","date":"\/Date(1590863400000)\/","isTrue":true}""", new ClayOptions
{
DateJsonToDateTime = true
});

// date 属性已被自动转换为 DateTime 类型
var date = clay.date;

29.4.11 KeyValueJsonToObject 属性

KeyValueJsonToObject 属性用于控制是否将键值对格式的 JSON 数组转换为单一对象。

默认情况下,解析后的 clay 对象保持原始的 JSON 数组结构:

dynamic clay = Clay.Parse("""
[
{
"key": "id",
"value": 1
},
{
"key": "name",
"value": "Furion"
}
]
""");

此时对应的 JSON 结构为:

[
{
"key": "id",
"value": 1
},
{
"key": "name",
"value": "Furion"
}
]

当将 KeyValueJsonToObject 设置为 true 时:

dynamic clay = Clay.Parse("""
[
{
"key": "id",
"value": 1
},
{
"key": "name",
"value": "Furion"
}
]
""", new ClayOptions
{
KeyValueJsonToObject = true
});

解析后的 clay 对象将转换为单一对象结构:

{ "id": 1, "name": "Furion" }
key/value 键的大小写不敏感

在解析键值对格式的 JSON 字符串时,keyvalue 的大小写是被忽略的,即 Key/Valuekey/value 同样有效。这意味着你可以根据需要灵活地使用不同的大小写形式,而不会影响解析结果。

29.4.12 PropertyNameCaseInsensitive 属性(不区分大小写)

默认情况下,流变对象的属性访问是区分大小写的。例如:

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""");

var id = clay.id; // 1
var Id = clay.Id; // 抛出 KeyNotFoundException 异常:The property `Id` was not found in the Clay.

为了支持不区分大小写的属性访问,可以将 PropertyNameCaseInsensitive 属性设置为 true

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""", new ClayOptions
{
PropertyNameCaseInsensitive = true
});

var id = clay.id; // 1
var Id = clay.Id; // 1(不区分大小写)

注意:启用 PropertyNameCaseInsensitive 后,所有与属性名相关的操作(如访问、设置、检查属性是否存在等)都将忽略大小写差异。

29.4.13 PathSeparator 属性

PathSeparator 属性用于定义路径标识符层级的分隔符。默认情况下,它使用 :(冒号)作为层级分隔符。使用示例如下:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"]
}
}
}
""");

var name = clay.PathValue("AppInfo:Name"); // "Furion"
var city = clay.PathValue("AppInfo:Company:Address:City"); // "中国"
var telephone = clay.PathValue("AppInfo:Company:Telephones:0"); // "0760-88888888"
支持路径索引

除了使用 PathValue(path) 方法外,框架还支持通过路径索引的方式来访问嵌套数据。例如:

var name = clay["AppInfo:Name", true]; // "Furion"
var city = clay["AppInfo:Company:Address:City", true]; // "中国"
var telephone = clay["AppInfo:Company:Telephones:0", true]; // "0760-88888888"

在使用路径索引时,需要传入第二个布尔参数,用于指示当前访问方式是否为路径访问:

  • 当设置为 true 时,底层将使用 PathValue(path) 方法进行查找,按路径解析键值;
  • 若设置为 false,则会调用 Get(identifier) 方法,仅进行单层键的查找。

通过自定义 PathSeparator,您可以指定其他层级分隔符。例如:

dynamic clay = Clay.Parse("""
{
"AppInfo": {
"Name": "Furion",
"Version": "1.0.0",
"Company": {
"Name": "Baiqian",
"Address": {
"City": "中国",
"Province": "广东省",
"Detail": "中山市东区紫马公园西门"
},
"Telephones":["0760-88888888","0760-88888881"]
}
}
}
""", new ClayOptions
{
PathSeparator = [":", "/"] // 同时支持 : 和 /
});

var name = clay.PathValue("AppInfo:Name"); // "Furion"
var city = clay.PathValue("AppInfo:Company/Address:City"); // "中国"
var telephone = clay.PathValue("AppInfo/Company/Telephones/0"); // "0760-88888888"

在这个例子中,PathSeparator 被配置为同时接受 :/ 作为层级分隔符,从而提供了更灵活的路径访问方式。

29.4.14 ReadOnly 属性

ReadOnly 属性用于控制流变对象的可编辑状态。当设置为 true 时,流变对象进入只读模式,此时禁止所有修改数据的操作(如设置、删除等)。尝试这些操作将引发 InvalidOperationException 异常,异常信息为:“Operation cannot be performed because the Clay is in read-only mode.

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""", new ClayOptions
{
ReadOnly = true
});

clay.id = 10; // 抛出 InvalidOperationException 异常

若需要将流变对象重新转换为可编辑状态,可采用以下方法:

// 调用 AsMutable 方法
clay.AsMutable();

// 使用 Rebuilt 方法并传递新的选项
clay.Rebuilt(new ClayOptions { ReadOnly = false });
clay.Rebuilt(new Action<ClayOptions>(u => u.ReadOnly = false));

// 将 clay 实例作为方法调用并传递新的选项
clay(new ClayOptions { ReadOnly = false });
clay(new Action<ClayOptions>(u => u.ReadOnly = false));

29.4.15 JsonSerializerOptions 属性

JsonSerializerOptions 属性用于控制 C# 对象与流变对象之间的转换,以及流变对象输出为 JSON 字符串的格式。框架为该属性预设了合理的默认值,涵盖以下配置:

public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(JsonSerializerOptions.Default)
{
PropertyNameCaseInsensitive = true, // 忽略属性名大小写
NumberHandling = JsonNumberHandling.AllowReadingFromString, // 允许从字符串中解析数字
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 取消中文 Unicode 编码
AllowTrailingCommas = true, // 允许 JSON 字符串中存在尾随逗号
Converters = { new ClayJsonConverter() } // 注册流变对象专用的 JSON 转换器
};

若需调整 JsonSerializerOptions,推荐使用 Configure 方法而非直接实例化新的 JsonSerializerOptions。这样可确保在不影响其他配置的情况下,对既有选项进行安全修改:

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""", new ClayOptions
{
KeyValueJsonToObject = true
}.Configure(options =>
{
options.JsonSerializerOptions.Converters.Add(new DataTableJsonConverter()); // 添加额外的 JSON 转换器
}));

29.4.16 Configure 局部配置方法

Configure 方法旨在让你能够在不覆盖 ClayOptions 其他默认配置的前提下,安全地调整现有选项。该方法通过传入一个配置动作,允许你对 ClayOptions 实例进行定制:

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""", new ClayOptions()
.Configure(options =>
{
// 在此处添加你的自定义配置
}));

为了简化代码,你还可以利用 ClayOptions.Default 静态属性,它提供了一个预配置的 ClayOptions 实例,从而避免了显式的 new 操作:

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""", ClayOptions.Default
.Configure(options =>
{
// 在此处添加你的自定义配置
}));

优点:使用 Configure 方法不仅保持了代码的简洁性,还确保了 ClayOptions 的默认配置得以保留,仅在必要时进行局部调整。

29.4.17 在 ASP.NET 应用中配置

框架为 ASP.NET 应用提供了全局的 AddClayOptions 配置方法,用于控制如何将 HTTP 请求内容转换为流变对象。要在 ASP.NET 应用中使用流变对象,请确保完成以下配置:

独立库说明

Furion 框架已内置该功能,无需额外安装 NuGet 包。若您使用 Shapeless 独立库,请安装 Shapeless.AspNetCore 以替代 Shapeless

Startup.csProgram.cs 文件中配置 ClayOptions 选项服务:

services.AddControllers()
.AddClayOptions(options => // 或使用 .AddClayOptions();
{
// 控制是否将键值对格式的 JSON 转换为单一对象
options.KeyValueJsonToObject = true;
});

通过此配置,您可以在 ASP.NET 应用中全局控制流变对象的行为,例如将键值对格式的 JSON 自动转换为单一对象。

29.5 Clay 事件监听

流变对象支持对数据变更事件的监听,包括数据设置/修改前后以及数据删除前后的监听。由于流变对象通常使用 dynamic 关键字接收,这会导致在订阅事件时无法自动推断事件的委托类型。

为了解决这一问题,我们需要先将 dynamic 对象转换回 Clay 类型,以启用 IDE 的智能代码补全和类型推断功能。

29.5.1 单一对象数据变更事件监听

首先将 dynamic 类型的流变对象转换回 Clay 类型:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
Clay clayObject = clay;

以下代码展示了如何为单一对象设置数据变更事件监听,包括数据变更前后以及数据移除前后的处理逻辑:

// 数据变更之前
clayObject.Changing += (sender, args) =>
{
Console.WriteLine(args.IsFound
? $"变更之前 (键:{args.Identifier},值:{sender[args.Identifier]})"
: $"变更之前 (键: {args.Identifier}) 不存在");
};

// 数据变更之后
clayObject.Changed += (sender, args) =>
{
Console.WriteLine($"变更之后 (键:{args.Identifier},值:{sender[args.Identifier]})");
};

// 数据移除之前
clayObject.Removing += (sender, args) =>
{
Console.WriteLine(args.IsFound
? $"移除之前 (键:{args.Identifier},值:{sender[args.Identifier]})"
: $"移除之前 (键: {args.Identifier}) 不存在");
};

// 数据移除之后
clayObject.Removed += (sender, args) =>
{
Console.WriteLine($"移除之后 (键: {args.Identifier}) 不存在");
};

// 触发数据变更和移除事件
clay.id = 2;
clay.name = "Shapeless";
clay.author = "百小僧";

clay.Remove("author");

控制台输出结果如下:

变更之前 (键:id,值:1)
变更之后 (键:id,值:2)
变更之前 (键:name,值:shapeless)
变更之后 (键:name,值:Shapeless)
变更之前 (键: author) 不存在
变更之后 (键:author,值:百小僧)
移除之前 (键:author,值:百小僧)
移除之后 (键: author) 不存在

通过上述代码,可以清晰地监听流变对象的数据变更和移除事件,并实时获取变更前后的状态信息。

29.5.2 集合或数组数据变更事件监听

首先将 dynamic 类型的流变对象转换回 Clay 类型:

dynamic array = Clay.Parse("[1,2,10.3,true,false]");
Clay clayArray = array;

以下代码展示了如何为集合或数组设置数据变更事件监听,包括数据变更前后以及数据移除前后的处理逻辑:

// 数据变更之前
clayArray.Changing += (sender, args) =>
{
Console.WriteLine(args.IsFound
? $"变更之前 (索引:{args.Identifier},值:{sender[args.Identifier]})"
: $"变更之前 (索引: {args.Identifier}) 不存在");
};

// 数据变更之后
clayArray.Changed += (sender, args) =>
{
Console.WriteLine($"变更之后 (索引:{args.Identifier},值:{sender[args.Identifier]})");
};

// 移除数据之前
clayArray.Removing += (sender, args) =>
{
Console.WriteLine(args.IsFound
? $"移除之前 (索引:{args.Identifier},值:{sender[args.Identifier]})"
: $"移除之前 (索引: {args.Identifier}) 不存在");
};

// 移除数据之后
clayArray.Removed += (sender, args) =>
{
Console.WriteLine($"移除之后 (索引: {args.Identifier}) 不存在");
};

array.Add("Furion");
array.Insert(0, "One");

array.Remove(3);

控制台输出将呈现为:

变更之前 (索引: 5) 不存在
变更之后 (索引:5,值:Furion)
变更之前 (索引:0,值:1)
变更之后 (索引:0,值:One)
移除之前 (索引:3,值:10.3)
移除之后 (索引: 3) 不存在

通过上述代码,可以清晰地监听流变对象的数据变更和移除事件,并实时获取变更前后的状态信息。

29.5.3 AddEvent 动态订阅

除了将 dynamic 类型转换为 Clay 类型以进行事件监听外,还可以通过 AddEvent 方法动态添加事件监听。这种方式无需类型转换,直接操作 dynamic 对象即可。

以下代码展示了如何使用 AddEvent 方法为 dynamic 对象添加事件监听:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");

// 数据变更之前
clay.AddEvent("Changing", new ClayEventHandler((sender, args) => {}));

// 数据变更之后
clay.AddEvent("Changed", new ClayEventHandler((sender, args) => {}));

// 数据移除之前
clay.AddEvent("Removing", new ClayEventHandler((sender, args) => {}));

// 数据移除之后
clay.AddEvent("Removed", new ClayEventHandler((sender, args) => {}));

此外,除了使用 ClayEventHandler 委托,还可以使用 Action<dynamic, ClayEventArgs> 委托来处理事件逻辑:

// 数据变更之前
clay.AddEvent("Changing", new Action<dynamic, ClayEventArgs>((sender, args) => {}));

// 数据变更之后
clay.AddEvent("Changed", new Action<dynamic, ClayEventArgs>((sender, args) => {}));

// 数据移除之前
clay.AddEvent("Removing", new Action<dynamic, ClayEventArgs>((sender, args) => {}));

// 数据移除之后
clay.AddEvent("Removed", new Action<dynamic, ClayEventArgs>((sender, args) => {}));

通过 AddEvent 方法,开发者可以直接为 dynamic 对象添加事件监听,避免了类型转换的步骤,同时保持了代码的简洁性和灵活性。

29.6 Clay 动态语法(dynamic) ✨

利用 DynamicObject 的强大功能,框架为流变对象提供了一系列扩展语法,使得在 C# 中操作对象变得与在 JavaScript 中一样灵活自如,极大地提升了开发效率和代码的可读性。

29.6.1 动态属性

流变对象的核心优势在于其支持动态属性。这意味着你可以在运行时灵活地以属性形式为对象添加新成员,而无需预先定义类结构。例如:

dynamic clay = new Clay();

clay.id = 1; // 动态添加 id 属性
var id = clay.id; // 访问 id 属性

29.6.2 动态索引器

流变对象不仅支持动态属性,还具备动态索引器的功能,使得你可以像操作字典或数组那样,通过索引器的方式灵活地访问和设置对象的属性或数组项。

单一对象的流变对象

dynamic clay = new Clay();

clay["id"] = 1; // 动态添加 id 属性
var id = clay["id"]; // 访问 id 属性

集合或数组的流变对象

dynamic array = new Clay.Array();

array[0] = 1; // 设置 0 索引的值
var one = array[0]; // 访问 0 索引的值

29.6.3 动态属性名方法

框架为单一对象的流变对象增添了一项“魔法”功能——属性可以作为方法调用。这一特性赋予了属性双重身份:它们既可以作为传统属性进行访问,又可以作为方法进行调用,从而极大地丰富了对象的交互方式。

检查对象是否定义了某个属性

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");

bool hasId = clay.id(); // 返回 true
bool hasAge = clay.age(); // 返回 false

实现属性值类型转换

var idValue = clay.id<int>(); // 将 id 属性转换为 int 类型
var nameValue = clay.name(typeof(string)); // 将 name 属性转换为 string 类型

// 支持传入 JSON 序列化选项进行更精细的控制
var idWithOptions = clay.id<int>(new JsonSerializerOptions());
var nameWithOptions = clay.name(typeof(string), new JsonSerializerOptions());

// 支持自定义类型转换逻辑
var customNameValue = clay.name(new Func<string?, object?>(u => $"_{u}_"));
属性作为委托方法的直接调用

在流变对象框架中,若某个属性被赋予了一个委托方法(例如 FuncAction),则该属性将直接作为方法进行调用。

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");

// 添加属性方法
clay.sayHello = (Func<string>)(() => $"Hello, {clay.name}!");

// 调用 sayHello 方法
var hello = clay.sayHello();

29.6.4 动态实例方法

框架为流变对象增添的另一项“魔法”功能——实例可以作为方法调用。这一特性赋予了实例双重身份:它们既可以作为对象进行访问,又可以作为方法进行调用,从而极大地丰富了对象的交互方式。

作为索引器访问数据

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");
dynamic array = Clay.Parse("""[1,2,3]""");

var id = clay("id");
var name = clay("name");

var one = array(0);
var last = array(^1);

重建流变对象或调整 ClayOptions 配置

clay(new ClayOptions
{
KeyValueJsonToObject = true
});

clay(new Action<ClayOptions>(options => options.ReadOnly = true));

输出 JSON 字符串

var json = clay();

// 支持传入 JSON 序列化选项
var json = clay(new JsonSerializerOptions());

转换为目标类型

var model = clay(typeof(ClayModel));

// 支持传入 JSON 序列化选项
var model = clay(typeof(ClayModel), new ClayOptions());

合并多个流变对象

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"shapeless\"}");
dynamic clay2 = Clay.Parse("{\"name\":\"Furion\",\"age\":30}");
dynamic clay3 = Clay.Parse("{\"author\":\"MonkSoul\"}");
dynamic newClay = clay(clay2, clay3); // 结果:{"id":1,"name":"Furion","age":30,"author":"MonkSoul"}

dynamic array = Clay.Parse("[1,2,3,4]");
dynamic array2 = Clay.Parse("[4,5,6]");
dynamic array3 = Clay.Parse("[7,8]");
dynamic newArray = array(array2, array3); // 结果:[1,2,3,4,4,5,6,7,8]

29.6.5 动态类型转换

框架为流变对象增添的最后一项“魔法”功能——隐式和显式类型转换

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless"}""");

// 隐式转换(自动根据声明类型转换为目标类型)
ClayModel model = clay;

// 显式转换(强制转换)
var model = (ClayModel)clay;

通过动态属性、动态索引器、动态属性名方法、动态实例方法和动态类型转换,流变对象在 C# 中实现了类似 JavaScript 的灵活性和动态性。这些特性不仅简化了代码,还提升了开发效率,使得流变对象成为处理动态数据的强大工具。

29.7 应用案例

这里汇总了流变对象在应用开发中的常见案例。

29.7.1 在 HTTP 远程请求中使用

流变对象(Clay)在 HTTP 远程请求中的应用场景非常广泛,尤其是在与第三方 API 接口对接时。通常,这些接口需要传递或接收 JSON 格式的数据,而流变对象可以简化数据的构建和解析过程。以下是如何在 HTTP 远程请求模块中使用流变对象的配置步骤:

1. 配置流变对象 JSON 序列化转换器

在使用流变对象进行 HTTP 远程请求时,首先需要配置 AddClayConverters(),以便将流变对象序列化为 JSON 格式字符串。配置示例如下:

services.AddHttpRemote(options => {})
.ConfigureOptions(options =>
{
options.JsonSerializerOptions.AddClayConverters();
});

2. 发送和接收 JSON 数据

配置完成后,可以使用流变对象构建请求内容并发送 HTTP 请求,同时将响应内容转换为流变对象进行处理。以下是一个示例:

// 构建请求内容
dynamic payload = new Clay();
payload.id = 1;
payload.name = "furion";

// 发送 HTTP 远程请求
var content = await httpRemoteService.PostAsStringAsync("https://localhost:7044/HttpRemote/AddModel",
builder => builder.SetJsonContent(payload));

// 将响应内容转化为流变对象
dynamic clay = Clay.Parse(content);

自定义 Clay 内容转换器简化手动转换 ✅

为了简化代码,避免手动将 JSON 格式字符串转换为流变对象(如 dynamic clay = Clay.Parse(content);),可以自定义 ClayContentConverter 内容转换器。通过这种方式,您可以直接在 HTTP 请求中使用 Clay 类型作为泛型接收参数。以下是自定义转换器的实现:

【一键下载 ClayContentConverter.cs 文件】✅

public class ClayContentConverter : HttpContentConverterBase<Clay>
{
/// <inheritdoc />
public override Clay? Read(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default)
{
var str = httpResponseMessage.Content.ReadAsStringAsync(cancellationToken).GetAwaiter().GetResult();

return Clay.Parse(str); // 或使用 Clay.Parse(str, ClayOptions.Flexible); // 忽略属性大小写
}

/// <inheritdoc />
public override async Task<Clay?> ReadAsync(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default)
{
var str = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken);

return Clay.Parse(str); // 或使用 Clay.Parse(str, ClayOptions.Flexible); // 忽略属性大小写
}
}

// 支持 dynamic 类型转流变对象(可选,但推荐!!!)
public class DynamicContentConverter : HttpContentConverterBase<dynamic>
{
/// <inheritdoc />
public override dynamic? Read(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default)
{
var str = httpResponseMessage.Content.ReadAsStringAsync(cancellationToken).GetAwaiter().GetResult();

return Clay.Parse(str); // 或使用 Clay.Parse(str, ClayOptions.Flexible); // 忽略属性大小写
}

/// <inheritdoc />
public override async Task<dynamic?> ReadAsync(HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken = default)
{
var str = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken);

return Clay.Parse(str); // 或使用 Clay.Parse(str, ClayOptions.Flexible); // 忽略属性大小写
}
}

随后,在 Startup.csProgram.cs 文件中,配置并注册 HttpRemote 服务,以启用自定义内容转换器功能:

services.AddHttpRemote(options =>
{
options.AddHttpContentConverters(() => [ new ClayContentConverter(), new DynamicContentConverter()]); // new DynamicContentConverter()(可选,但推荐!!)
});

配置完成后,可以直接使用流变对象类型 Clay 作为泛型接收参数:

// 发送 HTTP 远程请求,将响应内容转化为流变对象
dynamic clay = await httpRemoteService.PostAsAsync<Clay>("https://localhost:7044/HttpRemote/AddModel",
builder => builder.SetJsonContent(payload));

// 如果配置了 DynamicContentConverter,那么也可以使用 dynamic 接收
dynamic clay = await httpRemoteService.PostAsAsync<dynamic>("https://localhost:7044/HttpRemote/AddModel",
builder => builder.SetJsonContent(payload));
ClayJsonConverter 与 ClayContentConverter 说明
  • ClayJsonConverter/AddClayConverters():用于将流变对象序列化为 JSON 格式字符串,通常作为输入参数使用。
  • ClayContentConverter/DynamicContentConverter:用于将 JSON 格式字符串反序列化为流变对象,通常作为输出参数使用。

通过结合流变对象与 HTTP 远程请求,开发者可以更高效地处理动态 JSON 数据,简化第三方 API 的对接流程。流变对象的动态特性使得数据的构建和解析更加灵活,同时自定义内容转换器进一步提升了开发效率。

29.8 常见问题

这里汇总了一些使用流变对象时可能遇到的常见问题。

29.8.1 允许属性名不区分大小写、访问不存在的属性或索引

如果需要允许属性名不区分大小写、访问不存在的属性或越界的数组索引,只需将 options 参数设置为 ClayOptions.Flexible

dynamic clay = Clay.Parse("{\"id\":1,\"name\":\"furion\",\"arr\":[1,2]}", ClayOptions.Flexible);

var age = clay.age; // 不抛异常,返回 null
var three = clay.arr[3] // 不抛异常,返回 null
var id = clay.Id; // 忽略大小写

29.8.2 特殊键名处理(关键字)

流变对象允许使用任意字符作为键名或索引,包括空字符串、特殊符号、中文甚至 C# 关键字。

dynamic clay = new Clay();
clay[""] = "Furion"; // 空字符串作为键名
clay.百小僧 = "monksoul"; // 中文作为键名
clay["!@#$%^&*()_+/{}\\;<>?/:'`-"] = "特殊符号"; // 特殊符号作为键名
clay.a = "A"; // 普通键名
clay.@int = 10; // 使用 @ 符号处理与 C# 关键字的冲突

Assert.Equal("Furion", clay[""]);
Assert.Equal("monksoul", clay.百小僧);
Assert.Equal("monksoul", clay["百小僧"]);
Assert.Equal("特殊符号", clay["!@#$%^&*()_+/{}\\;<>?/:'`-"]);
Assert.Equal("A", clay["a"]);
Assert.Equal("A", clay.a);
Assert.Equal("A", clay['a']);
Assert.Equal(10, clay.@int); // 使用 @ 符号访问关键字键名
Assert.Equal(10, clay["int"]); // 索引方式无需添加 @

Assert.Throws<KeyNotFoundException>(() => clay["@int"]); // 索引方式无需添加 @
处理与 C# 关键字冲突

在定义键名时,若遇到与 C# 关键字冲突的情况,只需在冲突的键名前添加一个 @ 符号,即可解决冲突问题。

29.8.3 捕获类型转换字段溢出

流变对象可能会包含一些实体对象中不存在的属性,这种情况称为“溢出”。默认情况下,溢出的属性会被忽略。如果希望捕获这些“溢出”的属性,可以在实体对象中声明一个类型为 Dictionary<string, object> 的属性,并对其应用 JsonExtensionData 特性标记。

ClayModel 类型为例,其定义如下:

public class ClayModel
{
public int Id { get; set; }
public string? Name { get; set; }

[JsonExtensionData]
public Dictionary<string, JsonElement> AdditionalData { get; set; }
}

类型转换与字段溢出示例:

dynamic clay = Clay.Parse("""{"id":1,"name":"shapeless","age":30}""");

// 隐式类型转换,同样支持显式类型转换
ClayModel model = clay;

// 获取溢出的 age 字段
var ageJsonElement = model.AdditionalData["age"];

在上述示例中,由于 ClayModel 类未定义 age 属性,因此 age 字段被视为溢出字段。在执行隐式或显式类型转换后,该字段将被添加到 AdditionalData 字典中,以便后续处理或访问。

29.8.4 自定义键命名策略

框架内置了多种键命名策略,以满足不同的需求:

  • 小驼峰命名法JsonNamingPolicy.CamelCase,例如将 TempCelsius 转换为 tempCelsius
  • 小写蛇形命名法JsonNamingPolicy.SnakeCaseLower,例如将 TempCelsius 转换为 temp_celsius
  • 大写蛇形命名法JsonNamingPolicy.SnakeCaseUpper,例如将 TempCelsius 转换为 TEMP_CELSIUS
  • 小写短横线命名法JsonNamingPolicy.KebabCaseLower,例如将 TempCelsius 转换为 temp-celsius
  • 大写短横线命名法JsonNamingPolicy.KebabCaseUpper,例如将 TempCelsius 转换为 TEMP-CELSIUS
  • 帕斯卡命名法:通过实例化 new PascalCaseNamingPolicy() 创建,例如将 tempCelsius 转换为 TempCelsius

若需自定义键命名策略,您只需创建一个继承自 JsonNamingPolicy 的新类型,并重写其 ConvertName(string name) 方法即可。这将允许您根据特定规则自定义 JSON 键的命名方式。例如,以下代码实现了一个首字母大写的自定义键命名策略 UpperCaseNamingPolicy

public class UpperCaseNamingPolicy : JsonNamingPolicy
{
/// <inheritdoc />
public override string ConvertName(string name)
{
return string.IsNullOrWhiteSpace(name) ? name : char.ToUpper(name[0]) + name[1..];
}
}

接下来,在流变对象中使用 UpperCaseNamingPolicy 自定义键命名策略:

dynamic clay = Clay.Parse("""{"id":1,"name":"Furion"}""");

Console.WriteLine(clay.ToJsonString(new JsonSerializerOptions
{
PropertyNamingPolicy = new UpperCaseNamingPolicy()
}));

控制台输出将呈现为:

{"Id":1,"Name":"Furion"}

通过这种方式,您可以灵活地定义符合项目需求的命名规则。

29.8.5 将 ExpandoObject 转换为流变对象

ExpandoObject 类型允许在运行时动态添加和删除实例成员,并能够对这些成员进行赋值和取值操作。这一特性与流变对象相似。框架提供了将 ExpandoObject 实例转换为流变对象的功能,示例如下:

dynamic expandoObject = new ExpandoObject();
expandoObject.id = 1;
expandoObject.name = "furion";

dynamic clay = Clay.Parse(expandoObject);
Assert.Equal("{\"id\":1,\"name\":\"furion\"}", clay.ToJsonString());
委托属性转换说明

在将 ExpandoObject 转换为流变对象时,不支持委托属性。例如:

dynamic expandoObject = new ExpandoObject();
expandoObject.id = 1;
expandoObject.name = "furion";
expandoObject.sayHello = new Action(() => Console.WriteLine("Hello!"));

dynamic clay = Clay.Parse(expandoObject);
Assert.Equal("{\"id\":1,\"name\":\"furion\"}", clay.ToJsonString());

在上述代码中,sayHello 是一个委托属性,但在转换为流变对象时会被忽略,最终的 JSON 字符串中只包含 idname 属性。

29.8.6 中文编码(乱码)处理

默认情况下,流变对象输出为 JSON 格式字符串时,中文字符会被转换为 Unicode 编码。若希望保留中文字符而不进行 Unicode 编码,可以使用以下方法:

  • ToString(format) 方法:

通过指定格式化字符串 "U",可以取消中文字符的 Unicode 编码。

Console.WriteLine($"{clay:U}");
Console.WriteLine(clay.ToString("U"));
  • ToJsonString() 方法:

直接调用 ToJsonString() 方法,输出的 JSON 字符串将保留中文字符。

Console.WriteLine(clay.ToJsonString());

// 更多 JSON 序列化配置参数
Console.WriteLine(clay.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}));

通过这些方法,可以灵活控制 JSON 输出中的中文编码格式,避免乱码问题。

29.8.7 解决 ASP.NET WebAPI 接口返回 key/value 格式问题 ✨

ASP.NET WebAPI 项目中,如果使用流变对象 Clay 作为接口的返回值,默认情况下,Clay 类型会被序列化为字典格式,导致返回的 JSON 数据如下所示:

[
{
"key": "Id",
"value": 1
},
{
"key": "Name",
"value": "Furion"
}
]

然而,我们期望的返回格式应为:

{
"Id": 1,
"Name": "Furion"
}

要解决这个问题,只需在 Startup.csProgram.cs 文件中配置 ClayOptions 选项服务。具体配置如下:

services.AddControllers()
.AddClayOptions(options => {}); // 或使用 .AddClayOptions();

通过上述配置,Clay 对象将按照预期的格式进行序列化,从而避免 key/value 格式的问题。

使用 Newtonsoft.Json 的配置

若选择 Newtonsoft.Json 作为序列化工具时,需要进行以下额外配置:

services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.Converters.AddClayConverters(); // 支持通过 `toCamelCaseKey: true` 参数将键名转换为小驼峰格式。
});

29.8.8 在 Swagger 中配置流变对象的 Schema 和示例值

ASP.NET WebAPI 应用中,若使用流变对象作为输入或输出参数,因其动态特性,Swagger 文档无法自动推断其 Schema,也无法正确生成示例值(Example Value)。针对此问题,可通过在接口文档中添加 <remarks> 标签,并使用 Markdown 格式编写文档说明,为流变对象类型的输入或输出参数提供详细解释与示例。具体示例如下:

/// <summary>
/// 接收流变对象作为参数
/// </summary>
/// <remarks>
/// 这里该接口输入参数说明:
///
/// - 对象类型数据:
/// ```json
/// {
/// "id": 1,
/// "name": "Furion"
/// }
/// ```
/// - 数组类型数据:
/// ```json
/// [
/// {
/// "id": 1,
/// "name": "Furion"
/// },
/// {
/// "id": 1,
/// "name": "Furion"
/// }
/// ]
/// ```
/// 想了解更多可查看官方文档。
/// </remarks>
/// <param name="clay" example="&quot;{}&quot;">动态参数</param>
/// <returns>动态结果</returns>
[HttpPost]
public dynamic PostData([Clay] dynamic clay)
{
return clay;
}

此外,可通过为流变对象类型的输入参数添加注释 <param>,并设置其 example 属性,来指定该参数的示例值。示例如下:

/// <param name="clay" example="&quot;{}&quot;">动态参数</param>

上述代码会生成示例值为 {} 的参数。如果需要指定数组类型的示例值,只需将 example 属性修改为 example="&quot;[]&quot;" 即可。关于示例值的详细配置方法,可参考 Swashbuckle.AspNetCore - Include Descriptions from XML Comments

29.8.9 Newtonsoft.Json 配置

框架建议

请注意,除非有充分的理由,否则通常建议使用 System.Text.Json,因为它与 .NET Core 紧密集成,且性能优异。

ASP.NET 应用中,默认使用 System.Text.Json 进行对象的序列化和反序列化。如果需要改用 Newtonsoft.Json 作为序列化提供程序,可以通过以下代码进行配置:

services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.Converters.AddClayConverters(); // 支持通过 `toCamelCaseKey: true` 参数将键名转换为小驼峰格式。
});

29.9 反馈与建议

与我们交流

给 Furion 提 Issue

进一步了解

如需深入了解 DynamicObjectdynamic 的相关知识,可参考以下文档章节: