.NET Core WebApi中實現多態數據綁定

什麼是多態數據綁定?

咱們都知道在ASP.NET Core WebApi中數據綁定機制(Data Binding)負責綁定請求參數, 一般狀況下大部分的數據綁定都能在默認的數據綁定器(Binder)中正常的進行,可是也會出現少數不支持的狀況,例如多態數據綁定。所謂的多態數據綁定(polymorphic data binding),即請求參數是子類對象的Json字符串, 而action中定義的是父類類型的變量,默認狀況下ASP.NET Core WebApi是不支持多態數據綁定的,會形成數據丟失。json

如下圖爲例api

 

 

Person類是一個父類,Doctor類和Student類是Person類的派生類。Doctor類中持有的HospitalName屬性,Student中持有的SchoolName屬性。app

 

接下來咱們建立一個Web Api項目並添加一個PeopleController。async

在PeopleController中咱們添加一個Add api,並將請求數據直接返回,以便查看效果。ide

 

[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內容以下。this

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

請求的返回內容lua

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

返回結果和咱們但願獲得的結果不太同樣,Student持有的SchoolName屬性和Doctor持有的HospitalName屬性都丟失了。spa

如今咱們啓動項目調試模式,從新使用Postman請求一次,獲得的結果以下調試

 

People集合中存放3個People類型的對象, 沒有出現咱們指望的Student類型對象和Doctor類型對象,這說明.NET Core WebApi默認是不支持多態數據綁定的,若是使用父類類型變量來接收數據,Data Binding只會實例化父類對象,而非一個派生類對象, 從而致使屬性丟失。code

 

自定義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.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類裏面的Deserialize方法會嘗試根據當前請求參數的類型,去查找並實例化一個合適的JsonConverter。 若是查找到匹配的Converter, 就使用該Converter進行實際的反序列化數據綁定操做。在當前例子中因爲api的參數類型是Person,因此它會匹配到PersonJsonConverter, 這就是爲何咱們經過添加PersonJsonConverter就完成了多態數據綁定的功能。

 

附源代碼

相關文章
相關標籤/搜索