【问题标题】:Multiple PUT methods in ASP.NET Web APIASP.NET Web API 中的多个 PUT 方法
【发布时间】:2012-09-03 04:12:48
【问题描述】:

我有一个控制器Groups,具有以下操作:

public GroupModel Get(int ID)

public GroupModel Post(CreateGroupModel model)

public void Put(PublicUpdateGroupModel model)

public void PutAddContacts(UpdateContactsModel model)

public void PutRemoveContacts(UpdateContactsModel model)

public void Delete(int ID)

而我想做的是使用标准的 REST 路由来调用标准的 get、post、put、delete 方法。但如果操作名称附加到 url,请调用 PutAddContactsPutRemoveContacts,例如:

GET 组/ - 调用 Get 方法

POST 组/ - 调用 Post 方法

PUT 组/ - 调用 Put 方法

DELETE 组/ - 调用 Delete 方法

PUT 组/添加联系人 - 调用 PutAddContacts 方法

PUT 组/删除联系人 - 调用 PutRemoveContacts 方法

是否可以设置路由来执行此操作,或者如果我想在我的 URL 中使用操作名称,我是否需要沿着 RPC 路由进行路由?

【问题讨论】:

    标签: c# .net asp.net-web-api


    【解决方案1】:

    你现在拥有什么
    要使用上述方法,您需要使用 RPC。那是因为您的示例已经一半沉浸在 RPC 做事风格中。默认的 WebAPI 路由鼓励 RESTful 设置,但如果您对路由进行了微小的更改,一切都会开始工作。例如,您可以将默认路由更改为典型的 MVC 路由:

    routes.MapRoute( name    : "Default",       
                     url     : "{controller}/{action}/{id}",
                     defaults: new { controller = "Home", 
                                     action     = "Index", 
                                     id         = UrlParameter.Optional });
    

    添加路由后,以典型的 MVC 方式调用事物,使用控制器名称和操作。然而,从你的问题来看,我怀疑你实际上想要成为 RESTful,而不是让它工作,所以请继续阅读......

    RESTful
    REST doesn't require HTTP,虽然这两者经常一起讨论。 REST 实际上是关于具有语义准确表示的每个资源。使用 HTTP 时,这意味着尊重 HTTP 语义的唯一 URI。例如,使用 HTTP GET 的调用不应该修改数据,因为这违反了 HTTP 对 GET 的定义,并混淆了缓存等 HTTP 基础设施。

    POST/PUT 与 MERGE/PATCH
    我们都熟悉 GET、POST、PUT、HEAD 等作为 HTTP 方法。 一般,GET 用于检索,POST 用于添加,PUT 用于修改(尽管有很多争论)。但是,您有两种类型的修改:添加项目和从集合中删除项目。那么这些都是 PUT 还是其他?社区hasn't quite settled 了解如何执行此操作。

    • 选项 1:自定义媒体类型 - HTTP 规范确实允许各种方法,是浏览器真正将我们限制在熟悉的子集中。因此,您可以创建MERGE (a Roy Fielding work around) or PATCH (an oData work around) 方法并定义这种新媒体类型的行为——可能一种用于添加,另一种用于删除。

    • 选项 2:使用 POST/PUT - 使用 PUT 添加和删除联系人。只需将 ID 列表视为一个切换(如果存在则删除,如果缺少添加)或交替包含足够的信息以知道该做什么。然后返回一个 HTTP 303 向客户端指示它具有陈旧状态并刷新。

    • 选项 3:完整列表 - 如果您的集合大小合理,您可以在每次想要更新时传递完整的联系人列表。这种方式的逻辑是超级简单的擦除和替换。

    从 RESTful 的角度来看,真正重要的是您的应用程序在所有方法中的行为方式一致。因此,如果 MERGE 意味着添加,它应该始终意味着添加。如果您希望将一组完整的 ID 传递给 PUT,则始终传递一组完整的 ID。

    控制器设计
    如果是我,我会把你的控制器分成多个控制器。一个控制器处理组,另一个处理联系人(作为一个组),第三个处理组内的一个联系人。有点像...

    //api/Group/
    public List<GroupModel> Get()
    public GroupModel Get(int ID)
    public GroupModel Post(GroupModel model)  //add a group
    public GroupModel Put(GroupModel model)   //update a group (see comments above)
    public void Delete(int ID)
    
    
    //api/GroupContacts/
    public ContactsModel Get()                    //gets complete list
    public void PostContacts(ContactsModel model) //pushes a COMPLETE new state
    public void Delete()                          //delete entire group of contacts
    
    
    //api/GroupContact/354/
    public ContactModel Get(int id)             //get contact id #354
    public void PostContact(ContactModel model) //add contact (overwrite if exits)
    public void Delete(int id)                  //delete contact if exists
    

    如果您希望您的网址显示为嵌套的(例如:/api/Group/Contacts/api/Group/Contact),您可以查看this other post I wrote。恕我直言,asp.net 的路由需要调整以更轻松地支持嵌套......但这是一个不同的问题;-)

    【讨论】:

    • 感谢您的精彩回答!我同意您的断言,即 REST 更多地是关于语义,这就是为什么我相信我的示例仍然是 REST。有趣的是,我实际上早些时候遇到了你的另一篇文章,并使用子文件夹实现了类似的东西,并为每个子文件夹设置了一条路径,如果没有人回复标记我的解决方案,我将自己回答这篇文章,但我猜我现在不需要打扰:D
    • 很高兴为您服务。我恰好正在从事一个在 webapi beta 期间开始的大型 API 项目。我花了一段时间才弄清楚 MVC 之间的微妙设计转变,我可以在控制器中有一堆方法,而 api 风格我有控制器扩散。
    • @EBarr 我很想知道您对我在回答这个问题时演示的分层路由的反馈。
    【解决方案2】:

    呼应 EBarr 所说的,在 Web API 中进行分层路由可能有点痛苦。我在我的服务中经常使用它,所以我为 Web API 构建了一个替代路由服务。 Nuget 是here,源是GitHub

    这种路由方法要求您将 URI 命名空间构建为路径段的层次结构。您无需将完整的 URI 模式与控制器匹配,而是将控制器附加到路径段树中的任意点。

    只是为了让您了解它的外观,我创建了一个小型自托管示例,其 URI 与您尝试做的类似:

    internal class Program
    {
       private static void Main(string[] args)
       {
        var baseAddress = new Uri("http://oak:8700/");
    
        var configuration = new HttpSelfHostConfiguration(baseAddress);
        var router = new ApiRouter("api", baseAddress);
    
        // /api/Contacts
        router.Add("Contacts", rcs => rcs.To<ContactsController>());
    
        // /api/Contact/{contactid}
        router.Add("Contact", rc =>
                              rc.Add("{contactid}", rci => rci.To<ContactController>()));
    
        // /api/Group/{groupid}
        // /api/Group/{groupid}/Contacts
        router.Add("Group", rg =>
                            rg.Add("{groupid}", rgi => rgi.To<GroupController>() 
                                                           .Add("Contacts", rgc => rgc.To<GroupContactsController>())));
    
    
        configuration.MessageHandlers.Add(router);
    
        var host = new HttpSelfHostServer(configuration);
        host.OpenAsync().Wait();
    
        Console.WriteLine("Host open.  Hit enter to exit...");
    
        Console.Read();
    
        host.CloseAsync().Wait();
      }
    }
    
    public class GroupController : TestController { }
    public class ContactsController : TestController { }
    public class ContactController : TestController { }
    public class GroupContactsController : TestController { }
    
    
    public class TestController : ApiController
    {
        public HttpResponseMessage Get()
        {
            var pathRouteData = (PathRouteData) Request.GetRouteData();
    
            var paramvalues = new StringBuilder();
    
            foreach (KeyValuePair<string, object> keyValuePair in pathRouteData.Values)
            {
                paramvalues.Append(keyValuePair.Key);
                paramvalues.Append(" = ");
                paramvalues.Append(keyValuePair.Value);
                paramvalues.Append(Environment.NewLine);
            }
    
            var url = pathRouteData.RootRouter.GetUrlForController(this.GetType());
    
            return new HttpResponseMessage()
                       {
                           Content = new StringContent("Response from " + this.GetType().Name + Environment.NewLine
                                                       + "Url: " + url.AbsoluteUri
                                                       + "Parameters: " + Environment.NewLine
                                                       + paramvalues.ToString())
                       };
        }
    }
    

    您应该能够将此代码粘贴到控制台应用程序中,并添加对 Microsoft.AspNet.WebApi.SelfHost 和 Tavis.WebApiRouter nuget 的引用并尝试一下。如果您对这种路由能走多远感到好奇,这里有一个更复杂的示例here

    【讨论】:

    • 有趣。我现在的截止日期有点紧,但我会回来写我定期写的长答案。我正计划编写自己的路线处理程序。我真正想要的是一个尊重我的命名空间(所以UI.Parent.Child.GrandChild 中的类将映射到/api/Parent/Child/Grandchild。所以也许你节省了我的时间:-)。我会回过头来评估一下,但可能需要几周时间。
    • @Ebarr 编写一些反映命名空间并寻找 ApiControllers 并自动构建层次结构的代码应该相当简单。只是需要某种约定的参数。我的命名空间也与我的 URI 匹配,所以这对我很有用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-24
    • 1970-01-01
    相关资源
    最近更新 更多