web-dev-qa-db-ja.com

複数の複雑なオブジェクトをpost / put Web APIメソッドに渡す

以下に示すように、C#コンソールアプリからWeb APIコントローラーに複数のオブジェクトを渡す方法を教えてください。

using (var httpClient = new System.Net.Http.HttpClient())
{
    httpClient.BaseAddress = new Uri(ConfigurationManager.AppSettings["Url"]);
    httpClient.DefaultRequestHeaders.Accept.Clear();
    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));   

    var response = httpClient.PutAsync("api/process/StartProcessiong", objectA, objectB);
}

私のWeb APIメソッドは次のようなものです。

public void StartProcessiong([FromBody]Content content, [FromBody]Config config)
{

}
39
SKumar

Web APIの現在のバージョンでは、複数の複合オブジェクトContentConfig複合オブジェクトのような)の使用 Web APIメソッドシグネチャ内では、は許可されませんconfig(2番目のパラメーター)が常にNULLとして返ってくるのは間違いないでしょう。これは、1つのリクエストでボディから解析できる複合オブジェクトは1つだけだからです。パフォーマンス上の理由から、Web APIリクエスト本文へのアクセスと解析のみが許可されますonce。そのため、「content」パラメータのリクエスト本文のスキャンと解析が行われた後、後続のすべての本文解析は「NULL」で終了します。だから基本的に:

  • [FromBody]に関連付けられるアイテムは1つだけです。
  • [FromUri]を使用して、任意の数のアイテムを関連付けることができます。

以下は Mike Stallの優れたブログ記事 (oldie but goldie!)からの便利な抜粋です。 項目4に注意を払う必要があります。

パラメーターをモデルバインディングまたはフォーマッターで読み込むかどうかを決定する基本的なルールは次のとおりです。

  1. パラメーターに属性がない場合、決定は純粋にパラメーターの.NETタイプで行われます。 「単純型」はモデルバインディングを使用します。複合型はフォーマッターを使用します。 「単純型」には、 プリミティブTimeSpanDateTimeGuidDecimalString、または文字列から変換するTypeConverterを含むものが含まれます。
  2. [FromBody]属性を使用して、パラメーターが本文からのものであることを指定できます。
  3. パラメーターまたはパラメーターのタイプで[ModelBinder]属性を使用して、パラメーターをモデルバインドするように指定できます。この属性では、モデルバインダーを構成することもできます。 [FromUri][ModelBinder]の派生インスタンスであり、URIのみを参照するようにモデルバインダーを具体的に構成します。
  4. 本文は一度しか読むことができません。したがって、署名に2つの複合型がある場合、そのうちの少なくとも1つに[ModelBinder]属性が必要です。

これらのルールが静的で予測可能であることが、主要な設計目標でした。

MVCとWeb APIの主な違いは、MVCがコンテンツ(リクエストボディなど)をバッファリングすることです。これは、MVCのパラメーターバインディングがパラメーターを部分的に検索するために、本体を繰り返し検索できることを意味します。一方、Web APIでは、リクエストの本文(HttpContent)は、読み取り専用、無限、バッファリングされていない、巻き戻せないストリームである場合があります。

あなたはこの信じられないほど便利な記事の残りを自分で読むことができますので、長い話を短くするために、あなたがしようとしていることは現在不可能ですそのように(意味、創造的になる必要があります)。以下は解決策ではなく、回避策であり、唯一の可能性です。他の方法があります。

ソリューション/回避策

免責事項:自分では使用していませんが、理論を知っているだけです!)

考えられる「解決策」の1つは、 JObject オブジェクトを使用することです。このオブジェクトは、JSONを操作するために特別に設計された具象型を提供します。

単にシグネチャを調整するだけで、ボディの複合オブジェクトJObjectを1つだけ受け入れることができます。それをstuffと呼びましょう。次に、JSONオブジェクトのプロパティを手動で解析し、ジェネリックを使用して具象型をハイドレートする必要があります。

たとえば、以下はアイデアを提供するための手っ取り早い例です:

public void StartProcessiong([FromBody]JObject stuff)
{
  // Extract your concrete objects from the json object.
  var content = stuff["content"].ToObject<Content>();
  var config = stuff["config"].ToObject<Config>();

  . . . // Now do your thing!
}

他の方法もあると言いました。たとえば、自分の作成したスーパーオブジェクトで2つのオブジェクトを単純にラップし、アクションメソッドに渡すことができます。または、URIで1つを指定することにより、リクエスト本文で2つの複雑なパラメーターの必要性を単純に排除できます。または...まあ、あなたはポイントを得る。

繰り返しますが、私はこれを自分で試したことはありませんが、理論上はすべて動作するはずです。

50
djikay

@djikayが述べたように、複数のFromBodyパラメーターを渡すことはできません。

回避策の1つは、CompositeObjectを定義することです。

public class CompositeObject
{
    public Content Content { get; set; }
    public Config Config { get; set; }
}

代わりにWebAPIにこのCompositeObjectをパラメーターとして使用させます。

public void StartProcessiong([FromBody] CompositeObject composite)
{ ... }
19
Maggie Ying

次のように、クライアントからマルチパートコンテンツを投稿してみることができます。

 using (var httpClient = new HttpClient())
{
    var uri = new Uri("http://example.com/api/controller"));

    using (var formData = new MultipartFormDataContent())
    {
        //add content to form data
        formData.Add(new StringContent(JsonConvert.SerializeObject(content)), "Content");

        //add config to form data
        formData.Add(new StringContent(JsonConvert.SerializeObject(config)), "Config");

        var response = httpClient.PostAsync(uri, formData);
        response.Wait();

        if (!response.Result.IsSuccessStatusCode)
        {
            //error handling code goes here
        }
    }
}

サーバー側では、次のようなコンテンツを読むことができます。

public async Task<HttpResponseMessage> Post()
{
    //make sure the post we have contains multi-part data
    if (!Request.Content.IsMimeMultipartContent())
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }

    //read data
    var provider = new MultipartMemoryStreamProvider();
    await Request.Content.ReadAsMultipartAsync(provider);

    //declare backup file summary and file data vars
    var content = new Content();
    var config = new Config();

    //iterate over contents to get Content and Config
    foreach (var requestContents in provider.Contents)
    {
        if (requestContents.Headers.ContentDisposition.Name == "Content")
        {
            content = JsonConvert.DeserializeObject<Content>(requestContents.ReadAsStringAsync().Result);
        }
        else if (requestContents.Headers.ContentDisposition.Name == "Config")
        {
            config = JsonConvert.DeserializeObject<Config>(requestContents.ReadAsStringAsync().Result);
        }
    }

    //do something here with the content and config and set success flag
    var success = true;

    //indicate to caller if this was successful
    HttpResponseMessage result = Request.CreateResponse(success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError, success);
    return result;

}

}

8
Brian Wenhold

私はこれが古い質問であることを知っていますが、私は同じ問題を抱えていました。これにより、リクエストURL(GET)でJSON形式のパラメーターを個別に渡すことができます。 (GET)または単一のJSON本文オブジェクト(POST)内。私の目標はRPCスタイルの機能でした。

HttpParameterBindingを継承して、カスタム属性とパラメーターバインディングを作成しました。

public class JSONParamBindingAttribute : Attribute
{

}

public class JSONParamBinding : HttpParameterBinding
{

    private static JsonSerializer _serializer = JsonSerializer.Create(new JsonSerializerSettings()
    {
        DateTimeZoneHandling = DateTimeZoneHandling.Utc
    });


    public JSONParamBinding(HttpParameterDescriptor descriptor)
        : base(descriptor)
    {
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                                HttpActionContext actionContext,
                                                CancellationToken cancellationToken)
    {
        JObject jobj = GetJSONParameters(actionContext.Request);

        object value = null;

        JToken jTokenVal = null;
        if (!jobj.TryGetValue(Descriptor.ParameterName, out jTokenVal))
        {
            if (Descriptor.IsOptional)
                value = Descriptor.DefaultValue;
            else
                throw new MissingFieldException("Missing parameter : " + Descriptor.ParameterName);
        }
        else
        {
            try
            {
                value = jTokenVal.ToObject(Descriptor.ParameterType, _serializer);
            }
            catch (Newtonsoft.Json.JsonException e)
            {
                throw new HttpParseException(String.Join("", "Unable to parse parameter: ", Descriptor.ParameterName, ". Type: ", Descriptor.ParameterType.ToString()));
            }
        }

        // Set the binding result here
        SetValue(actionContext, value);

        // now, we can return a completed task with no result
        TaskCompletionSource<AsyncVoid> tcs = new TaskCompletionSource<AsyncVoid>();
        tcs.SetResult(default(AsyncVoid));
        return tcs.Task;
    }

    public static HttpParameterBinding HookupParameterBinding(HttpParameterDescriptor descriptor)
    {
        if (descriptor.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<JSONParamBindingAttribute>().Count == 0 
            && descriptor.ActionDescriptor.GetCustomAttributes<JSONParamBindingAttribute>().Count == 0)
            return null;

        var supportedMethods = descriptor.ActionDescriptor.SupportedHttpMethods;

        if (supportedMethods.Contains(HttpMethod.Post) || supportedMethods.Contains(HttpMethod.Get))
        {
            return new JSONParamBinding(descriptor);
        }

        return null;
    }

