OData services入門----使用ASP.NET Web API描述


ODate 是一種應用層協議,設計它的目的在於提供一組通過HTTP的交互操作。除了提供一些基本的操作(像增刪改查),也提供了一些高級的操作類似過濾數據和實體的導航。

下面的文章我將用ODate 提供給ASP.NET Web API 的功能來建立一個小服務。

ODate

你現在可能在想為什么你的web apps需要另外的協議。JSON難道不是很簡單嗎?XML services 不夠好?嗯,事實上,ODate擴展了上述的協議但是不是取代他們。他可以被XML(ATOM)或者JSON取代但是ODate的重要在於它符合REST原則。在某種意義上,它建立在'簡單'的REST HTTP 服務上,並且有着清晰的目標——簡化和標准化我們操作和查詢數據的方式。如果你過去在給你的REST服務創建搜索、過濾、或者分頁API的時候感覺很麻煩,那么ODate將是一個不錯的選擇。

一些ODate查詢語法的規則:

  • Entity set – /Artists
  • Entity by id – /Artists(1)
  • Sorting – /Artists?$orderby=Name
  • Filtering – /Artists?$filter=Name eq 'Gridlock'

上面的這些只是冰山一角。

概念介紹的差不多了,讓我開始項目吧。很幸運,ASP.NET Web API 可以幫我們很方便的創建ODate。

創建項目

首先我們需要創建一個ASP.NET Web API項目。我們是不需要MVC的相關的東西(視圖,js庫,等)。

ODate的功能是由一個獨立的程序集提供的。請注意,現在這個程序集還是在預覽版,最新的官方發布版本是0.3 RC (this blogpost 點擊鏈接參看項目信息,這個是英文的哦)。

很不幸,使用最新的ODataLib程序集還有一些方法(例如:過濾)沒有實現,不過不用擔心最新的更新將會解決這個問題。因為沒有必要學習過時的API,我們將使用最新的發布版本在http://www.myget.org/F/aspnetwebstacknightly/,使用nuget。如果你不會使用nuget,可以看看這里here

一旦你獲取了nightly build nuget source。可以使用Manage NuGet Packages安裝最新的Web API OData 包,確保你選擇'Include Prerelease'在上面的下拉框中,如下圖所示。

另外需要注意的是Web API OData 還在開發階段,還有一些功能還不支持。不過基本功能已經完成。

數據模型

我們需要一些簡單的模型去操作,使用Entity Framework 和 SQL CE 4,但是Web API's OData也支持其他的數據庫和持久化技術。

CREATE TABLE [Album]
(
    [AlbumId] INT NOT NULL IDENTITY,
    [Title] NVARCHAR(160) NOT NULL,
    [ArtistId] INT NOT NULL,
    [GenreId] INT NOT NULL,
    [ReleaseDate] DATETIME,
    CONSTRAINT [PK_Album] PRIMARY KEY  ([AlbumId])
);
 
CREATE TABLE [Artist]
(
    [ArtistId] INT NOT NULL IDENTITY,
    [Name] NVARCHAR(120),
    CONSTRAINT [PK_Artist] PRIMARY KEY  ([ArtistId])
);
 
CREATE TABLE [Genre]
(
    [GenreId] INT NOT NULL IDENTITY,
    [Name] NVARCHAR(120),
    [Description] NVARCHAR(1020),
    CONSTRAINT [PK_Genre] PRIMARY KEY  ([GenreId])
);
 
ALTER TABLE [Album] ADD CONSTRAINT [FK_AlbumArtistId]
    FOREIGN KEY ([ArtistId]) REFERENCES [Artist] ([ArtistId])
      ON DELETE NO ACTION ON UPDATE NO ACTION;
 
CREATE INDEX [IFK_AlbumArtistId] ON [Album] ([ArtistId]);
 
ALTER TABLE [Album] ADD CONSTRAINT [FK_AlbumGenreId]
    FOREIGN KEY ([GenreId]) REFERENCES [Genre] ([GenreId])
      ON DELETE NO ACTION ON UPDATE NO ACTION;
 
CREATE INDEX [IFK_AlbumGenreId] ON [Album] ([GenreId]);

 

你可以創建一個新的SQL CE數據庫在App_Data文件夾下面,使用內置的環境去執行SQL代碼。請注意執行SQL只能一行一行執行,不支持一下子執行多次語句。一旦數據庫表結構完成了我們可以用VS2012向導生成Entity Data Model。

最后我們將得到一個DbContext類去進行數據操作。

$metadata endpoint 和 IEdmModel

正如我之前提到的,ODate標准定義了一個特別的metadata endpoint,它包含一個定義了實體集,關系,實體類型和操作的文檔。這些保證了ODate是自描述的,能夠讓客戶端去生成表示服務端類型的客戶端代碼,簡化了服務的訪問方式。Metadata endpoint應該在/$metadata下可用。如果你熟悉SOAP服務,你可以把ODate和它類比一下。

GET http://services.odata.org/Northwind/Northwind.svc/$metadata

Metadata文檔使用ODate通用架構定義(OData Common Schema Definition Language (CSDL))。很幸運,ASP.NET Web API可以把$metadata endpoint直接暴露給我們,只要我們的模型繼承IEdmModel

 

public class ModelBuilder
{
    public IEdmModel Build()
    {
        ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
        modelBuilder.EntitySet<Album>("Albums");
        modelBuilder.EntitySet<Artist>("Artists");
        modelBuilder.EntitySet<Genre>("Genres");
 
        return modelBuilder.GetEdmModel();
    }
}

 

public IEdmModel BuildExplicitly()
{
    ODataModelBuilder modelBuilder = new ODataModelBuilder();
    EntitySetConfiguration<Genre> genres = modelBuilder.EntitySet<Genre>("Genres");
    EntityTypeConfiguration<Genre> genre = genres.EntityType;
    genre.HasKey(g => g.GenreId);
    genre.Property(g => g.Name);
    genre.Property(g => g.Description);
 
    //(...)
 
    return modelBuilder.GetEdmModel();
}

 

 

 

使用ODate

Microsoft.AspNet.WebApi.OData提供可一系列的類擴展了Web API

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var modelBuilder = new ModelBuilder();
        IEdmModel model = modelBuilder.Build();
        config.Routes.MapODataRoute("OData", null, model);
        config.EnableQuerySupport();
    }
}

 

這些代碼(Global.asax.cs)做了兩件事情:

  1. 通過路由注冊我們的模型表示方法-
  2. 使查詢可用

現在我們的服務應該自動知道怎么去處理OData ~/$metadata 請求。很酷,不是嗎?

<edmx:Edmx xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx" Version="1.0">
  <edmx:DataServices xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" m:DataServiceVersion="1.0">
    <Schema xmlns="http://schemas.microsoft.com/ado/2009/11/edm" Namespace="Piotr.ODataWebApiService.Service.Models">
      <EntityType Name="Album">...</EntityType>
      <EntityType Name="Artist">...</EntityType>
      <EntityType Name="Genre">...</EntityType>
      <Association
        Name="Piotr_ODataWebApiService_Service_Models_Album_Artist_Piotr_ODataWebApiService_Service_Models_Artist_ArtistPartner">...</Association>
      <Association Name="Piotr_ODataWebApiService_Service_Models_Album_Genre_Piotr_ODataWebApiService_Service_Models_Genre_GenrePartner">...</Association>
      <Association Name="Piotr_ODataWebApiService_Service_Models_Artist_Albums_Piotr_ODataWebApiService_Service_Models_Album_AlbumsPartner">...</Association>
      <Association Name="Piotr_ODataWebApiService_Service_Models_Genre_Albums_Piotr_ODataWebApiService_Service_Models_Album_AlbumsPartner">...</Association>
    </Schema>
    <Schema xmlns="http://schemas.microsoft.com/ado/2009/11/edm" Namespace="Default">...</Schema>
  </edmx:DataServices>
</edmx:Edmx>

 

控制器

現在是時候去檢測一下我們的成果了。我們應該添加一個暴露ODate資源的控制器。正如你將看到的,這和一個平常的CRUD控制器很不一樣。暴露一個ODate實體非常容易。

 

[ODataRouting]
[ODataFormatting]
public class ArtistsController : ApiController
{
    private AlbumsContext db = new AlbumsContext();
 
    // GET /Artists
    // GET /Artists?$filter=startswith(Name,'Grid')
    [Queryable]
    public IQueryable<Artist> Get()
    {
        return db.Artists;
    }        
 
