Skip to content

29. 粘土对象

📝 模块更新日志 新特性*

+ 新增 粘土对象 `Clay` 转换为可枚举对象 `AsEnumerable()` 方法 4\.9\.2\.37 ⏱️2024\.05\.06 [b1c8fa4](https://gitee.com/dotnetchina/Furion/commit/b1c8fa4d3b4a46b22a97f3ef566bb927ee8cbd5f)
+ 新增 粘土对象进行固化类型时支持 `JsonSerializerOptions` 序列化配置 4\.9\.2\.24 ⏱️2024\.04\.17 [cc6dd13](https://gitee.com/dotnetchina/Furion/commit/cc6dd133a6e8f9ab03bf37b0011449e7f798bddd)
+ 新增 粘土对象支持无限极组合嵌套功能 4\.9\.2\.19 ⏱️2024\.04\.16 [b02916e](https://gitee.com/dotnetchina/Furion/commit/b02916e3397e07328f6d31c301034af004e37067)
+ 新增 粘土对象 `.ConvertTo` 支持自定义值提供器 4\.8\.8\.40 ⏱️2023\.08\.03 [70d5888](https://gitee.com/dotnetchina/Furion/commit/70d58888b3cec88c5c2a8458654dca1881e2a88b)
+ 新增 粘土对象支持结构 `struct` 对象类型 4\.8\.8\.7 ⏱️2023\.04\.30 [a0fa3aa](https://gitee.com/dotnetchina/Furion/commit/a0fa3aa7ae536e948740401b510d99cf45e251dc)
+ 新增 粘土对象可反射转换成特定 `IEnumerable<T>` 类型:`clay.ConverTo<T>()` 4\.8\.8 ⏱️2023\.04\.13 [5d54a65](https://gitee.com/dotnetchina/Furion/commit/5d54a6579be3d710649bb199dd985f60acaf9787)
+ 新增 **粘土对象可配置访问不存在 `Key` 时是抛异常还是返回 `null`** 4\.8\.7\.40 ⏱️2023\.04\.10 [e994d53](https://gitee.com/dotnetchina/Furion/commit/e994d53b64a825461673f48960df1716be44f192)
+ 新增 **粘土对象可转换成 `IEnumerable<T>` 对象并实现 `Lambda/Linq` 操作** 4\.8\.7\.19 ⏱️2023\.03\.22 [2b14ed9](https://gitee.com/dotnetchina/Furion/commit/2b14ed9da03699619b1fade6e053f65b77a5b0fe)

查看变化

dynamic clay = Clay.Parse("{\"Foo\":\"json\",\"Bar\":100,\"Nest\":{\"Foobar\":true},\"Arr\":[\"NOR\",\"XOR\"]}");  

// 将 clay.Arr 转换成 IEnumerable<dynamic>  
IEnumerable<dynamic> query = clay.Arr.AsEnumerator<dynamic>();  

// 实现 Lambda/Linq 操作  
var result = query.Where(u => u.StartsWith("N"))  
                  .Select(u => new  
                  {  
                      Name = u  
                  })  
                  .ToList();  

    • 新增 粘土对象支持任何字符作为 JSON/XML 键 4.8.6.9 ⏱️2023.02.19 f99aee8 #note_16329657
  • 问题修复

    • 修复 粘土对象无限嵌套粘土对象且 XElement 属性包含 type="null" 节点出现异常问题 4.9.2.21 ⏱️2024.04.16 9d5870f
    • 修复 粘土对象嵌套粘土对象只输出第一个属性问题 4.9.2.20 ⏱️2024.04.16 1a75778
    • 修复 粘土对象调整原先类型并设置混合类型异常问题 4.9.2.6 ⏱️2024.04.03 83b216f
    • 修复 粘土对象将 Object 类型设置给 Array 类型出现递归死循环问题 4.9.2.5 ⏱️2024.04.03 1126c74
    • 修复 粘土对象不支持嵌套粘土对象问题 4.9.2.4 ⏱️2024.04.02 fcb1223
    • 修复 粘土对象序列化后出现二次序列化成字符串问题 4.9.2.4 ⏱️2024.04.02 fcb1223
    • 修复 粘土对象转换成 Dictionary 对象不支持递归问题 4.9.1.13 ⏱️2023.12.11 #I8NFT4
    • 修复 粘土对象不支持枚举类型问题 4.8.8.41 ⏱️2023.08.25 #I7VDDL
    • 修复 ExpandoObject.ToDictionary() 转换异常 4.8.8.25 ⏱️2023.06.14 #I7BY0P
    • 修复 粘土对象转换为 Dictionary<string, object> 类型异常 4.8.7.41 ⏱️2023.04.11 f96baeb
    • 修复 粘土对象不支持运行时动态设置携带特殊字符的 Key 键 4.8.7.39 ⏱️2023.04.10 6572515
    • 修复 粘土对象遍历对象键值对因 4.8.7.19 版本更新导致异常 4.8.7.25 ⏱️2023.03.28 #I6R4ZU
    • 修复 粘土对象不支持 数字 作为 JSON/XML 键问题 4.8.6.9 ⏱️2023.02.19 #note_16329657
    • 修复 粘土对象不支持 中文 作为 JSON/XML 键问题 4.8.6.6 ⏱️2023.02.18 4961e01
    • 其他更改

    • 调整 粘土对象 number 类型处理,若含 .double 类型,否则转 long 类型 4.8.7.24 ⏱️2023.03.28 e82e883

版本说明以下内容仅限 Furion 2.1.12 + 版本使用。

29.1 关于粘土对象

粘土对象是 Furion 框架推出的一种动态类型,可以模拟弱(动态)语言操作特性,使 C# 对象实现类似 JavaScript 一样操作对象。只需通过 Clay 类初始化即可。

为什么起名为 “粘土” 呢?因为这个对象可以自由的添加属性,移除属性,又可以固化成任何对象,具有可拓展、可塑造的特点。

29.1.1 使用场景

粘土对象常用于需要动态构建对象的地方,如 CMS 系统的 ViewModel,或者运行时创建一个新的对象,或者请求第三方 API 情况。

29.1.2 关于性能

粘土性能实际上并不高效,但是性能也并不低下,只不过略输于强类型调用。什么时候使用可以看以上的【使用场景】。

29.2 Clay 对象

Clay 对象是继承自 DynamicObject 的一个特殊对象,提供了像弱(动态)语言一样操作对象的方法及索引获取方式。

29.3 如何使用

29.3.1 创建一个对象

// 创建一个空的粘土对象  
dynamic clay = new Clay();  

// 从现有的对象创建  
dynamic clay2 = Clay.Object(new {});  

// 从 json 字符串创建,可用于第三方 API 对接,非常有用  
dynamic clay3 = Clay.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }");  

29.3.2 读取/获取属性

dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    }  
});  

var r1 = clay.Foo; // "json" - string类型  
var r2 = clay.Bar; // 100 - double类型  
var r3 = clay.Nest.Foobar; // true - bool类型  
var r4 = clay["Nest"]["Foobar"]; // 还可以和 JavaScript 一样通过索引器获取  

29.3.3 新增属性

dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    }  
});  

// 新增  
clay.Arr = new string[] { "NOR", "XOR" }; // 添加一个数组  
clay.Obj1 = new City { }; // 新增一个实例对象  
clay.Obj2 = new { Foo = "abc", Bar = 100 }; // 新增一个匿名类  

29.3.4 更新属性值

dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    }  
});  