    private JObject GetJSONParameters(HttpRequestMessage request)
    {
        JObject jobj = null;
        object result = null;
        if (!request.Properties.TryGetValue("ParamsJSObject", out result))
        {
            if (request.Method == HttpMethod.Post)
            {
                jobj = JObject.Parse(request.Content.ReadAsStringAsync().Result);
            }
            else if (request.RequestUri.Query.StartsWith("?%7B"))
            {
                jobj = JObject.Parse(HttpUtility.UrlDecode(request.RequestUri.Query).TrimStart('?'));
            }
            else
            {
                jobj = new JObject();
                foreach (var kvp in request.GetQueryNameValuePairs())
                {
                    jobj.Add(kvp.Key, JToken.Parse(kvp.Value));
                }
            }
            request.Properties.Add("ParamsJSObject", jobj);
        }
        else
        {
            jobj = (JObject)result;
        }

        return jobj;
    }



    private struct AsyncVoid
    {
    }
}

WebApiConfig.csのRegisterメソッド内にバインディングルールを挿入します。

        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.ParameterBindingRules.Insert(0, JSONParamBinding.HookupParameterBinding);

            config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        }

これにより、デフォルトのパラメーター値と複雑さが混在するコントローラーアクションが可能になります。

[JSONParamBinding]
    [HttpPost, HttpGet]
    public Widget DoWidgetStuff(Widget widget, int stockCount, string comment="no comment")
    {
        ... do stuff, return Widget object
    }

投稿本文の例:

{ 
    "widget": { 
        "a": 1, 
        "b": "string", 
        "c": { "other": "things" } 
    }, 
    "stockCount": 42, 
    "comment": "sample code"
} 

またはGET単一パラメーター(URLエンコードが必要)

controllerPath/DoWidgetStuff?{"widget":{..},"comment":"test","stockCount":42}

またはGET multiple param(URL encodingが必要)

controllerPath/DoWidgetStuff?widget={..}&comment="test"&stockCount=42
5
user6775030

1つの複雑なオブジェクトを作成して、他の人が述べたようにContentとConfigを結合し、動的に使用して.ToObject()を実行します。なので:

[HttpPost]
public void StartProcessiong([FromBody] dynamic obj)
{
   var complexObj= obj.ToObject<ComplexObj>();
   var content = complexObj.Content;
   var config = complexObj.Config;
}
2
harlandgomez

複数の複雑なオブジェクトをwebapiサービスに渡す最良の方法は、動的なjson文字列のカスタムクラス以外のTupleを使用することです。

HttpClient.PostAsJsonAsync("http://Server/WebService/Controller/ServiceMethod?number=" + number + "&name" + name, Tuple.Create(args1, args2, args3, args4));

[HttpPost]
[Route("ServiceMethod")]
[ResponseType(typeof(void))]
public IHttpActionResult ServiceMethod(int number, string name, Tuple<Class1, Class2, Class3, Class4> args)
{
    Class1 c1 = (Class1)args.Item1;
    Class2 c2 = (Class2)args.Item2;
    Class3 c3 = (Class3)args.Item3;
    Class4 c4 = (Class4)args.Item4;
    /* do your actions */
    return Ok();
}

Tupleの使用中にオブジェクトの受け渡しをシリアライズおよびデシリアライズする必要はありません。 7つ以上の複雑なオブジェクトを送信する場合は、最後のTuple引数に内部Tupleオブジェクトを作成します。

1
kota

あなたに役立つかもしれない別のパターンがあります。 Getの場合ですが、Post/Putにも同じ原則とコードが適用されますが、逆になります。基本的に、オブジェクトをこのObjectWrapperクラスに変換し、Typeの名前を反対側に保持するという原則に基づいて動作します。

using Newtonsoft.Json;
using System;
using System.Collections.Generic;

namespace WebAPI
{
    public class ObjectWrapper
    {
        #region Public Properties
        public string RecordJson { get; set; }
        public string TypeFullName { get; set; }
        #endregion

        #region Constructors

        public ObjectWrapper() : this(null, null)
        {
        }

        public ObjectWrapper(object objectForWrapping) : this(objectForWrapping, null)
        {
        }

        public ObjectWrapper(object objectForWrapping, string typeFullName)
        {
            if (typeFullName == null && objectForWrapping != null)
            {
                TypeFullName = objectForWrapping.GetType().FullName;
            }
            else
            {
                TypeFullName = typeFullName;
            }

            RecordJson = JsonConvert.SerializeObject(objectForWrapping);
        }
        #endregion

        #region Public Methods
        public object ToObject()
        {
            var type = Type.GetType(TypeFullName);
            return JsonConvert.DeserializeObject(RecordJson, type);
        }
        #endregion

        #region Public Static Methods
        public static List<ObjectWrapper> WrapObjects(List<object> records)
        {
            var retVal = new List<ObjectWrapper>();
            records.ForEach
            (item =>
            {
                retVal.Add
                (
                    new ObjectWrapper(item)
                );
            }
            );

            return retVal;
        }

