Freigeben über


Erstellen einer REST-API mit Attributrouting in ASP.NET-Web-API 2

Web-API 2 unterstützt eine neue Art von Routing, die als Attributrouting bezeichnet wird. Eine allgemeine Übersicht über das Attributrouting finden Sie unter Attributrouting in Web-API 2. In diesem Tutorial erstellen Sie mithilfe des Attributroutings eine REST-API für eine Sammlung von Büchern. Die API unterstützt die folgenden Aktionen:

Action Beispiel-URI
Rufen Sie eine Liste aller Bücher ab. /api/books
Rufen Sie ein Buch nach ID ab. /api/books/1
Rufen Sie die Details eines Buches ab. /api/books/1/details
Rufen Sie eine Liste von Büchern nach Genre ab. /api/books/fantasy
Rufen Sie eine Liste der Bücher nach Veröffentlichungsdatum ab. /api/books/date/2013-02-16 /api/books/date/2013/02/16 (alternative Form)
Rufen Sie eine Liste der Bücher eines bestimmten Autors ab. /api/authors/1/books

Alle Methoden sind schreibgeschützt (HTTP GET-Anforderungen).

Für die Datenebene verwenden wir Entity Framework. Buchdatensätze weisen die folgenden Felder auf:

  • ID
  • Titel
  • Genre
  • Veröffentlichungsdatum
  • Preis
  • BESCHREIBUNG
  • AuthorID (Fremdschlüssel für eine Authors-Tabelle)

Bei den meisten Anforderungen gibt die API jedoch eine Teilmenge dieser Daten (Titel, Autor und Genre) zurück. Um den vollständigen Datensatz abzurufen, fordert der Client an /api/books/{id}/details.

Voraussetzungen

Visual Studio 2017 Community-, Professional- oder Enterprise-Edition.

Erstellen des Visual Studio-Projekts

Führen Sie zuerst Visual Studio 2013 aus. Wählen Sie im Menü Datei die Option Neuaus, und wählen Sie dann Projekt aus.

Erweitern Sie die Kategorie VisualC#-Installation>. Wählen Sie unter Visual C#die Option Web aus. Wählen Sie in der Liste der Projektvorlagen ASP.NET Webanwendung (.NET Framework) aus. Nennen Sie das Projekt "BooksAPI".

Abbildung des Dialogfelds

Wählen Sie im Dialogfeld Neue ASP.NET Webanwendung die Vorlage Leere aus. Aktivieren Sie unter "Ordner und Kernverweise hinzufügen für" das Kontrollkästchen Web-API . Klicken Sie auf OK.

Abbildung des neuen Dialogfelds

Dadurch wird ein Skelettprojekt erstellt, das für Web-API-Funktionen konfiguriert ist.

Domänenmodelle

Fügen Sie als Nächstes Klassen für Domänenmodelle hinzu. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Ordner „Modelle“. Wählen Sie Hinzufügen und dann Klasse aus. Geben Sie der Klassen den Namen Author.

Image der neuen Klasse erstellen

Ersetzen Sie den Code in Author.cs durch Folgendes:

using System.ComponentModel.DataAnnotations;

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

Fügen Sie nun eine weitere Klasse mit dem Namen hinzu 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; }
    }
}

Hinzufügen eines Web-API-Controllers

In diesem Schritt fügen wir einen Web-API-Controller hinzu, der Entity Framework als Datenebene verwendet.

Drücken Sie STRG+UMSCHALT+B, um das Projekt zu erstellen. Entity Framework verwendet Reflektion, um die Eigenschaften der Modelle zu ermitteln, sodass eine kompilierte Assembly zum Erstellen des Datenbankschemas erforderlich ist.

Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Ordner „Controller“. Wählen Sie Hinzufügen und dann Controller aus.

Abbildung des Add-Controllers

Wählen Sie im Dialogfeld Gerüst hinzufügendie Option Web-API 2 Controller mit Aktionen mithilfe von Entity Framework aus.

Abbildung des Gerüsts hinzufügen

