2013年12月27日金曜日

ハイブリッドクラウドとMTU

すでにコロケーションサービスを利用したりして、データセンターには自前のサーバーがいて。でも、今のデータセンターの利用を拡張していったり、するのもどうかなー、資産化するのもなー。という時にはパブリッククラウドにVPN接続させて、オンプレミスとのハイブリッド。
ハイブリッドクラウドですよね。IaaSとしてのハイブリッドだけじゃなく、SaaS利用でのハイブリッドももちろん最大限活用していくのが重要だと思いますが、今回はIaaSで。

ちょっと図にするのは苦手なので拝借したものを貼り付けますー。


Amazon Web Services Blog: Introducing Amazon Virtual Private Cloud (VPC)

こんな感じのイメージですよね。
データセンターからパブリッククラウド(とりあえずAWSでどうぞ)へVPN(IPSec)を利用して経路確保。

で、プライベートアドレスを使った、データセンター拡張の出来上がり!
もともとデータセンター使ってて、がっつり移行する場合は、気にしなくても意外といけちゃうんですけど、まぁ、ね。IaaS上のサービスがいかにハイパフォーマンスになっても、利用料だといってもですよ、コンピューティングリソースは仕入れです!仕入れ値が閾値を超えない範囲なら全然大活用するんですけど、微妙だなー、オンプレもあるしなー、っていうときはどっこいせと全面クラウド移行しないで、ハイブリッドが都合いいと思うわけです。もちろん値段だけじゃないですけど、値段気にしなくていいならね、そもそも自分たちでデータセンタ作るとか、そういうところまで行き着くとおもうので、その辺バランス大事ですね。

でですよ、提供してるサービスがさらに外部サービスを呼び出す場合にですね、AWS VPCから直接外に向かう方法と、データセンター経由する方法とあると思うんですけど、シンプルなのはデータセンター経由させる方法でしょうか。既存の拡張という意味で。トラフィックが問題になったり、いろいろあるなら、VPCからNATインスタンス(NAT インスタンス - Amazon Virtual Private Cloud)よろしく。

ここで、ちょっとあれです。外部サービスでHTTPSのものをVPC内からVPN経由で呼び出す際に、全然接続できない問題が起きることがあります。ありました。起きました。

面白いですよね。

データセンター内のHTTPSサーバーにはつながるから、ふむー、ってなんたんで、しょうがないからNetwork Monitorで確認してみたんすよ。

SSL (Secure Sockets Layer) と TLS (Transport Layer Security)

ね。うまく接続できるサイトと出来ないサイトの違いはServer Helloが返ってこないところ。




上のフローが正常に接続できるとき。下がダメなとき。ハンドシェークの途中、ダメなときはServer Helloが返ってきてないでしょ。


Client Helloの後の、TLS:Continued Data見てみると、TCP: Segment Lostってなってるあるよ。

12.04 - Can't connect to certain HTTPS sites - Ask Ubuntu

なんだ、つまり、curl -vでやれば確認できたのね。じゃぁ、これどうしちゃえば、いいんすか。




おぉー。Client Helloでとまりよった。

TCP PREVIOUS SEGMENT LOST #REALLY RARE CASE# - Wireshark Q&A

と、いうわけでMTUです。

Windows Azure の VPN 接続が不安定になる問題

接続が不安定、っていうか特定サイトにまったく接続できないんですけどね。

これね、きっとね、先方のネットワーク内もIPSec的な事になってるとね、大きすぎるわー、ってなっちゃうんでしょうね。
ちなみに、今回接続できなかったところって、PINGでMTUチェックできなくてさー。もう、じゃぁ、いくつならいいんだよ!と、思ってたところで、Azureでのはなしが出てたのを思い出したんスよね。

んじゃ、1350にしとこかな、って。

一件落着でした。
自分のところがVPNなら相手のところもVPNっていうのは当然あり得ることですよねー。あれー、おやー、と思ったら、この辺チェックしてみるといいと思います。

2013年12月26日木曜日

ARR Disk Cacheとフィーチャーフォン

Configure and Enable Disk Cache in Application Request Routing : The Official Microsoft IIS Site

ARRを導入してるならぜひとも使っておきたい機能ですね。
Disk Cacheの有無で内部トラフィックや内部サーバーはずいぶんと楽になります。もちろんCDNは使うんですけど、CDNを使うかどうかにかかわらずです。

Using Compression in Application Request Routing : The Official Microsoft IIS Site

ちなみにDisk CacheをOnにした場合、ScavengerっていうのがDiskのクリーニングをするんですが、マルチテナントだとそのIOがてーへんなことになるので、極力Flash Driveを使うようにしましょう。コンテンツがそんなに多くなくて問題にならない(確認しつつ)HDDでも全然さばけます。

サーバーファームにしてなくてもARRにするだけで接続をうまく制御できるので、おすすめです。って話は前と変わらないんですけど、今回はちょっと決定打が見当たらない問題についてです。

そもそもDisk CacheをOnにするときってWebFarm単位で指定するんですけど、その際にフィーチャーフォンなんかのutf-8でリクエストを受け取れないものがある場合、残念なことに502.3でエラーです。


URL引数に"?日本語"っていうのをShift_JISでURL Encodeして渡してます。これUTF-8だと問題ないんです。どういうことかというと、キャッシュされてるものをみるとなんとなくわかるんですけど。