        public static List<object> UnwrapObjects(IEnumerable<ObjectWrapper> objectWrappers)
        {
            var retVal = new List<object>();

            foreach(var item in objectWrappers)
            {
                retVal.Add
                (
                    item.ToObject()
                );
            }

            return retVal;
        }
        #endregion
    }
}

RESTコード内:

[HttpGet]
public IEnumerable<ObjectWrapper> Get()
{
    var records = new List<object>();
    records.Add(new TestRecord1());
    records.Add(new TestRecord2());
    var wrappedObjects = ObjectWrapper.WrapObjects(records);
    return wrappedObjects;
}

これは、RESTクライアントライブラリを使用するクライアント側(UWP)のコードです。クライアントライブラリは、Newtonsoft Jsonシリアル化ライブラリを使用しているだけです。

private static async Task<List<object>> Getobjects()
{
    var result = await REST.Get<List<ObjectWrapper>>("http://localhost:50623/api/values");
    var wrappedObjects = (IEnumerable<ObjectWrapper>) result.Data;
    var unwrappedObjects =  ObjectWrapper.UnwrapObjects(wrappedObjects);
    return unwrappedObjects;
}
0

遅い答えですが、オブジェクトが共通のプロパティ名を共有しない限り、1つのJSON文字列から複数のオブジェクトをデシリアライズできるという事実を利用できます。

    public async Task<HttpResponseMessage> Post(HttpRequestMessage request)
    {
        var jsonString = await request.Content.ReadAsStringAsync();
        var content  = JsonConvert.DeserializeObject<Content >(jsonString);
        var config  = JsonConvert.DeserializeObject<Config>(jsonString);
    }
0
Rob Sedgwick

ここで、 JObject を使用してjqueryからWEB APIに複数の汎用オブジェクトを(jsonとして)渡してから、APIコントローラーで必要な特定のオブジェクトタイプにキャストする回避策を見つけました。このオブジェクトは、JSONを操作するために特別に設計された具象型を提供します。

var combinedObj = {}; 
combinedObj["obj1"] = [your json object 1]; 
combinedObj["obj2"] = [your json object 2];

$http({
       method: 'POST',
       url: 'api/PostGenericObjects/',
       data: JSON.stringify(combinedObj)
    }).then(function successCallback(response) {
         // this callback will be called asynchronously
         // when the response is available
         alert("Saved Successfully !!!");
    }, function errorCallback(response) {
         // called asynchronously if an error occurs
         // or server returns response with an error status.
         alert("Error : " + response.data.ExceptionMessage);
});

そして、あなたはあなたのコントローラーでこのオブジェクトを得ることができます

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public [OBJECT] PostGenericObjects(object obj)
    {
        string[] str = GeneralMethods.UnWrapObjects(obj);
        var item1 = JsonConvert.DeserializeObject<ObjectType1>(str[0]);
        var item2 = JsonConvert.DeserializeObject<ObjectType2>(str[1]);

        return *something*;
    } 

複雑なオブジェクトをアンラップする汎用関数を作成したので、送信およびアンラップ中にオブジェクトの数に制限はありません。 3つ以上のオブジェクトを送信することもできます

public class GeneralMethods
{
    public static string[] UnWrapObjects(object obj)
    {
        JObject o = JObject.Parse(obj.ToString());

        string[] str = new string[o.Count];

        for (int i = 0; i < o.Count; i++)
        {
            string var = "obj" + (i + 1).ToString();
            str[i] = o[var].ToString(); 
        }

        return str;
    }

}

ブログにソリューションを投稿しました。簡単なコードで簡単に統合できるようにもう少し説明します。

複数の複雑なオブジェクトをWeb APIに渡す

私はそれが誰かを助けることを願っています。この方法論を使用することの長所と短所について、ここの専門家から聞いてみたいと思います。

0
Sheikh M. Haris

基本的に、特別なことをすることなく、複雑なオブジェクトを送信できます。または、Web-Apiを変更せずに。つまり、Web-Apiを呼び出すコードに問題があるのに、なぜWeb-Apiを変更する必要があるのでしょうか。

必要なことは、NewtonSoftのJsonライブラリを次のように使用することだけです。

string jsonObjectA = JsonConvert.SerializeObject(objectA);
string jsonObjectB = JsonConvert.SerializeObject(objectB);
string jSoNToPost = string.Format("\"content\": {0},\"config\":\"{1}\"",jsonObjectA , jsonObjectB );
//wrap it around in object container notation
jSoNToPost = string.Concat("{", jSoNToPost , "}"); 
//convert it to JSON acceptible content
HttpContent content = new StringContent(jSoNToPost , Encoding.UTF8, "application/json"); 

var response = httpClient.PutAsync("api/process/StartProcessiong", content);
0
Manzar Zafar