2016-01-20 57 views
2

我創建了一個非常簡單的OData v4控制器。該控制器主要包含以下Pet實體實體框架支持的CRUD方法:如何防止在ASP.NET Web API OData服務中發佈底稿?

public class Pet 
{ 
    public int Id { get; set; } 

    [Required] 
    public string Name { get; set; } 

    public int Age { get; set; } 
} 

一個重要位置的事情是,Pet.Age就是非空的必需的屬性。

這裏是控制器本身(只顯示Post法):

public class PetController : ODataController 
{ 
    private DatabaseContext db = new DatabaseContext(); 

    // POST: odata/Pet 
    public IHttpActionResult Post(Pet pet) 
    { 
     if (!ModelState.IsValid) 
     { 
      return BadRequest(ModelState); 
     } 

     db.Pet.Add(pet); 
     db.SaveChanges(); 

     return Created(pet); 
    } 

    // Other controller methods go here... 
} 

這是我WebApiConfig控制器配置:

ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); 
builder.EntitySet<Pet>("Pet"); 
config.MapODataServiceRoute("odata", "odata", builder.GetEdmModel()); 

現在,如果我想創建一個新的Pet我數據庫,我發出POST這樣的請求:

POST http://localhost:8080/odata/Pet 
Content-type: application/json 

{ Name: "Cat", Age: 5 } 

但是,我可以簡單地忽略JSON請求負載中的Age屬性,因此JSON解串器將使用默認值0,而我希望返回400 Bad Request狀態。這個問題被稱爲under-posting。

使用常規的WebApi控制器可以輕鬆解決該問題(解決方案描述爲here)。你只需要創建一個PetViewModel,讓你的控制器接受PetViewModel而不是實際Pet實體:

public class PetViewModel 
{ 
    // Make the property nullable and set the Required attribute 
    // to distinguish between "zero" and "not set" 
    [Required] 
    public int? Age { get; set; } 

    // Other properties go here... 
} 

然後在您的控制器,你只是轉換PetViewModelPet實體,並將其保存到數據庫如常。

可惜,這種做法不符合的OData控制器的工作:如果我改變Post方法接受PetViewModel,而不是Pet,我收到以下錯誤:

System.Net.Http.UnsupportedMediaTypeException: No MediaTypeFormatter is available to read an object of type 'PetViewModel' from content with media type 'application/json'.

at System.Net.Http.HttpContentExtensions.ReadAsAsync[T](HttpContent content, Type type, IEnumerable'1 formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)

at System.Net.Http.HttpContentExtensions.ReadAsAsync(HttpContent content, Type type, IEnumerable'1 formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)

at System.Web.Http.ModelBinding.FormatterParameterBinding.ReadContentAsync(HttpRequestMessage request, Type type, IEnumerable`1 formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)

那麼,有沒有什麼辦法來防止下在使用OData控制器時發貼?

+1

在這種情況下,您可以使用RangeAttribute並在1到999之間指定它。然後'ModelState.IsValid'應該捕獲0值不在範圍內並返回BadRequest'狀態。另一種選擇是創建一個自定義過濾器並在映射到模型之前手動解析傳入的JSON,但這看起來像是矯枉過正。 – Igor

+0

@Igor我已經使用第二種方法解決了問題,因爲需要區分默認值和空值的通用解決方案。如果您有興趣,請查看答案。感謝您的幫助! –

回答

1

經過一番調查,我已經解決了這個問題。不知道它是否是一種「官方」或解決OData中未解決問題的首選方式,但至少對我來說它工作正常。因此,對於缺乏官方信息,這裏是我的食譜:

首先,創建相應的驗證ViewModel您OData的實體:

public class PetViewModel 
{ 
    public int Id { get; set; } 

    [Required] 
    [StringLength(50)] 
    public string Name { get; set; } 

    // Make the property nullable and set the Required attribute 
    // to distinguish between "zero" and "not set" 
    [Required] 
    public new int? Age { get; set; } 
} 

然後,添加自己的ODataUnderpostingValidationAttribute。我的實現看起來是這樣的:

public class ODataUnderpostingValidationAttribute: ActionFilterAttribute 
{ 
    public ODataUnderpostingValidationAttribute(Type viewModelType) 
    { 
     ViewModelType = viewModelType; 
    } 

    public Type ViewModelType { get; set; } 

    public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) 
    { 
     // Rewind requestStream so it can be read again. 
     var requestStream = await actionContext.Request.Content.ReadAsStreamAsync(); 
     if (requestStream.CanSeek) 
     { 
      requestStream.Position = 0; 
     } 

     // Read the actual JSON payload. 
     var json = await actionContext.Request.Content.ReadAsStringAsync(); 

     // Deserialize JSON to corresponding validation ViewModel. 
     var viewModel = JsonConvert.DeserializeObject(json, ViewModelType); 
     var context = new ValidationContext(viewModel); 
     var results = new List<ValidationResult>(); 
     var isValid = Validator.TryValidateObject(viewModel, context, results); 

     if (!isValid) 
     { 
      // Throw HttpResponseException instead of setting actionContext.Response, so the exception will be logged by the ExceptionLogger. 
      var responseMessage = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage))); 
      throw new HttpResponseException(responseMessage); 
     } 

     await base.OnActionExecutingAsync(actionContext, cancellationToken); 
    } 
} 

之後,將此自定義過濾器您ODataController

[ODataUnderpostingValidation(typeof(PetViewModel))] 
public class PetController : ODataController 
{ /* Implementation here */ } 

瞧!現在你擁有了一切。後置驗證工作正常。

0

你有一對夫婦的選擇,因爲我看到它:

首先在你的控制器,你可以檢查整數值,如果它一定的價值迴歸低於404

if (Age <= 0) 
    return NotFound(); 

這可能是勞動密集型,如果你爲每種控制器方法做這件事,它不是很乾。

第二次在您的寵物類中,您可以使用DataAnnotations屬性範圍,例如

[Range(0, 80, ErrorMessage = "Value for {0} must be between {1} and {2}")] 
public int Age { get; set; } 

哪裏年齡可以是最多80歲。 https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.rangeattribute(v=vs.110).aspx

最後,我想你了,你一個更永久的解決方案是創建自己的驗證:

public class AgeValidation : ValidationAttribute { 
public override bool IsValid(object value) { 
    if (Object.Equals(value, null)) { 
     return false; 
    } 
    int getage; 
    if (int.TryParse(value.ToString(), out getage)) { 

     if (getage == 0) 
      return false; 

     if (getage > 0) 
      return true; 
    } 
    return false; 
} 

}

然後在你的寵物類添加:

[AgeValidation(ErrorMessage = "Age is wack")] 
public int Age { get; set; } 

借來自How to do Integer model validation in asp.net mvc 2

+0

不幸的是,這種方法並沒有解決區分_0_和_not set_值的真正問題。 –

+0

那麼將Age屬性添加到[Age]屬性會有什麼危害?有沒有這種情況,當你想要年齡不存在? –

+0

'Age'屬性的類型爲'int',它是值類型,而不是引用類型。在反序列化之後它永遠不會被設置爲'null'。使用'Required'屬性時,如果屬性爲空,包含空字符串(「」)或僅包含空格字符,則會引發驗證異常。所以這個屬性在這種情況下是無用的。 –