由于开发功能的需要,又懒得新建太多的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的序列化就没办法了

部分代码地址:

https://github.com/kugarliyifan/Kugar.UI.Web/blob/master/Kugar.Core.Web.NetCore/Formatters/ValueTupleOutputFormatter.cs

https://github.com/kugarliyifan/Kugar.UI.Web/blob/master/Kugar.Core.Web.NetCore/Converters/ValueTupleConverter.cs

https://github.com/kugarliyifan/Kugar.UI.Web/blob/master/Kugar.Core.Web.NetCore/ValueTupleContractResolver.cs

使用newtonsoft完美序列化WebApi返回的ValueTuple的更多相关文章

  1. 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的返回值自动进行各种序列化处理(序列化为 ...

  2. ASP.NET MVC WebApi 返回数据类型序列化控制(json,xml)

    我们都知道在使用WebApi的时候Controller会自动将Action的返回值自动进行各种序列化处理(序列化为json,xml等),但是如果Controller的自动序列化后的结果不是我们想要的该 ...

  3. ASP.NET MVC WebApi 返回数据类型序列化控制(json)

    注:本文为个人学习摘录,原文地址:http://www.cnblogs.com/OpenCoder/p/4628557.html 我们都知道在使用WebApi的时候Controller会自动将Acti ...

  4. MVC web api 返回JSON的几种方式,Newtonsoft.Json序列化日期时间去T的几种方式。

    原文链接:https://www.muhanxue.com/essays/2015/01/8623699.html MVC web api 返回JSON的几种方式 1.在WebApiConfig的Re ...

  5. webapi返回json格式优化

    一.设置webapi返回json格式 在App_Start下的WebApiConfig的注册函数Register中添加下面这代码 config.Formatters.Remove(config.For ...

  6. WebAPI搭建(二) 让WebAPI 返回JSON格式的数据

    在RestFul风格盛行的年代,对接接口大多数人会选择使用JSON,XML和JSON的对比传送(http://blog.csdn.net/liaomin416100569/article/detail ...

  7. webapi返回json格式优化 转载https://www.cnblogs.com/GarsonZhang/p/5322747.html

    一.设置webapi返回json格式 在App_Start下的WebApiConfig的注册函数Register中添加下面这代码 1 config.Formatters.Remove(config.F ...

  8. WebApi返回Json格式字符串

    WebApi返回json格式字符串, 在网上能找到好几种方法, 其中有三种普遍的方法, 但是感觉都不怎么好. 先贴一下, 网上给的常用方法吧. 方法一:(改配置法) 找到Global.asax文件,在 ...

  9. webapi返回json格式,并定义日期解析格式

    1.webapi返回json格式 var json = config.Formatters.JsonFormatter; json.SerializerSettings.PreserveReferen ...

随机推荐

  1. luogu 题解 P2380 【狗哥采矿】

    拿到dp题我们就要想如何推方程 “最北边有bloggium的收集站,最西边有 yeyenum 的收集站.现在要你在这些格子上面安装向北或者向西的传送带(每个格子只能装一种).” 这说明了什么,对于某一 ...

  2. 单调队列优化 dp

    The only difference between easy and hard versions is the constraints. Vova likes pictures with kitt ...

  3. MySql查看修改l时区

    # 查看时区 show variables like '%time_zone%'; # 设置全局 set global time_zone='+8:00';  # 设置当前会话 set time_zo ...

  4. SpringSecurity 默认表单登录页展示流程源码

    SpringSecurity 默认表单登录页展示流程源码 本篇主要讲解 SpringSecurity提供的默认表单登录页 它是如何展示的的流程, 涉及 1.FilterSecurityIntercep ...

  5. Codeforces I Wanna Be the Guy 题解

    这道题非常简单,有两种做法: 1. 用一个数组标记是不是每个关卡小X或小Y都可以通过 2. 用set储存小X和小Y能够通过的关卡(set有去重功能),最后判断set的长度是否等于n 因为楼上已经有第一 ...

  6. 基于Bootstrap和Knockout.js的ASP.NET MVC开发实战

    之前在一家公司里用过Knockout,是easyui 和 Knockout结合 的.下面的这本应该不错. 目录 前言 第一部分入门指南 第1章MVC介绍 创建第一个项目 分析HomeControlle ...

  7. Ubuntu学习之路1

    最近重装了系统,对自己很久的win7 say goodBye.学习了自制U盘系统盘,重装了win8,然后在win8上倒腾,发现还是不错的.于是又在win8上装回了win7上的VMware,捣鼓了一下U ...

  8. LeetCode 11 水池蓄水问题

    今天给大家分享的是一道LeetCode中等难度的题,难度不大,但是解法蛮有意思.我们一起来看题目: Link Container With Most Water Difficulty Medium 题 ...

  9. py2 to py3

    网络下载的python代码,版本参差,从python2.x迁移python3.x的过程中,存在print语法问题,即python2.x中print无括号,python3.x中print有括号. 逐行添 ...

  10. iOS--->运行程序屏幕上下有黑边

    iOS--->运行程序屏幕上下有黑边 原因是LaunchImage没有设置好,图片等尺寸没有和对应的手机屏幕大小适配导致. 解决方法 就是要增加启动图片,把屏幕拉伸,匹配好每个机型的屏幕大小,这 ...