分享基于.NET动态编译&Newtonsoft.Json封装实现JSON转换器(JsonConverter)原理及JSON操作技巧
看文章标题就知道,本文的主题就是关于json,json转换器(jsonconverter)具有将c#定义的类源代码直接转换成对应的json字符串,以及将json字符串转换成对应的c#定义的类源代码,而json操作技巧则说明如何通过jpath来快速的定位json的属性节点从而达到灵活读写json目的。
一、json转换器(jsonconverter)使用及原理介绍篇
现在都流行微服务,前后端分离,而微服务之间、前后端之间数据交互更多的是基于rest ful风格的api,api的请求与响应一般常用格式都是json。当编写了一些api后,为了能够清楚的描述api的请求及响应数据格式(即:json格式),以便提供给api服务的消费者(其它微服务、前端)开发人员进行对接开发,通常是编写api说明文档,说明文档中一般包含入参json格式说明以及响应的json格式说明示例,但如果api涉及数目较多,全由开发人员人工编写,那效率就非常低下,而且不一定准确。于是就有了swagger,在api项目中集成swagger组件,就会由swagger根据api的action方法定义及注解生成标准的在线api说明文档,具体用法请参见网上相关文章说明。当然除了swagger还有其它类似的集成式的生成在线api说明文档,大家有兴趣的话可以去网上找找资源。虽说swagger组件确实解放了开发人员的双手,无需人工编写就自动生成在线api文档,但我认为还是有一些不足,或者说是不太方便的地方:一是必需集成到api项目中,与api项目本身有耦合与依赖,无法单独作为api帮助文档项目,在有些情况下可能并不想依赖swagger,不想时刻把swagger生成api文档暴露出来;二是目前都是生成的在线api文档,如果api在某些网络环境下不可访问(比如:受限),那在线的api文档基本等同于没用,虽说swagger也可以通过复杂的配置或改造支持导出离线的api文档,但总归是有一定的学习成本。那有没有什么替代方案能解决swagger类似的在线api文档的不足,又避免人工低效编写的状况呢?可能有,我(梦在旅途)没了解过,但我为了解决上述问题,基于.net动态编译&newtonsoft.json封装实现了一个json转换器(jsonconverter),采用人工编写+json自动生成的方式来实现灵活、快速、离线编写api说明文档。
先来看一下jsonconverter工具的界面吧,如下图示:
工具界面很简单,下面简要说明一下操作方法:
class类源代码转换成json字符串:先将项目中定义的class类源代码复制粘贴到class code文本框区域【注意:若有继承或属性本身又是另一个类,则相关的class类定义源代码均应一同复制,using合并,namespace允许多个,目的是确保可以动态编译通过】,然后点击上方的【parse】按钮,以便执行动态编译并解析出class code文本框区域中所包含的class type,最后选择需要生成json的class type,点击中间的【to json】按钮,即可将选择的class type 序列化生成json字符串并展示在右边的json string文本框中;
示例效果如下图示:(支持继承,复杂属性)
有了这个功能以后,api写好后,只需要把action方法的入参class源代码复制过来然后进行class to json转换即可快速生成入参json,不论是自己测试还是写文档都很方便。建议使用markdown语法来编写api文档。
json字符串转换成class类定义源代码:先将正确的json字符串复制粘贴到json string文本框中,然后直接点击中间的【to class】按钮,弹出输入要生成的class名对话框,输入后点击确定就执行转换逻辑,最终将转换成功的class定义源代码展示在左边的class code文本框区域中;
示例效果如下图示:(支持复杂属性,能够递归生成json所需的子类,类似如下的address,注意暂不支持数组嵌套数组这种非常规的格式,即:[ [1,2,3],[4,5,6] ])
jsonconverter工具实现原理及代码说明:
class code to json 先利用.net动态编译程序集的方式,把class code动态编译成一个内存的临时程序集assembly,然后获得该assembly中的class type,最后通过反射创建一个class type空实例,再使用newtonsoft.json 序列化成json字符串即可。
动态编译是:parse,序列化是:tojsonstring,需要关注的点是:动态编译时,需要引用相关的.net运行时dll,而这些dll必需在工具的根目录下,否则可能导致引用找不到dll导致编译失败,故项目中引用了常见的几个dll,并设置了复制到输出目录中,如果后续有用到其它特殊的类型同样参照该方法先把dll包含到项目中,并设置复制到输出目录中,然后在动态编译代码中使用cp.referencedassemblies.add("xxxx.dll");进行添加。核心代码如下:
private list<string> parse(string cscode)
{
var provider = new csharpcodeprovider();
var cp = new compilerparameters();
cp.generateexecutable = false;
cp.generateinmemory = true;
cp.includedebuginformation = false;
//cp.referencedassemblies.add("mscorlib.dll");
cp.referencedassemblies.add("system.dll");
cp.referencedassemblies.add("system.data.dll");
cp.referencedassemblies.add("system.linq.dll");
cp.referencedassemblies.add("system.componentmodel.dataannotations.dll");
cp.referencedassemblies.add("newtonsoft.json.dll");
compilerresults result = provider.compileassemblyfromsource(cp, cscode);
list<string> errlist = new list<string>();
if (result.errors.count > 0)
{
foreach (compilererror err in result.errors)
{
errlist.add(string.format("line:{0},errornumber:{1},errortext:{2}", err.line, err.errornumber, err.errortext));
}
messagebox.show("compile error:\n" + string.join("\n", errlist));
return null;
}
dyassembly = result.compiledassembly;
return dyassembly.gettypes().select(t => t.fullname).tolist();
}
private string tojsonstring(string targettype)
{
if (dyassembly == null)
{
messagebox.show("dyassembly is null!");
return null;
}
var type = dyassembly.gettype(targettype);
var typeconstructor = type.getconstructor(type.emptytypes);
var obj = typeconstructor.invoke(null);
return jsonconvert.serializeobject(obj, formatting.indented, new jsonserializersettings { dateformatstring = "yyyy-mm-dd hh:mm:ss" });
}
json to class code 先使用jobject.parse将json字符串转换为通用的json类型实例,然后直接通过获取所有json属性集合并遍历这些属性,通过判断属性节点的类型,若是子json类型【即:jobject】则创建对象属性字符串 同时递归查找子对象,若是数组类型【即:jarray】则创建list集合属性字符串,同时进一步判断数组的元素类型,若是子json类型【即:jobject】则仍是递归查找子对象,最终拼接成所有类及其子类的class定义源代码字符串。核心代码如下:
private string toclasscode(jobject jobject, string classname)
{
var classcodes = new dictionary<string, string>();
classcodes.add(classname, buildclasscode(jobject, classname, classcodes));
stringbuilder codebuidler = new stringbuilder();
foreach (var code in classcodes)
{
codebuidler.appendline(code.value);
}
return codebuidler.tostring();
}
private dictionary<jtokentype, string> jtokenbasetypemappings = new dictionary<jtokentype, string> {
{ jtokentype.integer,"int" },{ jtokentype.date,"datetime" },{ jtokentype.bytes,"byte[]"},{ jtokentype.boolean,"bool"},{ jtokentype.string,"string"},
{ jtokentype.null,"object"},{ jtokentype.float,"float"},{ jtokentype.timespan,"long"}
};
private string buildclasscode(jobject jobject, string classname, dictionary<string, string> classcodes)
{
stringbuilder classbuidler = new stringbuilder();
classbuidler.append("public class " + classname + " \r\n { \r\n");
foreach (var jprop in jobject.properties())
{
string propclassname = "object";
if (jprop.value.type == jtokentype.object)
{
if (jprop.value.hasvalues)
{
propclassname = getclassname(jprop.name);
if (classcodes.containskey(propclassname))
{
propclassname = classname + propclassname;
}
classcodes.add(propclassname, buildclasscode((jobject)jprop.value, propclassname, classcodes));
}
classbuidler.appendformat("public {0} {1} {2}\r\n", propclassname, jprop.name, "{get;set;}");
}
else if (jprop.value.type == jtokentype.array)
{
if (jprop.value.hasvalues)
{
var jproparritem = jprop.value.first;
if (jproparritem.type == jtokentype.object)
{
propclassname = getclassname(jprop.name);
if (classcodes.containskey(propclassname))
{
propclassname = classname + propclassname;
}
propclassname += "item";
classcodes.add(propclassname, buildclasscode((jobject)jproparritem, propclassname, classcodes));
}
else
{
if (jtokenbasetypemappings.containskey(jproparritem.type))
{
propclassname = jtokenbasetypemappings[jproparritem.type];
}
else
{
propclassname = jproparritem.type.tostring();
}
}
}
classbuidler.appendformat("public list<{0}> {1} {2}\r\n", propclassname, jprop.name, "{get;set;}");
}
else
{
if (jtokenbasetypemappings.containskey(jprop.value.type))
{
propclassname = jtokenbasetypemappings[jprop.value.type];
}
else
{
propclassname = jprop.value.type.tostring();
}
classbuidler.appendformat("public {0} {1} {2} \r\n", propclassname, jprop.name, "{get;set;}");
}
}
classbuidler.append("\r\n } \r\n");
return classbuidler.tostring();
}
把json字符串转换为class类源代码,除了我这个工具外,网上也有一些在线的转换网页可以使用,另外我再分享一个小技巧,即:直接利用vs的编辑-》【选择性粘贴】,然后选择粘贴成json类或xml即可,菜单位置:
通过这种粘贴到json与我的这个工具的效果基本相同,只是多种选择而矣。
jsonconverter工具已开源并上传至github,地址:https://github.com/zuowj/jsonconverter
二、json操作技巧篇
下面再讲讲json数据的读写操作技巧。
一般操作json,大多要么是把class类的实例数据序列化成json字符串,以便进行网络传输,要么是把json字符串反序列化成class类的数据实例,以便可以在程序获取这些数据。然而其实还有一些不常用的场景,也是与json有关,详见如下说明。
场景一:如果已有json字符串,现在需要获得指定属性节点的数据,且指定的属性名不确定,由外部传入或逻辑计算出来的【即:不能直接在代码中写死要获取的属性逻辑】,那么这时该如何快速的按需获取任意json节点的数据呢?
常规解决方案:先反序列化成某个class的实例对象(或jobject实例对象),然后通过反射获取属性,并通过递归及比对属性名找出最终的属性,最后返回该属性的值。
场景二:如果已有某个class实例对象数据,现在需要动态更改指定属性节点的数据【即动态给某个属性赋值】,该如何操作呢?
常规解决方案:通过反射获取属性,并通过递归及比对属性名找出最终的属性,最后通过反射给该属性设置值。
场景三:如果已有json字符串,现在需要动态添加新属性节点,该属性节点可以是任意嵌套子对象的属性节点中,该如何操作呢?
常规解决方案:先反序列化成jobject实例对象,然后递归查找目标位置,最后在指定的位置创建新的属性节点。
三种场景归纳一下其实就是需要对json的某个属性节点数据可以快速动态的增、改、删、查操作,然而常规则解决方案基本上都是需要靠递归+反射+比对,运行性能可想而知,而我今天分享的json操作技巧就是解决上述问题的。
重点来了,我们可以通过jpath表达式来快速定位查找json的属性节点,就像xml利用xpath一样查找dom节点。
jpath表达式是什么呢? 详见:https://goessner.net/articles/jsonpath/ ,xpath与jsonpath对比用法如下图示(图片来源于https://goessner.net/articles/jsonpath/文中):
代码中如何使用jpath表达式呢?使用jobject.selecttokens 或 selecttoken方法即可,我们可以使用selecttokens("jpath")表达式直接快速定位指定的属性节点,然后就可以获得该属性节点的值,若需要该属性设置值,则可以通过该节点找到对应的所属属性信息进行设置值即可,而动态根据指定位置【一般是某个属性节点】添加一个新的属性节点,则可以直接使用jtoken的addbeforeself、addafterself在指定属性节点的前面或后面创建同级新属性节点,是不是非常简单。原理已说明,最后贴出已封装好的实现代码:
using newtonsoft.json.linq;
using system;
using system.collections.generic;
using system.linq;
namespace zuowj.easyutils
{
/// <summary>
/// jobject扩展类
/// author:zuowenjun
/// 2019-6-15
/// </summary>
public static class jobjectextensions
{
/// <summary>
/// 根据jpath查找json指定的属性节点值
/// </summary>
/// <param name="jobj"></param>
/// <param name="fieldpath"></param>
/// <returns></returns>
public static ienumerable<jtoken> findjsonnodevalues(this jobject jobj, string fieldpath)
{
var tks = jobj.selecttokens(fieldpath, true);
list<jtoken> nodevalues = new list<jtoken>();
foreach (var tk in tks)
{
if (tk is jproperty)
{
var jprop = tk as jproperty;
nodevalues.add(jprop.value);
}
else
{
nodevalues.add(tk);
}
}
return nodevalues;
}
/// <summary>
/// 根据jpath查找json指定的属性节点并赋值
/// </summary>
/// <param name="jobj"></param>
/// <param name="fieldpath"></param>
/// <param name="value"></param>
public static void setjsonnodevalue(this jobject jobj, string fieldpath, jtoken value)
{
var tks = jobj.selecttokens(fieldpath, true);
jarray targetjarray = null;
list<int> arrindexs = new list<int>();
foreach (var tk in tks)
{
jproperty jprop = null;
if (tk is jproperty)
{
jprop = tk as jproperty;
}
else if (tk.parent is jproperty)
{
jprop = (tk.parent as jproperty);
}
else if (tk.parent is jobject)
{
jprop = (tk.parent as jobject).property(tk.path.substring(tk.path.lastindexof('.') + 1));
}
if (jprop != null)
{
jprop.value = value;
}
else if (tk.parent is jarray) //注意不能直接在for循环中对jarray元素赋值,否则会导致报错
{
targetjarray = tk.parent as jarray;
arrindexs.add(targetjarray.indexof(tk));
}
else
{
throw new exception("无法识别的元素");
}
}
//单独对找到的数组元素进行重新赋值
if (targetjarray != null && arrindexs.count > 0)
{
foreach (int i in arrindexs)
{
targetjarray[i] = value;
}
}
}
/// <summary>
/// 在指定的jpath的属性节点位置前或后创建新的属性节点
/// </summary>
/// <param name="jobj"></param>
/// <param name="fieldpath"></param>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="addbefore"></param>
/// <returns></returns>
public static void appendjsonnode(this jobject jobj, string fieldpath, string name, jtoken value, bool addbefore = false)
{
var nodevalues = findjsonnodevalues(jobj, fieldpath);
if (nodevalues == null || !nodevalues.any()) return;
foreach (var node in nodevalues)
{
var targetnode = node;
if (node is jobject) //注意只能对普能单值 的jtoken对象(jproptery)允许添加,若不是则应找对应的属性信息
{
targetnode = node.parent;
}
var jprop = new jproperty(name, value);
if (addbefore)
{
targetnode.addbeforeself(jprop);
}
else
{
targetnode.addafterself(jprop);
}
}
}
}
}
用法示例如下代码:
var jsonobj = new
{
root = new
{
lv1 = new
{
col1 = 123,
col2 = true,
col3 = new
{
f1 = "aa",
f2 = "bb",
f3 = "cc",
lv2 = new
{
flv1 = 1,
flv2 = "flv2-2"
}
}
}
},
main = new[] {
new{
mf1="x",
mf2="y",
mf3=123
},
new{
mf1="x2",
mf2="y2",
mf3=225
}
}
};
string json = jsonconvert.serializeobject(jsonobj, formatting.indented);
console.writeline("json1:" + json);
var jobj = jobject.fromobject(jsonobj); //jobject.parse(json);
var findresult = jobj.findjsonnodevalues("root.lv1.col3.lv2.*");
console.writeline("findjsonnodevalues:" + string.join(",", findresult));
jobj.setjsonnodevalue("main[*].mf2", "*changed value*");
json = jsonconvert.serializeobject(jobj, formatting.indented);
console.writeline("json2:" + json);
jobj.appendjsonnode("root.lv1.col3.lv2", "lv2-new", jobject.fromobject(new {flv21="a2",flv22=221,flv23=true }));
// jobj.appendjsonnode("root.lv1.col3.lv2", "lv2-2","single value");
json = jsonconvert.serializeobject(jobj, formatting.indented);
console.writeline("json3:" + json);
console.readkey();
控制台输出的结果如下:可以观察json1(原json),json2(改变了json值),json3(增加了json属性节点)
json1:{
"root": {
"lv1": {
"col1": 123,
"col2": true,
"col3": {
"f1": "aa",
"f2": "bb",
"f3": "cc",
"lv2": {
"flv1": 1,
"flv2": "flv2-2"
}
}
}
},
"main": [
{
"mf1": "x",
"mf2": "y",
"mf3": 123
},
{
"mf1": "x2",
"mf2": "y2",
"mf3": 225
}
]
}
findjsonnodevalues:1,flv2-2
json2:{
"root": {
"lv1": {
"col1": 123,
"col2": true,
"col3": {
"f1": "aa",
"f2": "bb",
"f3": "cc",
"lv2": {
"flv1": 1,
"flv2": "flv2-2"
}
}
}
},
"main": [
{
"mf1": "x",
"mf2": "*changed value*",
"mf3": 123
},
{
"mf1": "x2",
"mf2": "*changed value*",
"mf3": 225
}
]
}
json3:{
"root": {
"lv1": {
"col1": 123,
"col2": true,
"col3": {
"f1": "aa",
"f2": "bb",
"f3": "cc",
"lv2": {
"flv1": 1,
"flv2": "flv2-2"
},
"lv2-new": {
"flv21": "a2",
"flv22": 221,
"flv23": true
}
}
}
},
"main": [
{
"mf1": "x",
"mf2": "*changed value*",
"mf3": 123
},
{
"mf1": "x2",
"mf2": "*changed value*",
"mf3": 225
}
]
}
好了,本文的内容就分享到这里。更多以往的编码实用技巧详见:《》;
温馨提示:近期还会分期更多编程实用技能,欢迎关注评论,谢谢!
上一篇: 几个高逼格 Linux 命令!