Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Considere usar ASP.NET API web Core. Tem as seguintes vantagens sobre ASP.NET API Web 4.x:
- ASP.NET Core é uma framework de código aberto e multiplataforma para construir aplicações web modernas baseadas na cloud no Windows, macOS e Linux.
- Os controladores ASP.NET Core MVC e os controladores web API estão unificados.
- Arquitetado para a testabilidade.
- Capacidade de desenvolver e correr em Windows, macOS e Linux.
- Código aberto e focado na comunidade.
- Integração de frameworks modernos do lado do cliente e fluxos de trabalho de desenvolvimento.
- Um sistema de configuração pronto para a cloud e baseado no ambiente.
- Injeção de dependência incorporada.
- Um pipeline de pedidos HTTP leve, de alto desempenho e modular.
- Capacidade de hospedar em Kestrel, IIS, HTTP.sys, Nginx, Apache e Docker.
- Controle de versões simultâneas.
- Ferramentas que simplificam o desenvolvimento web moderno.
Este artigo descreve como a Web API vincula parâmetros e como pode personalizar o processo de ligação. Quando a Web API chama um método num controlador, esta deve definir valores para os parâmetros, um processo chamado binding.
Por defeito, a Web API utiliza as seguintes regras para atribuir parâmetros:
- Se o parâmetro for do tipo "simples", a Web API tenta obter o valor do URI. Tipos simples incluem os tipos primitivos .NET (int, bool, double, e assim por diante), mais TimeSpan, DateTime, Guid, decimal e string, além de qualquer tipo com um conversor de tipos que possa converter a partir de uma string. (Falarei mais sobre conversores de tipos mais à frente.)
- Para tipos complexos, a Web API tenta ler o valor do corpo da mensagem, usando um formatador de tipos de media.
Por exemplo, aqui está um método típico de controlador Web API:
HttpResponseMessage Put(int id, Product item) { ... }
O parâmetro id é do tipo "simples", por isso a Web API tenta obter o valor do URI do pedido. O parâmetro item é de tipo complexo, por isso a Web API utiliza um formatador do tipo media para ler o valor do corpo do pedido.
Para obter um valor do URI, a Web API verifica os dados da rota e a cadeia de consulta do URI. Os dados da rota são preenchidos quando o sistema de encaminhamento analisa o URI e o associa a uma rota. Para mais informações, consulte Seleção de Roteamento e Ações.
No resto deste artigo, vou mostrar-lhe como pode personalizar o processo de vinculação de modelos. Para tipos complexos, no entanto, considere usar formatadores de tipos de media sempre que possível. Um princípio fundamental do HTTP é que os recursos são enviados no corpo da mensagem, usando negociação de conteúdo para especificar a representação do recurso. Os formateadores do tipo media foram concebidos exatamente para este propósito.
A usar [FromUri]
Para forçar a Web API a ler um tipo complexo do URI, adicione o atributo [FromUri] ao parâmetro. O exemplo seguinte define um GeoPoint tipo, juntamente com um método 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 consulta e a Web API irá usá-los para construir um GeoPoint. Por exemplo:
http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989
A usar [FromBody]
Para forçar a Web API a ler um tipo simples do corpo do pedido, adicione o atributo [FromBody] ao parâmetro:
public HttpResponseMessage Post([FromBody] string name) { ... }
Neste exemplo, a Web API usará um formador do tipo media para ler o valor do nome do corpo do pedido. Aqui está um exemplo de pedido de 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 Web API utiliza o cabeçalho Content-Type para selecionar um formatador. Neste exemplo, o tipo de conteúdo é "application/json" e o corpo do pedido é uma string JSON bruta (não um objeto JSON).
No máximo, um parâmetro é permitido para ler do corpo da mensagem. Portanto, isto não vai funcionar:
// Caution: Will not work!
public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }
A razão para esta regra é que o corpo do pedido pode ser armazenado num fluxo não bufferizado que só pode ser lido uma vez.
Conversores de Tipos
Pode fazer com que a Web API trate uma classe como um tipo simples (para que a Web API tente associá-la a partir do URI) criando um TypeConverter e fornecendo uma conversão de cadeias.
O código seguinte mostra uma GeoPoint classe que representa um ponto geográfico, mais um TypeConverter que converte de strings para GeoPoint instâncias. A GeoPoint classe é decorada com um atributo [TypeConverter] para especificar o conversor de tipos. (Este exemplo foi inspirado no artigo de blog de 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);
}
}
Agora, a Web API tratará GeoPoint como um tipo simples, o que significa que tentará associar parâmetros GeoPoint do URI. Não precisas de incluir [FromUri] no parâmetro.
public HttpResponseMessage Get(GeoPoint location) { ... }
O cliente pode invocar o método com um URI assim:
http://localhost/api/values/?location=47.678558,-122.130989
Associadores de Modelos
Uma opção mais flexível do que um conversor de tipos é criar um binder personalizado. Com um binder de modelos, tens acesso a coisas como o pedido HTTP, a descrição da ação e os valores brutos dos dados da rota.
Para criar um binder de modelos, implemente a interface IModelBinder . Esta interface define um único método, BindModel:
bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);
Aqui está um fichário 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 binder de modelos obtém valores brutos de entrada de um fornecedor de valor. Este design separa duas funções distintas:
- O fornecedor de valor recebe o pedido HTTP e preenche um dicionário de pares-chave-valor.
- O vinculador de modelos utiliza este dicionário para preencher o modelo.
O fornecedor de valor padrão na Web API obtém valores dos dados de rota e da cadeia de consulta. Por exemplo, se o URI for http://localhost/api/values/1?location=48,-122, o fornecedor de valor cria os seguintes pares-chave-valor:
- id = "1"
- localização = "48,-122"
(Estou a assumir o modelo de rota padrão, que é "api/{controller}/{id}".)
O nome do parâmetro a ligar está armazenado na propriedade ModelBindingContext.ModelName . O model binder procura uma chave com o valor correspondente no dicionário. Se o valor existir e puder ser convertido em um GeoPoint, o "binder" do modelo atribui o valor vinculado à propriedade ModelBindingContext.Model.
Note que o vinculador de modelo não está limitado a uma simples conversão de tipo. Neste exemplo, o associador de modelos procura primeiro numa tabela de localizações conhecidas e, caso esta falhe, utiliza a conversão de tipos.
Configuração do Vinculador de Modelo
Existem várias formas de configurar um vinculador de modelo. Primeiro, podes adicionar um atributo [ModelBinder] ao parâmetro.
public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
Também pode adicionar um atributo [ModelBinder] ao tipo. A Web API utilizará o binder de modelo especificado para todos os parâmetros desse tipo.
[ModelBinder(typeof(GeoPointModelBinder))]
public class GeoPoint
{
// ....
}
Finalmente, pode adicionar um fornecedor de associador de modelos ao HttpConfiguration. Um fornecedor de model binder é simplesmente uma classe de fábrica que cria um model binder. Pode criar um fornecedor derivando da classe ModelBinderProvider . No entanto, se o seu associador de modelo gerir um único tipo, é mais fácil usar o SimpleModelBinderProvider incorporado, que é projetado para este propósito. 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 fornecedor de ligação de modelos, ainda precisa de adicionar o atributo [ModelBinder] ao parâmetro, para indicar à Web API que deve usar um ligador de modelo e não um formatador de tipo de mídia. Mas agora já não é necessário especificar o tipo de associador de modelos no atributo.
public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }
Fornecedores de Valor
Mencionei que um binder de modelos obtém valores de um fornecedor de valor. Para escrever um fornecedor 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;
}
}
Também precisa de criar uma fábrica de fornecedor 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 fornecedor de valor ao HttpConfiguration da seguinte forma.
public static void Register(HttpConfiguration config)
{
config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());
// ...
}
Web API compõe todos os fornecedores de valor, por isso, quando um binder de modelos chama ValueProvider.GetValue, o binder de modelos recebe o valor do primeiro fornecedor de valor que consegue produzi-lo.
Em alternativa, pode definir a fábrica de fornecedores de valor ao nível dos parâmetros usando o atributo ValueProvider , da seguinte forma:
public HttpResponseMessage Get(
[ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)
Isto instrui a API da Web a usar a vinculação de modelos com a fábrica de provedores de valor especificada, e para não utilizar nenhum dos outros provedores de valor registados.
HttpParameterBinding
Os binders modelo são um exemplo específico de um mecanismo mais geral. Se olhar para o atributo [ModelBinder ], verá que deriva da classe abstrata ParameterBindingAttribute . Esta classe define um único método, GetBinding, que devolve um objeto HttpParameterBinding :
public abstract class ParameterBindingAttribute : Attribute
{
public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);
}
Um HttpParameterBinding é responsável por ligar um parâmetro a um valor. No caso de [ModelBinder], o atributo devolve uma implementação HttpParameterBinding que utiliza um IModelBinder para realizar a ligação real. Também pode implementar o seu próprio HttpParameterBinding.
Por exemplo, suponha que quer obter ETags nos cabeçalhos if-match e if-none-match do pedido. Vamos começar por definir uma classe para representar ETags.
public class ETag
{
public string Tag { get; set; }
}
Também iremos definir uma enumeração para indicar se devemos obter o 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 o ETag do cabeçalho desejado e o liga 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 ligação. Neste método, adicione o valor do parâmetro limitado ao dicionário ActionArgument no HttpActionContext.
Observação
Se o seu método ExecuteBindingAsync leia o corpo da mensagem de requisição, sobreponha a propriedade WillReadBody para devolver true. O corpo do pedido pode ser um fluxo sem buffer que só pode ser lido uma vez, pelo que a Web API impõe uma regra que, no máximo, uma ligação pode ler o corpo da mensagem.
Para aplicar um HttpParameterBinding personalizado, pode definir um atributo que deriva de ParameterBindingAttribute. Para ETagParameterBinding, vamos definir 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 controlador que usa o [IfNoneMatch] atributo.
public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }
Além do ParameterBindingAttribute, existe 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, poderia adicionar uma regra que qualquer parâmetro ETag num método GET use 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 os parâmetros em que a associação não seja aplicável.
IActionValueBinder
Todo o processo de ligação de parâmetros é controlado por um serviço pluggable, IActionValueBinder. A implementação padrão do IActionValueBinder faz o seguinte:
Procura um ParameterBindingAttribute no parâmetro. Isto inclui [FromBody],[FromUri] e [ModelBinder], ou atributos personalizados.
Caso contrário, procure em HttpConfiguration.ParameterBindingRules uma função que devolve 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 tipos, associe a partir do URI. Isto equivale a colocar o atributo [FromUri] no parâmetro.
- Caso contrário, tente ler o parâmetro do corpo da mensagem. Isto equivale a colocar [FromBody] no parâmetro.
Se quiseres, podes substituir todo o serviço IActionValueBinder por uma implementação personalizada.
Recursos adicionais
Amostra de Vinculação de Parâmetro Personalizado
Mike Stall escreveu uma boa série de artigos no blogue sobre a ligação de parâmetros da Web API: