ASP.NET Web API 中的参数绑定
一、针对.net core中post类型的api注意的地方(前提是Controller上加[ApiController]特性)。默认是这个。
1、如果客户端Content-Type是application/json, api接口如果是用单个对象做参数的时候,加或者不加[FromBody]都可以正常解析参数,但是接口是用对象列表做参数时候,则必须加[FromBody],否则读取不到参数。
2、如果客户端Content-Type不是application/json,api接口必须加[FromForm],否则客户端调用接口会报400错误。
3、如果加上[FromBody],客户端Content-Type不是application/json,接口会报400错误。
二、Controller上不加[ApiController]特性),当application/json类型的时,读取的参数将全为空,而非application/json却可以正常解析,无论怎么客户端不会抛出400异常。
写两段测试代码Controller上加[ApiController]特性
[HttpPost("PostList")] public void PostList([FromBody]List<Person> person) { List<Person> personObj = person; } [HttpPost("PostSingle")] public void PostSingle(Person person) { Person personObj = person; }
综合上述,客户端如果是提交json数据时候建议都加上[FromBody]。
如果客户端提交的数据Content-Type如果不为application/json时,会报错,如果要解决报错,需要在接口上加上[FromForm]。
请考虑使用 ASP.NET Core Web API。 与 ASP.NET 4.x Web API 的比,它具有以下优势:
- ASP.NET Core是一个开源的跨平台框架,用于在 Windows、macOS 和 Linux 上构建基于云的新式 Web 应用。
- ASP.NET Core MVC 控制器和 Web API 控制器是统一的。
- 针对可测试性进行构建。
- 能够在 Windows、macOS 和 Linux 上进行开发和运行。
- 开源和关注社区。
- 新式客户端框架和开发工作流的集成。
- 一个云就绪、基于环境的配置系统。
- 内置依赖项注入。
- 轻型高性能模块化 HTTP 请求管道。
- 能够在 Kestrel、 IIS、 HTTP.sys、 Nginx、 Apache 和 Docker 上托管。
- 并行版本。
- 简化新式 Web 开发的工具。
本文介绍 Web API 如何绑定参数,以及如何自定义绑定过程。 当 Web API 在控制器上调用方法时,它必须为参数(称为 绑定的进程)设置值。
默认情况下,Web API 使用以下规则来绑定参数:
- 如果参数是“简单”类型,Web API 会尝试从 URI 获取值。 简单类型包括 .NET 基元类型 (int、 bool、 double 等) ,以及 TimeSpan、 DateTime、 Guid、 decimal 和 string, 以及 具有可从字符串转换的类型转换器的任何类型。 稍后 (有关类型转换器的详细信息。)
- 对于复杂类型,Web API 尝试使用 媒体类型格式化程序从消息正文读取值。
例如,下面是一个典型的 Web API 控制器方法:
HttpResponseMessage Put(int id, Product item) { ... }
id 参数是“简单”类型,因此 Web API 尝试从请求 URI 获取值。 项参数是复杂类型,因此 Web API 使用媒体类型格式化程序从请求正文中读取值。
若要从 URI 获取值,Web API 会查找路由数据和 URI 查询字符串。 路由系统分析 URI 并将其与路由匹配时,将填充路由数据。 有关详细信息,请参阅 路由和操作选择。
本文的其余部分介绍如何自定义模型绑定过程。 但是,对于复杂类型,请考虑尽可能使用媒体类型格式化程序。 HTTP 的一个关键原则是在消息正文中发送资源,使用内容协商指定资源的表示形式。 媒体类型格式化程序正是出于此目的而设计的。
使用 [FromUri]
若要强制 Web API 从 URI 读取复杂类型,请将 [FromUri] 属性添加到 参数。 以下示例定义一个 GeoPoint
类型,以及一个从 URI 获取 GeoPoint
的控制器方法。
public class GeoPoint
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
public ValuesController : ApiController
{
public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }
}
客户端可以将纬度和经度值放在查询字符串中,Web API 将使用它们来构造 GeoPoint
。 例如:
http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989
使用 [FromBody]
若要强制 Web API 从请求正文中读取简单类型,请将 [FromBody] 属性添加到 参数:
public HttpResponseMessage Post([FromBody] string name) { ... }
在此示例中,Web API 将使用媒体类型格式化程序从请求正文中读取 name 的值。 下面是一个示例客户端请求。
POST http://localhost:5076/api/values HTTP/1.1
User-Agent: Fiddler
Host: localhost:5076
Content-Type: application/json
Content-Length: 7
"Alice"
当参数具有 [FromBody]时,Web API 使用 Content-Type 标头来选择格式化程序。 在此示例中,内容类型为“application/json”,请求正文是原始 JSON 字符串 (不是 JSON 对象) 。
最多允许从消息正文读取一个参数。 因此,这不起作用:
// Caution: Will not work!
public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }
此规则的原因是请求正文可能存储在只能读取一次的非缓冲流中。
类型转换器
可以让 Web API 将类视为简单类型 (以便 Web API 通过创建 TypeConverter 并提供字符串转换来尝试从 URI) 绑定该类。
下面的代码显示了一个GeoPoint
类,该类表示地理点,以及一个从字符串转换为GeoPoint
实例的 TypeConverter。 类 GeoPoint
使用 [TypeConverter] 属性进行修饰,以指定类型转换器。 (此示例的灵感来自 Mike Stall 的博客文章 How to bind to custom objects in action signatures in MVC/WebAPI.)
[TypeConverter(typeof(GeoPointConverter))]
public class GeoPoint
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public static bool TryParse(string s, out GeoPoint result)
{
result = null;
var parts = s.Split(',');
if (parts.Length != 2)
{
return false;
}
double latitude, longitude;
if (double.TryParse(parts[0], out latitude) &&
double.TryParse(parts[1], out longitude))
{
result = new GeoPoint() { Longitude = longitude, Latitude = latitude };
return true;
}
return false;
}
}
class GeoPointConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
if (value is string)
{
GeoPoint point;
if (GeoPoint.TryParse((string)value, out point))
{
return point;
}
}
return base.ConvertFrom(context, culture, value);
}
}
现在,Web API 将被视为 GeoPoint
简单类型,这意味着它将尝试从 URI 绑定 GeoPoint
参数。 不需要在 参数中包含 [FromUri]。
public HttpResponseMessage Get(GeoPoint location) { ... }
客户端可以使用如下所示的 URI 调用 方法:
http://localhost/api/values/?location=47.678558,-122.130989
模型联编程序
比类型转换器更灵活的选项是创建自定义模型绑定器。 使用模型绑定器,可以访问 HTTP 请求、操作说明和路由数据的原始值等内容。
若要创建模型绑定器,请实现 IModelBinder 接口。 此接口定义单个方法 BindModel:
bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);
下面是对象的 GeoPoint
模型绑定器。
public class GeoPointModelBinder : IModelBinder
{
// List of known locations.
private static ConcurrentDictionary<string, GeoPoint> _locations
= new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);
static GeoPointModelBinder()
{
_locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
_locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
_locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(GeoPoint))
{
return false;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return false;
}
string key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Wrong value type");
return false;
}
GeoPoint result;
if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
{
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to GeoPoint");
return false;
}
}
模型绑定器从值提供程序获取原始输入 值。 此设计将两个不同的函数分开:
- 值提供程序接受 HTTP 请求并填充键值对的字典。
- 模型联编程序使用此字典来填充模型。
Web API 中的默认值提供程序从路由数据和查询字符串中获取值。 例如,如果 URI 为 http://localhost/api/values/1?location=48,-122
,则值提供程序将创建以下键值对:
- id = “1”
- location = “48,-122”
(我假设默认路由模板为“api/{controller}/{id}”。)
要绑定的参数的名称存储在 ModelBindingContext.ModelName 属性中 。 模型绑定器在字典中查找具有此值的键。 如果值存在并且可以转换为 GeoPoint
,则模型联编程序会将绑定值分配给 ModelBindingContext.Model 属性。
请注意,模型联编程序不限于简单类型转换。 在此示例中,模型联编程序首先查找已知位置的表中,如果失败,则使用类型转换。
设置模型绑定器
可通过多种方式设置模型联编程序。 首先,可以将 [ModelBinder] 属性添加到 参数。
public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
还可以向类型添加 [ModelBinder] 属性。 Web API 将为该类型的所有参数使用指定的模型联编程序。
[ModelBinder(typeof(GeoPointModelBinder))]
public class GeoPoint
{
// ....
}
最后,可以将 model-binder 提供程序添加到 HttpConfiguration。 模型绑定器提供程序只是创建模型绑定程序的工厂类。 可以通过从 ModelBinderProvider 类派生来创建提供程序。 但是,如果模型联编程序处理单个类型,则使用内置 SimpleModelBinderProvider 会更容易,它专为此目的而设计。 下面的代码演示如何执行此操作。
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var provider = new SimpleModelBinderProvider(
typeof(GeoPoint), new GeoPointModelBinder());
config.Services.Insert(typeof(ModelBinderProvider), 0, provider);
// ...
}
}
使用模型绑定提供程序时,仍需将 [ModelBinder] 属性添加到 参数,以告知 Web API 它应使用模型绑定器,而不是媒体类型格式化程序。 但现在无需在 属性中指定模型联编程序的类型:
public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }
值提供程序
我提到模型联编程序从值提供程序获取值。 若要编写自定义值提供程序,请实现 IValueProvider 接口。 以下示例从请求中的 Cookie 中拉取值:
public class CookieValueProvider : IValueProvider
{
private Dictionary<string, string> _values;
public CookieValueProvider(HttpActionContext actionContext)
{
if (actionContext == null)
{
throw new ArgumentNullException("actionContext");
}
_values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var cookie in actionContext.Request.Headers.GetCookies())
{
foreach (CookieState state in cookie.Cookies)
{
_values[state.Name] = state.Value;
}
}
}
public bool ContainsPrefix(string prefix)
{
return _values.Keys.Contains(prefix);
}
public ValueProviderResult GetValue(string key)
{
string value;
if (_values.TryGetValue(key, out value))
{
return new ValueProviderResult(value, value, CultureInfo.InvariantCulture);
}
return null;
}
}
还需要通过派生自 ValueProviderFactory 类来创建值提供程序工厂。
public class CookieValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(HttpActionContext actionContext)
{
return new CookieValueProvider(actionContext);
}
}
将值提供程序工厂添加到 HttpConfiguration ,如下所示。
public static void Register(HttpConfiguration config)
{
config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());
// ...
}
Web API 由所有值提供程序组成,因此当模型联编程序调用 ValueProvider.GetValue 时,模型联编程序将从能够生成该值的第一个值提供程序接收值。
或者,可以使用 ValueProvider 属性在参数级别设置值提供程序工厂,如下所示:
public HttpResponseMessage Get(
[ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)
这会告知 Web API 使用具有指定值提供程序工厂的模型绑定,而不使用任何其他已注册的值提供程序。
HttpParameterBinding
模型绑定器是更通用机制的特定实例。 如果查看 [ModelBinder] 属性,将看到它派生自抽象 ParameterBindingAttribute 类。 此类定义单个方法 GetBinding,该方法返回 HttpParameterBinding 对象:
public abstract class ParameterBindingAttribute : Attribute
{
public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);
}
HttpParameterBinding 负责将参数绑定到值。 对于 [ModelBinder],属性返回使用 IModelBinder 执行实际绑定的 HttpParameterBinding 实现。 还可以实现自己的 HttpParameterBinding。
例如,假设你想要从 if-match
请求中的 和 if-none-match
标头获取 ETag。 我们将首先定义一个表示 ETag 的类。
public class ETag
{
public string Tag { get; set; }
}
我们还将定义一个枚举,以指示是从 标头还是if-none-match
标头if-match
获取 ETag。
public enum ETagMatch
{
IfMatch,
IfNoneMatch
}
下面是一个 HttpParameterBinding ,它从所需的标头中获取 ETag 并将其绑定到 ETag 类型的参数:
public class ETagParameterBinding : HttpParameterBinding
{
ETagMatch _match;
public ETagParameterBinding(HttpParameterDescriptor parameter, ETagMatch match)
: base(parameter)
{
_match = match;
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext, CancellationToken cancellationToken)
{
EntityTagHeaderValue etagHeader = null;
switch (_match)
{
case ETagMatch.IfNoneMatch:
etagHeader = actionContext.Request.Headers.IfNoneMatch.FirstOrDefault();
break;
case ETagMatch.IfMatch:
etagHeader = actionContext.Request.Headers.IfMatch.FirstOrDefault();
break;
}
ETag etag = null;
if (etagHeader != null)
{
etag = new ETag { Tag = etagHeader.Tag };
}
actionContext.ActionArguments[Descriptor.ParameterName] = etag;
var tsc = new TaskCompletionSource<object>();
tsc.SetResult(null);
return tsc.Task;
}
}
ExecuteBindingAsync 方法执行绑定。 在此方法中,将绑定参数值添加到 HttpActionContext 中的 ActionArgument 字典。
备注
如果 ExecuteBindingAsync 方法读取请求消息的正文,请重写 WillReadBody 属性以返回 true。 请求正文可能是只能读取一次的无缓冲区流,因此 Web API 强制实施一个规则,即最多一个绑定可以读取消息正文。
若要应用自定义 HttpParameterBinding,可以定义派生自 ParameterBindingAttribute 的属性。 对于 ETagParameterBinding
,我们将定义两个属性,一个用于 if-match
标头,一个用于 if-none-match
标头。 两者都派生自抽象基类。
public abstract class ETagMatchAttribute : ParameterBindingAttribute
{
private ETagMatch _match;
public ETagMatchAttribute(ETagMatch match)
{
_match = match;
}
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
if (parameter.ParameterType == typeof(ETag))
{
return new ETagParameterBinding(parameter, _match);
}
return parameter.BindAsError("Wrong parameter type");
}
}
public class IfMatchAttribute : ETagMatchAttribute
{
public IfMatchAttribute()
: base(ETagMatch.IfMatch)
{
}
}
public class IfNoneMatchAttribute : ETagMatchAttribute
{
public IfNoneMatchAttribute()
: base(ETagMatch.IfNoneMatch)
{
}
}
下面是使用 特性的 [IfNoneMatch]
控制器方法。
public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }
除了 ParameterBindingAttribute,还有另一个用于添加自定义 HttpParameterBinding 的挂钩。 在 HttpConfiguration 对象上, ParameterBindingRules 属性是 (HttpParameterDescriptor ->HttpParameterBinding) 类型的匿名函数的集合。 例如,可以添加 GET 方法上的任何 ETag 参数与 一 ETagParameterBinding
起使用 if-none-match
的规则:
config.ParameterBindingRules.Add(p =>
{
if (p.ParameterType == typeof(ETag) &&
p.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get))
{
return new ETagParameterBinding(p, ETagMatch.IfNoneMatch);
}
else
{
return null;
}
});
对于绑定不适用的参数,函数应返回 null
。
IActionValueBinder
整个参数绑定过程由可插入服务 IActionValueBinder 控制。 IActionValueBinder 的默认实现执行以下操作:
-
在参数上查找 ParameterBindingAttribute 。 这包括 [FromBody]、 [FromUri]和 [ModelBinder]或自定义属性。
-
否则,请在 HttpConfiguration.ParameterBindingRules 中查找返回非 null HttpParameterBinding 的函数。
-
否则,请使用前面所述的默认规则。
- 如果参数类型为“简单”或具有类型转换器,请从 URI 绑定。 这相当于将 [FromUri] 属性放在 参数上。
- 否则,请尝试从消息正文中读取 参数。 这相当于将 [FromBody] 放在 参数上。
如果需要,可以使用自定义实现替换整个 IActionValueBinder 服务。
其他资源
Mike Stall 撰写了一系列关于 Web API 参数绑定的博客文章:
建议的内容
-
ApiController.BadRequest 方法 (System.Web.Http)
创建 BadRequestResult (400 错误的请求) 。
-
了解操作筛选器 (C#)
本教程的目标是说明操作筛选器。 操作筛选器是一个属性,可应用于控制器操作 -- 或整个控制器... (C#)
-
设置 ASP.NET Core Web API 中响应数据的格式
了解如何设置 ASP.NET Core Web API 中响应数据的格式。
-
ASP.NET Web API 中的异常处理 - ASP.NET 4.x
描述 ASP.NET Web API执行错误和异常处理,并提供错误和异常的示例。