如何防止在 ASP.NET Web API OData 服务中发布不足?
How to prevent under-posting in ASP.NET Web API OData service?
我创建了一个非常简单的 OData v4 控制器。控制器基本上包含以下 Pet
实体的 Entity Framework 支持的 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: 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 控制器时,有什么方法可以防止发布不足吗?
我认为你有几个选择:
首先在你的控制器中你可以检查整数值,如果它低于某个值 return 404.
if (Age <= 0)
return NotFound();
这可能是劳动密集型的,如果你对每个控制器方法都这样做,那就不是很干了。
第二个在你的 Pet class 你可以使用 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;
}
}
然后在你的宠物class中添加:
[AgeValidation(ErrorMessage = "Age is wack")]
public int Age { get; set; }
借自 How to do Integer model validation in asp.net mvc 2
经过一番调查,我已经解决了这个问题。不确定它是 "official" 还是解决 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 */ }
瞧!现在一切就绪。 Underposting 验证工作正常。
我创建了一个非常简单的 OData v4 控制器。控制器基本上包含以下 Pet
实体的 Entity Framework 支持的 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: 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 控制器时,有什么方法可以防止发布不足吗?
我认为你有几个选择:
首先在你的控制器中你可以检查整数值,如果它低于某个值 return 404.
if (Age <= 0)
return NotFound();
这可能是劳动密集型的,如果你对每个控制器方法都这样做,那就不是很干了。
第二个在你的 Pet class 你可以使用 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;
}
}
然后在你的宠物class中添加:
[AgeValidation(ErrorMessage = "Age is wack")]
public int Age { get; set; }
借自 How to do Integer model validation in asp.net mvc 2
经过一番调查,我已经解决了这个问题。不确定它是 "official" 还是解决 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 */ }
瞧!现在一切就绪。 Underposting 验证工作正常。