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".
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.
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
.
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.
Wählen Sie im Dialogfeld Gerüst hinzufügendie Option Web-API 2 Controller mit Aktionen mithilfe von Entity Framework aus.
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 "+".
Klicken Sie im Dialogfeld Neuer Datenkontext auf Hinzufügen.
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.
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.
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 BookDetailDto
hinzu.
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 PutBook
Methoden , PostBook
und 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.