ASP.NET Web API 2'de Öznitelik Yönlendirme ile REST API oluşturma
Web API 2, öznitelik yönlendirme olarak adlandırılan yeni bir yönlendirme türünü destekler. Öznitelik yönlendirmeye genel bir genel bakış için bkz. Web API 2'de Öznitelik Yönlendirme. Bu öğreticide, bir kitap koleksiyonu için REST API oluşturmak üzere öznitelik yönlendirmeyi kullanacaksınız. API aşağıdaki eylemleri destekleyecektir:
Eylem | Örnek URI |
---|---|
Tüm kitapların listesini alın. | /api/books |
Kimliğine göre bir kitap al. | /api/books/1 |
Bir kitabın ayrıntılarını alın. | /api/books/1/details |
Türe göre kitapların listesini alın. | /api/kitaplar/fantezi |
Yayın tarihine göre kitapların listesini alın. | /api/books/date/2013-02-16 /api/books/date/2013/02/16 (alternatif form) |
Belirli bir yazarın kitap listesini alın. | /api/authors/1/books |
Tüm yöntemler salt okunur (HTTP GET istekleri).
Veri katmanı için Entity Framework kullanacağız. Kitap kayıtları aşağıdaki alanlara sahip olacaktır:
- ID
- Başlık
- Tür
- Yayın tarihi
- Fiyat
- Description
- AuthorID (Yazarlar tablosunun yabancı anahtarı)
Ancak çoğu istek için API bu verilerin bir alt kümesini (başlık, yazar ve tarz) döndürür. Kaydın tamamını almak için istemci isteğinde bulunur /api/books/{id}/details
.
Önkoşullar
Visual Studio 2017 Community, Professional veya Enterprise sürümü.
Visual Studio Projesi Oluşturma
Visual Studio'yu çalıştırarak başlayın. Dosyamenüsünden Yeni'yi ve ardından Proje'yi seçin.
Yüklü>Visual C# kategorisini genişletin. Visual C# altında Web'i seçin. Proje şablonları listesinde web uygulaması (.NET Framework) ASP.NET seçin. Projeyi "BooksAPI" olarak adlandırın.
Yeni ASP.NET Web Uygulaması iletişim kutusunda Boş şablonu seçin. "Klasör ve çekirdek başvuruları ekle" altında Web API'sini onay kutusunu seçin. Tamam'a tıklayın.
Bu, Web API işlevselliği için yapılandırılmış bir iskelet proje oluşturur.
Etki Alanı Modelleri
Ardından, etki alanı modelleri için sınıflar ekleyin. Çözüm Gezgini'da Modeller klasörüne sağ tıklayın. Ekle'yi ve ardından Sınıf'ı seçin. sınıfını Author
adlandırın.
Author.cs dosyasındaki kodu aşağıdakilerle değiştirin:
using System.ComponentModel.DataAnnotations;
namespace BooksAPI.Models
{
public class Author
{
public int AuthorId { get; set; }
[Required]
public string Name { get; set; }
}
}
Şimdi adlı Book
başka bir sınıf ekleyin.
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; }
}
}
Web API Denetleyicisi Ekleme
Bu adımda, veri katmanı olarak Entity Framework kullanan bir Web API denetleyicisi ekleyeceğiz.
Projeyi oluşturmak için CTRL+SHIFT+B tuşlarına basın. Entity Framework, modellerin özelliklerini bulmak için yansıma kullanır, bu nedenle veritabanı şemasını oluşturmak için derlenmiş bir derleme gerektirir.
Çözüm Gezgini'da Denetleyiciler klasörüne sağ tıklayın. Ekle'yi ve ardından Denetleyici'yi seçin.
Yapı İskelesi Ekle iletişim kutusunda, Entity Framework kullanarak eylemlerle Web API 2 Denetleyicisi'ni seçin.
Denetleyici Ekle iletişim kutusundaki Denetleyici adı alanına "BooksController" yazın. "Zaman uyumsuz denetleyici eylemlerini kullan" onay kutusunu seçin. Model sınıfı için "Kitap" öğesini seçin. (Açılan listede sınıfı görmüyorsanız Book
projeyi oluşturduğunuzdan emin olun.) Ardından "+" düğmesine tıklayın.
Yeni Veri Bağlamı iletişim kutusunda Ekle'ye tıklayın.
Denetleyici Ekle iletişim kutusunda Ekle'ye tıklayın. yapı iskelesi, API denetleyicisini tanımlayan adlı BooksController
bir sınıf ekler. Ayrıca, Entity Framework için veri bağlamını tanımlayan Models klasörüne adlı BooksAPIContext
bir sınıf ekler.
Veritabanının Çekirdeğini Oluşturma
Araçlar menüsünde NuGet Paket Yöneticisi'ni ve ardından Paket Yöneticisi Konsolu'nu seçin.
Paket Yöneticisi Konsolu penceresinde aşağıdaki komutu girin:
Add-Migration
Bu komut bir Migrations klasörü oluşturur ve Configuration.cs adlı yeni bir kod dosyası ekler. Bu dosyayı açın ve yöntemine Configuration.Seed
aşağıdaki kodu ekleyin.
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},
});
}
Paket Yöneticisi Konsolu penceresinde aşağıdaki komutları yazın.
add-migration Initial
update-database
Bu komutlar yerel bir veritabanı oluşturur ve veritabanını doldurmak için Seed yöntemini çağırır.
DTO Sınıfları Ekleme
Uygulamayı şimdi çalıştırır ve /api/books/1'e bir GET isteği gönderirseniz, yanıt aşağıdakine benzer şekilde görünür. (Okunabilirlik için girinti ekledim.)
{
"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
}
Bunun yerine, bu isteğin alanların bir alt kümesini döndürmesini istiyorum. Ayrıca, yazar kimliği yerine yazarın adını döndürmesini istiyorum. Bunu başarmak için denetleyici yöntemlerini EF modeli yerine bir veri aktarım nesnesi (DTO) döndürecek şekilde değiştireceğiz. DTO, yalnızca veri taşımak için tasarlanmış bir nesnedir.
Çözüm Gezgini'da projeye sağ tıklayın veYeni Klasör Ekle'yi | seçin. Klasörü "DTO'lar" olarak adlandırın. Aşağıdaki tanımı kullanarak DTO klasörüne adlı BookDto
bir sınıf ekleyin:
namespace BooksAPI.DTOs
{
public class BookDto
{
public string Title { get; set; }
public string Author { get; set; }
public string Genre { get; set; }
}
}
adlı BookDetailDto
başka bir sınıf ekleyin.
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; }
}
}
Ardından sınıfını BooksController
örnekleri döndürecek BookDto
şekilde güncelleştirin. Örnekleri örneklere BookDto
yansıtmak Book
için Queryable.Select yöntemini kullanacağız. Denetleyici sınıfı için güncelleştirilmiş kod aşağıdadır.
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);
}
}
}
Not
Bu öğretici için PutBook
gerekli olmadığından , PostBook
ve DeleteBook
yöntemlerini sildim.
Şimdi uygulamayı çalıştırır ve /api/books/1 isteğinde bulunursanız yanıt gövdesi şu şekilde görünmelidir:
{"Title":"Midnight Rain","Author":"Ralls, Kim","Genre":"Fantasy"}
Yol Öznitelikleri Ekleme
Ardından, denetleyiciyi öznitelik yönlendirmesini kullanacak şekilde dönüştüreceğiz. İlk olarak, denetleyiciye bir RoutePrefix özniteliği ekleyin. Bu öznitelik, bu denetleyicideki tüm yöntemler için ilk URI kesimlerini tanımlar.
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
// ...
Ardından denetleyici eylemlerine aşağıdaki gibi [Route] öznitelikleri ekleyin:
[Route("")]
public IQueryable<BookDto> GetBooks()
{
// ...
}
[Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
// ...
}
Her denetleyici yöntemi için yol şablonu ön ekinin yanı sıra Route özniteliğinde belirtilen dizedir. GetBook
yöntemi için yol şablonu, URI kesimi bir tamsayı değeri içeriyorsa eşleşen parametreli "{id:int}" dizesini içerir.
Yöntem | Yol Şablonu | Örnek URI |
---|---|---|
GetBooks |
"api/books" | http://localhost/api/books |
GetBook |
"api/books/{id:int}" | http://localhost/api/books/5 |
Kitap Ayrıntılarını Alma
Kitap ayrıntılarını almak için, istemci adresine /api/books/{id}/details
bir GET isteği gönderir; burada {id} kitabın kimliğidir.
Sınıfına aşağıdaki yöntemi BooksController
ekleyin.
[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);
}
isteğinde /api/books/1/details
bulunmanız durumunda yanıt şu şekilde görünür:
{
"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"
}
Türe Göre Kitap Edinin
Belirli bir türdeki kitapların listesini almak için, müşteri adresine bir GET isteği /api/books/genre
gönderir; burada tarz türün adıdır. (Örneğin, /api/books/fantasy
.)
Aşağıdaki yöntemi öğesine BooksController
ekleyin.
[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);
}
Burada URI şablonunda {genre} parametresini içeren bir yol tanımlıyoruz. Web API'lerinin bu iki URI'yi ayırt edebildiğini ve bunları farklı yöntemlere yönlendirdiğini görebilirsiniz:
/api/books/1
/api/books/fantasy
Bunun nedeni, yönteminin GetBook
"id" kesiminin bir tamsayı değeri olması gereken bir kısıtlama içermesidir:
[Route("{id:int}")]
public BookDto GetBook(int id)
{
// ...
}
/api/books/fantasy isteğinde bulunmak isterseniz yanıt şu şekilde görünür:
[ { "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" } ]
Yazara Göre Kitap Alma
Belirli bir yazarın kitap listesini almak için, istemci adresine /api/authors/id/books
bir GET isteği gönderir; burada kimlik , yazarın kimliğidir.
Aşağıdaki yöntemi öğesine BooksController
ekleyin.
[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);
}
Bu örnek ilginçtir çünkü "kitaplar" "yazarların" alt kaynağı olarak ele alınır. Bu düzen RESTful API'lerinde oldukça yaygındır.
Yol şablonundaki tilde (~), RoutePrefix özniteliğindeki yol ön ekini geçersiz kılar.
Yayın Tarihine Göre Kitap Al
Kitap listesini yayın tarihine göre almak için, istemci adresine /api/books/date/yyyy-mm-dd
bir GET isteği gönderir; burada yyyy-aa-gg tarihtir.
Bunu şu şekilde yapabilirsiniz:
[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);
}
{pubdate:datetime}
parametresi bir DateTime değeriyle eşleşecek şekilde kısıtlanmış. Bu işe yarıyor ama aslında istediğimizden daha izinli. Örneğin, bu URI'ler şu rotayla da eşleşecektir:
/api/books/date/Thu, 01 May 2008
/api/books/date/2000-12-16T00:00:00
Bu URI'lere izin vermenin yanlış bir yanı yoktur. Ancak, yol şablonuna normal ifade kısıtlaması ekleyerek yolu belirli bir biçimle kısıtlayabilirsiniz:
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
// ...
}
Artık yalnızca "y-aa-gg" biçimindeki tarihler eşleşir. Gerçek bir tarih aldığımızı doğrulamak için regex kullanmadığımıza dikkat edin. Bu, Web API'sinin URI kesimini bir DateTime örneğine dönüştürmeye çalışması halinde işlenir. '2012-47-99' gibi geçersiz bir tarih dönüştürülemiyor ve istemci 404 hatası alacak.
Farklı bir regex ile başka bir [Route] özniteliği ekleyerek de eğik çizgi ayırıcısını (/api/books/date/yyyy/mm/dd
) destekleyebilirsiniz.
[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)
{
// ...
}
Burada ince ama önemli bir ayrıntı var. İkinci yol şablonunun {pubdate} parametresinin başında joker karakter (*) var:
{*pubdate: ... }
Bu, yönlendirme altyapısına {pubdate} öğesinin URI'nin geri kalanıyla eşleşmesi gerektiğini bildirir. Varsayılan olarak, şablon parametresi tek bir URI kesimiyle eşleşir. Bu durumda{ pubdate} öğesinin birkaç URI segmentine yayılmasını istiyoruz:
/api/books/date/2013/06/17
Denetleyici Kodu
BooksController sınıfının tam kodu aşağıdadır.
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);
}
}
}
Özet
Öznitelik yönlendirme, API'niz için URI'leri tasarlarken daha fazla denetim ve daha fazla esneklik sağlar.