简体   繁体   English

Web Api 2 中的 Mvc 风格参数绑定?

[英]Mvc-style parameter binding in Web Api 2?

I am trying to use both FromUri and FromBody in web api 2 to populate an incoming request model.我试图在 web api 2 中同时使用 FromUri 和 FromBody 来填充传入的请求模型。 I understand I need to write a custom model binder to do that.我知道我需要编写一个自定义模型绑定器来做到这一点。 Here is the example everyone references .下面是大家参考的例子 This solution has been incorporated into the WebAPIContrib nuGet pacakge whose source code can be seen here on github .此解决方案已合并到 WebAPIContrib nuGet pacakge 中,其源代码可在此处在 github 上查看

I'm having trouble getting the MvcActionValueBinder to work with application/json body content.我无法让 MvcActionValueBinder 处理应用程序/json 正文内容。 Here is part of the source that is throwing the exception.这是引发异常的源代码的一部分。

class MvcActionBinding : HttpActionBinding
{
    // Read the body upfront , add as a ValueProvider
    public override Task ExecuteBindingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        HttpRequestMessage request = actionContext.ControllerContext.Request;
        HttpContent content = request.Content;
        if (content != null)
        {
            FormDataCollection fd = content.ReadAsAsync<FormDataCollection>().Result;
            if (fd != null)
            {
                IValueProvider vp = new NameValuePairsValueProvider(fd, CultureInfo.InvariantCulture);
                request.Properties.Add(Key, vp);
            }
        }

        return base.ExecuteBindingAsync(actionContext, cancellationToken);
    }
}

This line is throwing the exception:这一行抛出异常:

FormDataCollection fd = content.ReadAsAsync<FormDataCollection>().Result;

Here is the exception:这是例外:

System.AggregateException System.AggregateException

{"Cannot deserialize the current JSON object (eg {\\"name\\":\\"value\\"}) into type 'System.Net.Http.Formatting.FormDataCollection' because the type requires a JSON array (eg [1,2,3]) to deserialize correctly.\\r\\nTo fix this error either change the JSON to a JSON array (eg [1,2,3]) or change the deserialized type so that it is a normal .NET type (eg not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.\\r\\nPath 'creditLimit', line 2, position 17."} {"无法将当前 JSON 对象(例如 {\\"name\\":\\"value\\"})反序列化为类型“System.Net.Http.Formatting.FormDataCollection”,因为该类型需要一个 JSON 数组(例如 [1,2 ,3]) 以正确反序列化。\\r\\n要修复此错误,请将 JSON 更改为 JSON 数组(例如 [1,2,3])或更改反序列化类型,使其成为正常的 .NET 类型(例如不是可以从 JSON 对象反序列化的原始类型(如整数,而不是像数组或列表这样的集合类型)。也可以将 JsonObjectAttribute 添加到该类型以强制其从 JSON 对象反序列化。\\r\\nPath 'creditLimit' ,第 2 行,位置 17。"}

How can I get the model binder to work with applciation/json content instead of x-www-form-urlencoded content?如何让模型绑定器使用 applciation/json 内容而不是 x-www-form-urlencoded 内容? Here is a similar question with no answer on the asp.net forums.这是一个类似的问题,在 asp.net 论坛上没有答案

Update: Here is the controller method:更新:这是控制器方法:

[Route("{accountId:int}/creditlimit")]
[HttpPut]
public async Task<IHttpActionResult> UpdateAccountCreditLimit(int accountId, [FromBody] RequestObject request)
{
     // omitted for brevity
}

Here is the RequestObject:这是请求对象:

class RequestObject
{
    public int AccountId { get; set; }
    public decimal CreditLimit { get; set; }
}

Here is the postman endpoint to test, its a PUT:这是要测试的邮递员端点,它是一个 PUT:

http://localhost/api/accounts/47358/creditlimit

The body I have set to application/json.我已设置为 application/json 的主体。 Here is sample content.这是示例内容。

{ "creditLimit": 125000.00 }

And yes, I realize I could change the controller method to do all FromUri or all FromBody instead.是的,我意识到我可以更改控制器方法以执行所有 FromUri 或所有 FromBody。 I do not have the liberty of doing that.我没有这样做的自由。 Thanks.谢谢。

I had the same issue and I think I finally figure this out.我有同样的问题,我想我终于弄清楚了。

Here is the code :这是代码:

internal sealed class MvcActionValueBinder : DefaultActionValueBinder
{
    private static readonly Type stringType = typeof(string);

    // Per-request storage, uses the Request.Properties bag. We need a unique key into the bag.
    private const string Key = "5DC187FB-BFA0-462A-AB93-9E8036871EC8";

    private readonly JsonSerializerSettings serializerSettings;

    public MvcActionValueBinder(JsonSerializerSettings serializerSettings)
    {
        this.serializerSettings = serializerSettings;
    }

    public override HttpActionBinding GetBinding(HttpActionDescriptor actionDescriptor)
    {
        var actionBinding = new MvcActionBinding(serializerSettings);

        HttpParameterDescriptor[] parameters = actionDescriptor.GetParameters().ToArray();
        HttpParameterBinding[] binders = Array.ConvertAll(parameters, DetermineBinding);

        actionBinding.ParameterBindings = binders;

        return actionBinding;
    }

    private HttpParameterBinding DetermineBinding(HttpParameterDescriptor parameter)
    {
        HttpConfiguration config = parameter.Configuration;
        var attr = new ModelBinderAttribute(); // use default settings

        ModelBinderProvider provider = attr.GetModelBinderProvider(config);
        IModelBinder binder = provider.GetBinder(config, parameter.ParameterType);

        // Alternatively, we could put this ValueProviderFactory in the global config.
        var valueProviderFactories = new List<ValueProviderFactory>(attr.GetValueProviderFactories(config)) { new BodyValueProviderFactory() };
        return new ModelBinderParameterBinding(parameter, binder, valueProviderFactories);
    }

    // Derive from ActionBinding so that we have a chance to read the body once and then share that with all the parameters.
    private class MvcActionBinding : HttpActionBinding
    {
        private readonly JsonSerializerSettings serializerSettings;

        public MvcActionBinding(JsonSerializerSettings serializerSettings)
        {
            this.serializerSettings = serializerSettings;
        }

        // Read the body upfront, add as a ValueProvider
        public override Task ExecuteBindingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            HttpRequestMessage request = actionContext.ControllerContext.Request;
            HttpContent content = request.Content;
            if (content != null)
            {
                string result = request.Content.ReadAsStringAsync().Result;

                if (!string.IsNullOrEmpty(result))
                {
                    var jsonContent = JObject.Parse(result);
                    var values = new Dictionary<string, object>();

                    foreach (HttpParameterDescriptor parameterDescriptor in actionContext.ActionDescriptor.GetParameters())
                    {
                        object parameterValue = GetParameterValue(jsonContent, parameterDescriptor);
                        values.Add(parameterDescriptor.ParameterName, parameterValue);
                    }

                    IValueProvider valueProvider = new NameValuePairsValueProvider(values, CultureInfo.InvariantCulture);
                    request.Properties.Add(Key, valueProvider);
                }
            }

            return base.ExecuteBindingAsync(actionContext, cancellationToken);
        }

        private object GetParameterValue(JObject jsonContent, HttpParameterDescriptor parameterDescriptor)
        {
            string propertyValue = jsonContent.Property(parameterDescriptor.ParameterName)?.Value.ToString();

            if (IsSimpleParameter(parameterDescriptor))
            {
                // No deserialization needed for value type, a cast is enough
                return Convert.ChangeType(propertyValue, parameterDescriptor.ParameterType);
            }

            return JsonConvert.DeserializeObject(propertyValue, parameterDescriptor.ParameterType, serializerSettings);
        }

        private bool IsSimpleParameter(HttpParameterDescriptor parameterDescriptor)
        {
            return parameterDescriptor.ParameterType.IsValueType || parameterDescriptor.ParameterType == stringType;
        }
    }

    // Get a value provider over the body. This can be shared by all parameters.
    // This gets the values computed in MvcActionBinding.
    private class BodyValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(HttpActionContext actionContext)
        {
            actionContext.Request.Properties.TryGetValue(Key, out object vp);
            return (IValueProvider)vp; // can be null 
        }
    }
}

To explain, the trick is to first read the request content as a string and then loading it into a JObject .解释一下,诀窍是首先将请求内容作为string读取,然后将其加载到JObject For each parameter present in actionContext.ActionDescriptor a dictionnary is populated with the parameter name as key and we use the parameter type to add the object value.对于actionContext.ActionDescriptor存在的每个参数,都会使用参数名称作为键填充字典,我们使用参数类型添加对象值。

Depending on the parameter type we either do a simple cast or use Json.NET to deserialize the value into the desired type.根据参数类型,我们要么进行简单的转换,要么使用 Json.NET 将值反序列化为所需的类型。 Please note you may need to add special case for value type to manage for example enumerations or Guid .请注意,您可能需要为值类型添加特殊情况以管理例如枚举或Guid

In my example, I pass around a JsonSerializerSettings because I have some custom converters that I want to use, be you may not need it.在我的示例中,我传递了一个JsonSerializerSettings因为我有一些想要使用的自定义转换器,但您可能不需要它。

You should be able to achieve this with the default model binding functionality in Web API 2 itself.您应该能够使用 Web API 2 本身中的默认模型绑定功能来实现这一点。 First thing you need to do is pass the data as JSON string as following.您需要做的第一件事是将数据作为 JSON 字符串传递,如下所示。

data: JSON.stringify({ "creditLimit": 125000.00 })

The accountId will be read from the URL and the default JsonFormatter of the Web API 2 will try to bind your second parameter request from the body. accountId 将从 URL 中读取,Web API 2 的默认 JsonFormatter 将尝试绑定来自正文的第二个参数请求。 It will find the creditLimit and will create an instance of RequestObject with the creditLimit populated.它将找到 creditLimit 并创建一个 RequestObject 的实例,其中填充了 creditLimit。

You can then, inside the controller, assign the accountId value to the RequestObject other property.然后,您可以在控制器内部将 accountId 值分配给 RequestObject 其他属性。 That way you don't need to pass the accountId as part of your request body.这样您就不需要将 accountId 作为请求正文的一部分传递。 You only pass that as part of your URL endpoint.您仅将其作为 URL 端点的一部分传递。

The following link is a good resource for more in-depth detail.以下链接是更深入细节的好资源。 http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM