2013-07-15 47 views
35

是否可以綁定來自Uri和Body的模型?WebApi - 從Uri和Body綁定

例如,給出以下:

routes.MapHttpRoute(
    name: "API Default", 
    routeTemplate: "api/{controller}/{id}", 
    defaults: new { id = RouteParameter.Optional } 
); 

public class ProductsController : ApiController 
{ 
    public HttpResponseMessage Put(UpdateProduct model) 
    { 

    } 
} 

public class UpdateProduct 
{ 
    int Id { get; set;} 
    string Name { get; set; } 
} 

是否有可能創建一個自定義的粘合劑,使得PUT

/API /產品/ 1

與一個JSON體:

{ 
    "Name": "Product Name" 
} 

將導致UpdateProduct模型填充Id = 1Name = "Product Name"

更新

我知道我可以在行動簽名更改爲

public HttpResponseMessage Put(int id, UpdateProduct model) 
{ 

} 

但是在這個問題說,我特別希望綁定到單個模型對象

我也發佈了這個問題WebApi Codeplex discussion forum

+1

如果您從'UpdateProduct'的'Id'和它到你的動作簽名:'公共HttpResponseMessage把(INT ID,UpdateProduct中模型)'這也將工作沒有任何自定義模型粘合劑。 – nemesv

+1

看看這篇文章看起來這是你需要的:http://blogs.msdn.com/b/jmstall/archive/2012/04/18/mvc-style-parameter-binding-for-webapi.aspx – nemesv

+1

Did你有沒有找到解決這個問題的方法?我有同樣的問題。在我看來,這是一個非常愚蠢和不直觀的行爲。 –

回答

0

如果我理解你,這個應該開箱即用,例如這個工作對我來說:

[HttpPost] 
    public ActionResult Test(TempModel model) 
    { 
     ViewBag.Message = "Test: " + model.Id +", " + model.Name; 

     return View("About"); 
    } 

public class TempModel 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
} 

routes.MapRoute(
      name: "Default", 
      url: "{controller}/{action}/{id}", 
      defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 
     ); 

和要求:本地主機:56329 /首頁/測試/ 22 與正文:{ 「名稱」: 「工具」}

我已經相應地設置我的模型的屬性到22和「工具」。

+1

Felix - 我相信它將適用於MVC,但我試圖讓它爲WebApi項目工作 – kimsagro

+0

正確,我的壞,沒有使用網絡API,所以不能真正說,我相信你已經看到這個http://blogs.msdn.com/b/jmstall/archive/2012/04/16/how-webapi-does-parameter-binding.aspx。據我瞭解,您需要創建自定義格式化程序來實現此 – Felix

5

好吧,我想出了一個辦法。基本上,我製作了一個動作過濾器,它將在模型從JSON填充後運行。然後它將查看URL參數,並在模型上設置適當的屬性。全部源代碼如下:

using System.ComponentModel; 
using System.Linq; 
using System.Net; 
using System.Net.Http; 
using System.Reflection; 
using System.Web.Http.Controllers; 
using System.Web.Http.Filters; 


public class UrlPopulatorFilterAttribute : ActionFilterAttribute 
{ 
    public override void OnActionExecuting(HttpActionContext actionContext) 
    { 
     var model = actionContext.ActionArguments.Values.FirstOrDefault(); 
     if (model == null) return; 
     var modelType = model.GetType(); 
     var routeParams = actionContext.ControllerContext.RouteData.Values; 

     foreach (var key in routeParams.Keys.Where(k => k != "controller")) 
     { 
      var prop = modelType.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); 
      if (prop != null) 
      { 
       var descriptor = TypeDescriptor.GetConverter(prop.PropertyType); 
       if (descriptor.CanConvertFrom(typeof(string))) 
       { 
        prop.SetValueFast(model, descriptor.ConvertFromString(routeParams[key] as string)); 
       } 
      } 
     } 
    } 
} 
+2

如果您有其他ActionFilterAttributes依賴於模型被完全綁定此解決方案將無法正常工作,因爲ActionFilterAttributes沒有保證的操作順序。 – odyth

+1

什麼是SetValueFast? – Sam

7

你可以定義你自己的DefaultActionValueBinder。然後你可以從身體和uri混合搭配。這裏是一個博客帖子,裏面有一個Web API的MvcActionValueBinder示例。使您自己的DefaultActionValueBinder成爲首選的解決方案,因爲它確保在執行任何其他ActionFilterAttribute之前,活頁夾將完成。

http://blogs.msdn.com/b/jmstall/archive/2012/04/18/mvc-style-parameter-binding-for-webapi.aspx

UPDATE:

我遇到了一些麻煩,在博客文章的實施,並試圖得到它使用我的自定義介質格式化。幸運的是,我的所有請求對象都是從基類中擴展的,所以我創建了自己的格式化程序。

在WebApiConfig

config.ParameterBindingRules.Insert(0, descriptor => descriptor.ParameterType.IsSubclassOf(typeof (Request)) ? new BodyAndUriParameterBinding(descriptor) : null); 

BodyAndUriParameterBinding。CS

