【问题标题】:How to prevent under-posting in ASP.NET Web API OData service?如何防止在 ASP.NET Web API OData 服务中发布不足?
【发布时间】:2016-01-20 15:10:58
【问题描述】:

我创建了一个非常简单的 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 content, Type type, IEnumerable'1 formatters, IFormatterLogger formatterLogger, CancellationToken cancelToken)

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

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

那么,在使用 OData 控制器时,有什么方法可以防止发布不足?

【问题讨论】:

  • 在这种情况下,您可以使用 RangeAttribute 并将其指定为 1 到 999。然后 ModelState.IsValid 应捕获值 0 不在范围内并返回 BadRequest 状态。另一种选择是创建一个自定义过滤器并在传入的 JSON 映射到模型之前手动解析它,但这似乎有点矫枉过正。
  • @Igor 我已经使用第二种方法解决了这个问题,因为需要区分默认值和null 值的通用解决方案。有兴趣的可以看看答案。感谢您的帮助!

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


【解决方案1】:

经过一番调查,我已经解决了这个问题。不确定这是否是解决 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 */ }

瞧!现在你已经准备好了一切。低估验证工作正常。

【讨论】:

    【解决方案2】:

    在我看来,你有几个选择:

    首先在您的控制器中,您可以检查整数值,如果它低于某个值,则返回 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; }
    

    其中 Age 最多可以是 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

    【讨论】:

    • 不幸的是,这种方法并不能解决区分 0not set 值的真正问题。
    • 那么将 [Required] 属性添加到 Age 属性会有什么危害?是否存在您希望 Age 不存在的情况?
    • Age 属性是int 类型,它是值类型,而不是引用类型。反序列化后永远不会设置为null。使用 Required 属性时,如果属性为 null、包含空字符串 ("") 或仅包含空白字符,则会引发验证异常。所以这个属性在这种情况下是没有用的。
    • 明白了。我刚刚对我的答案进行了编辑。尝试添加 typeof。
    • 我已经使用自定义过滤器解决了这个问题,因为需要通用解决方案来区分默认值和null 值。有兴趣的可以看看答案。感谢您的想法!
    猜你喜欢
    • 2010-12-19
    • 2011-04-02
    • 2011-06-19
    • 1970-01-01
    • 2012-12-28
    • 2016-01-16
    • 1970-01-01
    • 1970-01-01
    • 2016-03-17
    相关资源
    最近更新 更多