ASP.NET Web API 2 Odata 中的路由约定

本文介绍 ASP.NET 4.x 中的 Web API 2 用于 OData 终结点的路由约定。

Web API 获取 OData 请求时,它会将请求映射到控制器名称和操作名称。 映射基于 HTTP 方法和 URI。 例如, GET /odata/Products(1) 映射到 ProductsController.GetProduct

本文的第 1 部分介绍了内置的 OData 路由约定。 这些约定专为 OData 终结点设计,它们取代了默认的 Web API 路由系统。 (调用 MapODataRoute.)

第 2 部分介绍如何添加自定义路由约定。 目前,内置约定未涵盖整个 OData URI 范围,但你可以扩展它们以处理其他情况。

内置路由约定

在介绍 Web API 中的 OData 路由约定之前,了解 OData URI 会很有帮助。 OData URI 包括:

  • 服务根
  • 资源路径
  • 查询选项

显示 O 数据路由约定外观的屏幕截图,从左到右显示服务根、资源路径和查询选项。

对于路由,重要的是资源路径。 资源路径分为多个段。 例如, /Products(1)/Supplier 有三个段:

  • Products 引用名为“Products”的实体集。
  • 1 是实体键,从集中选择单个实体。
  • Supplier 是选择相关实体的导航属性。

因此,此路径选择产品 1 的供应商。

注意

OData 路径段并不总是与 URI 段相对应。 例如,“1”被视为路径段。

控制器名称。 控制器名称始终派生自资源路径根目录中的实体集。 例如,如果资源路径为 /Products(1)/Supplier,则 Web API 会查找名为 的 ProductsController控制器。

操作名称。 操作名称派生自路径段和实体数据模型 (EDM) ,如下表所示。 在某些情况下,操作名称有两种选择。 例如,“Get”或“GetProducts”。

查询实体

请求 示例 URI 操作名称 示例操作
GET /entityset /产品 GetEntitySet 或 Get GetProducts
GET /entityset (key) /Products (1) GetEntityType 或 Get GetProduct
GET /entityset (键) /cast /Products (1) /Models.Book GetEntityType 或 Get GetBook

有关详细信息,请参阅 创建Read-Only OData 终结点

创建、更新和删除实体

请求 示例 URI 操作名称 示例操作
POST /entityset /产品 PostEntityType 或 Post PostProduct
PUT /entityset (键) /Products (1) PutEntityType 或 Put PutProduct
PUT /entityset (键) /cast /Products (1) /Models.Book PutEntityType 或 Put PutBook
PATCH /entityset (key) /Products (1) PatchEntityType 或 Patch PatchProduct
PATCH /entityset (键) /cast /Products (1) /Models.Book PatchEntityType 或 Patch PatchBook
delete /entityset (key) /Products (1) DeleteEntityType 或 Delete DeleteProduct
delete /entityset (key) /cast /Products (1) /Models.Book DeleteEntityType 或 Delete DeleteBook

查询导航属性

请求 示例 URI 操作名称 示例操作
GET /entityset (键) /navigation /Products (1) /Supplier GetNavigationFromEntityType 或 GetNavigation GetSupplierFromProduct
GET /entityset (键) /cast/navigation /Products (1) /Models.Book/Author GetNavigationFromEntityType 或 GetNavigation GetAuthorFromBook

有关详细信息,请参阅 使用实体关系

创建和删除链接

请求 示例 URI 操作名称
POST /entityset (键) /$links/navigation /Products (1) /$links/Supplier CreateLink
PUT /entityset (键) /$links/navigation /Products (1) /$links/Supplier CreateLink
delete /entityset (key) /$links/navigation /Products (1) /$links/Supplier DeleteLink
DELETE /entityset (key) /$links/navigation (relatedKey) /Products/ (1) /$links/Suppliers (1) DeleteLink

有关详细信息,请参阅 使用实体关系

属性

需要 Web API 2

请求 示例 URI 操作名称 示例操作
GET /entityset (键) /property /Products (1) /Name GetPropertyFromEntityType 或 GetProperty GetNameFromProduct
GET /entityset (键) /cast/property /Products (1) /Models.Book/Author GetPropertyFromEntityType 或 GetProperty GetTitleFromBook

