Créer une API REST avec le routage d’attributs dans API Web ASP.NET 2

L’API web 2 prend en charge un nouveau type de routage, appelé routage d’attributs. Pour obtenir une vue d’ensemble du routage d’attributs, consultez Routage des attributs dans l’API web 2. Dans ce tutoriel, vous allez utiliser le routage d’attributs pour créer une API REST pour une collection de livres. L’API prend en charge les actions suivantes :

Action Exemple d’URI
Obtenez la liste de tous les livres. /api/books
Obtenez un livre par ID. /api/books/1
Obtenez les détails d’un livre. /api/books/1/details
Obtenez la liste des livres par genre. /api/books/fantasy
Obtenez la liste des livres par date de publication. /api/books/date/2013-02-16 /api/books/date/2013/02/16 (autre forme)
Obtenez la liste des livres d’un auteur particulier. /api/authors/1/books

Toutes les méthodes sont en lecture seule (requêtes HTTP GET).

Pour la couche de données, nous allons utiliser Entity Framework. Les enregistrements de livres auront les champs suivants :

  • ID
  • Titre
  • Genre
  • Date de publication
  • Prix
  • Description
  • AuthorID (clé étrangère d’une table Authors)

Toutefois, pour la plupart des requêtes, l’API retourne un sous-ensemble de ces données (titre, auteur et genre). Pour obtenir l’enregistrement complet, le client demande /api/books/{id}/details.

Prérequis

Visual Studio 2017 Édition Communauté, Professionnel ou Entreprise.

Créer le projet Visual Studio

Commencez par exécuter Visual Studio. Dans le menu File (Fichier), sélectionnez New (Nouveau), puis Project (Projet).

Développez la catégorieVisual C#installé>. Sous Visual C#, sélectionnez Web. Dans la liste des modèles de projet, sélectionnez ASP.NET’application web (.NET Framework). Nommez le projet « BooksAPI ».

Image de la boîte de dialogue Nouveau projet

Dans la boîte de dialogue Nouvelle application web ASP.NET , sélectionnez le modèle Vide . Sous « Ajouter des dossiers et des références principales pour », cochez la case API web . Cliquez sur OK.

Image de la boîte de dialogue d’application web A S P dot Net

Cela crée un projet squelette configuré pour les fonctionnalités d’API web.

Modèles de domaine

Ensuite, ajoutez des classes pour les modèles de domaine. Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le dossier Modèles. Sélectionnez Ajouter, puis Classe. Nommez la classe Author.

Image de créer une classe

Remplacez le code dans Author.cs par ce qui suit :

using System.ComponentModel.DataAnnotations;

namespace BooksAPI.Models
{
    public class Author
    {
        public int AuthorId { get; set; }
        [Required]
        public string Name { get; set; }
    }
}

Ajoutez maintenant une autre classe nommée 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; }
    }
}

Ajouter un contrôleur d’API web

Dans cette étape, nous allons ajouter un contrôleur d’API web qui utilise Entity Framework comme couche de données.

Appuyez sur CTRL+MAJ+B pour générer le projet. Entity Framework utilise la réflexion pour découvrir les propriétés des modèles. Il nécessite donc un assembly compilé pour créer le schéma de base de données.

Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le dossier Contrôleurs. Sélectionnez Ajouter, puis contrôleur.

Image de l’ajout d’un contrôleur

Dans la boîte de dialogue Ajouter une structure , sélectionnez Contrôleur d’API web 2 avec des actions, à l’aide d’Entity Framework.

Image d’ajouter une structure

Dans la boîte de dialogue Ajouter un contrôleur , pour Nom du contrôleur, entrez « BooksController ». Cochez la case « Utiliser des actions de contrôleur asynchrones ». Pour Classe de modèle, sélectionnez « Book ». (Si vous ne voyez pas la Book classe répertoriée dans la liste déroulante, assurez-vous que vous avez généré le projet.) Cliquez ensuite sur le bouton « + ».

Image de la boîte de dialogue Ajouter un contrôleur

Cliquez sur Ajouter dans la boîte de dialogue Nouveau contexte de données .

Image de la boîte de dialogue Nouveau contexte de données

Cliquez sur Ajouter dans la boîte de dialogue Ajouter un contrôleur . La structure ajoute une classe nommée BooksController qui définit le contrôleur d’API. Il ajoute également une classe nommée BooksAPIContext dans le dossier Models, qui définit le contexte de données pour Entity Framework.

Image de nouvelles classes

Amorcer la base de données

Dans le menu Outils, sélectionnez Gestionnaire de package NuGet, puis console du gestionnaire de package.

Dans la fenêtre Console du Gestionnaire de package, entrez la commande suivante :

Add-Migration

Cette commande crée un dossier Migrations et ajoute un nouveau fichier de code nommé Configuration.cs. Ouvrez ce fichier et ajoutez le code suivant à la Configuration.Seed méthode .

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},
    });
}

Dans la fenêtre Console du Gestionnaire de package, tapez les commandes suivantes.

add-migration Initial

update-database

Ces commandes créent une base de données locale et appellent la méthode Seed pour remplir la base de données.

Image de la console du Gestionnaire de package

Ajouter des classes DTO

Si vous exécutez l’application maintenant et que vous envoyez une requête GET à /api/books/1, la réponse ressemble à ce qui suit. (J’ai ajouté la mise en retrait pour plus de lisibilité.)

{
  "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
}

Au lieu de cela, je souhaite que cette demande retourne un sous-ensemble des champs. En outre, je souhaite qu’il retourne le nom de l’auteur, plutôt que l’ID d’auteur. Pour ce faire, nous allons modifier les méthodes du contrôleur pour renvoyer un objet de transfert de données (DTO) au lieu du modèle EF. Un DTO est un objet conçu uniquement pour transporter des données.

