WCF Web API, WCF Data Servicesとはまた違うREST APIライブラリ

※ご注意
WCF Web APIはマイクロソフトにより現在開発はキャンセルされ、ASP.NET MVC 4のASP.NET Web APIとして制作・公開されています。

.NET Framework/ASP.NETでRESTfulなAPI構築を目的に記事を探されている方は是非以下のリンクからASP.NET Web APIについてご確認下さい。
http://opcdiary.net/?page_id=5981


WCF Web APIはWCF Data Servicesと同様にWCFでREST APIを持つServiceを構築するためのライブラリですが、Data Servicesが基本的にData Baseのテーブルに対してREST APIを構築する物であったのに対して、Web APIはもう少しDBに対して抽象度が上がり、オブジェクトモデルに対するREST APIを構築するためのライブラリとなっていて、通常のWEB開発者にとってはこちらの方が素直なイメージに近い物だと思います。また、通常のWCFのホスト方法とは別にASP.NETでもサービスをホスト可能となっており、特にASP.NET MVCとの親和性が高く、既存のASP.NET MVCのアプリケーションにREST APIを追加したいような場合や、AzureのWEB Roleに配置したい場合に向いていると思います。

WCF REST Starter kitをご存じの方はそれの後継と考えて良さそうです。(追記:1:11)

この記事はMSエバンジェリストの@chackさんのMSDN Blog記事「Web API を始めてみよう」を発展させて、まずEntity Framework Code Firstのモデルを用意して、そのモデルに対してREST APIを用意するというシナリオで進めていきたいと思います。ですので、そこまでは終わっている前提で進めていきます。

Modelの追加

NuGetなどでEntity Framework 4.1への参照を追加します。

次にModelを追加します。Modelsフォルダの下にHero.csを追加し、以下のようにEF Code FirstのエンティティクラスとDbContextのクラスを追加します。

using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.SqlClient;
using System.ComponentModel.DataAnnotations;

namespace SuperHero.Models
{
    /// 
    /// Heroのモデルを追加
    /// 
    public class Hero
    {
        [Key]
        public int HeroId { get; set; }
        public string Name { get; set; }
        //とりあえずロックのことは考えないのでRowVersionのフィールドはつけない
    }

    /// 
    /// DbContext
    /// 
    public class HeroContext : DbContext
    {
        public DbSet Heros { get; set; }
    }
}

APIの実装

RESTのAPIの実装を追加します。GET, POST, DELETE, PUTを追加します。

まずはGET。これはChackさんのコードの改造です。

/// 
/// ヒーローのリストを取得する。
/// ODataのインターフェイスをサポートしている
/// 基本的に@Chackさんのサンプルを変更
/// 
/// ヒーローのリスト
[WebGet(UriTemplate = "")]
public IQueryable Get() {
    List heroes;
    using (var context = new Models.HeroContext()) {
        heroes = context.Heros.ToList();
    }
    return heroes.AsQueryable();
}

エンティティモデルをリスト化して素直に返すだけです。

次はPOST。

今回はリストで受けて複数のヒーローを一気に追加できるようにしています。

GET以外の処理についてはWebInvokeAttributeクラスを使用し、Method引数でHTTPのコマンドを指定しします。

13行、24行の処理はDBにデータがないときのための処理です。

/// 
/// POSTコマンド
/// ヒーローを追加する
/// 
/// 追加するHeroのリスト
/// 追加したHeroのリスト
[WebInvoke(UriTemplate = "", Method = "POST")]
public HttpResponseMessage> Add(IEnumerable heroes) {
    List addedHeroes = new List();
    using (var context = new Models.HeroContext()) {
        //最後のIDE接続の場合を取得する。
        int lastId = 0;
        if (context.Heros.Count() > 0) {
            lastId = context.Heros
                .OrderByDescending(h => h.HeroId).Last().HeroId;
        }
        //データの追加
        foreach (var _hero in heroes) {
            context.Heros.Add(_hero);
        }
        //DBへの反映
        context.SaveChanges();
        //クライアントに返す反映後のデータのリストを作成する
        if (lastId != 0) {
            addedHeroes = context.Heros.Where(h => h.HeroId > lastId)
                .OrderBy(h => h.HeroId).ToList();
        }
        else {
            addedHeroes = context.Heros.ToList();
        }
                
    }
    return new HttpResponseMessage>(addedHeroes);
}

