热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

.NETCOREWEBAPI中实现多态数据绑定

什么是多态数据绑定?我们都知道在ASP.NETCoreWebApi中数据绑定机制(DataBinding)负责绑定请求参数,通常情况下大部分的数据绑定都能在默认的数据绑定器(Bin

什么是多态数据绑定?

我们都知道在ASP.NET Core WebApi中数据绑定机制(Data Binding)负责绑定请求参数, 通常情况下大部分的数据绑定都能在默认的数据绑定器(Binder)中正常的进行,但是也会出现少数不支持的情况,例如多态数据绑定。所谓的多态数据绑定(polymorphic data binding),即请求参数是子类对象的Json字符串, 而action中定义的是父类类型的变量,默认情况下ASP.NET Core WebApi是不支持多态数据绑定的,会造成数据丢失。

以下图为例

 

 

Person类是一个父类,Doctor类和Student类是Person类的派生类。Doctor类中持有的HospitalName属性,Student中持有的SchoolName属性。

 

接下來我们创建一个Web Api项目并添加一个PeopleController。

在PeopleController中我们添加一个Add api,并将请求数据直接返回,以便查看效果。

 

[Route("api/people")]
public class PeopleController : Controller
{
    [HttpPost]
    [Route("")]
    public List Add([FromBody]List people)
    {
        return people;
    }
}

 

这里我们使用Postman请求这个api, 请求的Content-Type是application/json, 请求的Body内容如下。

[{
    firstName: 'Mike',
    lastName: 'Li'
}, {
    firstName: 'Stephie',
    lastName: 'Wang',
    schoolName: 'No.15 Middle School'
}, {
    firstName: 'Jacky',
    lastName: 'Chen',
    hospitalName: 'Center Hospital'
}]

请求的返回内容

[
    {
        "FirstName": "Mike",
        "LastName": "Li"
    },
    {
        "FirstName": "Stephie",
        "LastName": "Wang"
    },
    {
        "FirstName": "Jacky",
        "LastName": "Chen"
    }
]

返回结果和我们希望得到的结果不太一样,Student持有的SchoolName属性和Doctor持有的HospitalName属性都丢失了。

现在我们启动项目调试模式,重新使用Postman请求一次,得到的结果如下

 

People集合中存放3个People类型的对象, 没有出现我们期望的Student类型对象和Doctor类型对象,这说明.NET Core WebApi默认是不支持多态数据绑定的,如果使用父类类型变量来接收数据,Data Binding只会实例化父类对象,而非一个派生类对象, 从而导致属性丢失。

 

自定义JsonConverter来实现多态数据绑定

JsonConverter是Json.NET中的一个类,主要负责Json对象的序列化和反序列化。

首先我们创建一个泛型类JsonCreationConverter,并继承了JsonConverter类,代码如下:

 

public abstract class JsonCreationConverter : JsonConverter
{
    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }

    protected abstract T Create(Type objectType, JObject jObject);

    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }


    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader == null) throw new ArgumentNullException("reader");
        if (serializer == null) throw new ArgumentNullException("serializer");
        if (reader.TokenType == JsonToken.Null)
            return null;

        JObject jObject = JObject.Load(reader);
        T target = Create(objectType, jObject);
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

 

其中,我们加入了一个抽象方法Create,这个方法会负责根据Json字符串的内容,返回一个泛型类型对象,这里既可以返回一个当前泛型类型的对象,也可以返回一个当前泛型类型派生类的对象。JObject是Json.NET中的Json字符串读取器,负责读取Json字符串中属性的值。

另外我们还复写了ReadJson方法,在ReadJson中我们会先调用Create方法获取一个当前泛型类对象或者当前泛型类的派生类对象Json.NET中默认的KeyValuePairConverter会直接实例化当前参数类型对象,这也就是默认不支持多态数据绑定的主要原因,serializer.Popluate方法的作用是将Json字符串的内容映射到目标对象(当前泛型类对象或者当前泛型类的派生类对象)的对应属性。

这里由于我们只需要读取Json, 所以WriteJson的方法我们不需要实现,CanWrite属性我们也强制返回了False。

 

第二步,我们创建一个PersonJsonConverter类,它继承了JsonCreationConverter, 其代码如下

public class PersonJsonConverter : JsonCreationConverter
{
    protected override Person Create(Type objectType, JObject jObject)
    {
        if (jObject == null) throw new ArgumentNullException("jObject");

        if (jObject["schoolName"] != null)
        {
            return new Student();
        }
        else if (jObject["hospitalName"] != null)
        {
            return new Doctor();
        }
        else
        {
            return new Person();
        }
    }
}

在这个类中我们复写了Create方法,这里我们使用JObject来获取Json字符串中拥有的属性。

  • 如果字符串中包含schoolName属性,就返回一个新的Student对象
  • 如果字符串中包含hospitalName属性,就返回一个新的Doctor对象
  • 否则,返回一个新Person对象

最后一步,我们在Person类中使用特性标注Person类使用PersonJsonConverter来进行转换Json序列化和反序列化。

[JsonConverter(typeof(PersonJsonConverter))]
public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }
}

现在我们重新使用调试模式启动程序, 然后使用Postman请求当前api

 

我们会发现,people集合中已经正确绑定了的派生子类类型对象,最终Postman上我们得到以下响应结果

[
    {
        "FirstName": "Mike",
        "LastName": "Li"
    },
    {
        "SchoolName": "No.15 Middle School",
        "FirstName": "Stephie",
        "LastName": "Wang"
    },
    {
        "HospitalName": "Center Hospital",
        "FirstName": "Jacky",
        "LastName": "Chen"
    }
]

至此多态数据绑定成功。

 

 

刨根问底

为什么添加了一个PersonJsonConverter类,多态绑定就实现了呢?

让我们来一起Review一下MVC Core以及Json.NET的代码。

 

首先我们看一下MvcCoreMvcOptionsSetup代码

public class MvcCoreMvcOptionsSetup : IConfigureOptions
{
    private readonly IHttpRequestStreamReaderFactory _readerFactory;
    private readonly ILoggerFactory _loggerFactory;

    ......
        
    public void Configure(MvcOptions options)
    {
        options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider());
        options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
        options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options));
        ......
    }

    ......
    
}

MvcCoreMvcOptionsSetup类中的Configure方法设置的默认数据绑定使用Provider列表。

当一个api参数被标记为[FromBody]时,BodyModelBinderProvider会实例化一个BodyModelBinder对象来处理这个参数并尝试进行数据绑定。

 

BodyModelBinder类中有一个BindModelAsync方法,从名字的字面意思上我们很清楚的知道这个方法就是用来绑定数据的。

public async Task BindModelAsync(ModelBindingContext bindingContext)
{
    if (bindingCOntext== null)
    {
        throw new ArgumentNullException(nameof(bindingContext));
    }

     ….

    var formatter = (IInputFormatter)null;
    for (var i = 0; i <_formatters.Count; i++)
    {
         if (_formatters[i].CanRead(formatterContext))
        {
            formatter = _formatters[i];
            _logger?.InputFormatterSelected(formatter, formatterContext);
            break;
        }
        else
        {
             logger?.InputFormatterRejected(_formatters[i], formatterContext);
        }
    }

    ……

    try
    {
        var result = await formatter.ReadAsync(formatterContext);

        ……
    }
    catch (Exception exception) when (exception is InputFormatterException || ShouldHandleException(formatter))
    {
        bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);
    }
}

在这个方法中它会尝试寻找一个匹配的IInputFormatter对象来绑定数据,由于这时候请求的Content-Type是application/json, 所以这里会使用JsonInputFormatter对象来进行数据绑定。

 

下面我们看一下JsonInputFormatter类的部分关键代码

public override async Task ReadRequestBodyAsync(
            InputFormatterContext context,
            Encoding encoding)
{
    ......

    using (var streamReader = context.ReaderFactory(request.Body, encoding))
    {
        using (var jsOnReader= new JsonTextReader(streamReader))
        {
            …

            object model;
            try
            {
                model = jsonSerializer.Deserialize(jsonReader, type);
            }
            finally
            {
                jsonSerializer.Error -= ErrorHandler;
                ReleaseJsonSerializer(jsonSerializer);
            }

            …
        }
    }
}

JsonInputFormatter类中的ReadRequestBodyAsync方法负责数据绑定, 在该方法中使用了Json.NETJsonSerializer类的Deserialize方法来进行反序列化, 这说明Mvc Core的底层是直接使用Json.NET来操作Json的。

 

JsonSerializer类的部分关键代码

public object Deserialize(JsonReader reader, Type objectType)
{
    return DeserializeInternal(reader, objectType);
}

internal virtual object DeserializeInternal(JsonReader reader, Type objectType)
{
    ……

    JsonSerializerInternalReader serializerReader = new JsonSerializerInternalReader(this);
    object value = serializerReader.Deserialize(traceJsonReader ?? reader, objectType, CheckAdditionalContent);

    ……
    return value;
}

JsonSerializer会调用JsonSerializerInternalReader类的Deserialize方法将Json字符串内容反序列化。

最终我们看一下JsonSerializerInternalReader中的部分关键代码

public object Deserialize(JsonReader reader, Type objectType, bool checkAdditionalContent)
{
    …

    JsonConverter converter = GetConverter(contract, null, null, null);

    if (reader.TokenType == JsonToken.None && !reader.ReadForType(contract, converter != null))
    {
        ......

        object deserializedValue;

        if (converter != null && converter.CanRead)
        {
            deserializedValue = DeserializeConvertable(converter, reader, objectType, null);
        }
        else
        {
            deserializedValue = CreateValueInternal(reader, objectType, contract, null, null, null, null);
        }
     }
}

JsonSerializerInternalReader类里面的Deserailize方法会尝试根据当前请求参数的类型,去查找并实例化一个合适的JsonConverter, 如果查找成功就使用该Converter进行实际的反序列化数据绑定操作。在当前例子中由于api的参数类型是Person,所以它会匹配到PersonJsonConverter, 这就是为什么我们通过添加PersonJsonConverter就完成了多态数据绑定的功能。

 

附源代码

知识共享许可协议
作者:Lamond Lu
出处:https://www.cnblogs.com/lwqlun/p/9532803.html
本站使用「署名 4.0 国际」创作共享协议,转载请在文章明显位置注明作者及出处。


推荐阅读
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • SAP接口编程PyRFC 调用 BAPI_FIXEDASSET_CREATE1创建固定资产
    本篇演示通过PyRFC调用BAPI_FIXEDASSET_CREATE1在SAP系统中创建固定资产,再一次体验一下Python与其它语言相比的简洁性。首先简单说明B ... [详细]
  • Monkey《大话移动——Android与iOS应用测试指南》的预购信息发布啦!
    Monkey《大话移动——Android与iOS应用测试指南》的预购信息已经发布,可以在京东和当当网进行预购。感谢几位大牛给出的书评,并呼吁大家的支持。明天京东的链接也将发布。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 使用nodejs爬取b站番剧数据,计算最佳追番推荐
    本文介绍了如何使用nodejs爬取b站番剧数据,并通过计算得出最佳追番推荐。通过调用相关接口获取番剧数据和评分数据,以及使用相应的算法进行计算。该方法可以帮助用户找到适合自己的番剧进行观看。 ... [详细]
  • 如何实现织梦DedeCms全站伪静态
    本文介绍了如何通过修改织梦DedeCms源代码来实现全站伪静态,以提高管理和SEO效果。全站伪静态可以避免重复URL的问题,同时通过使用mod_rewrite伪静态模块和.htaccess正则表达式,可以更好地适应搜索引擎的需求。文章还提到了一些相关的技术和工具,如Ubuntu、qt编程、tomcat端口、爬虫、php request根目录等。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Metasploit攻击渗透实践
    本文介绍了Metasploit攻击渗透实践的内容和要求,包括主动攻击、针对浏览器和客户端的攻击,以及成功应用辅助模块的实践过程。其中涉及使用Hydra在不知道密码的情况下攻击metsploit2靶机获取密码,以及攻击浏览器中的tomcat服务的具体步骤。同时还讲解了爆破密码的方法和设置攻击目标主机的相关参数。 ... [详细]
  • 那么多优秀的自动化测试工具,而你只知道Selenium?
    如今,作为一名软件测试工程师,几乎所有人都需要具备自动化测试相关的知识,并且懂得如何去利用工具,来为企业减少时间成本和错误成 ... [详细]
  • 一、介绍:在测试和开发中,有一款API测试工具一直占据着武林盟主的地位,那就是声名远播的Google公司的Postman。Postman原先是Chrome浏览器的一个插件,后面发展 ... [详细]
  • postmain报400_Postman测试@RequestBody发送请求时报400错误
    postman测试requestbody时碰到400错误图1原因:传参数如果不使用RequestBody,在使用Postman进行Post请求时,通常做 ... [详细]
author-avatar
迷雾飘渺2702932540
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有