آموزش هوش مصنوعی از صفر ، بدون پیش‌نیاز آموزش هوش مصنوعی ، از صفر بدون پیش‌نیاز!
🎯 شروع یادگیری

آموزش ساخت REST API با Attribute Routing در ASP.NET Web API

ساختن REST API با Attribute Routing در ASP.NET Web API 2

همانطور که در مقاله قبل توضیح دادیم، Web API 2 یک نوع جدید از Routing را با نام Attribute Routing معرفی کرده است. در این مقاله قصد داریم بااستفاده از Attribute Routing ، REST API برای یک مجموعه از کتاب (book)ها بسازیم. این API شامل action های زیر می شود:

مثالی از URI
Action
/api/books
دریافت لیست همه ی کتاب ها
/api/books/1
دریافت کتاب با ID
/api/books/1/details
دریافت جزئیات یک کتاب
/api/books/fantasy
دریافت لیستی از کتاب ها بر اساس ژانر
/api/books/date/2013-02-16
/api/books/date/2013/02/16
دریافت لیست کتاب ها بر اساس تاریخ انتشار
/api/authors/1/books
دریافت لیستی از کتاب ها بر اساس یک نویسنده خاص

همه ی متد ها Read-only (درخواست HTTP GET) هستند.

در این مقاله از entity framework استفاده می کنیم و اطلاعات book در به صورت زیر می باشد:

  • ID
  • عنوان
  • ژانر
  • تاریخ چاپ
  • قیمت
  • توضیحات
  • ID نویسنده (کلید فرعی به یک جدول نویسنده ها)

در بیشتر درخواست ها، API یک زیرمجموعه از این داده ها(عنوان، نویسنده و ژانر) بر می گرداند. برای دریافت همه ی سوابق، کاربر باید /api/books/{id}/details را درخواست کند.

پیش نیاز ها

Visual studio 2013 یا visual studio express 2013

ساختن یک پروژه در visual studio

Visual studio را اجرا کنید. در منو File روی New و سپس Project کلیک کنید.

در قسمت Templates، Installed Templates را انتخاب و گره Visual C# را باز کنید. زیر Visual C#، Web را انتخاب کنید. در لیست Template های پروژه، ASP.NET MVC 4 Web Application را انتخاب کنید و نام آن را BooksAPI قرار دهید.

آموزش Web Api

در پنجره New ASP.NET Project، گزینه Empty را انتخاب کنید. زیر Add folders and core references for، گزینه Web API را انتخاب کنید و Create Project را کلیک کنید. این کار اسکلت پروژه را پی ریزی می کند.

آموزش Web Api

Domain Modelها

در مرحله بعد، به domain model ها کلاس اضافه می کنیم. در Solution Explorer روی فولدر models کلیک راست کنید،ابتدا Add را و سپس Class را انتخاب کنید و نام آن را Author قرار دهید.

آموزش Web Api

کد های درون Author.cs را با کدهای زیر جایگزین کنید:

1
2
3
4
5
6
7
8
9
10
11
using System.ComponentModel.DataAnnotations;
namespace BooksAPI.Models
{
   public class Author
   {
       public int AuthorId { get;set;}
       [Required]
       public string Name { get;set;}
   }
}
<button></button>

یک کلاس دیگر را ایجاد کنید و نام آن را Book قرار دهید.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;}
   }
}
<button></button>

اضافه کردن یک Web API Controller

در این قسمت، یک کنترولر Web API اضافه می کنیم که از Entity Framework استفاده می کند. کلید های CTRL+SHIFT+B را با هم فشار دهید تا پروژه Build شود.Entity Framework از reflection برای یافتن خاصیت های مدل ها استفاده می کند، برای همین به یک کامپایل جهت ساخت شمای پایگاه داده نیاز دارد.

در Solution Explorer روی فولدر controller راست کلیک کنید و Add را انتخاب و سپس controller را انتخاب کنید.

آموزش Web Api

در پنجره Add Scaffold، Web API 2 Controller with read/write actions, using Entity Framework را انتخاب کنید.

آموزش Web Api

در پنجره Add Controller نام BooksController را بنویسید و گزینه Use async controller actions را انتخاب کنید. برای Model class، Book را انتخاب کنید (اگر در dropdown کلاس Book را پیدا نکردید مطمئن شوید که پروژه را Build کرده باشید) سپس روی دکمه New data context کلیک کنید.

آموزش Web Api

در پنجره New Data Context روی Add کلیک کنید.

آموزش Web Api

درون پنجره Add controller روی Add کلیک کنید. عمل Scaffold کلاسی با نام BookController که API Controller را تعریف کند می سازد. همچنین کلاسی با نام BookAPIContex در فولدر Models می سازد که داده ها را برای entity framework تعریف می کند.

آموزش Web Api

آماده سازی یک پایگاه داده

در منو ابزار، Library Package Manager و سپس Package Manager Console را انتخاب کنید. در پنجره Package Manager Console کد زیر را وارد کنید:

1
2
enable-migrations
<button></button>

این خط کد یک فولدر Migrations می سازد و کد هایی با نام Configuration.cs اضافه می کند. این فایل را باز کنید و قطعه کد زیر را به متد Configuration.Seed اضافه کنید.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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},
   });
}
<button></button>

در پنجره Package Manager Console کد های زیر را وارد کنید.

1
2
3
add-migration Initial
update-database
<button></button>

این کدها یک پایگاه داده Local می سازد و متد Seed را برای پر کردن پایگاه داده فراخوانی می کند.

آموزش Web Api

اضافه کردن کلاس های DTO

اگر شما برنامه را اجرا کنید و یک درخواست GET به /api/books/1 بفرستید ، پاسخ برگشتی چیزی شبیه به کد زیر می باشد.

1
2
3
4
5
6
7
8
9
10
11
{
 "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
}
<button></button>

همچنین می خواهیم نام نویسنده را به جای ID آن بر گردانیم. برای این منظور ما متد Controller را برای data transfer object(DTO) به جای EF تغییر می دهیم.

در Solution explorer روی project کلیک راست کنید و Add و سپس New Folder را انتخاب کنید. نام فولدر را DTO بگذارید. نام کلاس را BookDto قرار دهید و مثل زیر تعریف کنید:

1
2
3
4
5
6
7
8
9
10
namespace BooksAPI.DTOs
{
   public class BookDto
   {
       public string Title { get;set;}
       public string Author { get;set;}
       public string Genre { get;set;}
   }
}
<button></button>

یک کلاس دیگر به نام BookDetailDto مانند زیر تعریف کنید:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;}
   }
}
<button></button>

در مرحله بعد، کلاس BooksController را برای برگرداندن BookDto به روز رسانی کنید. در این قسمت کد ها را برای کلاس Controller به روز رسانی کنید.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 >AsBookDto =
           x =>new BookDto
           {
               Title = x.Title,
               Author = x.Author.Name,
               Genre = x.Genre
           };
       // GET api/Books
       public IQueryableGetBooks()
       {
           return db.Books.Include(b =>b.Author).Select(AsBookDto);
       }
       // GET api/Books/5
       [ResponseType(typeof(BookDto))]
       public async TaskGetBook(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);
       }
   }
}
<button></button>

ما متد های PutBook، PostBook و DeleteBook را برای آنکه در این مقاله به آن ها نیازی نداریم حذف کردیم.

حال اگر شما برنامه را اجرا کنید و /api/books/1 را درخواست کنید، پاسخ برگردانده شده به صورت زیر است:

1
2
{"Title":"Midnight Rain","Author":"Ralls, Kim","Genre":"Fantasy"}
<button></button>

اضافه کردن Attribute Routing

در مرحله بعد، ما Controller را به Attribute Routing تبدیل می کنیم. ابتدا یک Attribute RoutePrefix را اضافه کنید. این Attribute قطعه های اولیه URI را برای کلیه متد های این Controller تعریف می کند:

1
2
3
4
5
6
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
   // ...
}
<button></button>

سپس مانند زیر، [Route] را به action های Controller اضافه کنید:

1
2
3
4
5
6
7
8
9
10
11
12
[Route("")]
public IQueryableGetBooks()
{
   // ...
}
[Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async TaskGetBook(int id)
{
   // ...
}
<button></button>

قالب مسیر برای هر متد Controller شامل پیشوند به علاوه ی رشته مشخص شده در attribute Route می باشد. برای متد GetBook، قالب مسیر شامل رشته {id:int} می شود که اگر قطعه URI شامل مقدار عددی باشد تطبیق می دهد.

متد
قالب مسیر
مثالی برای URI
GetBooks
"api/books"
http://localhost/api/books
GetBook
"api/books/{id:int}"
http://localhost/api/books/5

دریافت جزئیات کتاب

برای دریافت جزئیات book، کاربر یک درخواست GET به /api/books/{id}/details می فرستد که {id} همان ID book می باشد.

متد زیر را به کلاس BooksController اضافه کنید.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Route("{id:int}/details")]
[ResponseType(typeof(BookDetailDto))]
public async TaskGetBookDetail(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);
}
<button></button>

اگر شما /api/books/1/detail در خواست کنید، پاسخ به صورت زیر می باشد:

1
2
3
4
5
6
7
8
9
{
 "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"
}
<button></button>

دریافت کتاب ها با استفاده از ژانر

برای دریافت یک لیست از book در یک موضوع خاص، کاربر درخواست GET به /api/books/genre ارسال می کند که genre همان نام genre می باشد. (برای مثال /get/books/fantasy)

متد زیر را به BooksController اضافه کنید.

1
2
3
4
5
6
7
8
[Route("{genre}")]
public IQueryableGetBooksByGenre(string genre)
{
   return db.Books.Include(b =>b.Author)
       .Where(b =>b.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase))
       .Select(AsBookDto);
}
<button></button>

در اینجا ما مسیری داریم که شامل یک پارامتر {genre} در قالب URI می باشد. توجه داشته باشید که Web API می تواند این دو URI را از هم متمایز کند و آنها را به متد های مختلف مسیر دهد:

