如何防止ASP.NET Web API OData服务中的欠发布?

时间:2016-01-20 15:10:59

标签: asp.net .net asp.net-web-api odata asp.net-web-api-odata

我创建了一个非常简单的OData v4控制器。控制器基本上包含以下Pet实体的实体框架支持的CRUD方法:

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状态。此问题称为欠发布。

使用常规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...
}

然后在您的控制器中,您只需将PetViewModel转换为Pet实体,并照常将其保存到数据库中。

不幸的是,这种方法不适用于OData控制器:如果我将Post方法更改为接受PetViewModel而不是Pet,则会收到以下错误:

  

System.Net.Http.UnsupportedMediaTypeException:没有MediaTypeFormatter可用于从媒体类型为“application / json”的内容中读取“PetViewModel”类型的对象。

     

at System.Net.Http.HttpContentExtensions.ReadAsAsync [T](HttpContent内容,类型类型,IEnumerable'1格式化程序,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控制器时有什么方法可以防止欠发布吗?

2 个答案:

答案 0 :(得分:1)

经过一番调查后,我解决了这个问题。不确定它是否是&#34;官方&#34;或者在OData中解决欠载问题的首选方法,但至少它对我来说很好。所以,由于缺乏官方信息,这是我的食谱:

首先,为您的OData实体创建相应的验证ViewModel

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

瞧!现在你已经掌握了一切。底层验证工作正常。

答案 1 :(得分:0)

我看到你有几个选择:

首先在您的控制器中,您可以检查整数值,如果它低于某个值,则返回404。

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

这可能是劳动密集型的,如果您为每种控制器方法执行此操作,它就不会很干。

在Pet类中,您可以使用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;
}

}

然后在你的Pet类中添加:

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

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