使用 Web API 2.2 的 OData v4 中的包容Containment in OData v4 Using Web API 2.2

作者: Jinfu Tanby Jinfu Tan

通常,仅当实体封装在实体集内时,才能访问该实体。Traditionally, an entity could only be accessed if it were encapsulated inside an entity set. 但 OData v4 提供了两个额外的选项,即 Singleton 和包容,两者都支持 WebAPI 2.2。But OData v4 provides two additional options, Singleton and Containment, both of which WebAPI 2.2 supports.

本主题说明如何在 WebApi 2.2 的 OData 终结点中定义包含。This topic shows how to define a containment in an OData endpoint in WebApi 2.2. 有关包含的详细信息,请参阅包含的内容为 OData v4For more information about containment, see Containment is coming with OData v4. 若要在 Web API 中创建 OData V4 终结点,请参阅使用 ASP.NET Web API 2.2 创建 odata V4 终结点To create an OData V4 endpoint in Web API, see Create an OData v4 Endpoint Using ASP.NET Web API 2.2.

首先,我们将使用此数据模型在 OData 服务中创建包含域模型:First, we'll create a containment domain model in the OData service, using this data model:

数据模型

帐户包含许多 PaymentInstruments (PI),但我们未定义 PI 的实体集。An account contains many PaymentInstruments (PI), but we don't define an entity set for a PI. 相反,只能通过帐户访问 Pi。Instead, the PIs can only be accessed through an Account.

可以从CodePlex下载本主题中使用的解决方案。You can download the solution used in this topic from CodePlex.

定义数据模型Defining the data model

  1. 定义 CLR 类型。Define the CLR types.

    public class Account     
    {         
        public int AccountID { get; set; }         
        public string Name { get; set; }         
        [Contained]         
        public IList<PaymentInstrument> PayinPIs { get; set; }     
    }     
    
    public class PaymentInstrument     
    {         
        public int PaymentInstrumentID { get; set; }        
        public string FriendlyName { get; set; }     
    }
    

    Contained特性用于包含导航属性。The Contained attribute is used for containment navigation properties.

  2. 基于 CLR 类型生成 EDM 模型。Generate the EDM model based on the CLR types.

    public static IEdmModel GetModel()         
    {             
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();             
        builder.EntitySet<Account>("Accounts");             
        var paymentInstrumentType = builder.EntityType<PaymentInstrument>();             
        var functionConfiguration = 
            paymentInstrumentType.Collection.Function("GetCount");             
        functionConfiguration.Parameter<string>("NameContains");             
        functionConfiguration.Returns<int>();             
        builder.Namespace = typeof(Account).Namespace;             
        return builder.GetEdmModel();         
    }
    

    ODataConventionModelBuilder如果将 Contained 特性添加到相应的导航属性中,则将处理生成 EDM 模型。The ODataConventionModelBuilder will handle building the EDM model if the Contained attribute is added to the corresponding navigation property. 如果该属性是集合类型,则 GetCount(string NameContains) 还将创建一个函数。If the property is a collection type, a GetCount(string NameContains) function will also be created.

    生成的元数据将如下所示:The generated metadata will look like the following:

    <EntityType Name="Account">   
      <Key>     
        <PropertyRef Name="AccountID" />   
      </Key>   
      <Property Name="AccountID" Type="Edm.Int32" Nullable="false" />   
      <Property Name="Name" Type="Edm.String" />   
      <NavigationProperty 
        Name="PayinPIs" 
        Type="Collection(ODataContrainmentSample.PaymentInstrument)" 
        ContainsTarget="true" /> 
    </EntityType>
    

    ContainsTarget属性指示导航属性是一个包含。The ContainsTarget attribute indicates that the navigation property is a containment.

定义包含实体集控制器Define the containing entity set controller

包含的实体没有自己的控制器;操作是在包含实体集控制器中定义的。Contained entities don't have their own controller; the action is defined in the containing entity set controller. 在此示例中,有一个 AccountsController,但没有 PaymentInstrumentsController。In this sample, there is an AccountsController, but no PaymentInstrumentsController.

public class AccountsController : ODataController     
{         
    private static IList<Account> _accounts = null;         
    public AccountsController()         
    {             
        if (_accounts == null)             
        {                 
            _accounts = InitAccounts();             
        }         
    }         
    // PUT ~/Accounts(100)/PayinPIs         
    [EnableQuery] 
    public IHttpActionResult GetPayinPIs(int key)         
    {             
        var payinPIs = _accounts.Single(a => a.AccountID == key).PayinPIs;             
        return Ok(payinPIs);         
    }         
    [EnableQuery]         
    [ODataRoute("Accounts({accountId})/PayinPIs({paymentInstrumentId})")]         
    public IHttpActionResult GetSinglePayinPI(int accountId, int paymentInstrumentId)         
    {             
        var payinPIs = _accounts.Single(a => a.AccountID == accountId).PayinPIs;             
        var payinPI = payinPIs.Single(pi => pi.PaymentInstrumentID == paymentInstrumentId);             
        return Ok(payinPI);         
    }         
    // PUT ~/Accounts(100)/PayinPIs(101)         
    [ODataRoute("Accounts({accountId})/PayinPIs({paymentInstrumentId})")]         
    public IHttpActionResult PutToPayinPI(int accountId, int paymentInstrumentId, [FromBody]PaymentInstrument paymentInstrument)         
    {             
        var account = _accounts.Single(a => a.AccountID == accountId);             
        var originalPi = account.PayinPIs.Single(p => p.PaymentInstrumentID == paymentInstrumentId);             
        originalPi.FriendlyName = paymentInstrument.FriendlyName;             
        return Ok(paymentInstrument);         
    }         
    // DELETE ~/Accounts(100)/PayinPIs(101)         
    [ODataRoute("Accounts({accountId})/PayinPIs({paymentInstrumentId})")]         
    public IHttpActionResult DeletePayinPIFromAccount(int accountId, int paymentInstrumentId)         
    {             
        var account = _accounts.Single(a => a.AccountID == accountId);             
        var originalPi = account.PayinPIs.Single(p => p.PaymentInstrumentID == paymentInstrumentId);             
        if (account.PayinPIs.Remove(originalPi))             
        {                 
            return StatusCode(HttpStatusCode.NoContent);             
        }             
        else             
        {                 
            return StatusCode(HttpStatusCode.InternalServerError);             
        }         
    }         
    // GET ~/Accounts(100)/PayinPIs/Namespace.GetCount() 
    [ODataRoute("Accounts({accountId})/PayinPIs/ODataContrainmentSample.GetCount(NameContains={name})")]         
    public IHttpActionResult GetPayinPIsCountWhoseNameContainsGivenValue(int accountId, [FromODataUri]string name)         
    {             
        var account = _accounts.Single(a => a.AccountID == accountId);             
        var count = account.PayinPIs.Where(pi => pi.FriendlyName.Contains(name)).Count();             
        return Ok(count);         
    }         
    private static IList<Account> InitAccounts()         
    {             
        var accounts = new List<Account>() 
        { 
            new Account()                 
            {                    
                AccountID = 100,                    
                Name="Name100",                    
                PayinPIs = new List<PaymentInstrument>()                     
                {                         
                    new PaymentInstrument()                         
                    {                             
                        PaymentInstrumentID = 101,                             
                        FriendlyName = "101 first PI",                         
                    },                         
                    new PaymentInstrument()                         
                    {                             
                        PaymentInstrumentID = 102,                             
                        FriendlyName = "102 second PI",                         
                    },                     
                },                 
            },             
        };            
        return accounts;         
    }     
}

如果 OData 路径为4个或更多个段,则只能使用特性路由,如 [ODataRoute("Accounts({accountId})/PayinPIs({paymentInstrumentId})")] 以上控制器中所示。If the OData path is 4 or more segments, only attribute routing works, such as [ODataRoute("Accounts({accountId})/PayinPIs({paymentInstrumentId})")] in the above controller. 否则,特性和常规路由都适用:例如, GetPayInPIs(int key) 匹配 GET ~/Accounts(1)/PayinPIsOtherwise, both attribute and conventional routing works: for instance, GetPayInPIs(int key) matches GET ~/Accounts(1)/PayinPIs.

感谢在本文的原始内容中 Leo 的。Thanks to Leo Hu for the original content of this article.