Bagikan melalui


Membuat REST API dengan Perutean Atribut di ASP.NET Web API 2

Web API 2 mendukung jenis perutean baru, yang disebut perutean atribut. Untuk gambaran umum perutean atribut, lihat Perutean Atribut di Web API 2. Dalam tutorial ini, Anda akan menggunakan perutean atribut untuk membuat REST API untuk koleksi buku. API akan mendukung tindakan berikut:

Tindakan Contoh URI
Dapatkan daftar semua buku. /api/books
Dapatkan buku berdasarkan ID. /api/books/1
Dapatkan detail buku. /api/books/1/details
Dapatkan daftar buku berdasarkan genre. /api/books/fantasy
Dapatkan daftar buku berdasarkan tanggal publikasi. /api/books/date/2013-02-16 /api/books/date/2013/02/16 (formulir alternatif)
Dapatkan daftar buku oleh penulis tertentu. /api/authors/1/books

Semua metode bersifat baca-saja (permintaan HTTP GET).

Untuk lapisan data, kita akan menggunakan Kerangka Kerja Entitas. Rekaman buku akan memiliki bidang berikut:

  • ID
  • Judul
  • Genre
  • Tanggal publikasi
  • Harga
  • Deskripsi
  • AuthorID (kunci asing ke tabel Penulis)

Namun, untuk sebagian besar permintaan, API akan mengembalikan subset data ini (judul, penulis, dan genre). Untuk mendapatkan catatan lengkap, klien meminta /api/books/{id}/details.

Prasyarat

Visual Studio 2017 Edisi Komunitas, Profesional, atau Perusahaan.

Membuat Proyek Visual Studio

Mulailah dengan menjalankan Visual Studio. Dari menu File , pilih Baru lalu pilih Proyek.

Perluas kategoriVisual C#terinstal>. Di bawah Visual C#, pilih Web. Dalam daftar templat proyek, pilih Aplikasi Web ASP.NET (.NET Framework). Beri nama proyek "BooksAPI".

Gambar kotak dialog proyek baru

Dalam dialog Aplikasi Web ASP.NET Baru , pilih templat Kosong . Di bagian "Tambahkan folder dan referensi inti untuk", pilih kotak centang API Web . Klik OK.

Gambar dialog aplikasi web A S P dot Net baru

Ini membuat proyek kerangka yang dikonfigurasi untuk fungsionalitas Web API.

Model Domain

Selanjutnya, tambahkan kelas untuk model domain. Di Penjelajah Solusi, klik kanan folder Model. Pilih Tambahkan, lalu pilih Kelas. Beri nama kelas Author.

Gambar buat kelas baru

Ganti kode di Author.cs dengan yang berikut:

using System.ComponentModel.DataAnnotations;

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

Sekarang tambahkan kelas lain bernama 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; }
    }
}

Menambahkan Pengontrol API Web

Dalam langkah ini, kita akan menambahkan pengontrol API Web yang menggunakan Kerangka Kerja Entitas sebagai lapisan data.

Tekan CTRL+SHIFT+B untuk membangun proyek. Entity Framework menggunakan refleksi untuk menemukan properti model, sehingga memerlukan rakitan yang dikompilasi untuk membuat skema database.

Di Penjelajah Solusi, klik kanan folder Pengontrol. Pilih Tambahkan, lalu pilih Pengontrol.

Gambar tambahkan pengontrol

Dalam dialog Tambahkan Perancah , pilih Pengontrol API Web 2 dengan tindakan, menggunakan Kerangka Kerja Entitas.

Gambar tambahkan perancah

Dalam dialog Tambahkan Pengontrol , untuk Nama pengontrol, masukkan "BooksController". Pilih kotak centang "Gunakan tindakan pengontrol asinkron". Untuk Kelas model, pilih "Buku". (Jika Anda tidak melihat kelas yang Book tercantum di menu dropdown, pastikan Anda membuat proyek.) Kemudian klik tombol "+".

Gambar kotak dialog tambahkan pengontrol

Klik Tambahkan dalam dialog Konteks Data Baru .

Gambar kotak dialog konteks data baru

Klik Tambahkan dalam dialog Tambahkan Pengontrol . Perancah menambahkan kelas bernama BooksController yang mendefinisikan pengontrol API. Ini juga menambahkan kelas bernama BooksAPIContext di folder Model, yang menentukan konteks data untuk Kerangka Kerja Entitas.

Gambar kelas baru

Seed the Database

Dari menu Alat, pilih Manajer Paket NuGet, lalu pilih Konsol Manajer Paket.

Di jendela Konsol Manajer Paket, masukkan perintah berikut:

Add-Migration

Perintah ini membuat folder Migrasi dan menambahkan file kode baru bernama Configuration.cs. Buka file ini dan tambahkan kode berikut ke Configuration.Seed metode .

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

Di jendela Konsol Manajer Paket, ketik perintah berikut.

add-migration Initial

update-database

Perintah ini membuat database lokal dan memanggil metode Seed untuk mengisi database.

Gambar Konsol Manajer Paket

Menambahkan Kelas DTO

Jika Anda menjalankan aplikasi sekarang dan mengirim permintaan GET ke /api/books/1, responsnya terlihat mirip dengan yang berikut ini. (Saya menambahkan indentasi untuk keterbacaan.)

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

Sebaliknya, saya ingin permintaan ini mengembalikan subset bidang. Selain itu, saya ingin mengembalikan nama penulis, bukan ID penulis. Untuk mencapai hal ini, kami akan memodifikasi metode pengontrol untuk mengembalikan objek transfer data (DTO) alih-alih model EF. DTO adalah objek yang dirancang hanya untuk membawa data.

