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 proje iletişim kutusunun resmi

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.

Yeni A S P nokta Net web uygulaması iletişim kutusunun resmi

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ı Authoradlandırın.

Yeni sınıf oluşturma görüntüsü

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ı Bookbaş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.

Denetleyici ekleme görüntüsü

Yapı İskelesi Ekle iletişim kutusunda, Entity Framework kullanarak eylemlerle Web API 2 Denetleyicisi'ni seçin.

yapı iskelesi ekleme resmi

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.

Denetleyici ekle iletişim kutusunun resmi

Yeni Veri Bağlamı iletişim kutusunda Ekle'ye tıklayın.

Yeni veri bağlamı iletişim kutusunun resmi

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.

Yeni sınıfların resmi

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.

Paket Yöneticisi Konsolu görüntüsü

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ı BookDetailDtobaş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 PutBookgerekli olmadığından , PostBookve 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}/detailsbir 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/detailsbulunmanı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/genregönderir; burada tarz türün adıdır. (Örneğin, /api/books/fantasy.)

Aşağıdaki yöntemi öğesine BooksControllerekleyin.

[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/booksbir GET isteği gönderir; burada kimlik , yazarın kimliğidir.

Aşağıdaki yöntemi öğesine BooksControllerekleyin.

[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-ddbir 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.