Vytvoření rozhraní REST API se směrováním atributů ve webovém rozhraní API ASP.NET 2

Webové rozhraní API 2 podporuje nový typ směrování označovaný jako směrování atributů. Obecný přehled směrování atributů najdete v tématu Směrování atributů ve webovém rozhraní API 2. V tomto kurzu použijete směrování atributů k vytvoření rozhraní REST API pro kolekci knih. Rozhraní API bude podporovat následující akce:

Akce Příklad identifikátoru URI
Získejte seznam všech knih. /api/books
Získejte knihu podle ID. /api/books/1
Získejte podrobnosti o knize. /api/books/1/details
Získejte seznam knih podle žánru. /api/books/fantasy
Získání seznamu knih podle data publikace /api/books/date/2013-02-16 /api/books/date/2013/02/16 (alternativní formulář)
Získání seznamu knih od konkrétního autora /api/authors/1/books

Všechny metody jsou jen pro čtení (požadavky HTTP GET).

Pro datnou vrstvu použijeme Entity Framework. Záznamy knih budou obsahovat následující pole:

  • ID
  • Nadpis
  • Žánr
  • Datum publikování
  • Cena
  • Description
  • AuthorID (cizí klíč k tabulce Authors)

U většiny požadavků ale rozhraní API vrátí podmnožinu těchto dat (název, autor a žánr). K získání kompletního záznamu klient požádá ./api/books/{id}/details

Požadavky

Visual Studio 2017 Edice Community, Professional nebo Enterprise.

Vytvoření projektu sady Visual Studio

Začněte spuštěním sady Visual Studio. V nabídce File (Soubor) vyberte New (Nový) a pak vyberte Project (Projekt).

Rozbalte kategorii Installed Visual C# (Nainstalované>visual C# ). V části Visual C# vyberte Web. V seznamu šablon projektů vyberte ASP.NET Webová aplikace (.NET Framework). Pojmenujte projekt BooksAPI.

Obrázek dialogového okna Nový projekt

V dialogovém okně Nová webová aplikace ASP.NET vyberte šablonu Empty (Vyprázdnit ). V části Přidat složky a základní odkazy pro zaškrtněte políčko Webové rozhraní API . Klikněte na OK.

Obrázek nového dialogového okna webové aplikace A S P dot Net

Tím se vytvoří kostru projektu, který je nakonfigurovaný pro funkce webového rozhraní API.

Doménové modely

Dále přidejte třídy pro doménové modely. V Průzkumník řešení klikněte pravým tlačítkem na složku Modely. Vyberte Přidat a pak vyberte Třída. Pojmenujte třídu Author.

Obrázek vytvoření nové třídy

Nahraďte kód v souboru Author.cs následujícím kódem:

using System.ComponentModel.DataAnnotations;

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

Teď přidejte další třídu s názvem 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; }
    }
}

Přidání kontroleru webového rozhraní API

V tomto kroku přidáme kontroler webového rozhraní API, který jako datovou vrstvu používá Entity Framework.

Sestavte projekt stisknutím kombinace kláves CTRL+SHIFT+B. Entity Framework používá reflexi ke zjištění vlastností modelů, takže k vytvoření schématu databáze vyžaduje zkompilované sestavení.

V Průzkumník řešení klikněte pravým tlačítkem na složku Kontrolery. Vyberte Přidat a pak vyberte Kontroler.

Obrázek přidání kontroleru

V dialogovém okně Přidat uživatelské rozhraní vyberte Kontroler webového rozhraní API 2 s akcemi pomocí Entity Framework.

Obrázek přidání uživatelského rozhraní

V dialogovém okně Přidat kontroler jako Název kontroleru zadejte "BooksController". Zaškrtněte políčko Použít akce asynchronního kontroleru. V části Třída modelu vyberte "Kniha". (Pokud v rozevíracím Book seznamu nevidíte třídu, ujistěte se, že jste projekt vytvořili.) Potom klikněte na tlačítko +.

Obrázek dialogového okna přidat kontroler

V dialogovém okně Nový kontext dat klikněte na Přidat.

Obrázek dialogového okna Nový kontext dat

V dialogovém okně Přidat kontroler klikněte na Přidat. Generování přidá třídu s názvem BooksController , která definuje kontroler rozhraní API. Přidá také třídu s názvem BooksAPIContext do složky Models, která definuje kontext dat pro Entity Framework.

Obrázek nových tříd

Přidání dat do databáze

V nabídce Nástroje vyberte Správce balíčků NuGet a pak vyberte Konzola Správce balíčků.

V okně konzoly Správce balíčků zadejte následující příkaz:

Add-Migration

Tento příkaz vytvoří složku Migrations a přidá nový soubor kódu s názvem Configuration.cs. Otevřete tento soubor a do Configuration.Seed metody přidejte následující kód.

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

V okně Konzola Správce balíčků zadejte následující příkazy.

add-migration Initial

update-database

Tyto příkazy vytvoří místní databázi a vyvolají metodu Seed k naplnění databáze.

Obrázek konzoly Správce balíčků

Přidání tříd DTO

Pokud teď aplikaci spustíte a odešlete požadavek GET na adresu /api/books/1, bude odpověď vypadat nějak takto. (Kvůli čitelnosti jsem přidal odsazení.)

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