Geben Sie im Dialogfeld Controller hinzufügen unter Controllername "BooksController" ein. Aktivieren Sie das Kontrollkästchen "Asynchrone Controlleraktionen verwenden". Wählen Sie unter Model-Klasse die Option "Buch" aus. (Wenn die Klasse in der Book Dropdownliste nicht aufgeführt wird, stellen Sie sicher, dass Sie das Projekt erstellt haben.) Klicken Sie dann auf die Schaltfläche "+".

Abbildung des Dialogfelds

Klicken Sie im Dialogfeld Neuer Datenkontext auf Hinzufügen.

Abbildung des Dialogfelds

Klicken Sie im Dialogfeld Controller hinzufügen auf Hinzufügen. Das Gerüst fügt eine Klasse mit dem Namen hinzu BooksController , die den API-Controller definiert. Außerdem wird eine Klasse mit dem Namen BooksAPIContext im Ordner Models hinzugefügt, die den Datenkontext für Entity Framework definiert.

Abbildung neuer Klassen

Durchführen eines Datenbankseedings

Wählen Sie im Menü Extras die Option NuGet-Paket-Manager und dann Paket-Manager-Konsole aus.

Geben Sie im Fenster Paket-Manager-Konsole den folgenden Befehl ein:

Add-Migration

Mit diesem Befehl wird ein Migrationsordner erstellt und eine neue Codedatei mit dem Namen Configuration.cs hinzugefügt. Öffnen Sie diese Datei, und fügen Sie der -Methode den Configuration.Seed folgenden Code hinzu.

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

Geben Sie im Fenster Paket-Manager-Konsole die folgenden Befehle ein.

add-migration Initial

update-database

Mit diesen Befehlen wird eine lokale Datenbank erstellt und die Seed-Methode aufgerufen, um die Datenbank aufzufüllen.

Abbildung der Paket-Manager-Konsole

Hinzufügen von DTO-Klassen

Wenn Sie die Anwendung jetzt ausführen und eine GET-Anforderung an /api/books/1 senden, sieht die Antwort wie folgt aus. (Aus Gründen der Lesbarkeit habe ich Einzug hinzugefügt.)

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

Stattdessen soll diese Anforderung eine Teilmenge der Felder zurückgeben. Außerdem soll der Name des Autors und nicht die Autoren-ID zurückgegeben werden. Um dies zu erreichen, ändern wir die Controllermethoden so, dass ein DTO (Data Transfer Object ) anstelle des EF-Modells zurückgegeben wird. Ein DTO ist ein Objekt, das nur zum Übertragen von Daten konzipiert ist.

Klicken Sie in Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, und wählen SieNeuen Ordnerhinzufügen | aus. Nennen Sie den Ordner "DTOs". Fügen Sie dem ORDNER DTOs eine Klasse mit der BookDto folgenden Definition hinzu:

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

Fügen Sie eine weitere Klasse namens BookDetailDtohinzu.

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

Aktualisieren Sie als Nächstes die BooksController -Klasse, um Instanzen zurückzugeben BookDto . Wir verwenden die Queryable.Select-Methode, um Instanzen in Instanzen zu BookDto projizierenBook. Hier ist der aktualisierte Code für die Controllerklasse.

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

Hinweis

Ich habe die PutBookMethoden , PostBookund DeleteBook gelöscht, da sie für dieses Tutorial nicht benötigt werden.

Wenn Sie nun die Anwendung ausführen und /api/books/1 anfordern, sollte der Antworttext wie folgt aussehen:

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

Hinzufügen von Routenattributen

Als Nächstes konvertieren wir den Controller in die Verwendung des Attributroutings. Fügen Sie zunächst dem Controller ein RoutePrefix-Attribut hinzu. Dieses Attribut definiert die anfänglichen URI-Segmente für alle Methoden auf diesem Controller.

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

Fügen Sie dann [Route] -Attribute wie folgt zu den Controlleraktionen hinzu:

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

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

Die Routenvorlage für jede Controllermethode ist das Präfix und die im Route-Attribut angegebene Zeichenfolge. Für die GetBook -Methode enthält die Routenvorlage die parametrisierte Zeichenfolge "{id:int}", die übereinstimmt, wenn das URI-Segment einen ganzzahligen Wert enthält.

Methode Routenvorlage Beispiel-URI
GetBooks "api/books" http://localhost/api/books
GetBook "api/books/{id:int}" http://localhost/api/books/5

