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

时间:2016-04-27 14:34:25

标签: c# json asp.net-web-api model-binding

我正在尝试在web api 2中使用FromUri和FromBody来填充传入的请求模型。我知道我需要编写一个自定义模型绑定器来做到这一点。这是the example everyone references。此解决方案已合并到WebAPIContrib nuGet pacakge中,其源代码为can be seen here on github

我无法使用MvcActionValueBinder来处理application / json正文内容。以下是抛出异常的源代码的一部分。

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);
    }
}

此行抛出异常:

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

以下是例外:

System.AggregateException

  

{“无法反序列化当前的JSON对象(例如   {\“name \”:\“value \”})进入类型   'System.Net.Http.Formatting.FormDataCollection'因为类型   需要一个JSON数组(例如[1,2,3])才能正确反序列化。\ r \ nTo   修复此错误要么将JSON更改为JSON数组(例如[1,2,3])   或更改反序列化类型,使其成为正常的.NET类型(例如   不是像整数这样的原始类型,不是像数组那样的集合类型   或者List)可以从JSON对象反序列化。   JsonObjectAttribute也可以添加到类型中以强制它   从JSON对象反序列化。\ r \ n“'creditLimit',第2行,   第17位。“}

如何让模型绑定器使用applciation / json内容而不是x-www-form-urlencoded内容?这是asp.net论坛上的similar question with no answer

更新 这是控制器方法:

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

这是RequestObject:

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

这是要测试的邮递员端点,它是一个PUT:

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

我设置为application / json的正文。这是示例内容。

{ "creditLimit": 125000.00 }

是的,我知道我可以改变控制器方法来代替所有FromUri或所有FromBody。我没有这样做的自由。感谢。

2 个答案:

答案 0 :(得分:1)

我遇到了同样的问题,我想我终于明白了。

这是代码:

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 
        }
    }
}

要说明,技巧是首先以string的形式读取请求内容,然后将其加载到JObject中。 对于actionContext.ActionDescriptor中存在的每个参数,将使用参数名称作为键填充字典,然后使用参数类型添加对象值。

根据参数类型,我们可以执行简单的转换,也可以使用Json.NET将值反序列化为所需的类型。 请注意,您可能需要为值类型添加特殊情况以管理枚举或Guid

在我的示例中,我绕过JsonSerializerSettings,因为我有一些要使用的自定义转换器,因为您可能不需要它。

答案 1 :(得分:0)

您应该能够使用Web API 2中的默认模型绑定功能来实现此目的。 您需要做的第一件事是将数据作为JSON字符串传递如下。

for name ... [ in word ... ] term do list done

将从URL读取accountId,Web API 2的默认JsonFormatter将尝试绑定来自正文的第二个参数请求。它将找到creditLimit并将创建一个RequestObject实例,并填充了creditLimit。

然后,您可以在控制器内部将accountId值分配给RequestObject其他属性。这样您就不需要将accountId作为请求正文的一部分传递。您只能将其作为URL端点的一部分传递。

以下链接是更深入细节的良好资源。 http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api