ASP.NET Web API中的自定义方法名

116

我正在从WCF Web API转换到新的ASP.NET MVC 4 Web API。我有一个UsersController,并且我想要有一个名为Authenticate的方法。我看到了如何执行GetAll、GetOne、Post和Delete的示例,但是如果我想在这些服务中添加额外的方法怎么办?例如,我的UsersService应该有一个名为Authenticate的方法,他们可以通过用户名和密码进行验证,但它不起作用。

public class UsersController : BaseApiController
{
    public string GetAll()
    {
        return "getall!";
    }

    public string Get(int id)
    {
        return "get 1! " + id;
    }

    public User GetAuthenticate(string userName, string password, string applicationName)
    {
        LogWriter.Write(String.Format("Received authenticate request for username {0} and password {1} and application {2}",
            userName, password, applicationName));

        //check if valid leapfrog login.
        var decodedUsername = userName.Replace("%40", "@");
        var encodedPassword = password.Length > 0 ? Utility.HashString(password) : String.Empty;
        var leapFrogUsers = LeapFrogUserData.FindAll(decodedUsername, encodedPassword);

        if (leapFrogUsers.Count > 0)
        {
            return new User
            {
                Id = (uint)leapFrogUsers[0].Id,
                Guid = leapFrogUsers[0].Guid
            };
        }
        else
            throw new HttpResponseException("Invalid login credentials");
    }
}
我可以浏览到myapi/api/users/并调用GetAll,我还可以浏览到myapi/api/users/1并调用Get方法,但是如果我调用myapi/api/users/authenticate?username={0}&password={1},它将调用Get(而不是Authenticate),并显示错误信息:

The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.String Get(Int32)' in 'Navtrak.Services.WCF.NavtrakAPI.Controllers.UsersController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.

如何调用自定义方法名称,例如Authenticate?

请参考此链接:第5个答案 https://dev59.com/F2cs5IYBdhLWcg3wh0f7#40261803 - Vishwa G
8个回答

140

默认情况下,路由配置遵循RESTful约定,这意味着它只接受Get、Post、Put和Delete动作名称(查看global.asax中的路由配置 => 默认情况下,它不允许您指定任何动作名称 => 它使用HTTP动词进行分发)。因此,当您发送GET请求到/api/users/authenticate时,实际上是调用Get(int id)操作并传递id=authenticate,这显然会崩溃,因为您的Get操作期望一个整数。

如果您想要不同于标准动作名称的名称,可以在global.asax中修改路由定义:

Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{action}/{id}",
    defaults: new { action = "get", id = RouteParameter.Optional }
);

现在您可以导航至/api/users/getauthenticate以对用户进行身份验证。


21
还有其他操作的情况下,是否有办法让它仍然使用Get(id),Get(),Put,Delete和Post呢? - Shawn Mclean
@ShawnMclean 我猜你可以指定另一条路由,不带有{action},但对{id}设置了限制,这样除了intGuid(或其他任何类型)之外的任何内容都不会匹配。然后它应该能够落入Darin建议的那个路由中。 - Steve Greatrex
2
这里还有一件重要的事情,就是使用这种路由风格时,必须使用属性来指定允许的HTTP方法(例如[HttpGet])。 - Himalaya Garg
1
你确定需要使用其他操作吗?你真的尝试过将你所做的事情适应REST规范吗?不应该需要使用其他操作。 - niico
1
@niico:想象一下你想要一个Count()方法,返回Get()将返回的元素数量。我不知道如何将其融入Get()、Get(id)、Post(...)、Put(...)或Delete(id)中。当然,我还能想象出无数种可能的方法。 - Jens Mander
关于Web Api 2,导航到 /api/users/getauthenticate 需要一个额外的步骤,这个帖子的其他答案表明:要么使用Attribute Routing(参见此答案),要么使用 HttpGet + ActionName 属性(参见此答案),或使用 HttpGet + 查询参数(参见此答案)。 - OfirD

89

目前为止,我想到的最佳方法是支持常规REST方法,并同时添加额外的GET方法。请将以下路由添加到您的WebApiConfig中:

routes.MapHttpRoute("DefaultApiWithId", "Api/{controller}/{id}", new { id = RouteParameter.Optional }, new { id = @"\d+" });
routes.MapHttpRoute("DefaultApiWithAction", "Api/{controller}/{action}");
routes.MapHttpRoute("DefaultApiGet", "Api/{controller}", new { action = "Get" }, new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) });
routes.MapHttpRoute("DefaultApiPost", "Api/{controller}", new {action = "Post"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Post)});

我使用下面的测试类验证了这个解决方案。我成功地调用了我的控制器中的每个方法:

public class TestController : ApiController
{
    public string Get()
    {
        return string.Empty;
    }

    public string Get(int id)
    {
        return string.Empty;
    }

    public string GetAll()
    {
        return string.Empty;
    }

    public void Post([FromBody]string value)
    {
    }

    public void Put(int id, [FromBody]string value)
    {
    }

    public void Delete(int id)
    {
    }
}

我确认它支持以下请求:

GET /Test
GET /Test/1
GET /Test/GetAll
POST /Test
PUT /Test/1
DELETE /Test/1

注意,如果你的额外GET动作不以 "Get" 开头,则可能需要向该方法添加 HttpGet 属性。


1
很好的解决方案,您能告诉我如果我像您在getpost上所做的那样配置putdelete动词,是否也能正常工作? - Felipe Oriani
1
在我看来,这应该被包含在WebAPI项目的默认设置中(或许是被注释掉的)。它同时为您提供WebAPI和MVC风格的路由... - John Culviner
1
@FelipeOriani,我认为你不需要配置putdelete动词,因为这些请求通常会携带一个id参数来标识您要应用该操作的资源。对/api/foodelete调用应该会抛出错误,因为您想要删除哪个foo?因此,默认的DefaultApiWithId路由应该可以很好地处理这些情况。 - nwayve
4
这对我来说一点都不起作用。当我尝试进行基本的GET操作时,收到了错误消息。 - Matt
这个配置很恶心,我花了一整天才发现这是导致我的CORS预检请求失败的原因。 如果你需要CORS支持,请不要使用它,因为第一个映射已经阻止了OPTION请求(DefaultApiWithId)。 - kipusoep
显示剩余2条评论

22

我已经进入了MVC4的世界。

就我所知,我有一个SitesAPIController,并且我需要一个自定义方法,可以像这样调用:

http://localhost:9000/api/SitesAPI/Disposition/0

通过为最后一个参数使用不同的值,以获取具有不同处理方式的记录。

对我最终起作用的是:

SitesAPIController中的方法:

// GET api/SitesAPI/Disposition/1
[ActionName("Disposition")]
[HttpGet]
public Site Disposition(int disposition)
{
    Site site = db.Sites.Where(s => s.Disposition == disposition).First();
    return site;
}

还有在 WebApiConfig.cs 中需要这样做:

// this was already there
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

// this i added
config.Routes.MapHttpRoute(
    name: "Action",
    routeTemplate: "api/{controller}/{action}/{disposition}"
 );

只要我将{disposition}称为{id},我就会遇到:

{
"Message": "No HTTP resource was found that matches the request URI 'http://localhost:9000/api/SitesAPI/Disposition/0'.",
"MessageDetail": "No action was found on the controller 'SitesAPI' that matches the request."
}

当我将其重命名为{disposition}时,它开始起作用了。因此,参数名称与占位符中的值相匹配。

随意编辑此答案以使其更准确/说明性更强。


谢谢你的提示。我和你一样犯了同样的错误。 - abhi

16

如果您正在使用 ASP.NET 5ASP.NET MVC 6,那么大多数答案都不适用,因为通常您会让MVC为您创建适当的路由集合(使用默认的RESTful约定),这意味着您不会找到任何要编辑的Routes.MapRoute()调用。

Startup.cs文件调用的ConfigureServices()方法将MVC注册到内置于ASP.NET 5中的依赖项注入框架中:这样,当您在该类中稍后调用ApplicationBuilder.UseMvc()时,MVC框架将自动向您的应用程序添加这些默认路由。我们可以通过查看框架源代码中UseMvc()方法的实现来了解其背后发生了什么:

public static IApplicationBuilder UseMvc(
    [NotNull] this IApplicationBuilder app,
    [NotNull] Action<IRouteBuilder> configureRoutes)
{
    // Verify if AddMvc was done before calling UseMvc
    // We use the MvcMarkerService to make sure if all the services were added.
    MvcServicesHelper.ThrowIfMvcNotRegistered(app.ApplicationServices);

    var routes = new RouteBuilder
    {
        DefaultHandler = new MvcRouteHandler(),
        ServiceProvider = app.ApplicationServices
    };

    configureRoutes(routes);

    // Adding the attribute route comes after running the user-code because
    // we want to respect any changes to the DefaultHandler.
    routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
        routes.DefaultHandler,
        app.ApplicationServices));

    return app.UseRouter(routes.Build());
}

这个好处在于框架现在处理所有的繁重工作,迭代所有控制器的操作并设置它们的默认路由,从而为您节省一些冗余工作。

坏处是,几乎没有关于如何添加自己的路由的文档。幸运的是,您可以使用基于约定和/或属性路由方法(又称属性路由)轻松实现。

基于约定

在您的 Startup.cs 类中,将此替换为:

app.UseMvc();

使用这个:

app.UseMvc(routes =>
            {
                // Route Sample A
                routes.MapRoute(
                    name: "RouteSampleA",
                    template: "MyOwnGet",
                    defaults: new { controller = "Items", action = "Get" }
                );
                // Route Sample B
                routes.MapRoute(
                    name: "RouteSampleB",
                    template: "MyOwnPost",
                    defaults: new { controller = "Items", action = "Post" }
                );
            });

基于属性

MVC6的一个很棒的特性是,你可以通过为Controller类和/或Action方法添加适当的RouteAttribute和/或HttpGet/HttpPost模板参数来定义每个控制器的路由,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;

namespace MyNamespace.Controllers
{
    [Route("api/[controller]")]
    public class ItemsController : Controller
    {
        // GET: api/items
        [HttpGet()]
        public IEnumerable<string> Get()
        {
            return GetLatestItems();
        }

        // GET: api/items/5
        [HttpGet("{num}")]
        public IEnumerable<string> Get(int num)
        {
            return GetLatestItems(5);
        }       

        // GET: api/items/GetLatestItems
        [HttpGet("GetLatestItems")]
        public IEnumerable<string> GetLatestItems()
        {
            return GetLatestItems(5);
        }

        // GET api/items/GetLatestItems/5
        [HttpGet("GetLatestItems/{num}")]
        public IEnumerable<string> GetLatestItems(int num)
        {
            return new string[] { "test", "test2" };
        }

        // POST: /api/items/PostSomething
        [HttpPost("PostSomething")]
        public IActionResult Post([FromBody]string someData)
        {
            return Content("OK, got it!");
        }
    }
}

此控制器将处理以下请求:

 [GET] api/items
 [GET] api/items/5
 [GET] api/items/GetLatestItems
 [GET] api/items/GetLatestItems/5
 [POST] api/items/PostSomething

还要注意,如果您同时使用这两种方法,基于属性的路由(在定义时)将覆盖基于约定的路由,而且它们都将覆盖由UseMvc()定义的默认路由。

有关更多信息,您也可以在我的博客上阅读以下文章


1
这太完美了!其他答案都没有真正做到我需要的,但是你救了我 :) - King Arthur the Third
有没有办法将预定义模型用作第二个参数?例如,当我像这样修补某个用户时:public IActionResult Patch(int id, [FromQuery] Person person)所有传入的属性都为空! - King Arthur the Third

16

默认情况下,Web Api 期望 URL 的格式为 api/{controller}/{id}。要覆盖此默认路由,您可以使用以下两种方式之一设置路由。

第一种选择:

在 WebApiConfig.cs 中添加以下路由注册。

config.Routes.MapHttpRoute(
    name: "CustomApi",
    routeTemplate: "api/{controller}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

使用以下参数为你的动作方法装饰HttpGet

[HttpGet]
public HttpResponseMessage ReadMyData(string param1,
                        string param2, string param3)

 {

// your code here

}

调用上述方法的URL如下:

http://localhost:[yourport]/api/MyData/ReadMyData?param1=value1&param2=value2&param3=value3

第二种选择 在控制器类中添加路由前缀,并像下面这样装饰您的操作方法使用HttpGet。在这种情况下,无需更改任何WebApiConfig.cs文件。它可以具有默认路由。

[RoutePrefix("api/{controller}/{action}")]
public class MyDataController : ApiController
{

[HttpGet]
public HttpResponseMessage ReadMyData(string param1,
                        string param2, string param3)

{

// your code here

}

}

调用上述方法的URL将如下所示

http://localhost:[yourport]/api/MyData/ReadMyData?param1=value1&param2=value2&param3=value3


我非常喜欢第二个选项。你能告诉我如何在VB.net中使用它吗?非常感谢。 - user1617676

9

1
Web API 2及更高版本支持一种新型路由,称为属性路由。顾名思义,属性路由使用属性来定义路由。属性路由使您对web API中的URI具有更多控制权。例如,您可以轻松创建描述资源层次结构的URI。
例如:
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

完美无缺,你不需要任何额外的代码,例如在WebApiConfig.cs中。 只需确保Web API路由是否在WebApiConfig.cs中启用,如果没有,可以像下面这样激活:

        // Web API routes
        config.MapHttpAttributeRoutes();

您不需要在WebApiConfig.cs中做更多的事情或更改任何内容。有关详细信息,您可以查看this article

0

只需按照以下方式修改您的 WebAPIConfig.cs 文件

Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{action}/{id}",
  defaults: new { action = "get", id = RouteParameter.Optional });

然后按照以下方式实现您的API

    // GET: api/Controller_Name/Show/1
    [ActionName("Show")]
    [HttpGet]
    public EventPlanner Id(int id){}

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