キャッシュしたファイル名にURL引数の値がそのままくっついて生成されます。もちろんキャッシュするさいにQuery StringをIgnoreにすると関係なくなるんですけどね。

Configure Caching with Query String Support in Application Request Routing : The Official Microsoft IIS Site

で、じゃー、system.webのglobalization(globalization 要素 (ASP.NET 設定スキーマ))で指定しとけばいいじゃん、て思うでしょ?
ダメなんすよ、タイミング的に先にキャッシュの有無を確認してからだし、そもそもARRのアプリケーションプールはマネージコード無しだしー。先にDiskをチェックするからこそ!ですよね。

要求トレースしとくと、エラー出てますよね。



気持ちはわかりますよ。Windows 用の Http.sys レジストリ設定とかでFavorUTF8とかつっても、ARRがエラーです、って言ってんだから。そもそもDiskキャッシュOnにしなければ、Shift_JISだろうとエラーにならないわけですから、この辺も関係ないんですよね。

つまり、C:\Program Files\IIS\External Disk Cacheにインストールされるecache.dllがですね、ネイティブですけど、文字コード固定なんですよ、きっと。できることなら指定できるようにしてほしい。そもそもQueryStringそのまま信じて処理しないでほしい。ファイル名もエンコードしたものでそのまま生成してくれればいいのに。


UTF-8縛りでいいじゃん!っていうのもね、そうなんですけどー。なかなかさー。
こういう場合はどういう方法とるのがいいのか、悩ましいところですけど、管理手間は増えるけどDiskキャッシュのOn/OffでそれぞれのWebFarmを用意する、っていうのしか手がなさそうなんですよねー。

  http://normal.website
  http://sjis.website
  http://contents.website

とかURLがそもそもわかれる場合は3番目のサイトだけDiskキャッシュOnとかさ。でも、混在させるじゃない、何気に。

  http://composite.website
  http://composite.website/mobile

とかね。そーするとWebFarmのルーティングルールでフォルダ単位でWebFarm区切る。
なかなか、手間かかるところでは、ありますが、最初の定義だけなんでね、その辺は。それよりサーバー利用効率あげたいしー。

と、いうわけで、決め手に欠けるエラー回避(消極的な)しか思いつかなくて、しょんぼりです。

2013年12月5日木曜日

ASP.NET IdentityとTable Per Hierarchy

Getting Started with ASP.NET Identity : The Official Microsoft ASP.NET Site

なかなか面白いですね。

SimpleMembership, Membership Providers, Universal Providers and the new ASP.NET 4.5 Web Forms and ASP.NET MVC 4 templates - Jon Galloway

Membershipそのものをいろいろやっていこうとしてたところ、やっぱり筋がよくないなー、ってなってしまったのか。

認証と承認、アプリケーション機能を明確に分離してしまったほうが、いいんじゃないか。っていうところに行き着いて、外部認証(OAuth)なんかも取り込んで行きたいし、Claimだしー、みたいなのを考えるとー、っていうところでしょうか。

The good, the bad and the ugly of ASP.NET Identity | brockallen

気持ちはわかりますが、アプリケーション機能を除外して認証に特化するUserManager系列と、承認は既存パイプラインに乗せて、OWINやらHttpModuleやらで個別に実装するか、標準のものを利用するか、でずいぶんと筋のいい発展だと思います。

んじゃ、いつどこでMembershipProviderからASP.NET Identityに乗り換えるんでしょう。どういうパターンで乗り換えましょうね。


  • 新しいプロジェクトで新しいユーザーストアに適用。DBは新規。
  • 新しいプロジェクトで既存ユーザーストアを利用。DBは既存。
  • 既存プロジェクトで新しいデータストアを再構築。DBは移行。
  • 既存のプロジェクトで既存ユーザーストアを利用。DBは既存。

とかー。
新規にDB作ったり移行する分には簡単ですよ。きっと。かってにマイグレーションされてDB出来たり、Migrating an Existing Website from SQL Membership to ASP.NET Identity : The Official Microsoft ASP.NET Site こういうのもあって、移行も楽ちん。この辺はいろいろ情報もあるしちょうと小野さんが遊んでるようなので、そちらのエントリーを。

問題は既存のDBを利用するパターンでしょうか。しかもMembershipを使ってない時なんていうのはもう完全にFluent API(Entity Framework Fluent API - Configuring/Mapping Properties & Types)でのマッピングがわからないと手のつけようがないでしょう。

例えばこのサンプル。そのままだとちゃんと動かないんだけど。

rustd/AspnetIdentitySample

namespace AspnetIdentitySample.Models
{
    public class MyUser : IdentityUser
    {
        public string HomeTown { get; set; }
        public virtual ICollection ToDoes { get; set; }
    }

    public class ToDo
    {
        public int Id { get; set; }
        public string Description { get; set; }
        public bool IsDone { get; set; }
        public virtual MyUser User { get; set; }
    }
    public class MyDbContext : IdentityDbContext
    {
        public MyDbContext()
            : base("DefaultConnection")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity()
                .ToTable("Users");
            modelBuilder.Entity()
                .ToTable("Users");
        }

        public System.Data.Entity.DbSet ToDoes { get; set; }
    }
}

モデルはこんな感じですよね。これってDB上どうなるのかっていうと、マイグレーションの結果IdentityUserのプロパティと、MyUserで追加してるプロパティ(HomeTownとTodoes)が展開できるようになりますよね。
で、modelBuilderでToTable("Users")ってなってるので、AspNetUsersテーブルじゃなくてUsersテーブルを利用。そこまではいいですよね。