次はDELETE。

今度はidを引数でとって、そのidのレコードを削除する仕様にしました。

WebInvokeAttributeクラスのUriTemplate引数でidを引数としてとるように指定しています。

/// 
/// DELETEコマンド
/// ヒーローを削除する
/// 
/// 削除するヒーローのid番号
/// 削除されたヒーロー
[WebInvoke(UriTemplate = "{id}", Method = "DELETE")]
public Models.Hero Delete(int id) {
    Models.Hero _hero;
    using (var context = new Models.HeroContext()) {
        _hero = context.Heros.Find(id);
        if (_hero != null) {
            context.Heros.Remove(_hero);
            context.SaveChanges();
        }
        else {
            //idのヒーローが見つからなかったときクライアントには500が返る
            throw new ArgumentException("id not found.");
        }
    }
    return _hero;
}

次はPUT。

単独オブジェクトを受けて、DBを更新します。

/// 
/// PUTコマンド
/// ヒーローを更新する
/// 
/// 更新するヒーロー
/// 更新されたヒーロー
[WebInvoke(UriTemplate = "", Method = "PUT")]
public Models.Hero Update(Models.Hero hero) { 
    using (var context = new Models.HeroContext()) {
        var _hero = context.Heros.Find(hero.HeroId);
        if (_hero != null) {
            _hero.Name = hero.Name;
            context.SaveChanges();
        }
        else {
            //idが存在しないヒーローを更新しようとした場合には例外とする。
            throw new ArgumentException("hero's id not found.");
        }
    }
    return hero;
}

基礎的な内容は上記のような内容になります。

POSTなどに複数のメソッドを割り付けたい場合にはメソッドを追加し、WebInvokeアトリビュートのMethodは同じ(POSTならPOST) としつつUriTemplateで追加したメソッドに合わせたuri(処理や引数指定 「Sub?x={x}&y={y}」とか)を設定していくことになります。

追加リソース

次を進めていくための追加リソースです。

WCFのCodePlexがまずは古本的なリソース源です。

WCF Web API ヘルプファイル

これはCodePlexで公開されているWCF Web APIのCHMファイルのZIPファイルで、現在まとまったドキュメントがないWeb APIの理解のためにリファレンスできるドキュメントはほぼこれだけです。特にWCFWebAPI_Con.chmは総論の内容で、まず始めるに当たってはこの中のGetting Startは目を通すべきでしょう。もう一つのWebAPI_mref.chmはライブラリのリファレンスです。

Implementing an Authorization Attribute for WCF Web API(英文)

こちらはASP.NET MVC & Web APIで公開したREST APIに対して認証をかける場合の方法です。

Microsoft Web API – the REST is done by WCF (Part 1)

大変良くまとまったBlog記事です。

全体コード

APIs/SuperHeroApi.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Net.Http;
using SuperHero.Resources;
using Models = SuperHero.Models;

namespace SuperHero.APIs
{
    /// 
    /// REST API Sample
    /// 
    [ServiceContract]
    public class SuperHeroApi
    {
        /// 
        /// ヒーローのリストを取得する。
        /// ODataのインターフェイスをサポートしている
        /// 基本的に@Chackさんのサンプルを変更
        /// 
        /// ヒーローのリスト
        [WebGet(UriTemplate = "")]
        public IQueryable Get() {
            List heroes;
            using (var context = new Models.HeroContext()) {
                heroes = context.Heros.ToList();
            }
            return heroes.AsQueryable();
        }

        /// 
        /// POSTコマンド
        /// ヒーローを追加する
        /// 
        /// 追加するHeroのリスト
        /// 追加したHeroのリスト
        [WebInvoke(UriTemplate = "", Method = "POST")]
        public HttpResponseMessage> Add(IEnumerable heroes) {
            List addedHeroes = new List();
            using (var context = new Models.HeroContext()) {
                //最後のIDE接続の場合を取得する。
                int lastId = 0;
                if (context.Heros.Count() > 0) {
                    lastId = context.Heros
                        .OrderByDescending(h => h.HeroId).Last().HeroId;
                }
                //データの追加
                foreach (var _hero in heroes) {
                    context.Heros.Add(_hero);
                }
                //DBへの反映
                context.SaveChanges();
                //クライアントに返す反映後のデータのリストを作成する
                if (lastId != 0) {
                    addedHeroes = context.Heros.Where(h => h.HeroId > lastId)
                        .OrderBy(h => h.HeroId).ToList();
                }
                else {
                    addedHeroes = context.Heros.ToList();
                }
                
            }
            return new HttpResponseMessage>(addedHeroes);
        }

        /// 
        /// DELETEコマンド
        /// ヒーローを削除する
        /// 
        /// 削除するヒーローのid番号
        /// 削除されたヒーロー
        [WebInvoke(UriTemplate = "{id}", Method = "DELETE")]
        public Models.Hero Delete(int id) {
            Models.Hero _hero;
            using (var context = new Models.HeroContext()) {
                _hero = context.Heros.Find(id);
                if (_hero != null) {
                    context.Heros.Remove(_hero);
                    context.SaveChanges();
                }
                else {
                    //idのヒーローが見つからなかったときクライアントには500が返る
                    throw new ArgumentException("id not found.");
                }
            }
            return _hero;
        }

        /// 
        /// PUTコマンド
        /// ヒーローを更新する
        /// 
        /// 更新するヒーロー
        /// 更新されたヒーロー
        [WebInvoke(UriTemplate = "", Method = "PUT")]
        public Models.Hero Update(Models.Hero hero) { 
            using (var context = new Models.HeroContext()) {
                var _hero = context.Heros.Find(hero.HeroId);
                if (_hero != null) {
                    _hero.Name = hero.Name;
                    context.SaveChanges();
                }
                else {
                    //idが存在しないヒーローを更新しようとした場合には例外とする。
                    throw new ArgumentException("hero's id not found.");
                }
            }
            return hero;
        }
    }
}

Nodels/Hero.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.SqlClient;
using System.ComponentModel.DataAnnotations;

namespace SuperHero.Models
{
    /// 
    /// Heroのモデルを追加
    /// 
    public class Hero
    {
        [Key]
        public int HeroId { get; set; }
        public string Name { get; set; }
        //とりあえずロックのことは考えないのでRowVersionのフィールドはつけない
    }

    /// 
    /// DbContext
    /// 
    public class HeroContext : DbContext
    {
        public DbSet Heros { get; set; }
    }
}

Global.asax.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.ServiceModel.Activation;
using SuperHero.APIs;
using SuperHero.Models;
using Microsoft.ApplicationServer.Http;
using Microsoft.ApplicationServer.Http.Activation;
//これ以下追加
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.SqlClient;
using System.ComponentModel.DataAnnotations;

namespace SuperHero
{
    // メモ: IIS6 または IIS7 のクラシック モードの詳細については、
    // http://go.microsoft.com/?LinkId=9394801 を参照してください

    public class MvcApplication : System.Web.HttpApplication
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
            filters.Add(new HandleErrorAttribute());
        }

        public static void RegisterRoutes(RouteCollection routes) {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            //TestとHelpのページも自動作成させる
            var config = new HttpConfiguration() { 
                EnableTestClient = true, EnableHelpPage = true };
            routes.Add(new ServiceRoute("api/heroes", 
                new HttpServiceHostFactory() { Configuration = config }, 
                typeof(SuperHeroApi)));

            routes.MapRoute(
                "Default", // ルート名
                "{controller}/{action}/{id}", // パラメーター付きの URL
                new { controller = "Home", 
                      action = "Index", 
                      id = UrlParameter.Optional } // パラメーターの既定値
            );

        }

        protected void Application_Start() {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);
            //追加箇所
            //Databaseのオブジェクトに変更があった場合には作り直す
            Database.SetInitializer(
                new DropCreateDatabaseIfModelChanges());
        }
    }
}

プロジェクトのダウンロード

2 thoughts on “WCF Web API, WCF Data Servicesとはまた違うREST APIライブラリ”

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください