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について知らないと、結構厳しいとは思うけど、何でもありな感じに出来上がってて、とても好感触です。

dotnetConf2015 Japan

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