操作

请求 示例 URI 操作名称 示例操作
POST /entityset (键) /action /Products (1) /Rate ActionNameOnEntityType 或 ActionName RateOnProduct
POST /entityset (键) /cast/action /Products (1) /Models.Book/CheckOut ActionNameOnEntityType 或 ActionName CheckOutOnBook

有关详细信息,请参阅 OData 操作

方法签名

下面是方法签名的一些规则:

  • 如果路径包含键,则操作应具有名为 key 的参数。
  • 如果路径包含导航属性中的键,则操作应具有名为 relatedKey 的参数
  • 使用 [FromODataUri] 参数修饰relatedKey 参数。
  • POST 和 PUT 请求采用实体类型的参数。
  • PATCH 请求采用 Delta<T> 类型的参数,其中 T 是实体类型。

为了参考,下面是一个示例,其中显示了每个内置 OData 路由约定的方法签名。

public class ProductsController : ODataController
{
    // GET /odata/Products
    public IQueryable<Product> Get()

    // GET /odata/Products(1)
    public Product Get([FromODataUri] int key)

    // GET /odata/Products(1)/ODataRouting.Models.Book
    public Book GetBook([FromODataUri] int key)

    // POST /odata/Products 
    public HttpResponseMessage Post(Product item)

    // PUT /odata/Products(1)
    public HttpResponseMessage Put([FromODataUri] int key, Product item)

    // PATCH /odata/Products(1)
    public HttpResponseMessage Patch([FromODataUri] int key, Delta<Product> item)

    // DELETE /odata/Products(1)
    public HttpResponseMessage Delete([FromODataUri] int key)

    // PUT /odata/Products(1)/ODataRouting.Models.Book
    public HttpResponseMessage PutBook([FromODataUri] int key, Book item)

    // PATCH /odata/Products(1)/ODataRouting.Models.Book
    public HttpResponseMessage PatchBook([FromODataUri] int key, Delta<Book> item)

    // DELETE /odata/Products(1)/ODataRouting.Models.Book
    public HttpResponseMessage DeleteBook([FromODataUri] int key)

    //  GET /odata/Products(1)/Supplier
    public Supplier GetSupplierFromProduct([FromODataUri] int key)

    // GET /odata/Products(1)/ODataRouting.Models.Book/Author
    public Author GetAuthorFromBook([FromODataUri] int key)

    // POST /odata/Products(1)/$links/Supplier
    public HttpResponseMessage CreateLink([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)

    // DELETE /odata/Products(1)/$links/Supplier
    public HttpResponseMessage DeleteLink([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)

    // DELETE /odata/Products(1)/$links/Parts(1)
    public HttpResponseMessage DeleteLink([FromODataUri] int key, string relatedKey, string navigationProperty)

    // GET odata/Products(1)/Name
    // GET odata/Products(1)/Name/$value
    public HttpResponseMessage GetNameFromProduct([FromODataUri] int key)

    // GET /odata/Products(1)/ODataRouting.Models.Book/Title
    // GET /odata/Products(1)/ODataRouting.Models.Book/Title/$value
    public HttpResponseMessage GetTitleFromBook([FromODataUri] int key)
}

自定义路由约定

目前,内置约定并不涵盖所有可能的 OData URI。 可以通过实现 IODataRoutingConvention 接口来添加新约定。 此接口有两种方法:

string SelectController(ODataPath odataPath, HttpRequestMessage request);
string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, 
    ILookup<string, HttpActionDescriptor> actionMap);
  • SelectController 返回控制器的名称。
  • SelectAction 返回操作的名称。

对于这两种方法,如果约定不适用于该请求,该方法应返回 null。

ODataPath 参数表示已分析的 OData 资源路径。 它包含 一个 ODataPathSegment 实例列表,其中一个用于资源路径的每个段。 ODataPathSegment 是一个抽象类;每个段类型由派生自 ODataPathSegment 的类表示。

ODataPath.TemplatePath 属性是一个字符串,表示所有路径段的串联。 例如,如果 URI 为 /Products(1)/Supplier,则路径模板为“~/entityset/key/navigation”。 请注意,段不直接对应于 URI 段。 例如,实体键 (1) 表示为其自己的 ODataPathSegment

通常, IODataRoutingConvention 的实现将执行以下操作:

  1. 比较路径模板以查看此约定是否适用于当前请求。 如果不适用,则返回 null。
  2. 如果约定适用,请使用 ODataPathSegment 实例的属性派生控制器和操作名称。
  3. 对于操作,请将任何值添加到路由字典中,这些值应绑定到操作参数 (通常为实体键) 。

让我们看一个具体示例。 内置路由约定不支持将索引编制到导航集合中。 换句话说,URI 没有如下所示的约定:

/odata/Products(1)/Suppliers(1)

下面是用于处理此类查询的自定义路由约定。

using Microsoft.Data.Edm;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.OData.Routing;
using System.Web.Http.OData.Routing.Conventions;

namespace ODataRouting
{
    public class NavigationIndexRoutingConvention : EntitySetRoutingConvention
    {
        public override string SelectAction(ODataPath odataPath, HttpControllerContext context, 
            ILookup<string, HttpActionDescriptor> actionMap)
        {
            if (context.Request.Method == HttpMethod.Get && 
                odataPath.PathTemplate == "~/entityset/key/navigation/key")
            {
                NavigationPathSegment navigationSegment = odataPath.Segments[2] as NavigationPathSegment;
                IEdmNavigationProperty navigationProperty = navigationSegment.NavigationProperty.Partner;
                IEdmEntityType declaringType = navigationProperty.DeclaringType as IEdmEntityType;

                string actionName = "Get" + declaringType.Name;
                if (actionMap.Contains(actionName))
                {
                    // Add keys to route data, so they will bind to action parameters.
                    KeyValuePathSegment keyValueSegment = odataPath.Segments[1] as KeyValuePathSegment;
                    context.RouteData.Values[ODataRouteConstants.Key] = keyValueSegment.Value;

                    KeyValuePathSegment relatedKeySegment = odataPath.Segments[3] as KeyValuePathSegment;
                    context.RouteData.Values[ODataRouteConstants.RelatedKey] = relatedKeySegment.Value;

                    return actionName;
                }
            }
            // Not a match.
            return null;
        }
    }
}

注意:

  1. 我派生自 EntitySetRoutingConvention,因为该类中的 SelectController 方法适用于此新的路由约定。 这意味着我不需要重新实现 SelectController
  2. 该约定仅适用于 GET 请求,并且仅当路径模板为“~/entityset/key/navigation/key”时。
  3. 操作名称为“Get{EntityType}”,其中 {EntityType} 是导航集合的类型。 例如,“GetSupplier”。 可以使用喜欢的任何命名约定 - 只需确保控制器操作匹配即可。
  4. 该操作采用两个名为 keyrelatedKey 的参数。 (有关某些预定义参数名称的列表,请参阅 ODataRouteConstants.)

下一步是将新约定添加到路由约定列表中。 这在配置期间发生,如以下代码所示:

using ODataRouting.Models;
using System.Web.Http;
using System.Web.Http.OData.Builder;
using System.Web.Http.OData.Routing;
using System.Web.Http.OData.Routing.Conventions;

namespace ODataRouting
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
            // Create EDM (not shown).

            // Create the default collection of built-in conventions.
            var conventions = ODataRoutingConventions.CreateDefault();
            // Insert the custom convention at the start of the collection.
            conventions.Insert(0, new NavigationIndexRoutingConvention());

            config.Routes.MapODataRoute(routeName: "ODataRoute",
                routePrefix: "odata",
                model: modelBuilder.GetEdmModel(),
                pathHandler: new DefaultODataPathHandler(),
                routingConventions: conventions);

        }
    }
}

下面是一些其他可用于研究的示例路由约定:

当然,Web API 本身是开源的,因此可以查看内置路由约定的 源代码 。 这些在 System.Web.Http.OData.Routing.Conventions 命名空间中定义。