Associação de parâmetro na API Web ASP.NET
Considere usar ASP.NET API Web principal. Ele tem as seguintes vantagens sobre ASP.NET API Web 4.x:
- ASP.NET Core é uma estrutura de plataforma cruzada de código aberto para criar aplicativos Web modernos baseados em nuvem no Windows, macOS e Linux.
- Os controladores MVC do ASP.NET Core e os controladores de API Web são unificados.
- Projetado para capacidade de teste.
- Capacidade de desenvolver e executar no Windows, macOS e Linux.
- De software livre e voltado para a comunidade.
- Integração de estruturas modernas do lado do cliente e fluxos de trabalho de desenvolvimento.
- Um sistema de configuração pronto para a nuvem, baseado no ambiente.
- Injeção de dependência interna.
- Um pipeline de solicitação HTTP leve, modular e de alto desempenho.
- Capacidade de hospedar no Kestrel, IIS, HTTP.sys, Nginx, Apache e Docker.
- Controle de versão lado a lado.
- Ferramentas que simplificam o moderno desenvolvimento para a Web.
Este artigo descreve como a API Web associa parâmetros e como você pode personalizar o processo de associação. Quando a API Web chama um método em um controlador, ela deve definir valores para os parâmetros, um processo chamado associação.
Por padrão, a API Web usa as seguintes regras para associar parâmetros:
- Se o parâmetro for um tipo "simples", a API Web tentará obter o valor do URI. Os tipos simples incluem os tipos primitivos do .NET (int, bool, double e assim por diante), além de TimeSpan, DateTime, Guid, decimal e string, além de qualquer tipo com um conversor de tipo que pode converter de uma cadeia de caracteres. (Mais sobre conversores de tipo mais tarde.)
- Para tipos complexos, a API Web tenta ler o valor do corpo da mensagem, usando um formatador de tipo de mídia.
Por exemplo, aqui está um método típico do controlador da API Web:
HttpResponseMessage Put(int id, Product item) { ... }
O parâmetro id é um tipo "simples", portanto, a API Web tenta obter o valor do URI da solicitação. O parâmetro item é um tipo complexo, portanto, a API Web usa um formatador de tipo de mídia para ler o valor do corpo da solicitação.
Para obter um valor do URI, a API Web examina os dados da rota e a cadeia de caracteres de consulta do URI. Os dados da rota são preenchidos quando o sistema de roteamento analisa o URI e o corresponde a uma rota. Para obter mais informações, consulte Roteamento e seleção de ação.
No restante deste artigo, mostrarei como você pode personalizar o processo de associação de modelos. Para tipos complexos, no entanto, considere o uso de formatadores de tipo de mídia sempre que possível. Um princípio fundamental do HTTP é que os recursos são enviados no corpo da mensagem, usando a negociação de conteúdo para especificar a representação do recurso. Os formatadores do tipo mídia foram projetados exatamente para esse propósito.
Usando [FromUri]
Para forçar a API Web a ler um tipo complexo do URI, adicione o atributo [FromUri] ao parâmetro. O exemplo a seguir define um GeoPoint
tipo, juntamente com um método de controlador que obtém o GeoPoint
do URI.
public class GeoPoint
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
public ValuesController : ApiController
{
public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }
}
O cliente pode colocar os valores de Latitude e Longitude na cadeia de caracteres de consulta e a API Web os usará para construir um GeoPoint
. Por exemplo:
http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989
Usando [FromBody]
Para forçar a API Web a ler um tipo simples do corpo da solicitação, adicione o atributo [FromBody] ao parâmetro:
public HttpResponseMessage Post([FromBody] string name) { ... }
Neste exemplo, a API Web usará um formatador de tipo de mídia para ler o valor de name do corpo da solicitação. Aqui está um exemplo de solicitação do cliente.
POST http://localhost:5076/api/values HTTP/1.1
User-Agent: Fiddler
Host: localhost:5076
Content-Type: application/json
Content-Length: 7
"Alice"
Quando um parâmetro tem [FromBody], a API Web usa o cabeçalho Content-Type para selecionar um formatador. Neste exemplo, o tipo de conteúdo é "application/json" e o corpo da solicitação é uma cadeia de caracteres JSON bruta (não um objeto JSON).
No máximo, um parâmetro tem permissão para ler a partir do corpo da mensagem. Portanto, isso não funcionará:
// Caution: Will not work!
public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }
O motivo dessa regra é que o corpo da solicitação pode ser armazenado em um fluxo sem buffer que só pode ser lido uma vez.
Conversores de tipo
Você pode fazer com que a API Web trate uma classe como um tipo simples (para que a API Web tente associá-la do URI) criando um TypeConverter e fornecendo uma conversão de cadeia de caracteres.
O código a seguir mostra uma GeoPoint
classe que representa um ponto geográfico, além de um TypeConverter que converte de cadeias de caracteres em GeoPoint
instâncias. A GeoPoint
classe é decorada com um atributo [TypeConverter] para especificar o conversor de tipo. (Este exemplo foi inspirado na postagem do blog de Mike StallComo associar a objetos personalizados em assinaturas de ação no 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);
}
}
Agora, a API Web tratará GeoPoint
como um tipo simples, o que significa que ela tentará associar GeoPoint
parâmetros do URI. Você não precisa incluir [FromUri] no parâmetro.
public HttpResponseMessage Get(GeoPoint location) { ... }
O cliente pode invocar o método com um URI como este:
http://localhost/api/values/?location=47.678558,-122.130989
Fichários de modelo
Uma opção mais flexível do que um conversor de tipo é criar um associador de modelo personalizado. Com um associador de modelos, você tem acesso a itens como a solicitação HTTP, a descrição da ação e os valores brutos dos dados da rota.
Para criar um associador de modelo, implemente a interface IModelBinder . Essa interface define um único método, BindModel:
bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);
Aqui está um associador de modelos para GeoPoint
objetos.
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;
}
}
Um associador de modelo obtém valores de entrada brutos de um provedor de valor. Este design separa duas funções distintas:
- O provedor de valor usa a solicitação HTTP e preenche um dicionário de pares de chave-valor.
- O associador de modelos usa esse dicionário para preencher o modelo.
O provedor de valor padrão na API Web obtém valores dos dados de rota e da cadeia de caracteres de consulta. Por exemplo, se o URI for http://localhost/api/values/1?location=48,-122
, o provedor de valor criará os seguintes pares de chave-valor:
- id = "1"
- localização = "48,-122"
(Estou assumindo o modelo de rota padrão, que é "api / {controller} / {id}".)
O nome do parâmetro a ser associado é armazenado na propriedade ModelBindingContext.ModelName . O associador de modelos procura uma chave com esse valor no dicionário. Se o valor existir e puder ser convertido em um GeoPoint
, o associador de modelos atribuirá o valor associado à propriedade ModelBindingContext.Model .
Observe que o associador de modelos não se limita a uma conversão de tipo simples. Neste exemplo, o associador de modelos primeiro procura em uma tabela de locais conhecidos e, se isso falhar, ele usará a conversão de tipo.
Configurando o associador de modelo
Há várias maneiras de definir um fichário de modelo. Primeiro, você pode adicionar um atributo [ModelBinder] ao parâmetro.
public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
Você também pode adicionar um atributo [ModelBinder] ao tipo. A API Web usará o associador de modelo especificado para todos os parâmetros desse tipo.
[ModelBinder(typeof(GeoPointModelBinder))]
public class GeoPoint
{
// ....
}
Por fim, você pode adicionar um provedor de associador de modelo ao HttpConfiguration. Um provedor de associador de modelo é simplesmente uma classe de fábrica que cria um associador de modelo. Você pode criar um provedor derivando da classe ModelBinderProvider . No entanto, se o associador de modelos manipular um único tipo, será mais fácil usar o SimpleModelBinderProvider interno, que foi projetado para essa finalidade. O código a seguir mostra como fazer isso.
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);
// ...
}
}
Com um provedor de associação de modelo, você ainda precisa adicionar o atributo [ModelBinder] ao parâmetro, para informar à API Web que ela deve usar um associador de modelo e não um formatador de tipo de mídia. Mas agora você não precisa especificar o tipo de associador de modelo no atributo:
public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }
Provedores de valor
Mencionei que um associador de modelos obtém valores de um provedor de valor. Para escrever um provedor de valor personalizado, implemente a interface IValueProvider . Aqui está um exemplo que extrai valores dos cookies na solicitação:
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;
}
}
Você também precisa criar uma fábrica de provedores de valor derivando da classe ValueProviderFactory .
public class CookieValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(HttpActionContext actionContext)
{
return new CookieValueProvider(actionContext);
}
}
Adicione a fábrica do provedor de valor ao HttpConfiguration da seguinte maneira.
public static void Register(HttpConfiguration config)
{
config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());
// ...
}
A API Web compõe todos os provedores de valor, portanto, quando um associador de modelo chama ValueProvider.GetValue, o associador de modelo recebe o valor do primeiro provedor de valor capaz de produzi-lo.
Como alternativa, você pode definir a fábrica do provedor de valor no nível do parâmetro usando o atributo ValueProvider , da seguinte maneira:
public HttpResponseMessage Get(
[ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)
Isso informa à API Web para usar a associação de modelo com a fábrica de provedores de valor especificada e não usar nenhum dos outros provedores de valor registrados.
HttpParameterBinding
Os associadores de modelo são uma instância específica de um mecanismo mais geral. Se você observar o atributo [ModelBinder], verá que ele deriva da classe abstrata ParameterBindingAttribute . Essa classe define um único método, GetBinding, que retorna um objeto HttpParameterBinding :
public abstract class ParameterBindingAttribute : Attribute
{
public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);
}
Um HttpParameterBinding é responsável por associar um parâmetro a um valor. No caso de [ModelBinder], o atributo retorna uma implementação HttpParameterBinding que usa um IModelBinder para executar a associação real. Você também pode implementar seu próprio HttpParameterBinding.
Por exemplo, suponha que você queira obter ETags de if-match
e if-none-match
cabeçalhos na solicitação. Começaremos definindo uma classe para representar ETags.
public class ETag
{
public string Tag { get; set; }
}
Também definiremos uma enumeração para indicar se devemos obter a ETag do if-match
cabeçalho ou do if-none-match
cabeçalho.
public enum ETagMatch
{
IfMatch,
IfNoneMatch
}
Aqui está um HttpParameterBinding que obtém a ETag do cabeçalho desejado e a associa a um parâmetro do tipo 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;
}
}
O método ExecuteBindingAsync faz a associação. Dentro desse método, adicione o valor do parâmetro associado ao dicionário ActionArgument no HttpActionContext.
Observação
Se o método ExecuteBindingAsync ler o corpo da mensagem de solicitação, substitua a propriedade WillReadBody para retornar true. O corpo da solicitação pode ser um fluxo sem buffer que só pode ser lido uma vez, portanto, a API Web impõe uma regra de que, no máximo, uma associação pode ler o corpo da mensagem.
Para aplicar um HttpParameterBinding personalizado, você pode definir um atributo derivado de ParameterBindingAttribute. Para ETagParameterBinding
, definiremos dois atributos, um para if-match
cabeçalhos e outro para if-none-match
cabeçalhos. Ambos derivam de uma classe base abstrata.
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)
{
}
}
Aqui está um método de controlador que usa o [IfNoneMatch]
atributo.
public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }
Além de ParameterBindingAttribute, há outro gancho para adicionar um HttpParameterBinding personalizado. No objeto HttpConfiguration, a propriedade ParameterBindingRules é uma coleção de funções anônimas do tipo (HttpParameterDescriptor ->HttpParameterBinding). Por exemplo, você pode adicionar uma regra que qualquer parâmetro ETag em um método GET usa ETagParameterBinding
com 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;
}
});
A função deve retornar null
para parâmetros em que a associação não é aplicável.
IActionValueBinder
Todo o processo de associação de parâmetros é controlado por um serviço conectável, IActionValueBinder. A implementação padrão de IActionValueBinder faz o seguinte:
Procure um ParameterBindingAttribute no parâmetro. Isso inclui [FromBody], [FromUri] e [ModelBinder] ou atributos personalizados.
Caso contrário, procure em HttpConfiguration.ParameterBindingRules uma função que retorne um HttpParameterBinding não nulo.
Caso contrário, use as regras padrão que descrevi anteriormente.
- Se o tipo de parâmetro for "simples" ou tiver um conversor de tipo, associe a partir do URI. Isso equivale a colocar o atributo [FromUri] no parâmetro.
- Caso contrário, tente ler o parâmetro do corpo da mensagem. Isso equivale a colocar [FromBody] no parâmetro.
Se você quiser, poderá substituir todo o serviço IActionValueBinder por uma implementação personalizada.
Recursos adicionais
Exemplo de associação de parâmetro personalizado
Mike Stall escreveu uma boa série de postagens de blog sobre a vinculação de parâmetros da API Web: