从WebApi 1迁移到WebAPI 2要改变配置代码如下:
WebApi 1:
protected void Application_Start() { // WARNING - Not compatible with attribute routing. WebApiConfig.Register(GlobalConfiguration.Configuration); }
WebAPI 2:
protected void Application_Start() { // Pass a delegate to the Configure method. GlobalConfiguration.Configure(WebApiConfig.Register); }
添加一个Route Attributes
public class OrdersController : ApiController { [Route("customers/{customerId}/orders")] [HttpGet] public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... } }
可以匹配一下URL:
- http://localhost/customers/1/orders
- http://localhost/customers/bob/orders
- http://localhost/customers/1234-5678/orders
匹配http post请求的CreateBook
[Route("api/books")] [HttpPost] public HttpResponseMessage CreateBook(Book book) { ... }
[Route("api/books")] [AcceptVerbs("MKCOL")] public void MakeCollection() {
Route Prefixes
对于同一个controller路由有相同的前缀, 此时可以使用 [RoutePrefix] attribute
[RoutePrefix("api/books")] public class BooksController : ApiController { // GET api/books [Route("")] public IEnumerable<Book> Get() { ... } // GET api/books/5 [Route("{id:int}")] public Book Get(int id) { ... } // POST api/books [Route("")] public HttpResponseMessage Post(Book book) { ... } }
可以在方法的attribute中使用(~)重写路由前缀
[RoutePrefix("api/books")] public class BooksController : ApiController { // GET /api/authors/1/books [Route("~/api/authors/{authorId:int}/books")] public IEnumerable<Book> GetByAuthor(int authorId) { ... } // ... }
路由前缀中可以包含参数
[RoutePrefix("customers/{customerId}")] public class OrdersController : ApiController { // GET customers/1/orders [Route("orders")] public IEnumerable<Order> Get(int customerId) { ... } }
路由约束
路由约束用于严格匹配路由中的参数,通用语法是{参数:约束},如下:
[Route("users/{id:int}"] public User GetUserById(int id) { ... } [Route("users/{name}"] public User GetUserByName(string name) { ... }
支持的约束如下表所示:
Constraint | Description | Example |
---|---|---|
alpha | Matches uppercase or lowercase Latin alphabet characters (a-z, A-Z) | {x:alpha} |
bool | Matches a Boolean value. | {x:bool} |
datetime | Matches a DateTime value. | {x:datetime} |
decimal | Matches a decimal value. | {x:decimal} |
double | Matches a 64-bit floating-point value. | {x:double} |
float | Matches a 32-bit floating-point value. | {x:float} |
guid | Matches a GUID value. | {x:guid} |
int | Matches a 32-bit integer value. | {x:int} |
length | Matches a string with the specified length or within a specified range of lengths. | {x:length(6)} {x:length(1,20)} |
long | Matches a 64-bit integer value. | {x:long} |
max | Matches an integer with a maximum value. | {x:max(10)} |
maxlength | Matches a string with a maximum length. | {x:maxlength(10)} |
min | Matches an integer with a minimum value. | {x:min(10)} |
minlength | Matches a string with a minimum length. | {x:minlength(10)} |
range | Matches an integer within a range of values. | {x:range(10,50)} |
regex | Matches a regular expression. | {x:regex(^d{3}-d{3}-d{4}$)} |
注意:带括号的约束,如"min",可以对路由中的参数应用多个约束,用冒号分割约束
[Route("users/{id:int:min(1)}")] public User GetUserById(int id) { ... }
自定义路由约束
public class NonZeroConstraint : IHttpRouteConstraint { public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { object value; if (values.TryGetValue(parameterName, out value) && value != null) { long longValue; if (value is long) { longValue = (long)value; return longValue != 0; } string valueString = Convert.ToString(value, CultureInfo.InvariantCulture); if (Int64.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) { return longValue != 0; } } return false; } } 修改WebAPIConfig.cs public static class WebApiConfig { public static void Register(HttpConfiguration config) { var constraintResolver = new DefaultInlineConstraintResolver(); constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint)); config.MapHttpAttributeRoutes(constraintResolver); } } 实际应用: [Route("{id:nonzero}")] public HttpResponseMessage GetNonZero(int id) { ... }
可选的URI参数和默认值
第一种方式,默认值直接分配给了方法参数,因此参数获得的是精确的值
第二种方式,默认值通过model-binding进行处理,默认的model-binder将字符串类型的默认值"1033"转为int形的1033,对于model-binder可能有所不同。
1 第一种写法 2 public class BooksController : ApiController 3 { 4 [Route("api/books/locale/{lcid:int?}")] 5 public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... } 6 } 7 8 第二种写法 9 public class BooksController : ApiController 10 { 11 [Route("api/books/locale/{lcid:int=1033}")] 12 public IEnumerable<Book> GetBooksByLocale(int lcid) { ... } 13 }
Route Names
web api中每个路由有一个name,Route names可以用来生成links
1 public class BooksController : ApiController 2 { 3 [Route("api/books/{id}", Name="GetBookById")] 4 public BookDto GetBook(int id) 5 { 6 // Implementation not shown... 7 } 8 9 [Route("api/books")] 10 public HttpResponseMessage Post(Book book) 11 { 12 // Validate and add book to database (not shown) 13 14 var response = Request.CreateResponse(HttpStatusCode.Created); 15 16 // Generate a link to the new book and set the Location header in the response. 17 string uri = Url.Link("GetBookById", new { id = book.BookId }); 18 response.Headers.Location = new Uri(uri); 19 return response; 20 } 21 }
Route Order
通过设置RouteOrder属性在route attribute上,可以指定Route的顺序,值越小先被计算,默认顺序对应的值是0
路由顺序的选择:
- 比较RouteOrder属性
- 对于URI部分按如下进行排序
- URI的固定部分(literal segments)
- 带约束的路由参数
- 不带约束的路由参数
- 带约束的通配符
- 不带约束的通配符
- route的排序不区分大小写的
[RoutePrefix("orders")] public class OrdersController : ApiController { [Route("{id:int}")] // constrained parameter public HttpResponseMessage Get(int id) { ... } [Route("details")] // literal public HttpResponseMessage GetDetails() { ... } [Route("pending", RouteOrder = 1)] public HttpResponseMessage GetPending() { ... } [Route("{customerName}")] // unconstrained parameter public HttpResponseMessage GetByCustomer(string customerName) { ... } [Route("{*date:datetime}")] // wildcard public HttpResponseMessage Get(DateTime date) { ... } }
路由排序如下:
- orders/{details}
- orders/{id}
- orders/{customreName}
- orders/{*date}
- orders/pending
Route Attribute的使用完整例子:
1 using BooksAPI.DTOs; 2 using BooksAPI.Models; 3 using System; 4 using System.Data.Entity; 5 using System.Linq; 6 using System.Linq.Expressions; 7 using System.Threading.Tasks; 8 using System.Web.Http; 9 using System.Web.Http.Description; 10 11 namespace BooksAPI.Controllers 12 { 13 [RoutePrefix("api/books")] 14 public class BooksController : ApiController 15 { 16 private BooksAPIContext db = new BooksAPIContext(); 17 18 // Typed lambda expression for Select() method. 19 private static readonly Expression<Func<Book, BookDto>> AsBookDto = 20 x => new BookDto 21 { 22 Title = x.Title, 23 Author = x.Author.Name, 24 Genre = x.Genre 25 }; 26 27 // GET api/Books 28 [Route("")] 29 public IQueryable<BookDto> GetBooks() 30 { 31 return db.Books.Include(b => b.Author).Select(AsBookDto); 32 } 33 34 // GET api/Books/5 35 [Route("{id:int}")] 36 [ResponseType(typeof(BookDto))] 37 public async Task<IHttpActionResult> GetBook(int id) 38 { 39 BookDto book = await db.Books.Include(b => b.Author) 40 .Where(b => b.BookId == id) 41 .Select(AsBookDto) 42 .FirstOrDefaultAsync(); 43 if (book == null) 44 { 45 return NotFound(); 46 } 47 48 return Ok(book); 49 } 50 51 [Route("{id:int}/details")] 52 [ResponseType(typeof(BookDetailDto))] 53 public async Task<IHttpActionResult> GetBookDetail(int id) 54 { 55 var book = await (from b in db.Books.Include(b => b.Author) 56 where b.AuthorId == id 57 select new BookDetailDto 58 { 59 Title = b.Title, 60 Genre = b.Genre, 61 PublishDate = b.PublishDate, 62 Price = b.Price, 63 Description = b.Description, 64 Author = b.Author.Name 65 }).FirstOrDefaultAsync(); 66 67 if (book == null) 68 { 69 return NotFound(); 70 } 71 return Ok(book); 72 } 73 74 [Route("{genre}")] 75 public IQueryable<BookDto> GetBooksByGenre(string genre) 76 { 77 return db.Books.Include(b => b.Author) 78 .Where(b => b.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase)) 79 .Select(AsBookDto); 80 } 81 82 [Route("~api/authors/{authorId}/books")] 83 public IQueryable<BookDto> GetBooksByAuthor(int authorId) 84 { 85 return db.Books.Include(b => b.Author) 86 .Where(b => b.AuthorId == authorId) 87 .Select(AsBookDto); 88 } 89 90 [Route("date/{pubdate:datetime:regex(\d{4}-\d{2}-\d{2})}")] 91 [Route("date/{*pubdate:datetime:regex(\d{4}/\d{2}/\d{2})}")] 92 public IQueryable<BookDto> GetBooks(DateTime pubdate) 93 { 94 return db.Books.Include(b => b.Author) 95 .Where(b => DbFunctions.TruncateTime(b.PublishDate) 96 == DbFunctions.TruncateTime(pubdate)) 97 .Select(AsBookDto); 98 } 99 100 protected override void Dispose(bool disposing) 101 { 102 db.Dispose(); 103 base.Dispose(disposing); 104 } 105 } 106 }
其中以下路由中的匹配符*的作用:
[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> GetBooks2(DateTime pubdate) { /* * 路由中的*匹配符的作用,是为了告诉route engine占位符{pubdate}应该匹配剩余的URI, * 默认情况下,一个模板参数仅仅匹配一个URI。 * This tells the routing engine that {pubdate} should match the rest of the URI. By default, * a template parameter matches a single URI segment. In this case, we want {pubdate} to * span several URI segments: */ }