web-dev-qa-db-ja.com

複雑な抽象オブジェクトのWebAPIカスタムモデルバインディング

これは難しいものです。 JSONからモデルをバインドする際に問題が発生しました。多態的に解決しようとしています-解決するレコードのタイプで提供されるレコードです(将来、多くのレコードタイプを追加できるようにしたいと思います)。 次の例 を使用してエンドポイントを呼び出すときにモデルを解決しようとしましたが、この例はMVCでのみ機能し、WebAPIアプリケーションでは機能しません。

IModelBinderとBindModel(HttpActionContext actionContext、ModelBindingContext bindingContext)を使用して記述しようとしました。ただし、System.Web.Http名前空間でModelMetadataProvidersに相当するものが見つかりません。

誰もが与えることができるどんな助けにも感謝します。

次のオブジェクト構造を持つWebAPI2アプリケーションがあります。

public abstract class ResourceRecord
{
    public abstract string Type { get; }
}

public class ARecord : ResourceRecord
{
    public override string Type
    {
        get { return "A"; }
    }

    public string AVal { get; set; }

}

public class BRecord : ResourceRecord
{
    public override string Type
    {
        get { return "B"; }
    }

    public string BVal { get; set; }
}

public class RecordCollection
{
    public string Id { get; set; }

    public string Name { get; set; }

    public List<ResourceRecord> Records { get; }

    public RecordCollection()
    {
        Records = new List<ResourceRecord>();
    }
}

JSON構造

{
  "Id": "1",
  "Name": "myName",
  "Records": [
    {
      "Type": "A",
      "AValue": "AVal"
    },
    {
      "Type": "B",
      "BValue": "BVal"
    }
  ]
}
7
garyamorris

いくつかの調査の結果、メタデータプロバイダーがWebAPI内に存在しないことがわかりました。複雑な抽象オブジェクトにバインドするには、独自に作成する必要があります。

私は、カスタムタイプ名のJSonシリアライザーを使用して、新しいモデルバインディングメソッドを作成することから始め、最後に、カスタムバインダーを使用するようにエンドポイントを更新しました。以下は本文内のリクエストでのみ機能することに注意してください。ヘッダー内のリクエストに対して何か他のものを記述する必要があります。 Adam Freemanの MVC開発者向けのエキスパートASP.NET Web API 2 および複雑なオブジェクトバインディングの第16章を読むことをお勧めします。

次のコードを使用して、リクエストの本文からオブジェクトをシリアル化することができました。

WebAPI構成

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Services.Insert(typeof(ModelBinderProvider), 0,
            new SimpleModelBinderProvider(typeof(RecordCollection), new JsonBodyModelBinder<RecordCollection>()));
    }
}

カスタムモデルバインダー

public class JsonBodyModelBinder<T> : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext,
        ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(T))
        {
            return false;
        }

        try
        {
            var json = ExtractRequestJson(actionContext);

            bindingContext.Model = DeserializeObjectFromJson(json);

            return true;
        }
        catch (JsonException exception)
        {
            bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);

            return false;
        }


        return false;
    }

    private static T DeserializeObjectFromJson(string json)
    {
        var binder = new TypeNameSerializationBinder("");

        var obj = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Auto,
            Binder = binder
        });
        return obj;
    }

    private static string ExtractRequestJson(HttpActionContext actionContext)
    {
        var content = actionContext.Request.Content;
        string json = content.ReadAsStringAsync().Result;
        return json;
    }
}

カスタムシリアル化バインディング

public class TypeNameSerializationBinder : SerializationBinder
{
    public string TypeFormat { get; private set; }

    public TypeNameSerializationBinder(string typeFormat)
    {
        TypeFormat = typeFormat;
    }

    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = null;
        typeName = serializedType.Name;
    }

    public override Type BindToType(string assemblyName, string typeName)
    {
        string resolvedTypeName = string.Format(TypeFormat, typeName);

        return Type.GetType(resolvedTypeName, true);
    }
}

エンドポイントの定義

    [HttpPost]
    public void Post([ModelBinder(BinderType = typeof(JsonBodyModelBinder<RecordCollection>))]RecordCollection recordCollection)
    {
    }
14
garyamorris

TypeNameSerializationBinderクラスは、WebApiConfig構成と同様に不要になりました。

まず、レコードタイプの列挙型を作成する必要があります。

public enum ResourceRecordTypeEnum
{
    a,
    b
}

次に、ResourceRecordの「Type」フィールドを先ほど作成した列挙型に変更します。

public abstract class ResourceRecord
{
    public abstract ResourceRecordTypeEnum Type { get; }
}

次に、次の2つのクラスを作成する必要があります。

モデルバインダー

public class ResourceRecordModelBinder<T> : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(T))
            return false;

        try
        {
            var json = ExtractRequestJson(actionContext);
            bindingContext.Model = DeserializeObjectFromJson(json);
            return true;
        }
        catch (JsonException exception)
        {
            bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);
            return false;
        }
    }

    private static T DeserializeObjectFromJson(string json)
    {
        // This is the main part of the conversion
        var obj = JsonConvert.DeserializeObject<T>(json, new ResourceRecordConverter());
        return obj;
    }

    private string ExtractRequestJson(HttpActionContext actionContext)
    {
        var content = actionContext.Request.Content;
        string json = content.ReadAsStringAsync().Result;
        return json;
    }
}

コンバータークラス

public class ResourceRecordConverter : CustomCreationConverter<ResourceRecord>
{
    private ResourceRecordTypeEnum _currentObjectType;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jobj = JObject.ReadFrom(reader);
        // jobj is the serialized json of the reuquest
        // It pulls from each record the "type" field as it is in requested json,
        // in order to identify which object to create in "Create" method
        _currentObjectType = jobj["type"].ToObject<ResourceRecordTypeEnum>();
        return base.ReadJson(jobj.CreateReader(), objectType, existingValue, serializer);
    }

    public override ResourceRecord Create(Type objectType)
    {
        switch (_currentObjectType)
        {
            case ResourceRecordTypeEnum.a:
                return new ARecord();
            case ResourceRecordTypeEnum.b:
                return new BRecord();
            default:
                throw new NotImplementedException();
        }
    }
}

コントローラー

[HttpPost]
public void Post([ModelBinder(BinderType = typeof(ResourceRecordModelBinder<RecordCollection>))] RecordCollection recordCollection)
{ 
}
1
MasterPiece