public class BodyAndUriParameterBinding : HttpParameterBinding 
{ 
    private IEnumerable<MediaTypeFormatter> Formatters { get; set; } 
    private IBodyModelValidator BodyModelValidator { get; set; } 
    public BodyAndUriParameterBinding(HttpParameterDescriptor descriptor) 
     : base (descriptor) 
    { 
     var httpConfiguration = descriptor.Configuration; 
     Formatters = httpConfiguration.Formatters; 
     BodyModelValidator = httpConfiguration.Services.GetBodyModelValidator(); 
    } 

    private Task<object> ReadContentAsync(HttpRequestMessage request, Type type, 
     IEnumerable<MediaTypeFormatter> formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken) 
    { 
     var content = request.Content; 
     if (content == null) 
     { 
      var defaultValue = MediaTypeFormatter.GetDefaultValueForType(type); 
      return defaultValue == null ? Task.FromResult<object>(null) : Task.FromResult(defaultValue); 
     } 

     return content.ReadAsAsync(type, formatters, formatterLogger, cancellationToken); 
    } 

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, 
     CancellationToken cancellationToken) 
    { 
     var paramFromBody = Descriptor; 
     var type = paramFromBody.ParameterType; 
     var request = actionContext.ControllerContext.Request; 
     var formatterLogger = new ModelStateFormatterLogger(actionContext.ModelState, paramFromBody.ParameterName); 
     return ExecuteBindingAsyncCore(metadataProvider, actionContext, paramFromBody, type, request, formatterLogger, cancellationToken); 
    } 

    // Perf-sensitive - keeping the async method as small as possible 
    private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, 
     HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request, IFormatterLogger formatterLogger, 
     CancellationToken cancellationToken) 
    { 
     var model = await ReadContentAsync(request, type, Formatters, formatterLogger, cancellationToken); 

     if (model != null) 
     { 
      var routeParams = actionContext.ControllerContext.RouteData.Values; 
      foreach (var key in routeParams.Keys.Where(k => k != "controller")) 
      { 
       var prop = type.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); 
       if (prop == null) 
       { 
        continue; 
       } 
       var descriptor = TypeDescriptor.GetConverter(prop.PropertyType); 
       if (descriptor.CanConvertFrom(typeof(string))) 
       { 
        prop.SetValue(model, descriptor.ConvertFromString(routeParams[key] as string)); 
       } 
      } 
     } 

     // Set the merged model in the context 
     SetValue(actionContext, model); 

     if (BodyModelValidator != null) 
     { 
      BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName); 
     } 
    } 
} 
11

這裏的odyth的答案的改進版本:

  1. Works的脫胎請求也和
  2. 除了從路由值獲取參數從查詢字符串。

爲了簡便起見,我只是發佈了ExecuteBindingAsyncCore方法和一個新的輔助方法,其餘的類都是一樣的。

private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, 
     HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request, IFormatterLogger formatterLogger, 
     CancellationToken cancellationToken) 
{ 
    var model = await ReadContentAsync(request, type, Formatters, formatterLogger, cancellationToken); 

    if(model == null) model = Activator.CreateInstance(type); 

    var routeDataValues = actionContext.ControllerContext.RouteData.Values; 
    var routeParams = routeDataValues.Except(routeDataValues.Where(v => v.Key == "controller")); 
    var queryStringParams = new Dictionary<string, object>(QueryStringValues(request)); 
    var allUriParams = routeParams.Union(queryStringParams).ToDictionary(pair => pair.Key, pair => pair.Value); 

    foreach(var key in allUriParams.Keys) { 
     var prop = type.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); 
     if(prop == null) { 
      continue; 
     } 
     var descriptor = TypeDescriptor.GetConverter(prop.PropertyType); 
     if(descriptor.CanConvertFrom(typeof(string))) { 
      prop.SetValue(model, descriptor.ConvertFromString(allUriParams[key] as string)); 
     } 
    } 

    // Set the merged model in the context 
    SetValue(actionContext, model); 

    if(BodyModelValidator != null) { 
     BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName); 
    } 
} 

private static IDictionary<string, object> QueryStringValues(HttpRequestMessage request) 
{ 
    var queryString = string.Join(string.Empty, request.RequestUri.ToString().Split('?').Skip(1)); 
    var queryStringValues = System.Web.HttpUtility.ParseQueryString(queryString); 
    return queryStringValues.Cast<string>().ToDictionary(x => x, x => (object)queryStringValues[x]); 
} 
+2

這看起來不錯。此代碼是否執行默認的「FromBody」*和*'FromUri'屬性所做的所有操作? 另外,您是否知道ASP.NET團隊決定不包含「FromBodyAndUri」這種開箱即用的原因?我經歷了無數的博客文章,但無法真正找到爲什麼這是不好的做法... 我所有的控制器都接受'IRequest <>'([Jimmy Bogard的調解器模式](https:// github) .com/jbogard/MediatR)),它包含一個對象中的所有參數。能夠用body和uri參數填充它會很棒。 –

+0

謝謝!我使用這個與自定義'ParameterBindingAttribute'結合使用,就像這樣'public IHttpActionResult Put([FromUriAndBody] ComplexType param)' – SimonGates

+0

Konamiman - 'System.Web.HttpUtility'需要對'System.Web'的引用,它是冗餘的一個Web API項目。有人建議使用'System.Net.Http.UriExtensions.ParseQueryString()'。因此,QueryStringValues()中的代碼應該如下所示: 'var queryStringValues = request.RequestUri.ParseQueryString();'... – Tohid