ASP.NET Web API 2에서 특성 라우팅을 사용하여 REST API 만들기
Web API 2는 특성 라우팅이라는 새로운 유형의 라우팅을 지원합니다. 특성 라우팅에 대한 일반적인 개요는 Web API 2의 특성 라우팅을 참조하세요. 이 자습서에서는 특성 라우팅을 사용하여 책 컬렉션에 대한 REST API를 만듭니다. API는 다음 작업을 지원합니다.
작업 | 예제 URI |
---|---|
모든 책의 목록을 가져옵니다. | /api/books |
ID로 책을 가져옵니다. | /api/books/1 |
책의 세부 정보를 가져옵니다. | /api/books/1/details |
장르별 책 목록을 가져옵니다. | /api/books/fantasy |
발행일별로 책 목록을 가져옵니다. | /api/books/date/2013-02-16 /api/books/date/2013/02/16(대체 양식) |
특정 저자의 책 목록을 가져옵니다. | /api/authors/1/books |
모든 메서드는 읽기 전용(HTTP GET 요청)입니다.
데이터 계층의 경우 Entity Framework를 사용합니다. 책 레코드에는 다음 필드가 있습니다.
- ID
- 제목
- Genre
- 게시 날짜
- 가격
- 설명
- AuthorID(Authors 테이블에 대한 외래 키)
그러나 대부분의 요청에서 API는 이 데이터의 하위 집합(제목, 작성자 및 장르)을 반환합니다. 전체 레코드를 가져오기 위해 클라이언트는 를 요청합니다 /api/books/{id}/details
.
사전 요구 사항
Visual Studio 2017 Community, Professional 또는 Enterprise Edition.
Visual Studio 프로젝트 만들기
먼저 Visual Studio를 실행합니다. 파일 메뉴에서 새로 만들기를 선택한 후 프로젝트를 선택합니다.
설치된Visual C# 범주를> 확장합니다. Visual C#에서 웹을 선택합니다. 프로젝트 템플릿 목록에서 ASP.NET 웹 애플리케이션(.NET Framework)을 선택합니다. 프로젝트의 이름을 "BooksAPI"로 지정합니다.
새 ASP.NET 웹 애플리케이션 대화 상자에서 빈 템플릿을 선택합니다. "폴더 및 핵심 참조 추가"에서 Web API 확인란을 선택합니다. 확인을 클릭합니다.
그러면 Web API 기능에 대해 구성된 기본 프로젝트가 만들어집니다.
도메인 모델
다음으로 도메인 모델에 대한 클래스를 추가합니다. 솔루션 탐색기에서 Models 폴더를 마우스 오른쪽 단추로 클릭합니다. 추가를 선택한 다음, 클래스를 선택합니다. 클래스 Author
이름을 지정합니다.
Author.cs의 코드를 다음으로 바꿉니다.
using System.ComponentModel.DataAnnotations;
namespace BooksAPI.Models
{
public class Author
{
public int AuthorId { get; set; }
[Required]
public string Name { get; set; }
}
}
이제 라는 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; }
}
}
Web API 컨트롤러 추가
이 단계에서는 Entity Framework를 데이터 계층으로 사용하는 Web API 컨트롤러를 추가합니다.
Ctrl+Shift+B를 눌러 프로젝트를 빌드합니다. Entity Framework는 리플렉션을 사용하여 모델의 속성을 검색하므로 데이터베이스 스키마를 만들려면 컴파일된 어셈블리가 필요합니다.
솔루션 탐색기에서 Controllers 폴더를 마우스 오른쪽 단추로 클릭합니다. 추가를 선택한 다음 컨트롤러를 선택합니다.
스캐폴드 추가 대화 상자에서 Entity Framework를 사용하여 작업이 있는 Web API 2 컨트롤러를 선택합니다.
컨트롤러 추가 대화 상자에서 컨트롤러 이름에 "BooksController"를 입력합니다. "비동기 컨트롤러 작업 사용" 확인란을 선택합니다. 모델 클래스에서 "Book"을 선택합니다. (드롭다운에 Book
나열된 클래스가 표시되지 않으면 프로젝트를 빌드했는지 확인합니다.) 그런 다음"+" 단추를 클릭합니다.
새 데이터 컨텍스트 대화 상자에서 추가를 클릭합니다.
컨트롤러 추가 대화 상자에서 추가를 클릭합니다. 스캐폴딩은 API 컨트롤러를 정의하는 라는 BooksController
클래스를 추가합니다. 또한 Entity Framework에 대한 데이터 컨텍스트를 정의하는 Models 폴더에 라는 BooksAPIContext
클래스를 추가합니다.
데이터베이스 시드
도구 메뉴에서 NuGet 패키지 관리자를 선택한 다음 패키지 관리자 콘솔을 선택합니다.
패키지 관리자 콘솔 창에서 다음 명령을 입력합니다.
Add-Migration
이 명령은 Migrations 폴더를 만들고 Configuration.cs라는 새 코드 파일을 추가합니다. 이 파일을 열고 메서드에 다음 코드를 추가합니다 Configuration.Seed
.
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},
});
}
패키지 관리자 콘솔 창에서 다음 명령을 입력합니다.
add-migration Initial
update-database
이러한 명령은 로컬 데이터베이스를 만들고 Seed 메서드를 호출하여 데이터베이스를 채웁니다.
DTO 클래스 추가
지금 애플리케이션을 실행하고 /api/books/1에 GET 요청을 보내는 경우 응답은 다음과 유사합니다. (가독성을 위해 들여쓰기를 추가했습니다.)
{
"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
}
대신 이 요청이 필드의 하위 집합을 반환하려고 합니다. 또한 작성자 ID가 아닌 작성자의 이름을 반환하려고 합니다. 이를 위해 컨트롤러 메서드를 수정하여 EF 모델 대신 DTO( 데이터 전송 개체 )를 반환합니다. DTO는 데이터를 전달하도록 설계된 개체입니다.
솔루션 탐색기 프로젝트를 마우스 오른쪽 단추로 클릭하고새 폴더추가 | 를 선택합니다. 폴더 이름을 "DTO"로 지정합니다. 다음 정의를 사용하여 라는 BookDto
클래스를 DTO 폴더에 추가합니다.
namespace BooksAPI.DTOs
{
public class BookDto
{
public string Title { get; set; }
public string Author { get; set; }
public string Genre { get; set; }
}
}
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; }
}
}
다음으로, 인스턴스를 BooksController
반환 BookDto
하도록 클래스를 업데이트합니다. Queryable.Select 메서드를 사용하여 인스턴스를 인스턴스에 BookDto
프로젝 Book
트합니다. 컨트롤러 클래스에 대한 업데이트된 코드는 다음과 같습니다.
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);
}
}
}
참고
이 자습서에는 PutBook
필요하지 않으므로 , PostBook
및 DeleteBook
메서드를 삭제했습니다.
이제 애플리케이션을 실행하고 /api/books/1을 요청하는 경우 응답 본문은 다음과 같습니다.
{"Title":"Midnight Rain","Author":"Ralls, Kim","Genre":"Fantasy"}
경로 특성 추가
다음으로, 특성 라우팅을 사용하도록 컨트롤러를 변환합니다. 먼저 컨트롤러에 RoutePrefix 특성을 추가합니다. 이 특성은 이 컨트롤러의 모든 메서드에 대한 초기 URI 세그먼트를 정의합니다.
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
// ...
그런 다음 다음과 같이 컨트롤러 작업에 [Route] 특성을 추가합니다.
[Route("")]
public IQueryable<BookDto> GetBooks()
{
// ...
}
[Route("{id:int}")]
[ResponseType(typeof(BookDto))]
public async Task<IHttpActionResult> GetBook(int id)
{
// ...
}
각 컨트롤러 메서드의 경로 템플릿은 접두사와 Route 특성에 지정된 문자열입니다. 메서드의 GetBook
경우 경로 템플릿에는 URI 세그먼트에 정수 값이 포함된 경우 일치하는 매개 변수가 있는 문자열 "{id:int}"이 포함됩니다.
메서드 | 경로 템플릿 | 예제 URI |
---|---|---|
GetBooks |
"api/books" | http://localhost/api/books |
GetBook |
"api/books/{id:int}" | http://localhost/api/books/5 |
책 세부 정보 가져오기
책 세부 정보를 가져오기 위해 클라이언트는 GET 요청을 로 /api/books/{id}/details
보냅니다. 여기서 {id} 는 책의 ID입니다.
다음 메서드를 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);
}
를 요청하는 /api/books/1/details
경우 응답은 다음과 같습니다.
{
"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"
}
장르별 책 받기
특정 장르의 책 목록을 가져오기 위해 클라이언트는 GET 요청을 에 /api/books/genre
보냅니다. 여기서 장르는 장르 의 이름입니다. (예: /api/books/fantasy
)
에 다음 메서드를 추가합니다 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);
}
여기서는 URI 템플릿에 {genre} 매개 변수가 포함된 경로를 정의합니다. Web API는 다음 두 URI를 구분하고 다른 방법으로 라우팅할 수 있습니다.
/api/books/1
/api/books/fantasy
메서드에 GetBook
"id" 세그먼트가 정수 값이어야 한다는 제약 조건이 포함되어 있기 때문입니다.
[Route("{id:int}")]
public BookDto GetBook(int id)
{
// ...
}
/api/books/fantasy를 요청하는 경우 응답은 다음과 같습니다.
[ { "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" } ]
저자가 책 받기
특정 작성자의 책 목록을 가져오려면 클라이언트는 GET 요청을 /api/authors/id/books
에 보냅니다. 여기서 id 는 작성자의 ID입니다.
에 다음 메서드를 추가합니다 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);
}
이 예제는 "books"가 "authors"의 자식 리소스로 취급되므로 흥미롭습니다. 이 패턴은 RESTful API에서 매우 일반적입니다.
경로 템플릿의 타일(~)은 RoutePrefix 특성의 경로 접두사를 재정의합니다.
발행일별 도서 가져오기
게시 날짜별로 책 목록을 가져오기 위해 클라이언트는 GET 요청을 에 /api/books/date/yyyy-mm-dd
보냅니다. 여기서 yyyy-mm-dd 는 날짜입니다.
이 작업을 수행하는 한 가지 방법은 다음과 같습니다.
[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}
매개 변수는 DateTime 값과 일치하도록 제한됩니다. 이것은 효과가 있지만 실제로는 우리가 좋아하는 것보다 더 관대합니다. 예를 들어 이러한 URI는 경로와도 일치합니다.
/api/books/date/Thu, 01 May 2008
/api/books/date/2000-12-16T00:00:00
이러한 URI를 허용하는 데는 아무런 문제가 없습니다. 그러나 경로 템플릿에 정규식 제약 조건을 추가하여 경로를 특정 형식으로 제한할 수 있습니다.
[Route("date/{pubdate:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IQueryable<BookDto> GetBooks(DateTime pubdate)
{
// ...
}
이제 "yyyy-mm-dd" 형식의 날짜만 일치합니다. 실제 날짜가 있는지 확인하기 위해 regex를 사용하지 않습니다. Web API가 URI 세그먼트를 DateTime instance 변환하려고 할 때 처리됩니다. '2012-47-99'와 같은 잘못된 날짜는 변환되지 않으며 클라이언트에 404 오류가 발생합니다.
다른 정규식으로 다른 [Route] 특성을 추가하여 슬래시 구분 기호(/api/books/date/yyyy/mm/dd
)를 지원할 수도 있습니다.
[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)
{
// ...
}
여기에 미묘하지만 중요한 세부 사항이 있습니다. 두 번째 경로 템플릿에는 {pubdate} 매개 변수의 시작 부분에 와일드카드 문자(*)가 있습니다.
{*pubdate: ... }
그러면 라우팅 엔진에 {pubdate}이(가) 나머지 URI와 일치해야 한다는 것을 알 수 있습니다. 기본적으로 템플릿 매개 변수는 단일 URI 세그먼트와 일치합니다. 이 경우 {pubdate}가 여러 URI 세그먼트에 걸쳐 있도록 합니다.
/api/books/date/2013/06/17
컨트롤러 코드
다음은 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);
}
}
}
요약
특성 라우팅은 API에 대한 URI를 디자인할 때 더 많은 제어와 유연성을 제공합니다.