如何在RESTful ASP.NET Web API中路由非CRUD操作?

14

我正在尝试使用ASP.NET Web API为我们的服务设计RESTful Web API。 我在确定如何将非CRUD操作路由到正确的控制器操作方面遇到了麻烦。 假设我的资源是一扇门。 我可以使用我的门执行所有熟悉的CRUD操作。 假设我的门的模型是:

public class Door
{
   public long Id { get; set; }
   public string InsideRoomName { get; set; }
   public string OutsideRoomName { get; set; }
}

我可以通过我的 Web API 进行所有标准的 CRUD 操作:

POST: http://api.contoso.com/v1/doors
GET: http://api.contoso.com/v1/doors
GET: http://api.contoso.com/v1/doors/1234
GET: http://api.contoso.com/v1/doors?InsideRoomName=Cafeteria
PUT: http://api.contoso.com/v1/doors/1234
DELETE: http://api.contoso.com/v1/doors/1234

等等,我遇到问题的地方是需要对我的门进行非CRUD操作建模。我想针对我的资源建模锁定和解锁动作。阅读ASP.NET文章后,指南似乎是在使用自定义操作时切换为RPC风格调用。这给了我一条路径:

PUT: http://api.contoso.com/v1/doors/1234/lock
PUT: http://api.contoso.com/v1/doors/1234/unlock

这似乎与REST的精神相冲突,因为REST旨在使用路径表示资源。我想我可以将动词建模为资源:

POST: http://api.contoso.com/v1/doors/1234/lockrequests
POST: http://api.contoso.com/v1/doors/1234/unlockrequests
在这种情况下,我仍然可以使用推荐的{controller}/{id}/{action},但似乎仍在创建混合的RPC / REST API。就REST接口而言,是否可能甚至建议将自定义操作放入参数列表中?
PUT: http://api.contoso.com/v1/doors/1234?lock
PUT: http://api.contoso.com/v1/doors/1234?unlock

我能预见需要支持此调用的查询参数,例如:

PUT: http://api.contoso.com/v1/doors?lock&InsideRoomName=Cafeteria

我该如何创建路由以将此请求映射到我的DoorsController?

public class DoorsController : ApiController
{
   public IEnumerable<Doord> Get();
   public Door Get(long id);
   public void Put(long id, Door door);
   public void Post(Door door);
   public void Delete(long id);

   public void Lock(long id);
   public void Unlock(long id);
   public void Lock(string InsideRoomName);
}

我可能会在这里做出一些关于REST API设计的最佳实践的错误假设,因此如果有任何指导,将不胜感激。


Google使用REST API与Blogger进行交互,并且在REST中使用动作!请访问https://developers.google.com/blogger/docs/3.0/reference/posts/publish了解更多信息。 - padibro
3个回答

8
从RESTful原则来看,也许最好引入一个“状态”属性来管理那些非CURD操作。但我认为这并不符合实际生产开发的需求。
针对这种问题的每个答案,似乎都必须使用一种解决方法来强制执行API设计符合RESTful。但我担心的是,这样真的方便用户和开发人员吗?
让我们来看看Google Bloger的API3.0设计:https://developers.google.com/blogger/docs/3.0/reference,它使用了很多URL来处理非CURD操作。
这很有趣,
POST  /blogs/blogId/posts/postId/comments/commentId/spam

描述:

将评论标记为垃圾邮件。这将设置评论的状态为垃圾邮件,并在默认评论呈现中隐藏它。

可以看到,评论有一个状态来指示它是否是垃圾邮件,但它不像JoannaTurban上面提到的答案那样设计。

我认为从用户的角度来看,这更方便。不需要关心“状态”的结构和枚举值。实际上,您可以将许多属性放入“状态”的定义中,例如“isItSpam”,“isItReplied”,“isItPublic”等。如果状态有很多东西,设计会变得不友好。

在某些业务逻辑要求中,使用易于理解的动词而不是尝试使其完全成为“真正”的RESTful对于用户和开发人员都更具生产力。这是我的观点。


6
为了处理“锁定/解锁”场景,您可以考虑向“门”对象添加一个“状态”属性。
   public State State { get; set; }

其中 State 是可用值的枚举类型,例如:

{
LockedFromOutsideRoom,
LockedFromInsideRoom,
Open
}

澄清一下:向对象添加状态并不违反RESTful原则,因为每次使用门执行某些操作时,该状态都会通过API传递。

然后,通过API发送PUT / POST请求以更改每次锁定/解锁门的状态。由于只更新一个属性,因此Post可能更好:

POST: http://api.contoso.com/v1/doors/1234/state
body: {"State":"LockedFromInsideRoom"}

我需要在 WebApiConfig.cs 中注册自定义路由吗?这似乎仍然遵循 {controller}/{id}/{action} 的模式,其中我的控制器现在将具有 DoorsController::State(long id, State state); 方法。 - JadeMason
1
@JoannaTurban 很好的回答!我甚至会使用PUT,像这样:PUT: http://api.contoso.com/v1/doors/1234 body: {"State":"LockedFromInsideRoom"} 因为你实际上是更新状态。此外,感觉更符合PUT的幂等性:每次请求此URL都会产生完全相同的结果。 - zafeiris.m
1
这种方法的危险在于,您可能会创建许多相互关联的细粒度资源,违反HTTP的粗粒度特性,并使缓存失效变得更加困难。然而,在某些情况下,这可能是可行的选择。 - Darrel Miller
我喜欢提出的解决方案,即将状态作为门资源的属性插入,我可以通过POST(或PUT修改???)设置,而不是使用一个动作setState。 - padibro
谷歌使用Blogger的REST API,并在其中使用REST操作!https://developers.google.com/blogger/docs/3.0/reference/posts/publish - padibro
Google使用REST API与Blogger进行交互,并在REST中使用操作!https://developers.google.com/blogger/docs/3.0/reference/posts/publish - padibro

1

从REST的角度来看,您可能希望将锁定视为资源本身。这样,您可以独立于门创建和删除锁定(尽管可能会从门表示中找到锁定端点)。资源的URL可能与门的URL相关,但从RESTful的角度来看,这是无关紧要的。 REST涉及资源之间的关系,因此重要的是锁的url可以从门的表示中发现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接