2013年12月30日月曜日

棟梁と頭領

最近「木に学べ」っていう本読んだんです。

Amazon.co.jp: 木に学べ―法隆寺・薬師寺の美 (小学館文庫): 西岡 常一: 本

すごいですよ。500年後の1尺を考慮する宮大工の話。
前半は法隆寺と薬師寺の話でぽかんとするんだけど、後半から宮大工とは、っていう話から自身の話(見習いとか弟子入りの話とか、継承の話とか)になってすごい面白かったんですよ。

学ぼうという心がないと、ただ仕事をするだけになってしまうんです。
どんなに修行しても、みんなが宮大工になるわけじゃない。ほとんどの人が宮大工に達しないんだって。

みんな技術者ですのや。技能者がおりませんのや。仕事をする人がおらん。
これ、なかなかすごいところをついてると思うんですよね。宮大工の話ですよ。でも、神仏について理解できない、木について考えない、そういうことの積み重ねで技術はわかっても、宮大工の心はついてこない。設計の基本の考えがわからない、っていうところに行きつくんだって。

本質を知らずに、形だけを追い求めるのを文化だと、勘違いしている人が仰山おりますからな。
なんていうか、目に見えるものだけが文化を形作るわけじゃないじゃない、ていうさ。自分の仕事に置き換えるのなんてもうおこがましくて恥ずかしいですけど、サイエンスも大事だけど、実現することがなんなのか(人に関係ないシステムなんて作らないのに、人を無視して技術がどやこやなんてね)をちゃんと見極め、何を見て何をするのか、何に価値があって、何が人に影響を及ぼして、どうすることが単なる搾取システムじゃないのか。先は長いぜ!

でも、あれだって、それもこれも「心がけの問題」だと。

この本の中で棟梁が頭領を束ねて、それぞれのプロフェッショナルに仕事は任せてる、ていうくだりがあるんですね。すごいなー、って。棟梁ですよ。西岡さんが指す棟梁はもう薬師寺でなくなるとまで言ってます。すごい話です。すごい世界です。

で、先日のMTUの話ですけどー。

GRE および IP セキュリティでの IP フラグメンテーション、MTU、MSS、および PMTUD の問題の解決 - Cisco Systems

さすがCISCOさんです。超詳しく書いてます。
フラグメントダメってなってたり、ファイアウォールでICMP全拒否してると最適値に近づけないから、ダメになっちゃう。

pingでMTUサイズを調査する - @IT

この辺でも。
単にIPSecだけが問題になるわけじゃなく、ファイアウォールとの絡みもあってってことになるわけですねー。

しかも、フラグメント処理できたとしてもルーターがてんやわんやになるので、やっぱり調整したほうがいいね、っていうことになりますね。

いやはや。

2013年12月29日日曜日

組み合わせると安くなる CDNの例

SaaSで使った分だけのCDNといえばAmazon CloudFrontがいい感じですよね。

Amazon CloudFront(コンテンツ・ストリーミング配信ネットワーク) | アマゾン ウェブ サービス (AWS 日本語)

CDNの老舗 Akamai には憧れます。F5 Big-IPくらい憧れます。
ほかに気軽に始めることのできるCDNってどんなものがあるんでしょうね。

Compare CDN | CDN Comparison, CDN Performance, CDN Providers

いっぱいあるなー。
パフォーマンスももちろんだけど、可用性が気になるところです。さらに言えば、先日の仕入れの話に戻って、お値段なんていうのももちろん気になりますよね!

CDNの機能として、ぜひとも提供しておいて欲しいものってどんなものでしょう。

  • オリジン指定での透過アクセス(PUSHしない)
  • HTTP GET以外のリクエストも転送
  • ワイルドカード証明書でのHTTPS
  • カスタムドメイン証明書でのHTTPS
  • ログの取得

とかかなー。
あとは、そうねリアルタイムレポートとか、自動フェールオーバー(エッジ間とかで)とか、Cookie対応、CORS、Dynamic IP Restrictみたいな動的にDoS判定して拒否(これはぜひとも欲しいけど、こんなことすると商売あがったりだぜ、という気もしなくもなく)とか?

なんにせよ日本にエッジがないことにはちょっと辛いですよね。
そんなあなたにおすすめなのが↓ここ。

KeyCDN - Content Delivery Network

なんか、聞いたことないよー、ってなるでしょ。スイスで頑張ってるベンチャーだってさ。
不思議なことに日本にもエッジ(POP:Point Of Presence)があってね。
とにかく安い。
しかもプリペイ。事前にチャージしとく。チャージが尽きたら止まるのかよ!って、そーなんだけど、すぐには止まりません。と、言われました。
なんにせよですよ、とにかく安いんですよ。

KeyCDN - Content Delivery Network

トラフィック$0.04/GB、ストレージ$0.9/GB。ストレージはちょっと謎なんだけど、ログの保持料と、たぶんキャッシュ。ただ、何にどのくらい使ってるのかはわからない。けど、まぁ、変な感じになってはないです。

月額10TBで$400、CloudFrontだとトラフィックだけで、その倍額なうえにリクエスト数にも課金なので、いくらお安いとはいえ、KeyCDNと比べると高いなー、って思える。ドメインいくつあろうとエイリアスで別名どんだけつけようと値段はトラフィック(とストレージ)のみっていうわかりやすい価格設定。

ただね、このKeyCDN。ちょいちょい落ちる。結構がっつり。最近はそんなことないけど、これがびっくりするくらい最初は落ちまくってた。
いくら安くてもこれじゃー、ちょっと。って思ったんだけど、そこでふと思ったですよ。

Amazon Route 53でフェールオーバーさせちゃえば、そこそこ乗り切れるんじゃね?って。

ワイルドカード証明書(カスタムは高いのよー)でのHTTPSはあきらめなきゃいけないけど、HTTPでのアクセスが圧倒的に多い場合、それだけでもKeyCDNに飛ばして、HTTPSはCloudFront。KeyCDNがおちたら、CloudFrontにフェールオーバー。そういう構成にすればいいじゃんねー。

Amazon Route 53(SLA100%のDNSサービス) | アマゾン ウェブ サービス(AWS 日本語)

別名つけないといけないから、それようにドメイン(サブドメインでも)用意しておいて、KeyCDN/CloudFrontのDNS名をそれぞれCNAMEで登録。
でもって、Route 53のヘルスチェック機能を使いたいんだけど、CDNがIP直さしなわけないので、別途固定IPをふれるサーバーから、Reverse Proxyさせて、KeyCDNへのアクセスを固定IPで見えるように(ヘルスチェック専用ね)。
CloudFrontは一応SLAもあって99.9%で頑張るって言ってるんで、そこはもうそれ。それで!
万が一でもCloudFrontがえらいことになったら(CDN参照はUrlRewriteのoutboundRulesに書いてHTMLそのものの書き換えで参照切り替えるようにしてるから)人力でWeb.Config書き換えてCDNをOffにする。
トラフィックさばけるのか怖いけど、サービス停止のほうがもっとこわいー。まぁ、2つの会社のサービスが同時に落ちることはそんな確率も高くないだろうから、なんとかなんじゃん。どうせSLA100%なんておかしな契約をすることなんてありえないんだしね。こういうところでもバランス大事だと思います。

やってることは単純だけど、文字にするとわかりにく。
つまり、組み合わせていいとこどりすれば、CDNの費用を5分の1とか10分の1に落とせるよ!CDNのサービスだって落ちるのを前提にして組み合わせることで、身の丈に合った費用対効果を出せて、うへへな気持ちで年を越せます。

これでなんとか5エントリー...。
来年はもう少し書きたいです...。
書かないと忘れちゃうから。

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


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

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

dotnetConf2015 Japan

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