1. 程式人生 > >使用newtonsoft完美序列化WebApi返回的ValueTuple

使用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函式,創建出預設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 : 0); ;  //計算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 = 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();
        }
    }
View Code

  接下來,就該定義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(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();
        }

  註冊完成後,用下面的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)>(("2222",222));
        }

        public Test<(string Y1, string Y2), (string str1, string t2)> Test2()
        {
            return new Test< (string Y1, string Y2),(string str1, string t2)>(("111","22222"),("3333","44444") );
        }
    }
View Code

   

  總結一下,上面實現的原理是: 自定義一個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