プロパティに持ってない項目がスキーマにできてるのわかります?



Discriminatorってだれだよ!
ASP.NET Identityの内部で自動生成されたりするんだろうか。って思っちゃいますよね。
違うんです!違う違う!

これねー、EntityFrameworkのCodeFirstの機能なんすよ。


同一テーブルを複数モデルのマッピング先に利用する場合、そのレコードがどのモデルクラスに復元されなければいけないか、を保持する列なんです。問答無用です。まじかよ!ってなりますよね。

なのでテーブルにこの項目がない場合IdentityUserの派生クラスをつかって、既存ユーザーストアにマッピングするのが厳しくなります。

なんでかというと、UserStoreのジェネリックがIdentityUser派生をwhereで指定してるので、IUserから直接だと無理ー。

既存ユーザーストアなんてUserNameとかPasswordHashとかSecurityスタンプとかって名前でテーブル定義してるわけないじゃないですか。

もっとシンプルに

CREATE TABLE [dbo].[UserMaster] (
    [Id]       NVARCHAR (128) NOT NULL,
    [Name]     NVARCHAR (MAX) NOT NULL,
    [Password] NVARCHAR (MAX) NOT NULL,
    [Status]   INT           NOT NULL,
    [Description] NVARCHAR (MAX) NOT NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC)
);

こんなもんだったりしないすか。例えばの話ですよ。

じゃぁ、既存のこのスキーマにマッピングするためのFluent定義はどうなるか、っていうと、さっきのコードを回収すると↓こうなります。

    public class ApplicationUser : IdentityUser
    {
        public string Description { get; set; }
    }

    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            //base.OnModelCreating(modelBuilder);

            modelBuilder.Ignore();
            modelBuilder.Ignore();
            modelBuilder.Ignore();
            modelBuilder.Ignore();

            modelBuilder.Entity().ToTable("UserMaster");
            modelBuilder.Entity().Property(t => t.Id).HasColumnName("Id");
            modelBuilder.Entity().Property(t => t.UserName).HasColumnName("Name");
            modelBuilder.Entity().Property(t => t.PasswordHash).HasColumnName("Password");
            modelBuilder.Entity().Ignore(t => t.SecurityStamp);

            modelBuilder.Entity().ToTable("UserMaster");
        }
    }

※baseのOnModelCreatingコメントアウトしないと、デフォルトのルールがはいってうるさい。
※あと、Migrationのテーブル消しとかないと、マイグレーションしようとしてうるさいから、テーブルも削除しときましょう。



実行するとこれです。Discriminatorがないです、って怒られます。なんでかっていうと、IdentityUserとApplicationUserが同じテーブルにマッピングされるから。これはねー、難しいと思うんですよ。既存ユーザーストア利用する場合。

Fluent APIで必殺コマンドを使って、Discriminatorを参照しないようにすると、解決できなくはないですけどー。


↑これの、Table Per Hierarcht(TPH)でやってるMapの部分。

そういうことができるカラムが存在してるなら、なんとかなるとは思う。

            modelBuilder.Entity()
                        .Map(m => m.Requires("Status").HasValue(0))
                        .Map(m => m.Requires("Status").HasValue(1));

例えばこうやって。
そうもいかないじゃないすかー。アプリケーションとしてユーザーデータはリードオンリーの部分にだけASP.NET Identityを使うなら、既存カラムの値決め打ちでMapしちゃってもいけるとは思います。が、そうじゃないならどうするか、っていうことになりますよねー。

今のところ思いつくのはあれです、テーブルはそのままに新たにViewを作成して、Viewに'ApplicationUser' as DiscriminatorをSelectに混ぜ込む。更新はINSTEAD OF トリガの使用で乗り切る。くらいでしょうか。テーブルにDiscriminator項目を追加する、か。

他にやりようがあるんだろーかー。
とは言え、ASP.NET Identity奥深し。EF CodeFirstについて知らないと、結構厳しいとは思うけど、何でもありな感じに出来上がってて、とても好感触です。

2013年11月27日水曜日

DataAnnotationsModelValidatorProviderとマルチテナント

1年に1回!
そんな感じですけど、元気にやってます。

マルチテナント花盛りですが、マルチテナントといっても
  • シングルソースシングルデプロイ
    DBが共有か個別かは、別パラメータで調整するのでもいいし。でも拡張は絶望。だってAppDomainにテナント固有コードいれるとかちょっとなんか本末転倒。
  • シングルソースマルチデプロイ
    この場合デプロイのソース割合としては、カスタム2~3%で別アセンブリになってて、97~98%は共通コア。割合は適当だけど、そのくらいなら全然管理できるし落としどころとしてはいいんじゃないかと思う。
  • マルチソースマルチデプロイ
    これが一番厳しいけどとりあえず動かす、っていうのなら、この辺。っていうか運用がマルチテナントなだけでシステムはほぼ個別。コードも10%以上がカスタム。つか、もう、ソースをコピーしちゃってぜ、っていうパターン。
などなど。まぁ、良い悪いも視点しだいなんだけど、デベロップメント視点でいうと、マルチソースは避けたいところですね。

つい先日もStackoverflow.comでおなじみのJeff Atwoodさん話の流れで出てきてましたが、

「コードベースは簡単に再利用できるという発言はよく聞くけど、大きなサイトのコードが本当に再利用できるのは稀。同じコードベースでサイトを3つ運営できるようになったら、本当に再利用できるコードと言える。」

予告ホームランを打とう - ワザノバ | wazanova


最近、必須項目を落としたいっていう"冗談は顔だけにしろよ"っていうオーダーが入ってですね、Validationルールどうしようか、天を仰いだんすよ。もちろんモデルは共通だしー。カスタムモデルも使えるようにはしてるけど、ちょっとそういうのなしね、っていうモデル。DBにべったりなモデル。

で、まぁ、すったもんだですけど、テナントごとに調整できるようにしたのが、↓こんな感じのカスタムModelValidatorProvider。

    public class VoidDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider
    {
        private static readonly Dictionary> _disableAttributes = new Dictionary>();

        public static string KeyFormat = "{0}.{1}";

        private static string GenerateRuleKeyName(Expression> _expression)
        {
            return string.Format(KeyFormat, typeof(T).FullName, ExpressionHelper.GetExpressionText(_expression));
        }

        public static void AddRule(string key, Type attributeType)
        {
            if (!_disableAttributes.ContainsKey(key))
                _disableAttributes.Add(key, new List());
         
            _disableAttributes[key].Add(attributeType);
        }

        public static void AddRule(Expression> _expression, Type attributeType)
        {
            var key = GenerateRuleKeyName(_expression);

            AddRule(key, attributeType);
        }

        public static void RemoveRule(string key)
        {
            _disableAttributes.Remove(key);
        }

        public static void RemoveRule(Expression> _expression)
        {
            var key = GenerateRuleKeyName(_expression);

            RemoveRule(key);
        }

        public static void ClearRules()
        {
            _disableAttributes.Clear();
        }

        protected override IEnumerable GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable attributes)
        {
            var exceptAttrs = attributes;
            if (metadata.ContainerType != null && !string.IsNullOrWhiteSpace(metadata.PropertyName))
            {
                var disableKey = string.Format(KeyFormat, metadata.ContainerType.FullName, metadata.PropertyName);
                if (_disableAttributes.ContainsKey(disableKey))
                {
                    exceptAttrs = attributes.Where(attr => !_disableAttributes[disableKey].Contains(attr.GetType()));
                }
            }
            
            var validators = base.GetValidators(metadata, context, exceptAttrs);
            return validators;
        }
    }

太字のところ。GetValidatorsで対象の属性を除外してModelValidatorを抽出。普通にAttributeじゃなくてAdapterで丸めてるので、単純比較はできないんだけど、attributesから除外しちゃえばModelValidatorも除外されるから、あんまり気にしなくてもよろしですね。

これを使うようにGlobal.asaxに登録。登録方法はApp_Codeに配置するなり(マルチデプロイの場合は)、なんだもいいんだけど。

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            // Nullableじゃない基本型の場合にRequiredを付与しないならfalseにしましょう。
            DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

            VoidDataAnnotationsModelValidatorProvider.AddRule("WebApplication2.Models.RegisterViewModel.UserName", typeof(RequiredAttribute));
            VoidDataAnnotationsModelValidatorProvider.AddRule((o) => o.Password, typeof(StringLengthAttribute));
            VoidDataAnnotationsModelValidatorProvider.AddRule((o) => o.ConfirmPassword, typeof(System.ComponentModel.DataAnnotations.CompareAttribute));

            ModelValidatorProviders.Providers.RemoveAt(0);
            ModelValidatorProviders.Providers.Insert(0, new VoidDataAnnotationsModelValidatorProvider());
        }

こんなのを書いて、標準のプロバイダーを差し替えましょう。
差し替える上に、モデルクラスと無効にしたい属性を指定しておきましょう。
AddRuleで除外したいものを登録するように書いたけど、そこはExpression使えたほうがなんかかっこいいから、ちょっとそんな雰囲気も出してみたよ。

そうすると、この場合カスタムコードはGlobal.asaxのみ。デプロイアセンブリは共通がいいなら、App_Codeにコードファイルをコピーでデプロイ。どっちでも同じですね。

これをVS2013でMVCプロジェクトを新規にテンプレートから作成したものに張り付けて動かすと、ユーザー登録のところで、ユーザ名必須が外れて、パスワード長の制限が外れて、パスワード確認用の一致チェックも外れる。けど、登録自体には失敗するからね。それはバリデーションの位置が違うので今回はほっときます。


わかりにくいけど↑こんな動き。

マルチテナントって楽しいね!

2012年12月28日金曜日

なんかかんか

すっかりブログ、書いてないね。前回は7/30だってさ。

もう今年も終わっちゃうねー。

今年もいろいろありました。

来年もいろいろあるでしょうね。

書きたいモチベーションが上がってきたら、ブログ書く。きっと書く。いろいろと。

Cookieのキャッシュで認証がとんでもないことになるとか、YSOD誰が出すんでしたっけ、とか?

いやいや、そーじゃなくてゴルフ始めたんですよ。今まで使ってなかった筋肉使って、さらなるパンプアップ!ベン・ホーガンってすごいね。

ホッケーも楽しくやってるんだよ。新しい友だちできたり、外人が「ガイコクジンと言え!」とか面白い。

仕事はなんかよくわかんなくなってきたから一旦整理する。何する人なのかイマイチ中途半端な事になってるし。

あとはー、そーだなー。今後も楽しくやってくよ!

2012年7月30日月曜日

外部のWebサービスを呼び出す側の簡単なユニットテスト

外部システムとの連携で、テストしにくいこといろいろあると思うんだけど、その辺について先日会社でこうするといいんじゃないのー?って話した内容のおさらい。

外部システムって言っても、同一システムの一部であることも含めます。とにかく、テストをしたいと思ってる部分から接続する必要のあるものすべて。社外か社内かはあんまり関係なく。WCFかASMXかRESTかすら関係なく。ホントはDBもだけど、今回はHTTPベースのWebサービスということで。

まずは、わかりやすくWCFでWebサービスを作ってみます。別にWebAPIでもいいしー。なんでもいいんだけどー。用意するのはVS2010とFiddlerとブラウザとテキストエディタ。

[ServiceContract]
public class WebScraper
{
  [OperationContract]
  public string ExtractPageTitle(string url)
  {
    var title = "-unknown-";
    var client = new HttpClient();
    var html = string.Empty;
    client.GetStringAsync(url)
          .ContinueWith(response =>
          {
            html = response.Result;
          })
          .Wait();

    var match = Regex.Match(html, "<title>([^<]+)</title>");
    if(match.Success)
    {
      title = match.Groups[1].Value;
      if (!string.IsNullOrWhiteSpace(title))
          title = HttpUtility.HtmlDecode(title);
    }

    return title;
  }
}

なんでもいいんだけど。こんなの。外部Webサービスのつもりで。URLを指定して呼び出すと、ページタイトル取得する、っていう意味不明なWebサービスがありました。として。

wcftest1

WcfTestClientで実行してみて動くのを確認。大丈夫だね。

これを呼び出すMvcアプリケーションも用意。HomeControllerしかいないよ。

private string Logic(string value)
{
  return new string(value.Reverse().ToArray()).ToUpper();
}

public ActionResult Index(string url)
{
  ViewBag.Message = "URLを入れるとページのタイトルを取得するよ";
  if(!string.IsNullOrWhiteSpace(url))
  {
      var service = new ApiService.WebScraperClient();
      var title = service.ExtractPageTitle(url);
      ViewBag.Message = Logic(title);
  }

  return View();
}

Indexアクションでは、サービス参照の追加で作成したサービスクライアントクラスを実行してページタイトル取得した後に、反転して大文字にするロジックが含まれてるつもりです。

wcftest2 wcftest3

はいはい。雰囲気出てますね。

このIndexアクションのテストを書いてみました。

[TestMethod]
public void Index_マイクロソフト()
{
  // Arrange
  HomeController controller = new HomeController();

  // Act
  ViewResult result = controller.Index("http://www.microsoft.com") as ViewResult;

  // Assert
  Assert.AreEqual("NOITAROPROC TFOSORCIM", result.ViewBag.Message);
}

[TestMethod]
public void Index_いろいろ()
{
  // Arrange
  var list = new[] { "http://www.google.com", "http://www.twitter.com", "http://takepara.blogspot.jp" };

  // Act
  var result = list.Select(url => new HomeController().Index(url) as ViewResult).ToArray();

  // Assert
  Assert.AreEqual("ELGOOG", result[0].ViewBag.Message);
  Assert.AreEqual("RETTIWT", result[1].ViewBag.Message);
  Assert.AreEqual("!だらかれこはみし楽お", result[2].ViewBag.Message);
}

これといって変なこともなく。HomeControllerのインスタンス作ってUrlを引数にIndexを呼び出したら、ページタイトルが反転して大文字になってるかどうかを確認してるだけです。

外部サービスがですね、ホントに社外のもので、そこのロジックがどういうものかは知らないっていうことになると、テストしたい部分ってどのになりますかね。通信経路ですかね?それとも、外部サービスのロジックですかね?ユニットテストでまわしたいとすると、そこってテストしたい部分じゃないと思います。自分書いたコードじゃないし、環境だし。

なので、ユニットテストしたいのは反転して大文字にするロジックですよね。たぶん。とはいえ、すでに存在するコードのテストをしたい、っていう場合もあるでしょう。みんなTDDなわけでもなく。そんな時はMVCを使って簡単なFakeサーバー。HTTP(S)だしー。なんでもいいでしょー。

実際の通信内容を保持しといて、リクエストのたびにその値を返せば開発環境で完結したテストが実行できて便利。何パターンか用意しておけばいいしね。その辺はテストしたい対象に合わせて良しなに用意しましょう。

今回は、上記テストがパスするように用意します。エラーのパターンがないのはご愛嬌。

public class FakeController : Controller
{
  private static string RequestBody(HttpContextBase context)
  {
      context.Request.InputStream.Position = 0;
      var reader = new StreamReader(context.Request.InputStream);
      return HttpUtility.UrlDecode(reader.ReadToEnd(), Encoding.UTF8);
  }

  private Dictionary<Func<HttpContextBase, bool>, Func<ActionResult>> _responseHandlers =
      new Dictionary<Func<HttpContextBase, bool>, Func<ActionResult>>
        {
          {
            (context) =>
                context.Request.CurrentExecutionFilePath == "/HttpApiServer.Fake/WebScraper.svc" && 
                RequestBody(context).Contains("www.microsoft.com"),
            ()=>new FilePathResult("~/App_Data/www.microsoft.com.txt", "text/xml; charset=utf-8")
          },
          {
            (context) =>
                context.Request.CurrentExecutionFilePath == "/HttpApiServer.Fake/WebScraper.svc" && 
                RequestBody(context).Contains("www.google.com"),
            ()=>new FilePathResult("~/App_Data/www.google.com.txt", "text/xml; charset=utf-8")
          },
          {
            (context) =>
                context.Request.CurrentExecutionFilePath == "/HttpApiServer.Fake/WebScraper.svc" && 
                RequestBody(context).Contains("www.twitter.com"),
            ()=>new FilePathResult("~/App_Data/www.twitter.com.txt", "text/xml; charset=utf-8")
          },
          {
            (context) =>
                context.Request.CurrentExecutionFilePath == "/HttpApiServer.Fake/WebScraper.svc" && 
                RequestBody(context).Contains("takepara.blogspot.jp"),
            ()=>new FilePathResult("~/App_Data/takepara.blogspot.jp.txt", "text/xml; charset=utf-8")
          },
        };

    
  public ActionResult Api()
  {
    var handler = _responseHandlers.Where(_ => _.Key(HttpContext)).Select(_ => _.Value).FirstOrDefault();
    return handler !=null ? 
            handler() : 
            new HttpStatusCodeResult(500);
  }
}

リクエストに合わせて結果を返すだけなんですけどね。ローカルにファイルを作成するために、一度Fiddlerで一通り動かして、データを取得しておきましょう!今回の場合だとWebサービスをIISで動かすと都合がよかったです。Fiddlerの都合で。

<endpoint address="http://localhost/HttpApiServer.Real/WebScraper.svc" binding="basicHttpBinding"
  bindingConfiguration="BasicHttpBinding_WebScraper" contract="ApiService.WebScraper"
  name="BasicHttpBinding_WebScraper" />

こんな感じのendpointだったものを以下のようにFiddler通すように変更。

<endpoint address="http://ipv4.fiddler/HttpApiServer.Real/WebScraper.svc" binding="basicHttpBinding"
  bindingConfiguration="BasicHttpBinding_WebScraper" contract="ApiService.WebScraper"
  name="BasicHttpBinding_WebScraper" />

wcftest4

そうすると、www.microsoft.comへを指定した、API呼出しのRequestとResponseは画像のような結果になります。ここから、ResponseのBody部だけ抜き出して、ファイルに保存しておき、それをリクエストに合わせてレスポンスするのがFakeサーバーの役目です。

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><ExtractPageTitleResponse xmlns="http://tempuri.org/"><ExtractPageTitleResult>Microsoft Corporation</ExtractPageTitleResult></ExtractPageTitleResponse></s:Body></s:Envelope>

↑たとえばこれをwww.microsoft.com.txtっていうファイルにしておきます。どんなパスを指定してもFakeControllerのApiアクションにリクエストが届くようにルーティング登録とWeb。configで.svcの拡張子を外しておきましょう。.svc外しとかないとSystem.ServiceModel.Activationが横取りするのでApiアクションにリクエストが届かず、404エラーになっちゃいます。

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}");

      routes.MapRoute(
          "FakeServer", // Route name
          "{*path}", // URL with parameters
          new { controller = "Fake", action = "Api" } // Parameter defaults
      );

  }

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

      RegisterGlobalFilters(GlobalFilters.Filters);
      RegisterRoutes(RouteTable.Routes);
  }
}

↑Global.asaxね。MapRouteだけいじってます。

<system.web>
  <compilation debug="true" targetFramework="4.0">
    <buildProviders>
      <remove extension=".svc"/>
    </buildProviders>
    <assemblies>
    :
    </assemblies>
  </compilation>

  <pages>
    <namespaces>
    :
    </namespaces>
  </pages>
</system.web>

↑web.configね。buildProvidersで.svcを削除してます。一通りファイルを用意して、テストのendpointをこのFakeサーバーに向けて実行します。

<endpoint address="http://localhost/HttpApiServer.Fake/WebScraper.svc" binding="basicHttpBinding"
  bindingConfiguration="BasicHttpBinding_WebScraper" contract="ApiService.WebScraper"
  name="BasicHttpBinding_WebScraper" />

ここまでが既に作成したものに対して、テスト実行したいときにとる手法で、WebサービスのFakeパターンですが、ユニットテストっていう意味ならテスト対象のIndexアクションが本当に通信するコードを呼び出さないほうが都合がいいと思います。

なので、これから作成するなら、Webサービスへのアクセスを抽象化し、ユニットテストで差し替えるようにすると、本来テストしたい部分に特化したテストができていいと思います。例えば以下のようにControllerのコードを変えてみます。

public interface IApiAdapter
{
  string Execute(string url);
}

public class HttpApiAdapter: IApiAdapter
{
  public string Execute(string url)
  {
    var service = new ApiService.WebScraperClient();
    return service.ExtractPageTitle(url);
  }
}

public class HomeController : Controller
{
  public static Func<IApiAdapter> ApiAdapterFactory = () => new HttpApiAdapter();

  private string Logic(string value)
  {
      return new string(value.Reverse().ToArray()).ToUpper();
  }

  public ActionResult Index(string url)
  {
    ViewBag.Message = "URLを入れるとページのタイトルを取得するよ";
    if (!string.IsNullOrWhiteSpace(url))
    {
      var adapter = ApiAdapterFactory();
      var title = adapter.Execute(url);
      ViewBag.Message = Logic(title);
    }

    return View("Index");
  }
}

IApiAdapterと、その実装であるHttpApiAdapterを用意して、インスタンスはファクトリメソッドから実行するように書き換えました。

テストのほうも同じく書き換えます。まずはFakeApiAdapterの追加。

 

public class FakeApiAdapter : IApiAdapter
{
  public string Execute(string url)
  {
    var list = new Dictionary<string,string> 
    { {"http://www.google.com","Google"}, 
      {"http://www.twitter.com","Twitter"}, 
      {"http://takepara.blogspot.jp","お楽しみはこれからだ!"},  };

    return list[url];
  }
}

あとはTestInitializerでファクトリメソッドのFuncデリゲート書き換え。

[TestInitialize]
public void Initialize()
{
  HomeController.ApiAdapterFactory = () => new FakeApiAdapter();
}

以上!

これで実際のプログラムにアクセスするときは、ClientBaseのWCFクライアントクラスから実Webサービス呼出しをするし、ユニットテストからは呼んだつもりで偽アダプター(Stub)が結果を返します。

wcftest5

準備するの面倒かもしれないけど、この辺もちゃんとテストしましょー。なるべく環境依存しなくて済むようにね。

2012年7月16日月曜日

c#でインスタンスをFreeze

クラスをジェネレートする際にメタデータでFreezableかどうか保持して作るのが楽ちんなのかなー。

Castle.DynamicProxyとPostSharpで実装してみました。

DynamicProxy | Castle Project

どうやってインストールするんですか!と、思ったけど、NuGetですね。Castle.Core で。

The Creators of PostSharp – SharpCrafters

こっちもNuGetで。ただ、無料のBASICライセンスを別途取得する必要がありました。むかーし、登録したアカウントが邪魔してちょっと手間取った...。

Castle.DynamicProxy

まずはCastle。ベケットは出てこないよ。

Dynamic Proxy Tutorial « Krzysztof Koźmic on software

ここに書いてるとおりです!

ようするに動的に派生クラスを生成するFactoryを使ってインスタンスを作るのと、virtualなプロパティ/メソッドに対してインターセプトできるようになる、っていうのがCastle.DynamicProxyです。overrideね。

    public interface IFreezable
    {
        bool IsFrozen { get; }
        void Freeze();
    }

Freezeできるようにするためのインターフェース。CastleでもPostSharpでも同じのを使うよ。

    public class Animal
    {
        public virtual string Name { get; set; }
        public virtual int Age { get; set; }

        public override string ToString()
        {
            return string.Format("Name: {0}, Age: {1}", Name, Age);
        }
    }

こんなモデルクラスを用意します。もちろんvirtualで。だけど、これ自体がIFreezableを実装する必要はないので注意。

ここからは、ちょっと長めのコードになるけど、簡単に説明するとIFreezableを実装したInterceptorを実装し、それを注入したProxyインスタンスを生成。Proxyインスタンスではvirtualなものにアクセスした場合、Interceptorが実行されるのでその時都度Freezeされたかチェックする、っていう流れです。

proxy_pipeline

こんなイメージ。公式サイトより

    class FreezableInterceptor : IInterceptor, IFreezable
    {
        private bool _isFrozen;

        public void Freeze()
        {
            _isFrozen = true;
        }

        public bool IsFrozen
        {
            get { return _isFrozen; }
        }

        public void Intercept(IInvocation invocation)
        {
            if (_isFrozen && invocation.Method.Name.StartsWith("set_", StringComparison.OrdinalIgnoreCase))
            {
                throw new FreezableException("object frozen!");
            }

            invocation.Proceed();
        }
    }

Interceptorが↑これ。続いてヘルパーのコード。

    static class Freezable
    {
        private static readonly ProxyGenerator _generator = new ProxyGenerator();

        public static bool IsFreezable(object obj)
        {
            return AsFreezable(obj) != null;
        }

        private static IFreezable AsFreezable(object target)
        {
            if (target == null)
                return null;
            var accessor = target as IProxyTargetAccessor;
            if (accessor == null)
                return null;
            return accessor.GetInterceptors().OfType<FreezableInterceptor>().FirstOrDefault();
        }

        public static bool IsFrozen(object obj)
        {
            var freezable = AsFreezable(obj);
            return freezable != null && freezable.IsFrozen;
        }

        public static void Freeze(object freezable)
        {
            var interceptor = AsFreezable(freezable);
            if (interceptor == null)
                throw new FreezableException("Not freezalbe!");
            interceptor.Freeze();
        }

        public static TFreezable CreateInstance<TFreezable>() where TFreezable : class, new()
        {
            var freezableInterceptor = new FreezableInterceptor();
            var proxy = _generator.CreateClassProxy<TFreezable>(freezableInterceptor);
            return proxy;
        }
    }