    protected override void Dispose(bool disposing)
    {
        db.Dispose();
        base.Dispose(disposing);
    }
}
[ODataRouting]
[ODataFormatting]
public class ArtistsController : ApiController
{
    private AlbumsContext _db = new AlbumsContext();
 
    // GET /Artists
    // GET /Artists?$filter=startswith(Name,'Grid')
    [Queryable]
    public IQueryable<Artist> Get()
    {
        return _db.Artists;
    }
 
    // GET /Artists(2)
    public HttpResponseMessage Get([FromODataUri]int id)
    {
        Artist artist = _db.Artists.SingleOrDefault(b => b.ArtistId == id);
        if (artist == null)
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
 
        return Request.CreateResponse(HttpStatusCode.OK, artist);
    }
 
    public HttpResponseMessage Put([FromODataUri] int id, Artist artist)
    {
        if (!_db.Artists.Any(a => a.ArtistId == id))
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
        //overwrite any existing id, as url is more explicit
        artist.ArtistId = id;
        _db.Entry(artist).State = EntityState.Modified;
 
        try
        {
            _db.SaveChanges();
        }
        catch (DbUpdateConcurrencyException)
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
 
        return Request.CreateResponse(HttpStatusCode.NoContent);
    }
 
    public HttpResponseMessage Post(Artist artist)
    {
        var odataPath = Request.GetODataPath();
        if (odataPath == null)
        {
            return Request.CreateErrorResponse(HttpStatusCode.BadRequest,
                "ODataPath not present in the request.");
        }
 
        var entitySetPathSegment
            = odataPath.Segments.FirstOrDefault() as EntitySetPathSegment;
 
        if (entitySetPathSegment == null)
        {
            return Request.CreateErrorResponse(HttpStatusCode.BadRequest,
                "ODataPath does not start with entity set path segment");
        }
 
        Artist addedArtist = _db.Artists.Add(artist);
        _db.SaveChanges();
        var response = Request
            .CreateResponse(HttpStatusCode.Created, addedArtist);
 
        response.Headers.Location = new Uri(Url.ODataLink(
                              entitySetPathSegment,
                              new KeyValuePathSegment(ODataUriUtils
                            .ConvertToUriLiteral(addedArtist.ArtistId
                            , ODataVersion.V3))));
        return response;
    }
 
    public HttpResponseMessage Patch([FromODataUri] int id,
        Delta<Artist> artistPatch)
    {
        Artist artist = _db.Artists
            .SingleOrDefault(p => p.ArtistId == id);
        if (artist == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
 
        artistPatch.Patch(artist);
        _db.SaveChanges();
 
        return Request.CreateResponse(HttpStatusCode.NoContent);
    }
 
    public HttpResponseMessage Delete([FromODataUri] int id)
    {
        Artist artist = _db.Artists.Find(id);
        if (artist == null)
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
 
        _db.Artists.Remove(artist);
 
        _db.SaveChanges();
        return Request.CreateResponse(HttpStatusCode.Accepted);
    }
 
    protected override void Dispose(bool disposing)
    {
        _db.Dispose();
        base.Dispose(disposing);
    }
}

 

 

測試這個服務

我將使用Fiddler去測試這個服務。

注意 Content-Type: application/json這個頭。應該加一個新的類型。如果我們想要更新實體的一部分,如下圖

現在這個類別為id=3將更新描述。

最后,我們進行一個文章實體排序操作如下圖,http://localhost:2537/Artists?$orderby=Name&$inlinecount=allpages

正如你以上看到的,我們沒有寫任何一個特別的邏輯去支持這些功能,全部都由框架來提供的,當然如果你願意,也可以自己寫控制器不限於ODate指定的CRUD操作。

ODate毫無疑問是一個有趣的協議。我感覺它更像一個加強的REST服務。

本文的源代碼在bitbucket

 

譯后語:

原文地址:http://www.piotrwalat.net/getting-started-with-odata-services-in-asp-net-web-api/

OData services作為一種最新的協議,將來可能會大規模使用,可能就沒有未來。但是關注一點新的技術總沒什么害處吧,萬一以后你的公司用了,你可以說一句"貌似我以前看過一點",與時俱進嘛。

翻譯中遇到的一些好玩,好用的句型:

a tip of an iceberg冰山一角

reap the reward 獲得獎勵

on steroids 加強的,這個單詞詞典的翻譯是"類固醇",太坑爹,這里完全不是這個意思,后來看了很多例句才體會出加強這個意思。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM