asp.net core 使用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 = 0; 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函數,創建出默認JsonPropertyvar 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還是T2var index = basePropertyType.GenericParameterPosition;//獲取屬性所在的序號,用于計算 mAttr.Names中的偏移量var skipNamesCount = (pi.DeclaringType as TypeInfo).GenericTypeArguments.Take(index).Sum(x => x.IsValueTuple() ? x.GenericTypeArguments.Length : 0); ; //計算TupleElementNamesAttribute.TransformNames中當前類的偏移量var names = mAttr.TransformNames.Skip(skipNamesCount).Take(pi.PropertyType.GenericTypeArguments.Length).ToArrayEx(); //獲取當前類的所有nameproperty.Converter = new ValueTupleConverter(names, this.NamingStrategy); //傳入converter}property.GetIsSpecified = x => true;property.ItemConverter = property.Converter; //傳入converterproperty.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 = 0; 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_1var ac = acce.ActionContext.ActionDescriptor as ControllerActionDescriptor; #endif #if NETCOREAPP3_0var endpoint = acce.ActionContext.HttpContext.GetEndpoint();var ac = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>(); //用來獲取當前Action對應的函數信息 #endifvar settings = _cacheSettings.GetOrAdd(ac.MethodInfo, m => //這里主要是為了配置settings,每個methodinfo對應一個自己的settings,當然也就是每個MethodInfo一個CustomContractResolver,防止相互沖突{var orgSettings = JsonConvert.DefaultSettings?.Invoke(); //獲取默認的JsonSettingsvar 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(0, 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(0,new ValueTupleOutFormatter(x =>{x.NamingStrategy= new CamelCaseNamingStrategy(true,true); //這里主要是為了演示對CustomContractResolver的配置,設置了所有屬性首字母小寫}));}).AddNewtonsoftJson();}總結一下,上面實現的原理是: 自定義一個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)>(("111","22222"),("3333","44444") );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
?
總結
以上是生活随笔為你收集整理的asp.net core 使用newtonsoft完美序列化WebApi返回的ValueTuple的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core开发实战(第5课:依赖
- 下一篇: 不要错过这轮疫情的“洗牌”机会