こっちがちょっと長い。ProxyインスタンスはIProxyTargetAccessorを実装してるので、それを利用して、Interceptorを取得してFreezeしたりIsFrozeしたりです。

    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void Freezeさせる()
        {
            var rex = Freezable.CreateInstance<Animal>();
            rex.Name = "Rex";
            Console.WriteLine(Freezable.IsFreezable(rex)
                                  ? "Rex is freezable!"
                                  : "Rex is not freezable. Something is not working");
            Console.WriteLine(rex.ToString());
            Console.WriteLine("Add 50 years");
            rex.Age += 50;
            Console.WriteLine("Age: {0}", rex.Age);

            Freezable.Freeze(rex);
            Console.WriteLine("Frozen? : " + Freezable.IsFrozen(rex));
            try
            {
                rex.Age++;
                Console.WriteLine("あれー?変更できるー。");
            }
            catch
            {
                Console.WriteLine("Frozen!!");
            }
        }

        [TestInitialize]
        public void Initialize()
        {
            var instance1 = new Animal();
            var instance2 = Freezable.CreateInstance<Animal>();            
        }

        [TestMethod]
        public void 普通のインスタンス作る()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000;i++ )
            {
                var instance = new Animal();
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms",sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void Proxyのインスタンス作る()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = Freezable.CreateInstance<Animal>();
                //Freezable.Freeze(instance);
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void Proxyのインスタンス作ってFreezeする()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = Freezable.CreateInstance<Animal>();
                Freezable.Freeze(instance);
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void 普通のインスタンスでプロパティアクセス()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = new Animal();
                instance.Age = 200;
                instance.Name = "Foo";
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void Proxyのインスタンスでプロパティアクセス()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = Freezable.CreateInstance<Animal>();
                instance.Age = 200;
                instance.Name = "Foo";
                //Freezable.Freeze(instance);                
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }
    }

動かしましょう。

  • Freezeさせる[0:00.218] Success
  • Proxyのインスタンスでプロパティアクセス[0:00.106] Success
  • Proxyのインスタンス作ってFreezeする[0:00.117] Success
  • Proxyのインスタンス作る[0:00.109] Success
  • 普通のインスタンスでプロパティアクセス[0:00.003] Success
  • 普通のインスタンス作る[0:00.002] Success

こんな感じです。Proxy経由は若干遅くなりますね。TestInitializeでわざわざインスタンス作ってるのは、Proxyの生成をここでやらせるためです。こうしとけば、各テストの計測では実行分のみの時間になるから。

DynamicProxyで、うーんと思ったのはvirtualの強制。まぁねー。クラス生成するんだし、そーだよなー、とは思いつつも。

PostSharp

続いてPostSharp。こっちはアセンブリを書き換えてインターセプト。クラス生成よりも強力。ちょっと使い方が分かりにくいところもあるけど、大体なんでもできちゃう系。

今回はInstanceLevelAspectでモデルクラスにインターフェース(IFreezable)を実装し、LocationInterceptionAspectでプロパティアクセスをインターセプト。OnSetValueでIsFrozenを確認してるよ。

    [Serializable]
    [IntroduceInterface(typeof(IFreezable))]
    class FreezableAttribute : InstanceLevelAspect, IFreezable
    {
        [NonSerialized]
        private bool _isFrozen;
        public bool IsFrozen
        {
            get { return _isFrozen; }
        }

        public void Freeze()
        {
            _isFrozen = true;
        }
    }

まずは、既存クラスにインターフェースを後付Mixin

    [Serializable]
    class FrozenAttribute : LocationInterceptionAspect
    {
        public override void OnSetValue(LocationInterceptionArgs args)
        {
            var freezable = args.Instance as IFreezable;
            if (freezable != null && freezable.IsFrozen)
            {
                throw new FreezableException(string.Format("frozen {0}", args.LocationFullName));
            }

            args.ProceedSetValue();
        }
    }

インターフェース実装されてるなら、Freezableだからプロパティアクセスにインターセプト。

これだけ。これだけだけど、これらを1個のAspectにしようとしてスゴイ遠回りした。結果、インターフェースのMixinとインターセプトは同一Aspectにできない、みたい。できる方法があるなら教えて!

    [Freezable, Frozen]
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public override string ToString()
        {
            return string.Format("Name: {0}, Age: {1}", Name, Age);
        }
    }

モデルね。属性に2つ書かないといけないのが残念ポイントだけど、virtualでもないし、これならCollection/List/DictionaryいろいろReadOnlyにしてしまえます。ね。

    [TestClass]
    public class UnitTest2
    {
        [TestMethod]
        public void Freezeさせる()
        {
            var rex = new Person();
            rex.Name = "Rex";
            Console.WriteLine(rex.ToString());
            Console.WriteLine("Add 50 years");
            rex.Age += 50;
            Console.WriteLine("Age: {0}", rex.Age);

            rex.Freeze();
            Console.WriteLine("Frozen? : " + rex.IsFrozen());

            try
            {
                rex.Age++;
                Console.WriteLine("あれー?変更できるー。");
            }
            catch
            {
                Console.WriteLine("Frozen!!");
            }
        }

        [TestMethod]
        public void Newする()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = new Person();
                instance.Freeze();
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void NewしてFreezeする()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = new Person();
                instance.Freeze();
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void Newしてプロパティアクセス()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = new Person();
                instance.Age = 200;
                instance.Name = "Foo";
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }
    }

こんなテストで実行。

  • Freezeさせる[0:00.084] Success
  • NewしてFreezeする[0:00.015] Success
  • Newしてプロパティアクセス[0:00.012] Success
  • Newする[0:00.004] Success

素のクラスに比べればそれでも遅くはなるけど、DynamicProxyよりは高速。そりゃそーだ。アセンブリ書き換えてんだから。

1個の属性でできるとうれしいなー。

いずれにせよ、モデルクラス1個ずつFreezableをプロパティごとに実装なんてコード生成以外にありえないと思うじゃない。でも、インターセプトするなら、それも何とか乗り越えられる。その際にProxyクラスにするか、アセンブリ書き換えるかは好みでいいとは思うけど、既存コードほとんどいじらず(Factoryでインスタンス作らなくていい)に済むし早いしでPostSharpが使いやすいかなーと思う海の日。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。