// 更新  
clay.Foo = "Furion";  
clay["Nest"].Foobar = false;  
clay.Nest["Foobar"] = true;  

29.3.5 删除属性

dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    },  
    Arr = new string[] { "NOR", "XOR" }  
});  

// 删除操作  
clay.Delete("Foo"); // 通过 Delete 方法删除  
clay.Arr.Delete(0); // 支持数组 Delete 索引删除  
clay("Bar");    // 支持直接通过对象作为方法删除  
clay.Arr(1);    // 支持数组作为方法删除  

29.3.6 判断键/索引是否存在

dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    },  
    Arr = new string[] { "NOR", "XOR" }  
});  

// 判断属性是否存在  
var a = clay.IsDefined("Foo"); // true  
var b = clay.IsDefined("Foooo"); // false  
var c = clay.Foo(); // true  
var d = clay.Foooo(); // false;  
var e = clay.Arr.IsDefined(0);  // true  
var f = clay.Arr.IsDefined(3);  // false  

29.3.7 遍历对象

dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    },  
    Arr = new string[] { "NOR", "XOR" }  
});  

// 遍历数组  
foreach (string item in clay.Arr)  
{  
    Console.WriteLine(item); // NOR, XOR  
}  

// 遍历整个对象属性及值,类似 JavaScript 的 for (var p in obj)  
foreach (KeyValuePair<string, dynamic> item in clay)  
{  
    Console.WriteLine(item.Key + ":" + item.Value); // Foo:json, Bar: 100, Nest: { "Foobar":true}, Arr:["NOR","XOR"]  
}  

