Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Considere usar API Web do ASP.NET Core. 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 testabilidade.
- 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 embutida.
- Um pipeline de solicitação HTTP leve, modular e de alto desempenho.
- Capacidade de hospedar no Kestrel, IIS, HTTP.sys, Nginx, Apache e Docker.
- Versionamento 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 faz a análise do URI e o associa 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 utilizar formatadores de tipos 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 strings 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 Stall Como 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
Vinculadores 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 vinculador 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 associador de modelos. 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 'model-binder' 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 binder 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 a API Web para usar a vinculaçã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 da solicitação. Começaremos definindo uma classe para representar ETags.
public class ETag
{
public string Tag { get; set; }
}
Definiremos uma enumeração para indicar se obtemos a ETag do cabeçalho if-match ou do cabeçalho if-none-match.
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 deste método, adicione o valor do parâmetro vinculado ao dicionário ActionArgument dentro do HttpActionContext.
Observação
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: