.NET Core WebApi中实现多态数据绑定

C#

浏览数:103

2019-5-5

什么是多态数据绑定?

我们都知道在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<Person> Add([FromBody]List<Person> 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<T> : 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<Person>, 其代码如下

public class PersonJsonConverter : JsonCreationConverter<Person>
{
    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<MvcOptions>
{
    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<InputFormatterResult> 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.NET的JsonSerializer类的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类里面的Deserialize方法会尝试根据当前请求参数的类型,去查找并实例化一个合适的JsonConverter。 如果查找到匹配的Converter, 就使用该Converter进行实际的反序列化数据绑定操作。在当前例子中由于api的参数类型是Person,所以它会匹配到PersonJsonConverter, 这就是为什么我们通过添加PersonJsonConverter就完成了多态数据绑定的功能。

 

附源代码

 

作者:LamondLu