Dans Explorateur de solutions, cliquez avec le bouton droit sur le projet et sélectionnez Ajouter un | nouveau dossier. Nommez le dossier « DTO ». Ajoutez une classe nommée BookDto au dossier DTO, avec la définition suivante :

namespace BooksAPI.DTOs
{
    public class BookDto
    {
        public string Title { get; set; }
        public string Author { get; set; }
        public string Genre { get; set; }
    }
}

Ajoutez une autre classe nommée 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; }
    }
}

Ensuite, mettez à jour la BooksController classe pour retourner BookDto des instances. Nous allons utiliser la méthode Queryable.Select pour projeter Book des instances sur BookDto des instances. Voici le code mis à jour pour la classe de contrôleur.

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);
        }
    }
}

Notes

J’ai supprimé les PutBookméthodes , PostBooket DeleteBook , car elles ne sont pas nécessaires pour ce tutoriel.

À présent, si vous exécutez l’application et que vous demandez /api/books/1, le corps de la réponse doit ressembler à ceci :

{"Title":"Midnight Rain","Author":"Ralls, Kim","Genre":"Fantasy"}

Ajouter des attributs de route

Ensuite, nous allons convertir le contrôleur pour utiliser le routage d’attributs. Tout d’abord, ajoutez un attribut RoutePrefix au contrôleur. Cet attribut définit les segments d’URI initiaux pour toutes les méthodes de ce contrôleur.

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // ...

Ajoutez ensuite des attributs [Route] aux actions du contrôleur, comme suit :

[Route("")]
public IQueryable<BookDto> GetBooks()
{
    // ...
}

[Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
    // ...
}

Le modèle d’itinéraire pour chaque méthode de contrôleur est le préfixe plus la chaîne spécifiée dans l’attribut Route . Pour la GetBook méthode, le modèle d’itinéraire inclut la chaîne paramétrable « {id:int} », qui correspond à si le segment URI contient une valeur entière.

Méthode Modèle de routage Exemple d’URI
GetBooks « api/books » http://localhost/api/books
GetBook « api/books/{id:int} » http://localhost/api/books/5

Obtenir les détails du livre

Pour obtenir les détails du livre, le client envoie une requête GET à /api/books/{id}/details, où {id} est l’ID du livre.

Ajoutez la méthode suivante à la classe BooksController.

[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);
}

Si vous demandez /api/books/1/details, la réponse se présente comme suit :

{
  "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"
}

Obtenir des livres par genre

Pour obtenir la liste des livres d’un genre spécifique, le client envoie une demande GET à /api/books/genre, où genre est le nom du genre. (Par exemple, /api/books/fantasy.)

Ajoutez la méthode suivante à 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);
}

Ici, nous définissons un itinéraire qui contient un paramètre {genre} dans le modèle URI. Notez que l’API web est en mesure de distinguer ces deux URI et de les acheminer vers différentes méthodes :

/api/books/1

/api/books/fantasy

En effet, la GetBook méthode inclut une contrainte selon laquelle le segment « id » doit être une valeur entière :

[Route("{id:int}")] 
public BookDto GetBook(int id)
{
    // ... 
}

Si vous demandez /api/books/fantasy, la réponse ressemble à ceci :

[ { "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" } ]

Obtenir des livres par auteur

Pour obtenir la liste d’un livre pour un auteur particulier, le client envoie une requête GET à /api/authors/id/books, où id est l’ID de l’auteur.

Ajoutez la méthode suivante à 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);
}

Cet exemple est intéressant, car « livres » est traité comme une ressource enfant de « auteurs ». Ce modèle est assez courant dans les API RESTful.

Le tilde (~) dans le modèle d’itinéraire remplace le préfixe d’itinéraire dans l’attribut RoutePrefix .

Obtenir des livres par date de publication

Pour obtenir la liste des livres par date de publication, le client envoie une demande GET à /api/books/date/yyyy-mm-dd, où aaaa-mm-jj est la date.

Voici une façon de procéder :

[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);
}

Le {pubdate:datetime} paramètre est contraint de correspondre à une valeur DateTime . Cela fonctionne, mais c’est en fait plus permissif que nous le souhaiterions. Par exemple, ces URI correspondent également à l’itinéraire :

/api/books/date/Thu, 01 May 2008

/api/books/date/2000-12-16T00:00:00

Il n’y a rien de mal à autoriser ces URI. Toutefois, vous pouvez limiter l’itinéraire à un format particulier en ajoutant une contrainte d’expression régulière au modèle de route :

[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
    // ...
}

Désormais, seules les dates au format « aaaa-mm-jj » correspondent. Notez que nous n’utilisons pas le regex pour valider que nous avons une date réelle. Cela est géré lorsque l’API web tente de convertir le segment URI en un instance DateTime. Une date non valide telle que « 2012-47-99 » n’est pas convertie et le client obtient une erreur 404.

Vous pouvez également prendre en charge un séparateur de barre oblique (/api/books/date/yyyy/mm/dd) en ajoutant un autre attribut [Route] avec un autre regex.

[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)
{
    // ...
}

Il y a ici un détail subtil mais important. Le deuxième modèle d’itinéraire a un caractère générique (*) au début du paramètre {pubdate} :

{*pubdate: ... }

Cela indique au moteur de routage que {pubdate} doit correspondre au reste de l’URI. Par défaut, un paramètre de modèle correspond à un seul segment d’URI. Dans ce cas, nous voulons que {pubdate} s’étende sur plusieurs segments d’URI :

/api/books/date/2013/06/17

Code du contrôleur

Voici le code complet de la classe BooksController.

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);
        }
    }
}

Résumé

Le routage d’attributs vous offre plus de contrôle et plus de flexibilité lors de la conception des URI pour votre API.