使用newtonsoft完美序列化WebApi返回的ValueTuple
由于开发功能的需要,又懒得新建太多的class,所以ValueTuple是个比较好的偷懒方法,但是,由于WebApi需要返回序列化后的json,默认的序列化只能将ValueTuple定义的各个属性序列化成Item1...n
但是微软还是良心的为序列化留下入口,编译器会在每个返回ValueTuple<>的函数或者属性上,增加一个TupleElementNamesAttribute特性,该类的TransformNames就是存着所设置的属性的名称(强烈需要记住:是每个使用到ValueTuple的函数或者属性才会添加,而不是加在有使用ValueTuple的类上),比如 (string str1,string str2) 那么 TransformNames=["str1","str2"],那么现在有如下一个class
public class A<T1,T2>
{
public T1 Prop1{set;get;}
public T2 Prop2{set;get;}
public (string str5,int int2) Prop3{set;get;}
}
经过测试,如下一个函数
public A<(string str1,string str2),(string str3,string str4)> testApi(){}
这样一个函数testApi 的会加上 TupleElementNamesAttribute 特性,,TransformNames=["str1","str2","str3","str4","str5","int2"],注意了,,这里只会添加一个TupleElementNamesAttribute特性,然后把A里所有的名字按定义的顺序包含进去.
然后我们需要定义一个JsonConverter,用来专门针对一个函数或一个属性的返回值进行了序列化
public class ValueTupleConverter : JsonConverter
{
private string[] _tupleNames = null;
private NamingStrategy _strategy = null; //也可以直接在这里传入特性
public ValueTupleConverter(TupleElementNamesAttribute tupleNames, NamingStrategy strategy = null)
{
_tupleNames = tupleNames.TransformNames.ToArrayEx();
_strategy = strategy;
} //这里在构造函数里把需要序列化的属性或函数返回类型的names传进来
public ValueTupleConverter(string[] tupleNames, NamingStrategy strategy = null)
{
_tupleNames = tupleNames;
_strategy = strategy;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null && value is ITuple v)
{
writer.WriteStartObject();
for (int i = ; i < v.Length; i++)
{
var pname = _tupleNames[i]; //根据规则,设置属性名
writer.WritePropertyName(_strategy?.GetPropertyName(pname, true) ?? pname); if (v[i] == null)
{
writer.WriteNull();
}
else
{
serializer.Serialize(writer, v[i]);
}
}
writer.WriteEndObject();
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//只需要实现序列化,,不需要反序列化,因为只管输出,所以,这个写不写无所谓
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return objectType.IsValueTuple();
}
}
接下来说说实现的原理:
1.newtonsoft.json的组件里,有一个ContactResolver类,用于对不同的类的解析,类库中自带的DefaultContractResolver默认定义了将类解析成各个JsonProperty,利用这个类,可用于将ValueTuple的定义的名字当做属性,返回给序列化器
2.asp.net core的Formatter,可以对Action输出的对象进行格式化,一般用于比如json的格式化器或者xml格式化器的定义,利用格式化器,在Action最后输出的时候,配合ContractResolver进行序列化
下面的实现中,很多地方需要判断是否为ValueTuple,为了节省代码,因此,先写一个Helper:
public static class ValueTupleHelper
{
private static ConcurrentDictionary<Type,bool> _cacheIsValueTuple=new ConcurrentDictionary<Type, bool>(); public static bool IsValueTuple(this Type type)
{
return _cacheIsValueTuple.GetOrAdd(type, x => x.IsValueType && x.IsGenericType &&
(x.FullName.StartsWith("System.ValueTuple") || x.FullName
?.StartsWith("System.ValueTuple`") == true)
); }
}
那么开始来定义一个ContractResolver,实现的原理请看注释
public class CustomContractResolver : DefaultContractResolver
{
private MethodInfo _methodInfo = null;
private IContractResolver _parentResolver = null; public CustomContractResolver(MethodInfo methodInfo, IContractResolver? parentContractResolver = null)
{
_methodInfo = methodInfo;
_parentResolver = parentContractResolver;
} public override JsonContract ResolveContract(Type type)
{
if (!type.GetProperties()
.Where(x => x.CanRead && x.PropertyType.IsValueTuple())
.Any()) //如果Type类中不包含可读的ValueTuple类型的属性,则调用预定义的Resolver处理,当前Resolver只处理包含ValueTuple的类
{
return _parentResolver?.ResolveContract(type);
} var rc = base.ResolveContract(type); return rc;
} public MethodInfo Method => _methodInfo; protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
//CreateProperty函数的结果,不需要额外加缓存,因为每个Method的返回Type,只会调用一次
JsonProperty property = base.CreateProperty(member, memberSerialization); //先调用默认的CreateProperty函数,创建出默认JsonProperty var pi = member as PropertyInfo; if (property.PropertyType.IsValueTuple())
{
var attr = pi.GetCustomAttribute<TupleElementNamesAttribute>(); //获取定义在属性上的特性 if (attr != null)
{
//如果该属性是已经编译时有添加了TupleElementNamesAttribute特性的,,则不需要从method获取
//这里主要是为了处理 (string str1,int int2) Prop3 这种情况
property.Converter = new ValueTupleConverter(attr, this.NamingStrategy);
}
else
{
//从输入的method获取,并且需要计算当前属性所属的泛型是在第几个,然后计算出在TupleElementNamesAttribute.Names中的偏移
//这个主要是处理比如T2 Prop2 T2=ValueTuple的这种情况
var mAttr = (TupleElementNamesAttribute)_methodInfo.ReturnTypeCustomAttributes.GetCustomAttributes(typeof(TupleElementNamesAttribute), true).FirstOrDefault(); //用来获取valueTuple的各个字段名称
var basePropertyClass = pi.DeclaringType.GetGenericTypeDefinition(); //属性定义的泛型基类 如 A<T1,T2>
var basePropertyType = basePropertyClass.GetProperty(pi.Name)!.PropertyType; //获取基类属性的返回类型 就是T1 ,比如获取在A<(string str1,string str2),(string str3,string str4)> 中 Prop1 返回的类型是对应基类中的T1还是T2
var index = basePropertyType.GenericParameterPosition;//获取属性所在的序号,用于计算 mAttr.Names中的偏移量
var skipNamesCount = (pi.DeclaringType as TypeInfo).GenericTypeArguments
.Take(index)
.Sum(x => x.IsValueTuple() ? x.GenericTypeArguments.Length : ); ; //计算TupleElementNamesAttribute.TransformNames中当前类的偏移量
var names = mAttr.TransformNames
.Skip(skipNamesCount)
.Take(pi.PropertyType.GenericTypeArguments.Length)
.ToArrayEx(); //获取当前类的所有name
property.Converter = new ValueTupleConverter(names, this.NamingStrategy); //传入converter
} property.GetIsSpecified = x => true;
property.ItemConverter = property.Converter; //传入converter
property.ShouldSerialize = x => true;
property.HasMemberAttribute = false;
}
return property;
}
protected override JsonConverter? ResolveContractConverter(Type objectType) //该函数可用于返回特定类型类型的JsonConverter
{
var type = base.ResolveContractConverter(objectType); //这里主要是为了忽略一些在class上定义了JsonConverter的情况,因为有些比如 A<T1,T2> 在序列化的时候,并无法知道ValueTuple定义的属性名,这里添加忽略是为了跳过已定义过的JsonConverter
//如有需要,可在这里多添加几个
if (type is ResultReturnConverter)
{
return null;
}
else
{
return type;
}
}
}
为了能兼容用于预先定义的ContractResolver,因此,先定义一个CompositeContractResolver,用于合并多个ContractResolver,可看可不看:
/// <summary>
/// 合并多个IContractResolver,,并只返回第一个返回非null的Contract,如果所有列表中的ContractResolver都返回null,则调用DefaultContractResolver返回默认的JsonContract
/// </summary>
public class CompositeContractResolver : IContractResolver, IEnumerable<IContractResolver>
{
private readonly IList<IContractResolver> _contractResolvers = new List<IContractResolver>();
private static DefaultContractResolver _defaultResolver = new DefaultContractResolver();
private ConcurrentDictionary<Type, JsonContract> _cacheContractResolvers=new ConcurrentDictionary<Type, JsonContract>(); /// <summary>
/// 返回列表中第一个返回非null的Contract,如果所有列表中的ContractResolver都返回null,则调用DefaultContractResolver返回默认的JsonContract
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public JsonContract ResolveContract(Type type)
{
return _cacheContractResolvers.GetOrAdd(type, m =>
{
for (int i = ; i < _contractResolvers.Count; i++)
{
var contact = _contractResolvers[i].ResolveContract(type); if (contact != null)
{
return contact;
}
} return _defaultResolver.ResolveContract(type);
});
} public void Add(IContractResolver contractResolver)
{
if (contractResolver == null) return; _contractResolvers.Add(contractResolver);
} public IEnumerator<IContractResolver> GetEnumerator()
{
return _contractResolvers.GetEnumerator();
} IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
接下来,就该定义OutputFormatter了
public class ValueTupleOutputFormatter : TextOutputFormatter
{
private static ConcurrentDictionary<Type, bool> _canHandleType = new ConcurrentDictionary<Type, bool>(); //缓存一个Type是否能处理,提高性能,不用每次都判断
private static ConcurrentDictionary<MethodInfo, JsonSerializerSettings> _cacheSettings = new ConcurrentDictionary<MethodInfo, JsonSerializerSettings>(); //用于缓存不同的函数的JsonSerializerSettings,各自定义,避免相互冲突 private Action<ValueTupleContractResolver> _resolverConfigFunc = null; /// <summary>
///
/// </summary>
/// <param name="resolverConfigFunc">用于在注册Formatter的时候对ContractResolver进行配置修改,比如属性名的大小写之类的</param>
public ValueTupleOutputFormatter(Action<ValueTupleContractResolver> resolverConfigFunc = null)
{
SupportedMediaTypes.Add("application/json");
SupportedMediaTypes.Add("text/json");
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode); _resolverConfigFunc = resolverConfigFunc;
} protected override bool CanWriteType(Type type)
{
return _canHandleType.GetOrAdd(type, t =>
{
return type.GetProperties() //判断该类是否包含有ValueTuple的属性
.Where(x => x.CanRead && (CustomAttributeExtensions.GetCustomAttribute<TupleElementNamesAttribute>((MemberInfo) x) != null || x.PropertyType.IsValueTuple()))
.Any();
});
} public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var acce = (IActionContextAccessor)context.HttpContext.RequestServices.GetService(typeof(IActionContextAccessor)); #if NETCOREAPP2_1
var ac = acce.ActionContext.ActionDescriptor as ControllerActionDescriptor;
#endif
#if NETCOREAPP3_0
var endpoint = acce.ActionContext.HttpContext.GetEndpoint();
var ac = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>(); //用来获取当前Action对应的函数信息
#endif
var settings = _cacheSettings.GetOrAdd(ac.MethodInfo, m => //这里主要是为了配置settings,每个methodinfo对应一个自己的settings,当然也就是每个MethodInfo一个CustomContractResolver,防止相互冲突
{
var orgSettings = JsonConvert.DefaultSettings?.Invoke(); //获取默认的JsonSettings
var tmp = orgSettings != null ? cloneSettings(orgSettings) : new JsonSerializerSettings(); //如果不存在默认的,则new一个,如果已存在,则clone一个新的
var resolver = new ValueTupleContractResolver(m, tmp.ContractResolver is CompositeContractResolver ? null : tmp.ContractResolver); //创建自定义ContractResolver,传入函数信息 _resolverConfigFunc?.Invoke(resolver); //调用配置函数 if (tmp.ContractResolver != null) //如果已定义过ContractResolver,则使用CompositeContractResolver进行合并
{
if (tmp.ContractResolver is CompositeContractResolver c) //如果定义的是CompositeContractResolver,则直接插入到最前
{
c.Insert(, resolver);
}
else
{
tmp.ContractResolver = new CompositeContractResolver()
{
resolver,
tmp.ContractResolver
};
}
}
else
{
tmp.ContractResolver = new CompositeContractResolver()
{
resolver
};
} return tmp;
}); var json = JsonConvert.SerializeObject(context.Object, Formatting.None, settings); //调用序列化器进行序列化
await context.HttpContext.Response.Body.WriteAsync(selectedEncoding.GetBytes(json));
} private JsonSerializerSettings cloneSettings(JsonSerializerSettings settings)
{
var tmp = new JsonSerializerSettings(); var properties = settings.GetType().GetProperties(); foreach (var property in properties)
{
var pvalue = property.GetValue(settings); if (pvalue is ICloneable p2)
{
property.SetValue(tmp, p2.Clone());
}
else
{
property.SetValue(tmp, pvalue);
}
} return tmp;
} }
到此,该定义的类都定义完了,下面是注册方法:在Start.cs中:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(opt =>
{
opt.OutputFormatters.Insert(,new ValueTupleOutFormatter(x =>
{
x.NamingStrategy= new CamelCaseNamingStrategy(true,true); //这里主要是为了演示对CustomContractResolver的配置,设置了所有属性首字母小写
}));
}).AddNewtonsoftJson();
}
注册完成后,用下面的Action可测试:
public class ApiTestController : ControllerBase
{
[FromBodyJson()]
public IActionResult test1(List<(string productid,int qty)> details)
{ return Content("success");
} public ResultReturn<(string str1, int int3)> Test()
{
return new SuccessResultReturn<(string str1, int int3)>(("",));
} public Test<(string Y1, string Y2), (string str1, string t2)> Test2()
{
return new Test< (string Y1, string Y2),(string str1, string t2)>(("",""),("","") );
}
}
总结一下,上面实现的原理是: 自定义一个OutputFormatter,在WriteResponseBodyAsync中,可以获取到当前的Action对应的MethodInfo,然后利用编译器在所有返回ValueTuple的地方,都加了TupleElementNamesAttribute的功能,获取到使用时定义的ValueTuple各个Item的名字,再利用ContractResolver的CreateProperty功能,将定义的各个Item转换为对应的name.然后使用newtonsoft的序列化器,进行json序列化.
以上代码只能处理返回时,返回的类型为ValueTuple<T1...n>或者返回的类型中包含了ValueTuple<T1....n>的属性,但是对于函数内,不用于返回的,则无法处理,比如
public object Test2()
{
var s= new Test< (string Y1, string Y2),(string str1, string t2)>(("",""),("","") );
JsonConvert.SerializeObject(s);
return null;
}
这种情况的变量s的序列化就没办法了
部分代码地址:
使用newtonsoft完美序列化WebApi返回的ValueTuple的更多相关文章
- ASP.NET MVC WebApi 返回数据类型序列化控制(json,xml) 用javascript在客户端删除某一个cookie键值对 input点击链接另一个页面,各种操作。 C# 往线程里传参数的方法总结 TCP/IP 协议 用C#+Selenium+ChromeDriver 生成我的咕咚跑步路线地图 (转)值得学习百度开源70+项目
ASP.NET MVC WebApi 返回数据类型序列化控制(json,xml) 我们都知道在使用WebApi的时候Controller会自动将Action的返回值自动进行各种序列化处理(序列化为 ...
- ASP.NET MVC WebApi 返回数据类型序列化控制(json,xml)
我们都知道在使用WebApi的时候Controller会自动将Action的返回值自动进行各种序列化处理(序列化为json,xml等),但是如果Controller的自动序列化后的结果不是我们想要的该 ...
- ASP.NET MVC WebApi 返回数据类型序列化控制(json)
注:本文为个人学习摘录,原文地址:http://www.cnblogs.com/OpenCoder/p/4628557.html 我们都知道在使用WebApi的时候Controller会自动将Acti ...
- MVC web api 返回JSON的几种方式,Newtonsoft.Json序列化日期时间去T的几种方式。
原文链接:https://www.muhanxue.com/essays/2015/01/8623699.html MVC web api 返回JSON的几种方式 1.在WebApiConfig的Re ...
- webapi返回json格式优化
一.设置webapi返回json格式 在App_Start下的WebApiConfig的注册函数Register中添加下面这代码 config.Formatters.Remove(config.For ...
- WebAPI搭建(二) 让WebAPI 返回JSON格式的数据
在RestFul风格盛行的年代,对接接口大多数人会选择使用JSON,XML和JSON的对比传送(http://blog.csdn.net/liaomin416100569/article/detail ...
- webapi返回json格式优化 转载https://www.cnblogs.com/GarsonZhang/p/5322747.html
一.设置webapi返回json格式 在App_Start下的WebApiConfig的注册函数Register中添加下面这代码 1 config.Formatters.Remove(config.F ...
- WebApi返回Json格式字符串
WebApi返回json格式字符串, 在网上能找到好几种方法, 其中有三种普遍的方法, 但是感觉都不怎么好. 先贴一下, 网上给的常用方法吧. 方法一:(改配置法) 找到Global.asax文件,在 ...
- webapi返回json格式,并定义日期解析格式
1.webapi返回json格式 var json = config.Formatters.JsonFormatter; json.SerializerSettings.PreserveReferen ...
随机推荐
- dfs 序 欧拉序
推荐博客 :https://www.cnblogs.com/stxy-ferryman/p/7741970.html DFS序其实就是一棵树顺次访问的结点的顺序,例如下面这棵树 它的 dfs 序就是 ...
- Spring Boot 事务的使用
Spring Boot 使用事务非常简单,首先使用注解 @EnableTransactionManagement 开启事务支持后,然后在访问数据库的Service方法上添加注解 @Transactio ...
- 机器学习-特征工程-Feature generation 和 Feature selection
概述:上节咱们说了特征工程是机器学习的一个核心内容.然后咱们已经学习了特征工程中的基础内容,分别是missing value handling和categorical data encoding的一些 ...
- Cannot create PoolableConnectionFactory (Could not create connection to database server.)
是由于mysql驱动版本太低导致. 本人 jdk版本是1.8.0_211,mysql版本是8.0.17,mysql-connector-java版本是5.1.39, 后来把mysql-connecto ...
- select的disabled形式的数据,使用表单序列化方式无法将数据传到后台
之前博客里有讲述到使用表单序列化的方式传递数据到后台,那里是将数据为disabled形式的内容剔除掉了,所以为disabled的select肯定也是传不过去的. 解决方式: 1.在序列化表单方法之前将 ...
- 缓存读写策略 - Cache Aside.md
场景描述 比如一条数据同时存在数据库.缓存,现在你要更新此数据,你会怎么更新? 先更新数据库?还是先更新缓存? 其实这两种方式都有问题. (1)先更新数据库,后更新缓存 这样会造成数据不一致. A 先 ...
- SpringSecurity 自定义表单登录
SpringSecurity 自定义表单登录 本篇主要讲解 在SpringSecurity中 如何 自定义表单登录 , SpringSecurity默认提供了一个表单登录,但是实际项目里肯定无法使用的 ...
- mysql使用唯一索引避免插入重复数据
使用MySQL 索引防止一个表中的一列或者多列产生重复值 一:介绍MYSQL唯一索引 如果要强烈使一列或多列具有唯一性,通常使用PRIMARY KEY约束. 但是,每个表只能有一个主键. 因此,如果使 ...
- mong 的 安装 和测试
<hr>
- 创建dynamics CRM client-side (十) - 用JS来获取form type
用户可以用以下代码来获取 form type 更多的信息可以查阅https://docs.microsoft.com/en-us/powerapps/developer/model-driven-ap ...