65.9K
CodeProject 正在变化。 阅读更多。
Home

RESTful 第 4 天:在 MVC 4 Web API 中使用属性路由进行自定义 URL 重写/路由

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (69投票s)

2015 年 6 月 10 日

CPOL

12分钟阅读

viewsIcon

196678

downloadIcon

8512

在本文中,我将解释如何使用属性路由编写自己的自定义路由。

目录

引言

我们已经学了很多关于 WebAPI 的知识。我之前已经解释过如何创建 WebAPI,如何使用 Entity Framework 将其与数据库连接,如何使用 Unity Container 和 MEF 解析依赖项。在我们所有的示例应用程序中,我们都使用了 MVC 为 CRUD 操作提供的默认路由。在本文中,我将解释如何使用属性路由编写自己的自定义路由。我们将处理操作级别路由和控制器级别路由。我将通过一个示例应用程序详细解释这一点。我的新读者可以使用他们已有的任何 Web API 示例,或者您也可以使用我们在我之前的文章中开发的示例应用程序。

路线图

让我们回顾一下我关于 Web API 的路线图,

这是我学习 RESTful API 的路线图,

我将特意使用 Visual Studio 2010 和 .NET Framework 4.0,因为在 .NET Framework 4.0 中,有些实现很难找到,但我会通过展示如何做到这一点来使其变得容易。

路由

从通用角度来看,任何服务、API、网站的路由都是一种模式定义系统,它试图映射来自客户端的所有请求,并通过为该请求提供一些响应来解析该请求。在 WebAPI 中,我们可以在 WebAPIConfig 文件中定义路由,这些路由定义在内部的路由表中。我们可以在该表中定义多组路由。

现有设计和问题

我们已经有了一个现有的设计。如果您打开解决方案,您会看到如下结构,

在我们的现有应用程序中,我们创建了 WebAPI,其中包含 WebApi 项目的 App_Start 文件夹中名为 WebApiConfig 的文件里提到的默认路由。路由在 Register 方法中定义如下:

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

不要被 MVC 路由混淆,因为我们使用的是 MVC 项目,所以我们也在 RouteConfig.cs 文件中定义了 MVC 路由,如下所示:

public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

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

我们需要关注第一个,即 WebAPI 路由。正如您在下图中所看到的,每个属性的含义如下:

我们有一个路由名称,一个通用的路由模式,以及提供可选参数的选项。

由于我们的应用程序没有特定的不同操作名称,并且我们使用 HTTP VERBS 作为操作名称,因此我们没有过多关注路由。我们的操作名称如下:

1.	public HttpResponseMessage Get()
2.	public HttpResponseMessage Get(int id)
3.	public int Post([FromBody] ProductEntity productEntity)
4.	public bool Put(int id, [FromBody]ProductEntity productEntity)
5.	public bool Delete(int id)

定义的默认路由不考虑 HTTP VERBS 操作名称,而是将它们视为默认操作,因此它不在 routeTemplate 中包含 {action}。但这并非没有限制,我们可以拥有在 WebApiConfig 中定义的我们自己的路由,例如,查看以下路由:

  public static void Register(HttpConfiguration config)
        {
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

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

等等。在上述路由中,如果我们有自定义操作,也可以包含操作名称。

因此,在 WebAPI 中定义路由没有限制。但有一些限制,请注意,我们正在谈论 WebAPI 1,我们在 Visual Studio 2010 中与 .NET Framework 4.0 一起使用它。Web API 2 已经克服了这些限制,并提供了我将在本文中解释的解决方案。让我们看看这些路由的限制:

是的,这是我在 Web API 1 中提到的限制。

如果我们有类似 routeTemplate: "api/{controller}/{id}" 或 routeTemplate: "api/{controller}/{action}/{id}" 或 routeTemplate: "api/{controller}/action/{action}/{id}" 的路由模板,

我们永远无法拥有自定义路由,而必须遵循 MVC 提供的旧路由约定。假设项目的客户端想要公开服务的多​​个端点,他无法做到。我们也无法为路由定义自己的名称,因此有很多限制。

假设我们想为我的 Web API 端点拥有以下类型的路由,其中我还可以定义版本:

v1/Products/Product/allproducts
v1/Products/Product/productid/1
v1/Products/Product/particularproduct/4
v1/Products/Product/myproduct/<带范围>
v1/Products/Product/create
v1/Products/Product/update/3

等等,那么我们无法在现有模型中实现这一点。

幸运的是,这些问题已经在 WebAPI 2 中通过 MVC 5 得到了解决,但对于这种情况,我们有 AttributeRouting 来解决和克服这些限制。

属性路由

Attribute Routing 主要是用于在控制器级别、操作级别创建自定义路由。我们可以使用 Attribute Routing 定义多个路由。我们也可以拥有路由的版本,总之,我们已经解决了现有问题。让我们直接开始如何在现有项目中实现这一点。我不会解释如何创建 WebAPI,您可以参考我的系列文章的第一篇

步骤 1:打开解决方案,然后打开程序包管理器控制台,如下图所示,

转到工具 -> 库程序包管理器 -> 程序包管理器控制台

步骤 2:在 Visual Studio 左上角的程序包管理器控制台窗口中,键入 Install-Package AttributeRouting.WebApi,然后选择 WebApi 项目或您正在使用其他代码示例的 API 项目,然后按 Enter。

步骤 3:程序包安装完成后,您会在 App_Start 文件夹中看到一个名为 AttributeRoutingHttpConfig.cs 的类。

此类有自己的 RegisterRoutes 方法,该方法会内部映射属性路由。它有一个启动方法,该方法会拾取 GlobalConfiguration 中定义的 Routes 并调用 RegisterRoutes 方法,

using System.Web.Http;
using AttributeRouting.Web.Http.WebHost;

[assembly: WebActivator.PreApplicationStartMethod(typeof(WebApi.AttributeRoutingHttpConfig), "Start")]

namespace WebApi 
{
    public static class AttributeRoutingHttpConfig
	{
		public static void RegisterRoutes(HttpRouteCollection routes) 
		{    
			// See http://github.com/mccalltd/AttributeRouting/wiki for more options.
			// To debug routes locally using the built in ASP.NET development server, go to /routes.axd

            routes.MapHttpAttributeRoutes();
		}

        public static void Start() 
		{
            RegisterRoutes(GlobalConfiguration.Configuration.Routes);
        }
    }
}

我们甚至不需要触碰这个类,我们的自定义路由将通过这个类自动处理。我们只需要专注于定义路由。无需编码 :) 您现在可以使用路由特定的内容,如路由名称、谓词、约束、可选参数、默认参数、方法、路由区域、区域映射、路由前缀、路由约定等。

设置 REST 端点/WebAPI 项目以定义路由

我们完成了 90% 的工作。

我们现在需要设置我们的 WebAPI 项目并定义我们的路由。

我们现有的 ProductController 类看起来如下:

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using BusinessEntities;
using BusinessServices;

namespace WebApi.Controllers
{
    public class ProductController : ApiController
    {

        private readonly IProductServices _productServices;

        #region Public Constructor

        /// <summary>
        /// Public constructor to initialize product service instance
        /// </summary>
        public ProductController(IProductServices productServices)
        {
            _productServices = productServices;
        }

        #endregion

        // GET api/product
        public HttpResponseMessage Get()
        {
            var products = _productServices.GetAllProducts();
            var productEntities = products as List<ProductEntity> ?? products.ToList();
            if (productEntities.Any())
                return Request.CreateResponse(HttpStatusCode.OK, productEntities);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
        }

        // GET api/product/5
        public HttpResponseMessage Get(int id)
        {
            var product = _productServices.GetProductById(id);
            if (product != null)
                return Request.CreateResponse(HttpStatusCode.OK, product);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
        }

        // POST api/product
        public int Post([FromBody] ProductEntity productEntity)
        {
            return _productServices.CreateProduct(productEntity);
        }

        // PUT api/product/5
        public bool Put(int id, [FromBody] ProductEntity productEntity)
        {
            if (id > 0)
            {
                return _productServices.UpdateProduct(id, productEntity);
            }
            return false;
        }

        // DELETE api/product/5
        public bool Delete(int id)
        {
            if (id > 0)
                return _productServices.DeleteProduct(id);
            return false;
        }
    }
}

其中我们有一个名为 Product 的控制器,以及以 Verbs 命名的操作。当我们运行应用程序时,只会得到以下类型的端点(请忽略端口和 localhost 设置,因为我是从本地环境运行此应用程序),

获取所有产品

https://:40784/api/Product

按 ID 获取产品

https://:40784/api/Product/3

创建产品

https://:40784/api/Product(带 JSON 正文)

更新产品。

https://:40784/api/Product/3(带 JSON 正文)

删除产品。

https://:40784/api/Product/3

步骤 1:在控制器中添加两个命名空间,

using AttributeRouting;
using AttributeRouting.Web.Http;

步骤 2:使用不同的路由装饰您的操作,

如上图所示,我定义了一个名为 productid 的路由,它以 id 作为参数。我们还必须在路由旁边提供谓词(GET、POST、PUT、DELETE、PATCH),如图像所示。所以它是 [GET("productid/{id?}")]。您可以为您的 Action 定义任何您想要的路由,例如 [GET("product/id/{id?}")], [GET("myproduct/id/{id?}")] 以及更多。

现在当我运行应用程序并导航到 /help 页面时,我会看到这个,

也就是说,我为获取产品 ID 获得了一个额外的路由。当您测试此服务时,您会看到您想要的 URL 类似于:https://:55959/Product/productid/3,这听起来就像真正的 REST :)

同样,使用多个路由装饰您的 Action,如下所示:

        // GET api/product/5
        [GET("productid/{id?}")]
        [GET("particularproduct/{id?}")]
        [GET("myproduct/{id:range(1, 3)}")]
        public HttpResponseMessage Get(int id)
        {
            var product = _productServices.GetProductById(id);
            if (product != null)
                return Request.CreateResponse(HttpStatusCode.OK, product);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
        }

因此,我们看到,我们可以拥有自定义路由名称,也可以为单个 Action 定义多个端点。这很令人兴奋。每个端点都会不同,但会提供相同的​​结果集。

  • {id?}:这里的 ‘?’ 表示参数是可选的。
  • [GET("myproduct/{id:range(1, 3)}")],表示在此范围内的产品 ID 才会被显示。

更多路由约束

您可以利用 Attribute Routing 提供的众多路由约束。我将以其中一些为例:

Range

要获取范围内的产品,我们可以定义该值,前提是它存在于数据库中。

        [GET("myproduct/{id:range(1, 3)}")]
        public HttpResponseMessage Get(int id)
        {
            var product = _productServices.GetProductById(id);
            if (product != null)
                return Request.CreateResponse(HttpStatusCode.OK, product);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
        }

正则表达式

您也可以更有效地使用它来处理文本/字符串参数。

        [GET(@"id/{e:regex(^[0-9]$)}")]
        public HttpResponseMessage Get(int id)
        {
            var product = _productServices.GetProductById(id);
            if (product != null)
                return Request.CreateResponse(HttpStatusCode.OK, product);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
        }

e.g. [GET(@"text/{e:regex(^[A-Z][a-z][0-9]$)}")]

可选参数和默认参数

您也可以将服务参数标记为可选。例如,您想通过姓名从数据库中获取员工详细信息,

[GET("employee/name/{firstname}/{lastname?}")]
public string GetEmployeeName(string firstname, string lastname="mittal")
{
   …………….
  ……………….
}

在上述代码中,我通过使用问号 ‘?’ 将姓氏标记为可选,以获取员工详细信息。我的最终用户可以选择是否提供姓氏。

因此,上述端点可以通过 GET 谓词和 URL 访问:

~/employee/name/akhil/mittal
~/employee/name/akhil

如果定义的路由参数被标记为可选,您还必须为该方法参数提供一个默认值。

在上面的例子中,我将“lastname”标记为可选,因此在方法参数中提供了默认值,如果用户不发送任何值,将使用“mittal”。

在 .NET 4.5 Visual Studio > 2010 和 WebAPI 2 中,您也可以将 DefaultRoute 定义为属性,您可以自己尝试一下。使用属性 [DefaultRoute] 来定义默认路由值。

您可以尝试为所有控制器操作提供自定义路由。

我将我的操作标记为:

        // GET api/product
        [GET("allproducts")]
        [GET("all")]
        public HttpResponseMessage Get()
        {
            var products = _productServices.GetAllProducts();
            var productEntities = products as List<ProductEntity> ?? products.ToList();
            if (productEntities.Any())
                return Request.CreateResponse(HttpStatusCode.OK, productEntities);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
        }

        // GET api/product/5
        [GET("productid/{id?}")]
        [GET("particularproduct/{id?}")]
        [GET("myproduct/{id:range(1, 3)}")]
        public HttpResponseMessage Get(int id)
        {
            var product = _productServices.GetProductById(id);
            if (product != null)
                return Request.CreateResponse(HttpStatusCode.OK, product);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
        }

        // POST api/product
        [POST("Create")]
        [POST("Register")]
        public int Post([FromBody] ProductEntity productEntity)
        {
            return _productServices.CreateProduct(productEntity);
        }

        // PUT api/product/5
        [PUT("Update/productid/{id}")]
        [PUT("Modify/productid/{id}")]
        public bool Put(int id, [FromBody] ProductEntity productEntity)
        {
            if (id > 0)
            {
                return _productServices.UpdateProduct(id, productEntity);
            }
            return false;
        }

        // DELETE api/product/5
        [DELETE("remove/productid/{id}")]
        [DELETE("clear/productid/{id}")]
        [PUT("delete/productid/{id}")]
        public bool Delete(int id)
        {
            if (id > 0)
                return _productServices.DeleteProduct(id);
            return false;
        }

因此,我们得到路由:

 

GET

POST / PUT / DELETE

查看更多约束这里

您必须会看到每个路由中的“v1/Products”,这是由于我在控制器级别使用了 RoutePrefix。让我们详细讨论 RoutePrefix。

RoutePrefix:在控制器级别进行路由

