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.
A Web API 2 suporta um novo tipo de encaminhamento, chamado encaminhamento por atributos. Para uma visão geral do encaminhamento de atributos, veja Roteamento de Atributos na Web API 2. Neste tutorial, vais usar o encaminhamento de atributos para criar uma API REST para uma coleção de livros. A API suportará as seguintes ações:
| Action | URI de exemplo |
|---|---|
| Obtenha uma lista de todos os livros. | /api/livros |
| Obtenha um livro por ID. | /api/books/1 |
| Obtenha os detalhes de um livro. | /api/livros/1/detalhes |
| Obtenha uma lista de livros por género. | /api/livros/fantasia |
| Obtenha uma lista de livros por data de publicação. | /api/books/date/2013-02-16 /api/books/date/2013/02/16 (forma alternativa) |
| Obtenha uma lista de livros de um autor em particular. | /API/autores/1/livros |
Todos os métodos são apenas de leitura (solicitações HTTP GET).
Para a camada de dados, vamos usar o Entity Framework. Os registos do livro terão os seguintes campos:
- ID
- Título
- Gênero
- Data de publicação
- Preço
- Descrição
- AuthorID (chave estrangeira para uma tabela de Autores)
Para a maioria dos pedidos, no entanto, a API devolverá um subconjunto desses dados (título, autor e género). Para obter o registo completo, o cliente solicita /api/books/{id}/details.
Pré-requisitos
Visual Studio 2017 Edição Comunitária, Profissional ou Empresarial.
Criar o Projeto Visual Studio
Comece por correr o Visual Studio. No menu Ficheiro , selecione Novo e depois selecione Projeto.
Expanda a categoria Instalado>Visual C#. Em Visual C#, selecione Web. Na lista de modelos de projeto, selecione ASP.NET Web Application (.NET Framework). Nomeie o projeto "BooksAPI".
No diálogo Nova Aplicação Web ASP.NET, selecione o Modelo Vazio. Em "Adicionar pastas e referências principais para", selecione a caixa de seleção Web API. Clique em OK.
Isto cria um projeto esqueleto configurado para funcionalidade da Web API.
Modelos de domínio
De seguida, adicione classes para modelos de domínio. No Explorador de Soluções, clique com o botão direito na pasta Models.
Seleciona Adicionar, depois seleciona Classe. Nomeia a classe Author.
Substitua o código em Author.cs pelo seguinte:
using System.ComponentModel.DataAnnotations;
namespace BooksAPI.Models
{
public class Author
{
public int AuthorId { get; set; }
[Required]
public string Name { get; set; }
}
}
Agora adicione outra classe chamada Book.
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BooksAPI.Models
{
public class Book
{
public int BookId { get; set; }
[Required]
public string Title { get; set; }
public decimal Price { get; set; }
public string Genre { get; set; }
public DateTime PublishDate { get; set; }
public string Description { get; set; }
public int AuthorId { get; set; }
[ForeignKey("AuthorId")]
public Author Author { get; set; }
}
}
Adicionar um Controlador de API Web
Neste passo, vamos adicionar um controlador de API Web que utiliza o Entity Framework como camada de dados.
Pressione CTRL+SHIFT+B para construir o projeto. O Entity Framework utiliza reflexão para descobrir as propriedades dos modelos, pelo que requer um assembly compilado para criar o esquema da base de dados.
No Explorador de Soluções, clique com o botão direito na pasta Controladores. Seleciona Adicionar, depois seleciona Controlador.
No diálogo Adicionar Estrutura, selecione Controlador da API Web 2 com ações, usando o Entity Framework.
No diálogo Adicionar Controlador , para o nome do Controlador, introduza "BooksController". Seleciona a caixa de seleção "Usar ações do controlador assíncrono". Para a classe Modelo, selecione "Livro". (Se não vires a Book classe listada no menu suspenso, certifica-te de que construíste o projeto.) Depois clica no botão "+".
Clique em Adicionar no diálogo Novo Contexto de Dados .
Clique em Adicionar no diálogo Adicionar Controlador . O andaime adiciona uma classe chamada BooksController que define o controlador da API. Também adiciona uma classe chamada BooksAPIContext na pasta Models, que define o contexto de dados para o Entity Framework.
Semear a Base de Dados
No menu de ferramentas, selecione Gestor de Pacotes NuGet e depois selecione Console do Gestor de Pacotes.
Na janela da Consola do Gestor de Pacotes, introduza o seguinte comando:
Add-Migration
Este comando cria uma pasta Migrations e adiciona um novo ficheiro de código chamado Configuration.cs. Abra este ficheiro e adicione o seguinte código ao Configuration.Seed método.
protected override void Seed(BooksAPI.Models.BooksAPIContext context)
{
context.Authors.AddOrUpdate(new Author[] {
new Author() { AuthorId = 1, Name = "Ralls, Kim" },
new Author() { AuthorId = 2, Name = "Corets, Eva" },
new Author() { AuthorId = 3, Name = "Randall, Cynthia" },
new Author() { AuthorId = 4, Name = "Thurman, Paula" }
});
context.Books.AddOrUpdate(new Book[] {
new Book() { BookId = 1, Title= "Midnight Rain", Genre = "Fantasy",
PublishDate = new DateTime(2000, 12, 16), AuthorId = 1, Description =
"A former architect battles an evil sorceress.", Price = 14.95M },
new Book() { BookId = 2, Title = "Maeve Ascendant", Genre = "Fantasy",
PublishDate = new DateTime(2000, 11, 17), AuthorId = 2, Description =
"After the collapse of a nanotechnology society, the young" +
"survivors lay the foundation for a new society.", Price = 12.95M },
new Book() { BookId = 3, Title = "The Sundered Grail", Genre = "Fantasy",
PublishDate = new DateTime(2001, 09, 10), AuthorId = 2, Description =
"The two daughters of Maeve battle for control of England.", Price = 12.95M },
new Book() { BookId = 4, Title = "Lover Birds", Genre = "Romance",
PublishDate = new DateTime(2000, 09, 02), AuthorId = 3, Description =
"When Carla meets Paul at an ornithology conference, tempers fly.", Price = 7.99M },
new Book() { BookId = 5, Title = "Splish Splash", Genre = "Romance",
PublishDate = new DateTime(2000, 11, 02), AuthorId = 4, Description =
"A deep sea diver finds true love 20,000 leagues beneath the sea.", Price = 6.99M},
});
}
Na janela da Consola do Gestor de Pacotes, escreva os seguintes comandos.
add-migration Initial
update-database
Estes comandos criam uma base de dados local e invocam o método Seed para preencher a base de dados.
Adicionar Classes DTO
Se executares a aplicação agora e enviares um pedido GET para /api/books/1, a resposta é semelhante à seguinte. (Adicionei indentação para maior legibilidade.)
{
"BookId": 1,
"Title": "Midnight Rain",
"Genre": "Fantasy",
"PublishDate": "2000-12-16T00:00:00",
"Description": "A former architect battles an evil sorceress.",
"Price": 14.95,
"AuthorId": 1,
"Author": null
}
Em vez disso, quero que este pedido devolva um subconjunto dos campos. Além disso, quero que devolva o nome do autor, em vez do ID do autor. Para isso, modificaremos os métodos do controlador para devolver um objeto de transferência de dados (DTO) em vez do modelo EF. Um DTO é um objeto concebido apenas para transportar dados.
No Explorador de Soluções, clique com o botão direito no projeto e selecione Adicionar | Nova Pasta. Nomeie a pasta como "DTOs". Adicione uma classe nomeada BookDto à pasta DTOs, com a seguinte definição:
namespace BooksAPI.DTOs
{
public class BookDto
{
public string Title { get; set; }
public string Author { get; set; }
public string Genre { get; set; }
}
}
Adicione outra classe chamada BookDetailDto.
using System;
namespace BooksAPI.DTOs
{
public class BookDetailDto
{
public string Title { get; set; }
public string Genre { get; set; }
public DateTime PublishDate { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public string Author { get; set; }
}
}
De seguida, atualize a classe BooksController para devolver instâncias de BookDto. Vamos usar o método Queryable.Select para transformar Book instâncias em BookDto instâncias. Aqui está o código atualizado para a classe controller.
using BooksAPI.DTOs;
using BooksAPI.Models;
using System;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
namespace BooksAPI.Controllers
{
public class BooksController : ApiController
{
private BooksAPIContext db = new BooksAPIContext();
// Typed lambda expression for Select() method.
private static readonly Expression<Func<Book, BookDto>> AsBookDto =
x => new BookDto
{
Title = x.Title,
Author = x.Author.Name,
Genre = x.Genre
};
// GET api/Books
public IQueryable<BookDto> GetBooks()
{
return db.Books.Include(b => b.Author).Select(AsBookDto);
}
// GET api/Books/5
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
BookDto book = await db.Books.Include(b => b.Author)
.Where(b => b.BookId == id)
.Select(AsBookDto)
.FirstOrDefaultAsync();
if (book == null)
{
return NotFound();
}
return Ok(book);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
Observação
Apaguei os PutBookmétodos , PostBook, e DeleteBook porque não são necessários para este tutorial.
Agora, se executares a aplicação e pedires /api/books/1, o corpo da resposta deve ser assim:
{"Title":"Midnight Rain","Author":"Ralls, Kim","Genre":"Fantasy"}
Adicionar Atributos de Rota
De seguida, vamos converter o controlador para usar o encaminhamento de atributos. Primeiro, adiciona um atributo RoutePrefix ao controlador. Este atributo define os segmentos iniciais de URI para todos os métodos deste controlador.
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
// ...
Depois adiciona atributos [Rota] às ações do comando, da seguinte forma:
[Route("")]
public IQueryable<BookDto> GetBooks()
{
// ...
}
[Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
// ...
}
O modelo de rota para cada método controlador é o prefixo mais a string especificada no atributo Rota . Para o GetBook método, o template de rota inclui a cadeia parametrizada "{id:int}", que corresponde se o segmento URI contiver um valor inteiro.
| Método | Modelo de rota | URI de exemplo |
|---|---|---|
GetBooks |
"API/Livros" | http://localhost/api/books |
GetBook |
"api/books/{id:int}" | http://localhost/api/books/5 |
Obtenha Detalhes do Livro
Para obter detalhes do livro, o cliente enviará um pedido GET para /api/books/{id}/details, onde {id} é o ID do livro.
Adicione o seguinte método à BooksController classe.
[Route("{id:int}/details")]
[ResponseType(typeof(BookDetailDto))]
public async Task<IHttpActionResult> GetBookDetail(int id)
{
var book = await (from b in db.Books.Include(b => b.Author)
where b.BookId == id
select new BookDetailDto
{
Title = b.Title,
Genre = b.Genre,
PublishDate = b.PublishDate,
Price = b.Price,
Description = b.Description,
Author = b.Author.Name
}).FirstOrDefaultAsync();
if (book == null)
{
return NotFound();
}
return Ok(book);
}
Se pedires /api/books/1/details, a resposta é esta:
{
"Title": "Midnight Rain",
"Genre": "Fantasy",
"PublishDate": "2000-12-16T00:00:00",
"Description": "A former architect battles an evil sorceress.",
"Price": 14.95,
"Author": "Ralls, Kim"
}
Obtenha Livros por Género
Para obter uma lista de livros de um género específico, o cliente enviará um pedido GET para /api/books/genre, onde género é o nome do género. (Por exemplo, /api/books/fantasy.)
Adicione o seguinte método a BooksController.
[Route("{genre}")]
public IQueryable<BookDto> GetBooksByGenre(string genre)
{
return db.Books.Include(b => b.Author)
.Where(b => b.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase))
.Select(AsBookDto);
}
Aqui estamos a definir uma rota que contém um parâmetro {género} no modelo URI. Note que a Web API consegue distinguir estes dois URIs e encaminhá-los para métodos diferentes:
/api/books/1
/api/books/fantasy
Isto porque o GetBook método inclui uma restrição de que o segmento "id" deve ser um valor inteiro:
[Route("{id:int}")]
public BookDto GetBook(int id)
{
// ...
}
Se pedires /api/books/fantasy, a resposta é a seguinte:
[ { "Title": "Midnight Rain", "Author": "Ralls, Kim", "Genre": "Fantasy" }, { "Title": "Maeve Ascendant", "Author": "Corets, Eva", "Genre": "Fantasy" }, { "Title": "The Sundered Grail", "Author": "Corets, Eva", "Genre": "Fantasy" } ]
Obtenha Livros por Autor
Para obter uma lista de livros para um determinado autor, o cliente enviará um pedido GET para /api/authors/id/books, onde id é o ID do autor.
Adicione o seguinte método a BooksController.
[Route("~/api/authors/{authorId:int}/books")]
public IQueryable<BookDto> GetBooksByAuthor(int authorId)
{
return db.Books.Include(b => b.Author)
.Where(b => b.AuthorId == authorId)
.Select(AsBookDto);
}
Este exemplo é interessante porque "livros" é tratado como um recurso infantil dos "autores". Este padrão é bastante comum em APIs RESTful.
O tilde (~) no template de rota sobrepõe o prefixo de rota no atributo RoutePrefix .
Obtenha Livros por Data de Publicação
Para obter uma lista de livros por data de publicação, o cliente enviará um pedido GET para /api/books/date/yyyy-mm-dd, onde yyyy-mm-dd é a data.
Aqui está uma forma de o fazer:
[Route("date/{pubdate:datetime}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
return db.Books.Include(b => b.Author)
.Where(b => DbFunctions.TruncateTime(b.PublishDate)
== DbFunctions.TruncateTime(pubdate))
.Select(AsBookDto);
}
O {pubdate:datetime} parâmetro está restrito para corresponder a um valor de DataHora . Isto funciona, mas na verdade é mais permissivo do que gostaríamos. Por exemplo, estes URIs também corresponderão à rota:
/api/books/date/Thu, 01 May 2008
/api/books/date/2000-12-16T00:00:00
Não há problema nenhum em permitir estes URIs. No entanto, pode restringir a rota a um determinado formato adicionando uma restrição de expressões regulares ao modelo da rota:
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
// ...
}
Agora só as datas na forma "yyyy-mm-dd" coincidem. Repara que não usamos a regex para validar que temos uma data real. Isso é tratado quando a Web API tenta converter o segmento URI numa instância DateTime . Uma data inválida como '2012-47-99' não será convertida, e o cliente receberá um erro 404.
Também pode suportar um separador de barra (/api/books/date/yyyy/mm/dd) adicionando outro atributo [Rota] com uma regex diferente.
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
[Route("date/{*pubdate:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")] // new
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
// ...
}
Há aqui um detalhe subtil mas importante. O segundo modelo de rota tem um carácter coringa (*) no início do parâmetro {data de publicação}:
{*pubdate: ... }
Isto indica ao motor de encaminhamento que {data de publicação} deve corresponder ao resto do URI. Por defeito, um parâmetro template corresponde a um único segmento de URI. Neste caso, queremos que {data de publicação} abrange vários segmentos de URI:
/api/books/date/2013/06/17
Código do controlador
Aqui está o código completo da classe BooksControler.
using BooksAPI.DTOs;
using BooksAPI.Models;
using System;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
namespace BooksAPI.Controllers
{
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
private BooksAPIContext db = new BooksAPIContext();
// Typed lambda expression for Select() method.
private static readonly Expression<Func<Book, BookDto>> AsBookDto =
x => new BookDto
{
Title = x.Title,
Author = x.Author.Name,
Genre = x.Genre
};
// GET api/Books
[Route("")]
public IQueryable<BookDto> GetBooks()
{
return db.Books.Include(b => b.Author).Select(AsBookDto);
}
// GET api/Books/5
[Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
BookDto book = await db.Books.Include(b => b.Author)
.Where(b => b.BookId == id)
.Select(AsBookDto)
.FirstOrDefaultAsync();
if (book == null)
{
return NotFound();
}
return Ok(book);
}
[Route("{id:int}/details")]
[ResponseType(typeof(BookDetailDto))]
public async Task<IHttpActionResult> GetBookDetail(int id)
{
var book = await (from b in db.Books.Include(b => b.Author)
where b.AuthorId == id
select new BookDetailDto
{
Title = b.Title,
Genre = b.Genre,
PublishDate = b.PublishDate,
Price = b.Price,
Description = b.Description,
Author = b.Author.Name
}).FirstOrDefaultAsync();
if (book == null)
{
return NotFound();
}
return Ok(book);
}
[Route("{genre}")]
public IQueryable<BookDto> GetBooksByGenre(string genre)
{
return db.Books.Include(b => b.Author)
.Where(b => b.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase))
.Select(AsBookDto);
}
[Route("~/api/authors/{authorId}/books")]
public IQueryable<BookDto> GetBooksByAuthor(int authorId)
{
return db.Books.Include(b => b.Author)
.Where(b => b.AuthorId == authorId)
.Select(AsBookDto);
}
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
[Route("date/{*pubdate:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
return db.Books.Include(b => b.Author)
.Where(b => DbFunctions.TruncateTime(b.PublishDate)
== DbFunctions.TruncateTime(pubdate))
.Select(AsBookDto);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
Resumo
O encaminhamento por atributos dá-lhe mais controlo e maior flexibilidade ao desenhar os URIs da sua API.