// 数组/集合 可使用 Lambda 方式,Furion 4.8.7.19+ 支持  
IEnumerable<dynamic> query = clay.Arr.AsEnumerator<dynamic>();   // 通过 .AsEnumerator<T>() 转换成 IEnumerable<T> 类型  
var result = query.Where(u => u.StartsWith("N"))  
                  .Select(u => new  
                  {  
                      Name = u  
                  })  
                  .ToList();  

// 也可以通过原生方法转换成 IEnumerable 对象  
IEnumerable<dynamic> query = ((System.Collections.IEnumerable)clay.Arr).Cast<dynamic>();   // 其中 Cast<T> 的 T 可以时任意类型,比如 Cast<string>();  

// 获取对象所有键或数组所有索引  
IEnumerable<string> keys = clay.GetDynamicMemberNames();  

// Furion 4.9.2.37+ 支持  
IEnumerable<dynamic> query = clay.Arr.AsEnumerable();  

29.3.8 转换成具体对象

dynamic clay = new Clay();  
clay.Arr = new string[] { "Furion", "Fur" };  

// 数组转换示例  
var a1 = clay.Arr.Deserialize<string[]>(); // 通过 Deserialize 方法  
var a2 = (string[])clay.Arr;    // 强制转换  
string[] a3 = clay.Arr; // 声明方式  

// 对象转换示例  
clay.City = new City { Id = 1, Name = "中山市" };  
var c1 = clay.City.Deserialize<City>(); // 通过 Deserialize 方法  
var c2 = (City)clay.City;    // 强制转换  
City c3 = clay.City; // 声明方式  

29.3.9 固化粘土

固化粘土在很多时候和序列化很像,但是如果直接调用 Deserialize<object>Deserialize<dynamic> 无法返回实际类型,所以就有了固化类型的功能,如:

// 返回 object  
var obj = clay.Solidify();  // 注意,Clay 本身就是 dynamic 类型 ,也就是这样做意义不大!  

// 返回 dynamic  
var obj1 = clay.Solidify<dynamic>();    // 注意,Clay 本身就是 dynamic 类型 ,也就是这样做意义不大!  

// 返回其他任意类型  
var obj2 = clay.Solidify<City>();  

// Furion 4.9.2.24+ 版本支持添加 JsonSerializerOptions/JsonSerializerSettings 选项参数(取决于用哪个序列化工具)  
var obj3 = clay.Solidify<City>(new JsonSerializerOptions   
{  
});  

29.3.10 输出 JSON

dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    },  
    Arr = new string[] { "NOR", "XOR" }  
});  

// 输出 JSON  
var json = clay.ToString(); // "{\"Foo\":\"json\",\"Bar\":100,\"Nest\":{\"Foobar\":true},\"Arr\":[\"NOR\",\"XOR\"]}"  

Clay 序列化成 JSON 键大小写控制默认情况下,Clay 输出成 JSON 后将保持原样输出,如果需要实现键命名控制,则需要先转换成 Dictionary 然后再配置 AddJsonOptions 服务,如:

public IActionResult OutputClay()  
{  
    dynamic clay = Clay.Object(new  
    {  
       // ....  
    });  

    // 转换成 dictionary  
    var dic = clay.ToDictionary();  // Furion 4.9.2.24+ 版本支持添加 JsonSerializerOptions/JsonSerializerSettings 选项参数(取决于用哪个序列化工具)  

    return new JsonResult(dic);  
}  

配置序列化 Dictionary 键命名策略支持:

services.AddControllers()  
        .AddJsonOptions(options =>  
         {  
            options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;    // 配置 Dictionary 类型序列化输出  
         });  

29.3.11 输出 XML 对象

dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    },  
    Arr = new string[] { "NOR", "XOR" }  
});  

// 输出 XElement  
var xml = clay.XmlElement;  

29.3.12 关键字处理

dynamic clay = new Clay();  
clay.@int = 1;  
clay.@event = "事件";  

29.3.13 转换成字典类型

dynamic clay = Clay.Object(new { name = "张三" });  
clay.name = "百小僧";  
Dictionary<string, object> parms = clay.ToDictionary();  

29.3.14 获取不存在 Key 处理

版本说明以下内容仅限 Furion 4.8.7.40 + 版本使用。

默认情况下,如果通过粘土对象获取不存在的值会抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException 异常,如:

dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    }  
});  

// 获取不存在的值  
var undefined = clay["undefined"];  // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException 异常  

这时我们可以配置粘土对象的 ThrowOnUndefined 属性行为,配置 true 时获取不存在的 Key 抛异常,false 表示返回 null。如:

// 方式一,初始化指定 throwOnUndefined: false,所有构造函数和静态初始化方法都有该参数  
dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    }  
}, throwOnUndefined: false);  

var undefined = clay.Undefined; // => null  

// 方式二,操作时指定,设置 ThrowOnUndefined 属性  
dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    }  
});  

var undefined = clay.Undefined; // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderExceptionnull  
clay.ThrowOnUndefined = false;  
var undefined = clay.Undefined; // => null  

// 方式三,只配置局部对象  
dynamic clay = Clay.Object(new  
{  
    Foo = "json",  
    Bar = 100,  
    Nest = new  
    {  
        Foobar = true  
    }  
});  

var undefined = clay.Undefined; // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException  
var bar = clay.Nest.Bar;    // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException  

// 设置 Nest 节点不抛异常  
clay.Nest.ThrowOnUndefined = false;  
var undefined = clay.Undefined; // 抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException  
var bar = clay.Nest.Bar; // => null  

29.3.15 转换为特定集合

版本说明以下内容仅限 Furion 4.8.8 + 版本使用。

dynamic clay = Clay.Parse(@"[]");  
IEnumerable<SysMenu> data = clay.ConvertTo<SysMenu>();  

这种方式区别于 .Solidify<T>ConvertTo<T> 内部通过反射创建对象并自动适配属性类型。

另外在 Furion 4.8.8.40+ 版本提供了 ConvertTo 自定义值参数,可以针对特定属性进行额外处理,如:

Func<PropertyInfo, object, object> valueProvider = (property, oldValue) =>  
{  
    // 例如针对属性名为 Device 的进行额外处理  
    if (property.Name == "Device")  
    {  
        return JsonSerializer.Deserialize<Device>(oldValue.ToString());  
    }  

    return oldValue;  
};  

IEnumerable<SysMenu> data = clay.ConvertTo<SysMenu>(valueProvider);  

29.4 序列化支持

默认情况下,Clay 为动态类型对象,不支持直接通过 System.Text.JsonNewtonsoft.Json 进行序列化和反序列化,这时只需添加以下配置即可。

  • System.Text.Json 方式
services.AddControllersWithViews()  
        .AddJsonOptions(options =>  
        {  
            options.JsonSerializerOptions.Converters.AddClayConverters();  
        });  

  • Newtonsoft.Json 方式
.AddNewtonsoftJson(options =>  
{  
     options.SerializerSettings.Converters.AddClayConverters();  
})  

29.5 反馈与建议

与我们交流给 Furion 提 Issue