首页 > 编程语言 >asp.net core 使用newtonsoft完美序列化WebApi返回的ValueTuple

asp.net core 使用newtonsoft完美序列化WebApi返回的ValueTuple

时间:2023-12-15 12:22:05浏览次数:36  
标签:WebApi core null ValueTuple var new 序列化 public

https://www.cnblogs.com/kugar/p/12334210.html

 

    由于开发功能的需要,又懒得新建太多的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,可看可不看:

 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可测试:

 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的序列化就没办法了

标签:WebApi,core,null,ValueTuple,var,new,序列化,public
From: https://www.cnblogs.com/chinasoft/p/17903116.html

相关文章

  • EFCore 简介(1)
    EFCore是微软的一个ORM框架,全称:Microsoft.EntityFrameworkCoreORM(ObjectRelationalMapping):对象关系映射,让开发者用对象的形式操作关系数据库常用的ORM框架:EF、EFCore、Dapper、SqlSugar、Freesql每种ORM都有自己的优缺点EFCore有两种模式:DBFirst(数据库优先)和CodeFirst(代码优......
  • 解决.net core开发过程中端口总是无缘无故被占用的问题,提示SocketException: 以一种访
    先给出原因和方案:  是Hyper-V每次启动随机保留端口段,导致所要使用的端口被系统保留,导致无法使用的问题解决方法就是,指定系统保留端口的范围,避开开发时使用的端口即可。powershell管理员身份使用下面的命令设置。netshintipv4setdynamictcpstart=49152nu......
  • 如何生成core文件进行项目调试
    由于项目前期的调试错误比较多,或者有某些隐藏危险:例如内存泄漏;偶尔才出现一次,如果没有捕捉错误的手段可能好不容易出现的机会就溜走了,所以生成core文件是必要的,发生段错误会生成相应的core文件,使用gdb可以查询错误原因和堆栈情况。生成core文件那么如何在程序发生段错误时生成co......
  • .net core 分布式锁 之 基于 Redis 的 RedLock
    使用场景分布式锁的业务场景涉及到并发控制、任务调度、缓存更新、分布式事务和防止重复操作等方面,能够保证分布式系统的数据一致性和正确性。并发控制:当多个线程或进程同时访问共享资源时,使用分布式锁可以确保只有一个线程或进程能够访问该资源,避免数据竞争和并发冲突。分......
  • ASP.NET WebApi(.Net Framework) 应用CacheManager
    ASP.NETWebApi(.NetFramework)应用CacheManager,内存+Redis1,WebApi版本选.net4.6.2以上版本2,nuget包Unity(4.0.0.1)Unity.AspNet.WebApi(4.0.0.1)CacheManager.CoreCacheManager.Microsoft.Extensions.Caching.MemoryCacheManager.Microsoft.Extensions.ConfigurationCacheMa......
  • Hadoop 数据类型及序列化
    1.Hadoop数据类型Java类型HadoopWritable类型BooleanBooleanWritableWritableWritableWritableWritableWritableWritableWritableWritableWritable2.为何Hadoop有自身序列化与反序列化Java自身的序列化除去本身Bean的数据......
  • Aapche Dubbo Java反序列化漏洞(CVE-2019-17564)
    AapcheDubboJava反序列化漏洞(CVE-2019-17564)漏洞描述ApacheDubbo是一款高性能、轻量级的开源JavaRPC服务框架。Dubbo可以使用不同协议通信,当使用http协议时,ApacheDubbo直接使用了Spring框架的org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter类做远程......
  • Asp.net core Net6.0 Webapi 项目如何优雅地使用内存缓存
    前言缓存是提升程序性能必不可少的方法,Asp.netcore支持多级缓存配置,主要有客户端缓存、服务器端缓存,内存缓存和分布式缓存等。其中客户端缓和服务器端缓存在使用上都有比较大的限制,而内存缓和分布式缓存则比较灵活。内存缓存就是一种把缓存数据放到应用程序内存中的机制。本......
  • .net core 同步锁/异步锁
    一、同步锁privatestaticreadonlyobject_lock=newobject();///同步锁publicvoidTestLock(){lock(_lock){//需要处理的业务Console.Write("输出内容");......
  • net core 异步超时取消机制
    方法一:利用Task.WhenAnynamespaceConsoleApp1{internalclassProgram{staticvoidMain(string[]args){Console.WriteLine("Hello,World!");CancellationTokenSourcects=newCancellationTokenSource......