Buchdetails abrufen

Um Buchdetails abzurufen, sendet der Client eine GET-Anforderung an /api/books/{id}/details, wobei {id} die ID des Buches ist.

Fügen Sie der BooksController-Klasse die folgende Methode hinzu.

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

Wenn Sie anfordern /api/books/1/details, sieht die Antwort wie folgt aus:

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

Bücher nach Genre abrufen

Um eine Liste von Büchern in einem bestimmten Genre abzurufen, sendet der Client eine GET-Anforderung an /api/books/genre, wobei Genre der Name des Genres ist. (Beispiel: /api/books/fantasy.)

Fügen Sie die folgende Methode hinzu 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);
}

Hier definieren wir eine Route, die einen {genre}-Parameter in der URI-Vorlage enthält. Beachten Sie, dass die Web-API in der Lage ist, diese beiden URIs zu unterscheiden und sie an verschiedene Methoden weiterzuleiten:

/api/books/1

/api/books/fantasy

Das liegt daran, dass die GetBook -Methode eine Einschränkung enthält, dass das "id"-Segment ein ganzzahliger Wert sein muss:

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

Wenn Sie /api/books/fantasy anfordern, sieht die Antwort wie folgt aus:

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

Bücher nach Autor abrufen

Um eine Liste mit büchern für einen bestimmten Autor abzurufen, sendet der Client eine GET-Anforderung an /api/authors/id/books, wobei id die ID des Autors ist.

Fügen Sie die folgende Methode hinzu 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);
}

Dieses Beispiel ist interessant, da "Bücher" als untergeordnete Ressource von "Autoren" behandelt wird. Dieses Muster ist in RESTful-APIs sehr häufig.

Die Tilde (~) in der Routenvorlage überschreibt das Routenpräfix im RoutePrefix-Attribut .

Bücher nach Veröffentlichungsdatum abrufen

Um eine Liste der Bücher nach Veröffentlichungsdatum abzurufen, sendet der Client eine GET-Anforderung an /api/books/date/yyyy-mm-dd, wobei jjjj-mm-tt das Datum ist.

Hier ist eine Möglichkeit, dies zu tun:

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

Der {pubdate:datetime} Parameter ist auf die Übereinstimmung mit einem DateTime-Wert beschränkt. Das funktioniert, ist aber tatsächlich freizügiger, als wir uns wünschen. Diese URIs stimmen beispielsweise auch mit der Route überein:

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

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

Es ist nichts Falsch daran, diese URIs zuzulassen. Sie können die Route jedoch auf ein bestimmtes Format beschränken, indem Sie der Routenvorlage eine Einschränkung für reguläre Ausdrücke hinzufügen:

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

Jetzt werden nur Datumsangaben in der Form "jjjj-mm-tt" übereinstimmen. Beachten Sie, dass wir den Regex nicht verwenden, um zu überprüfen, ob wir ein echtes Datum erhalten haben. Dies wird behandelt, wenn die Web-API versucht, das URI-Segment in eine DateTime-instance zu konvertieren. Ein ungültiges Datum wie "2012-47-99" kann nicht konvertiert werden, und der Client erhält den Fehler 404.

Sie können auch ein Schrägstrichtrennzeichen (/api/books/date/yyyy/mm/dd) unterstützen, indem Sie ein anderes [Route] -Attribut mit einem anderen Regex hinzufügen.

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

Hier gibt es ein subtiles, aber wichtiges Detail. Die zweite Routenvorlage weist am Anfang des {pubdate}-Parameters das Platzhalterzeichen (*) auf:

{*pubdate: ... }

Dadurch wird der Routing-Engine mitgeteilt, dass {pubdate} mit dem restlichen URI übereinstimmen soll. Standardmäßig entspricht ein Vorlagenparameter einem einzelnen URI-Segment. In diesem Fall soll sich {pubdate} über mehrere URI-Segmente erstrecken:

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

Controllercode

Hier ist der vollständige Code für die BooksController-Klasse.

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

Zusammenfassung

Attributrouting bietet Ihnen mehr Kontrolle und mehr Flexibilität beim Entwerfen der URIs für Ihre API.