Di Penjelajah Solusi, klik kanan proyek dan pilih Tambahkan | Folder Baru. Beri nama folder "DTO". Tambahkan kelas bernama BookDto ke folder DTO, dengan definisi berikut:

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

Tambahkan kelas lain bernama 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; }
    }
}

Selanjutnya, perbarui BooksController kelas untuk mengembalikan BookDto instans. Kita akan menggunakan metode Queryable.Select untuk memproyeksikan Book instans ke BookDto instans. Berikut adalah kode yang diperbarui untuk kelas pengontrol.

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

Catatan

Saya menghapus PutBookmetode , PostBook, dan DeleteBook , karena tidak diperlukan untuk tutorial ini.

Sekarang jika Anda menjalankan aplikasi dan meminta /api/books/1, isi respons akan terlihat seperti ini:

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

Menambahkan Atribut Rute

Selanjutnya, kita akan mengonversi pengontrol untuk menggunakan perutean atribut. Pertama, tambahkan atribut RoutePrefix ke pengontrol. Atribut ini mendefinisikan segmen URI awal untuk semua metode pada pengontrol ini.

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

Kemudian tambahkan atribut [Route] ke tindakan pengontrol, sebagai berikut:

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

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

Templat rute untuk setiap metode pengontrol adalah awalan ditambah string yang ditentukan dalam atribut Route . Untuk metode , GetBook templat rute menyertakan string parameter "{id:int}", yang cocok jika segmen URI berisi nilai bilangan bulat.

Metode Templat Rute Contoh URI
GetBooks "api/buku" http://localhost/api/books
GetBook "api/books/{id:int}" http://localhost/api/books/5

Dapatkan Detail Buku

Untuk mendapatkan detail buku, klien akan mengirim permintaan GET ke /api/books/{id}/details, di mana {id} adalah ID buku.

Tambahkan metode berikut ke kelas 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);
}

Jika Anda meminta /api/books/1/details, responsnya terlihat seperti ini:

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

Dapatkan Buku Berdasarkan Genre

Untuk mendapatkan daftar buku dalam genre tertentu, klien akan mengirim permintaan GET ke /api/books/genre, di mana genre adalah nama genre. (Contoh, /api/books/fantasy.)

Tambahkan metode berikut ke 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);
}

Di sini kita menentukan rute yang berisi parameter {genre} dalam templat URI. Perhatikan bahwa Web API dapat membedakan kedua URI ini dan merutekannya ke metode yang berbeda:

/api/books/1

/api/books/fantasy

Itu karena GetBook metode menyertakan batasan bahwa segmen "id" harus menjadi nilai bilangan bulat:

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

Jika Anda meminta /api/books/fantasy, responsnya terlihat seperti ini:

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

Dapatkan Buku Menurut Penulis

Untuk mendapatkan daftar buku untuk penulis tertentu, klien akan mengirim permintaan GET ke /api/authors/id/books, di mana id adalah ID penulis.

Tambahkan metode berikut ke 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);
}

Contoh ini menarik karena "buku" diperlakukan sumber daya anak dari "penulis". Pola ini cukup umum dalam RESTful API.

Tilde (~) dalam templat rute mengambil alih awalan rute di atribut RoutePrefix .

Dapatkan Buku Berdasarkan Tanggal Publikasi

Untuk mendapatkan daftar buku berdasarkan tanggal publikasi, klien akan mengirim permintaan GET ke /api/books/date/yyyy-mm-dd, di mana yyyy-mm-dd adalah tanggal.

Berikut adalah salah satu cara untuk melakukan ini:

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

Parameter {pubdate:datetime} dibatasi untuk mencocokkan nilai DateTime . Ini berhasil, tapi sebenarnya lebih permisif daripada yang kita inginkan. Misalnya, URI ini juga akan cocok dengan rute:

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

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

Tidak ada yang salah dengan mengizinkan URI ini. Namun, Anda dapat membatasi rute ke format tertentu dengan menambahkan batasan ekspresi reguler ke templat rute:

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

Sekarang hanya tanggal dalam bentuk "yyyy-mm-dd" yang akan cocok. Perhatikan bahwa kami tidak menggunakan regex untuk memvalidasi bahwa kami mendapatkan tanggal nyata. Itu ditangani ketika Web API mencoba mengonversi segmen URI menjadi instans DateTime . Tanggal yang tidak valid seperti '2012-47-99' akan gagal dikonversi, dan klien akan mendapatkan kesalahan 404.

Anda juga dapat mendukung pemisah garis miring (/api/books/date/yyyy/mm/dd) dengan menambahkan atribut [Route] lain dengan regex yang berbeda.

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

Ada detail yang halang tetapi penting di sini. Templat rute kedua memiliki karakter kartubebas (*) di awal parameter {pubdate}:

{*pubdate: ... }

Ini memberi tahu mesin perutean bahwa {pubdate} harus cocok dengan URI lainnya. Secara default, parameter templat cocok dengan satu segmen URI. Dalam hal ini, kami ingin {pubdate} menjangkau beberapa segmen URI:

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

Kode Pengontrol

Berikut adalah kode lengkap untuk kelas 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);
        }
    }
}

Ringkasan

Perutean atribut memberi Anda lebih banyak kontrol dan fleksibilitas yang lebih besar saat merancang URI untuk API Anda.