1
2
3
/api/books/1
/api/books/fantasy
<button></button>

دلیل این موضوع این است که متد GetBook شامل محدودیتی است که قطعه id باید حتما مقدار عددی صحیح باشد .

1
2
3
4
5
6
[Route("{id:int}")]
public BookDto GetBook(int id)
{
   // ...
}
<button></button>

اگر شما /api/books/fantasy را درخواست کنید پاسخ به صورت زیر می باشد:

1
2
[ { "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" } ]
<button></button>

دریافت book بر اساس author

برای دریافت لیستی از book برای یک نویسنده خاص، کاربر باید یک درخواست GET به /api/authors/id/books ارسال کند که id همان ID نویسنده است.

متد زیر را به BooksController اضافه کنید.

1
2
3
4
5
6
7
8
[Route("~api/authors/{authorId}/books")]
public IQueryableGetBooksByAuthor(int authorId)
{
   return db.Books.Include(b =>b.Author)
       .Where(b =>b.AuthorId == authorId)
       .Select(AsBookDto);
}
<button></button>

این مثال جالبی است زیرا books یک فرزند از authors می باشد. این الگو تا حدی در API های REST مشترک است. نشانه (~) در قالب مسیر پیشوند مسیر ، Attribute RoutePrefix را override می کند.

دریافت book با تاریخ انتشار

برای دریافت یک لیست از book با تاریخ انتشار، کاربر یک در خواست GET به /api/books/date/yyyy-mm-dd می فرستد که yyyy-mm-dd تاریخ می باشد. یکی از روش هایی برای این کار در زیر نوشته شده است:

1
2
3
4
5
6
7
8
9
[Route("date/{pubdate:datetime}")]
public IQueryableGetBooks(DateTime pubdate)
{
   return db.Books.Include(b =>b.Author)
       .Where(b =>DbFunctions.TruncateTime(b.PublishDate)
           == DbFunctions.TruncateTime(pubdate))
       .Select(AsBookDto);
}
<button></button>

پارامتر {pubdate:datetime} مجبور است مقدار DateTime را تطبیق دهد.

برای مثال این URI ها نیز مسیر را تطبیق می دهند:

1
2
3
/api/books/date/Thu, 01 May 2008
/api/books/date/2000-12-16T00:00:00
<button></button>

شما می توانید با اضافه کردن یک regular-expression به قالب مسیر ، مسیر را به یک فرمت خاص محدود سازید.

1
2
3
4
5
6
[Route("api/books/date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IQueryableGetBooks(DateTime pubdate)
{
   // ...
}
<button></button>

تنها تاریخ هایی که با فرمت yyyy-mm-dd باشد تطبیق داده می شوند. توجه داشته باشید ما از عبارت منظم (regex) برای اعتبار سنجی یک تاریخ واقعی استفاده نمی کنیم. این زمانی به کار گرفته می شود که Web API بخواهد قطعه URI را به یک نمونه DateTime تبدیل کند.. یک تاریخ نامعتبر مثل 2012-47-99 تبدیل نخواهد شد و برای کاربر خطای 404 را ارسال می کند.

شما همچنین می توانید یک / جداکننده (/api/books/date/yyyy/mm/dd) به [Route] در یک عبارت منظم دیگر اضافه کنید.

1
2
3
4
5
6
7
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
[Route("date/{*pubdate:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")] // new
public IQueryableGetBooks(DateTime pubdate)
{
   // ...
}
<button></button>

لازم است این نکته را بیان کنیم که قالب دوم مسیر یک کاراکتر عام (*) در ابتدای پارامتر {pubdate} دارد.

1
{*pubdate: ... }<button></button>

این کد می گوید که {pubdate} باید با باقی URI ها مطابقت داشته باشد. بصورت پیش فرض، یک پارامتر قالب با یک قطعه منفرد URI مطابقت دارد. در این صورت، ما به {pubdate} برای پوشش چندین قطعه URI نیاز داریم.

1
2
/api/books/date/2013/06/17
<button></button>

کد Controller

کد کامل کلاس BooksController را در زیر می بینید:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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 >AsBookDto =
           x =>new BookDto
           {
               Title = x.Title,
               Author = x.Author.Name,
               Genre = x.Genre
           };
       // GET api/Books
       [Route("")]
       public IQueryableGetBooks()
       {
           return db.Books.Include(b =>b.Author).Select(AsBookDto);
       }
       // GET api/Books/5
       [Route("{id:int}")]
       [ResponseType(typeof(BookDto))]
       public async TaskGetBook(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 TaskGetBookDetail(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 IQueryableGetBooksByGenre(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 IQueryableGetBooksByAuthor(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 IQueryableGetBooks(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);
       }
   }
}
<button></button>

خلاصه:

در زمان طراحی URI ها برای API، Routing Attribute کنترول بیشتر و انعطاف پذیری بالاتری به شما می دهد.

1395/03/04 1734 1660
رمز عبور : tahlildadeh.com یا www.tahlildadeh.com
نظرات شما

نظرات خود را ثبت کنید...