Místo toho chci, aby tento požadavek vrátil podmnožinu polí. Také chci, aby se místo ID autora vrátilo jméno autora. Abychom toho dosáhli, upravíme metody kontroleru tak, aby vracely objekt přenosu dat (DTO) místo modelu EF. Objekt DTO je objekt, který je určen pouze k přenosu dat.

V Průzkumník řešení klikněte pravým tlačítkem na projekt a vyberte Přidat | novou složku. Pojmenujte složku "DTO". Do složky DTOs přidejte třídu s názvem BookDto s následující definicí:

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

Přidejte další třídu s názvem 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; }
    }
}

Dále aktualizujte třídu tak BooksController , aby vracela BookDto instance. K promítání Book instancí do BookDto instancí použijeme metodu Queryable.Select. Tady je aktualizovaný kód třídy kontroleru.

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

Poznámka

Odstranil PutBook(a) jsem metody , PostBooka DeleteBook , protože pro tento kurz nejsou potřeba.

Pokud teď spustíte aplikaci a požádáte o /api/books/1, text odpovědi by měl vypadat takto:

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

Přidání atributů trasy

Dále převedeme kontroler na použití směrování atributů. Nejprve přidejte atribut RoutePrefix do kontroleru. Tento atribut definuje počáteční segmenty identifikátoru URI pro všechny metody v tomto kontroleru.

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

Potom do akcí kontroleru přidejte atributy [Route] následujícím způsobem:

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

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

Šablona trasy pro každou metodu kontroleru je předpona plus řetězec zadaný v atributu Route . Šablona trasy pro metodu GetBook obsahuje parametrizovaný řetězec {id:int}, který odpovídá, pokud segment identifikátoru URI obsahuje celočíselnou hodnotu.

Metoda Šablona trasy Příklad identifikátoru URI
GetBooks "api/books" http://localhost/api/books
GetBook "api/books/{id:int}" http://localhost/api/books/5

Získat podrobnosti o knize

Pokud chcete získat podrobnosti o knize, odešle klient požadavek GET na /api/books/{id}/detailsadresu , kde {id} je ID knihy.

Do třídy přidejte následující metodu 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);
}

Pokud požadujete /api/books/1/details, bude odpověď vypadat takto:

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

Získat knihy podle žánru

Pokud chcete získat seznam knih v určitém žánru, odešle klient požadavek GET na /api/books/genreadresu , kde žánr je název žánru. (Například /api/books/fantasy.)

Přidejte následující metodu do 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);
}

Tady definujeme trasu, která v šabloně identifikátoru URI obsahuje parametr {žánr}. Všimněte si, že webové rozhraní API dokáže rozlišit tyto dvě identifikátory URI a směrovat je na různé metody:

/api/books/1

/api/books/fantasy

Je to proto, že GetBook metoda obsahuje omezení, že segment "id" musí být celočíselná hodnota:

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

Pokud požádáte o /api/books/fantasy, bude odpověď vypadat takto:

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

Získat knihy podle autora

Pokud chcete získat seznam knih pro konkrétního autora, odešle klient požadavek GET na /api/authors/id/booksadresu , kde ID je ID autora.

Přidejte následující metodu do 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);
}

Tento příklad je zajímavý, protože "books" je považován za podřízený zdroj "autorů". Tento model je v rozhraních RESTful API poměrně běžný.

Vlnovka (~) v šabloně trasy přepíše předponu trasy v atributu RoutePrefix .

Získání knih podle data publikace

Pokud chcete získat seznam knih podle data publikace, odešle klient požadavek GET na /api/books/date/yyyy-mm-ddadresu , kde yy-mm-dd je datum.

Tady je jeden způsob, jak to udělat:

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

Parametr {pubdate:datetime} je omezen tak, aby odpovídal hodnotě DateTime . To funguje, ale ve skutečnosti je to více promisivní, než bychom chtěli. Například tyto identifikátory URI se budou shodovat také s trasou:

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

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

Na povolení těchto identifikátorů URI není nic špatného. Trasu ale můžete omezit na určitý formát přidáním omezení regulárního výrazu do šablony trasy:

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

Nyní se budou shodovat pouze data ve tvaru "yy-mm-dd". Všimněte si, že nepoužíváme regex k ověření, že jsme získali skutečné datum. To se zpracuje, když se webové rozhraní API pokusí převést segment identifikátoru URI na instanci DateTime . Neplatné datum, například 2012-47-99, se nepodaří převést a klientovi se zobrazí chyba 404.

Oddělovač lomítek (/api/books/date/yyyy/mm/dd) můžete také podporovat přidáním dalšího atributu [Route] s jiným výrazem regulárního výrazu.

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

Je tu drobný, ale důležitý detail. Druhá šablona trasy má na začátku parametru {pubdate} zástupný znak (*):

{*pubdate: ... }

To říká směrovacímu modulu, že {pubdate} by se měl shodovat se zbytkem identifikátoru URI. Ve výchozím nastavení parametr šablony odpovídá jednomu segmentu identifikátoru URI. V tomto případě chceme, aby {pubdate} pokrývá několik segmentů identifikátoru URI:

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

Kód kontroleru

Zde je úplný kód pro Třídu 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);
        }
    }
}

Souhrn

Směrování atributů poskytuje větší kontrolu a větší flexibilitu při navrhování identifikátorů URI pro vaše rozhraní API.