我们为每个操作添加了特定的路由,但您猜怎么着,我们也可以为控制器添加特定的路由名称,我们可以通过使用 AttributeRoutingRoutePrefix 属性来实现。我们的控制器名为 Product,我想在每个操作之前添加 Products/Product,因此,无需在每个操作中重复代码,我可以通过如下所示用此名称装饰我的 Controller 类:

    [RoutePrefix("Products/Product")]
    public class ProductController : ApiController
    {

现在,由于我的控制器被标记为此路由,它也会将其添加到每个操作中。例如,以下操作的路由:

        [GET("allproducts")]
        [GET("all")]
        public HttpResponseMessage Get()
        {
            var products = _productServices.GetAllProducts();
            var productEntities = products as List<ProductEntity> ?? products.ToList();
            if (productEntities.Any())
                return Request.CreateResponse(HttpStatusCode.OK, productEntities);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
        }

现在变成:

~/Products/Product/allproducts
~/Products/Product/all

RoutePrefix:版本控制

RoutePrefix 也可以用于端点的版本控制,例如在我的代码中,我在 RoutePrefix 中提供了“v1”作为版本,如下所示:

    [RoutePrefix("v1/Products/Product")]
    public class ProductController : ApiController
    {

因此,“v1”将被添加到服务的每个路由/端点。当我们发布下一个版本时,我们可以肯定地单独维护一个更改日志,并将端点标记为控制器级别的“v2”,这将把“v2”添加到所有操作中,

例如:

~/v1/Products/Product/allproducts
~/v1/Products/Product/all

~/v2/Products/Product/allproducts
~/v2/Products/Product/all

RoutePrefix:覆盖

此功能存在于 .NET 4.5 和 Visual Studio > 2010 配合 WebAPI 2 中。您可以在那里进行测试。

可能会出现我们不想为每个操作都使用 RoutePrefix 的情况。AttributeRouting 也提供了这种灵活性,即使控制器级别存在 RoutePrefix,单个操作也可以拥有自己的路由。它只需要覆盖默认路由,如下所示:

控制器中的 RoutePrefix

     [RoutePrefix("v1/Products/Product")]
    public class ProductController : ApiController
    {

操作的独立路由

       [Route("~/MyRoute/allproducts")]
        public HttpResponseMessage Get()
        {
            var products = _productServices.GetAllProducts();
            var productEntities = products as List<ProductEntity> ?? products.ToList();
            if (productEntities.Any())
                return Request.CreateResponse(HttpStatusCode.OK, productEntities);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
        }

禁用默认路由

您可能会想,在服务帮助页面上的所有 URL 列表中,我们看到了一些不同/其他我们没有通过属性路由定义的路由,它们以 ~/api/Product 开头。这些路由是我们从 WebApiConfig 文件中提供的默认路由的结果,还记得吗?如果您想摆脱那些不必要的路由,只需进入 Appi_Start 文件夹下的 WebApiConfig.cs 文件,注释掉 Register 方法中写的所有内容,

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

您也可以删除整个 Register 方法,但为此您需要从 Global.asax 文件中删除对其的调用。

运行应用程序

运行应用程序,我们得到:

我们已经添加了测试客户端,但对于新读者,只需右键单击 WebAPI 项目,转到管理 Nuget 包,然后在在线包的搜索框中键入 WebAPITestClient,

您将得到“ASP.NET Web API 的简单测试客户端”,只需添加它。您将在 Areas-> HelpPage 中得到一个帮助控制器,如下所示:

我已经在之前的文章中提供了数据库脚本和数据,您可以使用相同的。

在应用程序 URL 中追加 "/help",您将获得测试客户端,

GET

POST

PUT

删除

您可以通过单击来测试每个服务。一旦您点击服务链接,您将被重定向到该特定服务的测试页面。在该页面右下角有一个“测试 API”按钮,只需按下该按钮即可测试您的服务,

用于获取所有产品的服务,

同样,您可以测试所有服务端点。

结论

我们现在知道如何定义我们的自定义端点及其好处。我只想说这个库是由 Tim Call(http://attributerouting.net 的作者)引入的,并且 Microsoft 已将其默认包含在 WebAPI 2 中。我的下一篇文章将介绍使用 WepAPI 中的 ActionFilters 进行基于令牌的身份验证。在此之前,祝您编码愉快 :) 您也可以从GitHub 下载完整的源代码。添加缺少的必要程序包。

点击Github Repository 浏览完整的源代码。

参考文献

http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx

https://github.com/mccalltd/AttributeRouting

我的其他系列文章

MVC:https://codeproject.org.cn/Articles/620195/Learning-MVC-Part-Introduction-to-MVC-Architectu

OOP:https://codeproject.org.cn/Articles/771455/Diving-in-OOP-Day-Polymorphism-and-Inheritance-Ear

© . All rights reserved.