分享基于.NET动态编译&Newtonsoft.Json封装实现JSON转换器(JsonConverter)原理及JSON操作技巧

时间:2023-01-02 19:30:50

  看文章标题就知道,本文的主题就是关于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工具的界面吧,如下图示:

分享基于.NET动态编译&Newtonsoft.Json封装实现JSON转换器(JsonConverter)原理及JSON操作技巧

工具界面很简单,下面简要说明一下操作方法:

class类源代码转换成Json字符串:先将项目中定义的class类源代码复制粘贴到Class Code文本框区域【注意:若有继承或属性本身又是另一个类,则相关的class类定义源代码均应一同复制,using合并,namespace允许多个,目的是确保可以动态编译通过】,然后点击上方的【Parse】按钮,以便执行动态编译并解析出Class Code文本框区域中所包含的class Type,最后选择需要生成JSON的class Type,点击中间的【To Json】按钮,即可将选择的class Type 序列化生成JSON字符串并展示在右边的Json String文本框中;

示例效果如下图示:(支持继承,复杂属性)

分享基于.NET动态编译&Newtonsoft.Json封装实现JSON转换器(JsonConverter)原理及JSON操作技巧

有了这个功能以后,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] ])

分享基于.NET动态编译&Newtonsoft.Json封装实现JSON转换器(JsonConverter)原理及JSON操作技巧

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即可,菜单位置:

分享基于.NET动态编译&Newtonsoft.Json封装实现JSON转换器(JsonConverter)原理及JSON操作技巧

通过这种粘贴到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/文中):

分享基于.NET动态编译&Newtonsoft.Json封装实现JSON转换器(JsonConverter)原理及JSON操作技巧

代码中如何使用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
}
]
}

 好了,本文的内容就分享到这里。更多以往的编码实用技巧详见:《近期开发项目中用到的编码小技巧汇总说明(二)》;

温馨提示:近期还会分期更多编程实用技能,欢迎关注评论,谢谢! 

 

 

分享基于.NET动态编译&Newtonsoft.Json封装实现JSON转换器(JsonConverter)原理及JSON操作技巧的更多相关文章

  1. java动态编译 (java在线执行代码后端实现原理)(二)

    在上一篇java动态编译 (java在线执行代码后端实现原理(一))文章中实现了 字符串编译成字节码,然后通过反射来运行代码的demo.这一篇文章提供一个如何防止死循环的代码占用cpu的问题. 思路: ...

  2. java动态编译 (java在线执行代码后端实现原理)

    需求:要实现一个web网页中输入java代码,然后能知道编译结果以及执行结果 类似于菜鸟java在线工具的效果:https://c.runoob.com/compile/10 刚开始从什么概念都没有到 ...

  3. Spring AOP源码分析(三):基于JDK动态代理和CGLIB创建代理对象的实现原理

    AOP代理对象的创建 AOP相关的代理对象的创建主要在applyBeanPostProcessorsBeforeInstantiation方法实现: protected Object applyBea ...

  4. c&num; 动态编译继承接口

    c#里面的动态编译我就不讲了,主要的都有了.如果不熟悉我推荐博文 https://www.cnblogs.com/maguoyong/articles/5553827.html 标准的动态编译 这里主 ...

  5. http中使用json封装数据的性能测试

    http中使用json封装数据的性能测试     一个项目使用json封装数据,接口例如:   客户端发送:   POST /list.do HTTP/1.1   Host: zoomi.com.cn ...

  6. Go&sol;Python&sol;Erlang编程语言对比分析及示例 基于RabbitMQ&period;Client组件实现RabbitMQ可复用的 ConnectionPool(连接池) 封装一个基于NLog&plus;NLog&period;Mongo的日志记录工具类LogUtil 分享基于MemoryCache(内存缓存)的缓存工具类,C&num; B&sol;S 、C&sol;S项目均可以使用!

    Go/Python/Erlang编程语言对比分析及示例   本文主要是介绍Go,从语言对比分析的角度切入.之所以选择与Python.Erlang对比,是因为做为高级语言,它们语言特性上有较大的相似性, ...

  7. 基于roslyn的动态编译库Natasha

    人老了,玩不转博客园的编辑器,详细信息转到:https://mp.weixin.qq.com/s/1r6YKBkyovQSMUgfm_VxBg 关键字:Github, NCC, Natasha,Ros ...

  8. 分享公司DAO层动态SQL的一些封装

    主题 公司在DAO层使用的框架是Spring Data JPA,这个框架很好用,基本不需要自己写SQL或者HQL就能完成大部分事情,但是偶尔有一些复杂的查询还是需要自己手写原生的Native SQL或 ...

  9. 基于&period;net standard 的动态编译实现

    在前文[基于.net core 微服务的另类实现]结尾处,提到了如何方便自动的生成微服务的客户端代理,使对于调用方透明,同时将枯燥的东西使用框架集成,以提高使用便捷性.在尝试了基于 Emit 中间语言 ...

随机推荐

  1. mkdir创建目录

    mkdir:make directories(创建目录) 创建目录的首要条件:在当前目录或者欲创建目录下,该用户具有写入权限,mkdir详细功能如下: 1.mkdir不接任何参数时,即mkdir di ...

  2. Android 6 检查权限代码

    private static final int MY_PERMISSIONS_REQUEST_READ_CONTACTS= 0; //检查目前是否有权限 if (checkSelfPermissio ...

  3. 终端I&sol;O之终端标识

    历史沿袭至今,在大多数UNIX系统中,控制终端的名字是/dev/tty. POSIX.1提供了一个运行时函数,可被用来确定控制终端的名字. #include <stdio.h> char ...

  4. Spring EL method invocation example

    In Spring EL, you can reference a bean, and nested properties using a 'dot (.)' symbol. For example, ...

  5. Poj OpenJudge 百练 2632 Crashing Robots

    1.Link: http://poj.org/problem?id=2632 http://bailian.openjudge.cn/practice/2632/ 2.Content: Crashin ...

  6. 【PHP框架CodeIgniter学习】使用辅助函数—建立自己的JSONHelper

    本文使用的是2.1.4版本,看的时候请注意. 官方文档:http://codeigniter.org.cn/user_guide/general/helpers.html(关于辅助函数Helper的使 ...

  7. linux添加静态路由表,重启继续生效(转载)

    在日常的使用中,或者在服务器中,有两个网卡配置两个地址,访问不同的网络段,这种情况是非常常见的现象,但是,我们需要额外的添加路由表来决定发送的数据包经过正确的网关和interface才能正确的进行通信 ...

  8. SilkTest天龙八部系列6-用open agent进行测试

    SilkTest支持两种测试模式,一种是用classic agent,另一种就是用我们今天要介绍的open agent. open agent可以提供和classic agent差不多的录制回放功能. ...

  9. 第一章 初始java

    一.单词 public:公共的          static:静态的        void:空的          class:类       print:打印     line:排    pro ...

  10. linux下tomcat无法访问问题&lpar;换一种说法:无法访问8080端口&rpar;

    有时候linux下的tomcat其他机器无法访问,比如主机无法访问linux虚拟机的tomcat,这是因为tocat的端口,linux没有对外开放,所以只能localhost访问,但是别的机器访问不了 ...