きよくらの備忘録

「三日坊主と呼ばせない!日記」改め。主にソフトウェア開発関連の話題。

ASP.NET Web Pagesの入力フォームでタグ等の入力を受け入れる

ASP.NETの入力フォームに対する既定のバリデーション

ASP.NETでは以前から*1、既定の動作として入力フォームに対してフレームワーク側でバリデーションを行い、例えばタグとして解釈できるもの("<"と">"で囲まれた文字)等がある場合、HttpRequestValidationException 発生させるようになっています。

例えば、emailという名前のinput要素に『hoge 』と入力した場合、

危険な可能性のある Request.Form 値がクライアント (email="hoge <hoge@example.com>") から検出されました。

のように、HttpRequestValidationException が発生します。


『余計なお世話』と感じる方もいるかもしれませんが、『既定の動作をより安全な方に傾ける』という実装ですので、これはフレームワークの既定の動作としては正しい実装だと思います。

Web Pages(Razor)で検証させずに受け取る方法

ただしこのままだと、当然ですがタグ入力を受け入れたい場合等には仕様を満たせません。『フレームワーク側でバリデーションされないようしたい』という事は往々にして発生するでしょう。

例えば、上記のメールアドレスの入力*2を受け取ろうと思した場合、通常は以下のように受け取ると思いますが、このままですと上記の通りの例外が発生します

var email = Request.Form["email"];

では、Web Pages(Rasor)では、どうやって受け取れば良いか。

Web PagesではRequestオブジェクトにUnvalidatedという拡張メソッドがあり、これを使うことで、受け取る一つ一つのフォーム値に対して、『これは安全でなくても受け取る』という指定をしながら受け取ることができいます。

例えば以下のようなコードになります。

var email = Request.Unvalidated().Form["email"];

詳細については以下のMSDNドキュメントを参照ください:
http://msdn.microsoft.com/en-us/library/gg568738(v=vs.99).aspx

*1:多分、ASP.NET2.0から

*2:『hoge 』はRFC2822的にはValidなメールアドレス表現です。もちろん、これを受け入れるかどうかはアプリケーションの仕様次第ですが:p

ASP.NET Web Pagesのメンバシップで追加のユーザ情報を扱う

ASP.NET Web Pagesでは標準でWebMatrix.WebData.WebSecurityクラスを通してメンバシップ管理が利用できます。しかし、ユーザ情報としてはメンバシッププロバイダがログイン管理のための利用する情報以外の、ユーザの属性として利用できる情報はEmailくらいしかありません。
一方、多くのWebアプリケーションではユーザの氏名やその他の各種属性を保持・管理したい、という要求は一般的かと思います。


WebSecurityクラスを調べてみると、これらの情報も比較的簡単に扱えることがわかりました。
今回は、「姓、名、年齢」の属性を管理する場合を例にしながら、その方法を紹介してみたいと思います。

1.Webサイトを作成する

 WebMatrixの「スターターサイト」、もしくはVisual Studioで「ASP.NET Webサイト(Razor)」のテンプレートでサイトを新規作成します。言語はC#でもVB.NETでも良いですが、今回はC#でやってみます。

 テンプレートを展開すると、/Account配下にログインやアカウント登録関係の各種サンプルが展開されます。
 今回はこのサンプルを改造して利用します。

 
 【重要】
 展開が終わったら、一度実行(F5キー)してください。
 実行すると、データベースに自動的に必要なテーブルが作成されます。

2.App_Codeフォルダを追加する

 サイトにApp_Codeフォルダを追加します。

 必須というわけではないですが、今回、追加で管理する属性を単純なPOCO*1で管理したいので、クラスを追加するためのフォルダを追加します。
 Visual Studioの場合、ソリューションエクスプローラを右クリック、[ASP.NETフォルダーの追加]→[App_Code]を選択します。WebMatrixの場合は新規にフォルダを追加して、App_Codeにリネームしてください。

3.エンティティを追加

 App_Code配下に以下のクラス、「AdditionalUserInfoEntity.cs」を追加します。
 

/// <summary>
/// ユーザー追加属性のエンティティ
/// </summary>
public class AdditionalUserInfoEntity
{
    /// <summary>
    /// 姓
    /// </summary>
    public string LastName { get; set; }

    /// <summary>
    /// 名
    /// </summary>
    public string FirstName { get; set; }

    /// <summary>
    /// 年齢
    /// </summary>
    public int Age { get; set; }

    public AdditionalUserInfoEntity()
    {
        this.LastName = "";
        this.FirstName = "";
    }
}

4.テーブルにカラムを追加する

 自動作成されているUserProfileテーブルに、今回追加する属性を格納するカラムを追加します。

 例えばサーバエクスプローラでテーブルの定義を開き、以下の列を追加します。
 

列名 データ型
LastName nvarchar(50)
FirstName nvarchar(50)
Age int

5.コードの編集その1:宣言〜値受け取り〜バリデーション

 規定で展開されるRegister.cshtmlを編集します。
 まず、コードの先頭でいくつか変数を宣言しておきます。

 一つ目は、追加属性を格納するエンティティ、AdditionalUserInfoEntityのインスタンスと、項目に入力エラーがあったときのメッセージを格納する変数も宣言しておきます。

    var additionalUserInfo = new AdditionalUserInfoEntity();
    var lastNameErrorMessage = "";
    var firstNameErrorMessage = "";
    var ageErrorMessage = "";

 
 次に、if (IsPost) {}ブロック内に、追加した入力校項目を受け取ってバリデーションを行うコードを記述します。

        additionalUserInfo.LastName = Request.Form["lastName"];
        if (additionalUserInfo.LastName.IsEmpty())
        {
            lastNameErrorMessage = "姓を空白にすることはできません。";
            isValid = false;
        }
       
        additionalUserInfo.FirstName = Request.Form["firstName"];
        if (additionalUserInfo.FirstName.IsEmpty())
        {
            firstNameErrorMessage = "名を空白にすることはできません。";
            isValid = false;
        }
       
        additionalUserInfo.Age = Request.Form["age"].AsInt(0);
        if ( additionalUserInfo.Age < 1 )
        {
            ageErrorMessage = "年齢は1以上の整数で入力してください。";
            isValid = false;
        }

 ※上記のバリデーションはかなり適当です。実際の仕様に沿うようにやってください。

6.フォームの編集

 今回の核心となる部分のコードを触る前に、フォーム部分の編集を先にやっておきましょう。
 やることは、姓・名・年齢を入力するフィールドを用意することです。

 「パスワードの確認入力」の欄の下あたりにでも、以下のコードを挿入します。

            <li class="lastName">
                <label for="lastName">姓:</label>
                <input type="text" id="lastName" name="lastName" title="姓" value="@additionalUserInfo.LastName" @if(!lastNameErrorMessage.IsEmpty()){<text>class="error-field"</text>} />
                @if (!lastNameErrorMessage.IsEmpty()) {
                    <label for="confirmPassword" class="validation-error">@lastNameErrorMessage</label>
                }
            </li>
            <li class="firstName">
                <label for="firstName">名:</label>
                <input type="text" id="firstName" name="firstName" title="名" value="@additionalUserInfo.FirstName" @if(!firstNameErrorMessage.IsEmpty()){<text>class="error-field"</text>} />
                @if (!firstNameErrorMessage.IsEmpty()) {
                    <label for="confirmPassword" class="validation-error">@firstNameErrorMessage</label>
                }
            </li>
            <li class="age">
                <label for="age">年齢:</label>
                <input type="text" id="age" name="age" title="年齢" value="@additionalUserInfo.Age.ToString()" @if(!ageErrorMessage.IsEmpty()){<text>class="error-field"</text>} />
                @if (!ageErrorMessage.IsEmpty()) {
                    <label for="confirmPassword" class="validation-error">@ageErrorMessage</label>
                }
            </li>

 

7.コードの編集その2:登録ロジック

 ようやく、本件の核心部分です。
 先に解説しておきますと、今回、WebSecurityクラスのWebSecurity.CreateUserAndAccountメソッドを利用して実装します。
 
 テンプレートにより展開されてサンプル実装は自分でSQLを何度か発行しつつ、WebSecurity.CreateAccountを利用するというちょっとまどろっこしい実装になっていますので、この辺りをまるっと書き換えてしまうことにします。

 まず、

 var db = Database.Open("StarterSite");

を削除します。
 今回、自分で明示的にデータベースにはアクセスしない実装にしますので、この行は不要になります。

 次に、「 var user = db.QuerySingle("SELECT 〜")」でSQLを発行して同一ユーザが存在していないか確認している行がありますので、これをSQLでは無く、WebSecurity.UserExistsメソッドで確認するように変更します。

  var user = db.QuerySingle("SELECT Email FROM UserProfile WHERE LOWER(Email) = LOWER(@0)", email);
  if (user == null) {
   ...
  }

を、

  if (!WebSecurity.UserExists(email)) {
   ...
  }

に書き換えます。かなりシンプルですね。

次に、実際にユーザ情報を登録する個所を変更します。
サンプル実装では「db.Execute("INSERT INTO UserProfile (Email) VALUES (@0)", email);」でINSERT文を発行してユーザ情報をUserProfileに自力でインサートした後、WebSecurity.CreateAccountメソッドで各種メンバーシップ情報を生成しています。これをWebSecurity.CreateUserAndAccountメソッドを利用した形に書き換えます。

具体的には、INSERT文を発行している「db.Execute("INSERT INTO UserProfile (Email) VALUES (@0)", email);」の行を削除したあと、CreateAccountメソッドをコールしている行を以下のように書き換えます。

var token = WebSecurity.CreateUserAndAccount(email, password, additionalUserInfo, requireEmailConfirmation);

ちなみに、今回は追加した属性additionalUserInfoを第三引数として渡していますが、このパラメータをnullにすれば、標準のまま(UserProfleテーブルを拡張しないまま)の状態でユーザを追加することができます。


以上で終了です。
if (isValid) {..}の中を書き出してみると、以下のようになります。
DBに自力でアクセスする部分が無くなったこともあり、最初の実装よりもよりシンプルにになっている気もしますね。

        if (isValid) {
 

            //  ユーザーが既に存在するかどうかを確認します
            if (!WebSecurity.UserExists(email)) {
               
                //  メンバーシップ データベースに新しいエントリを作成して関連付けます。
                //  正常に完了した場合は、要求の処理を続行します
                try {
                    bool requireEmailConfirmation = !WebMail.SmtpServer.IsEmpty();
                    var token = WebSecurity.CreateUserAndAccount(email, password, additionalUserInfo, requireEmailConfirmation);
                    if (requireEmailConfirmation) {
                        var hostUrl = Request.Url.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
                        var confirmationUrl = hostUrl + VirtualPathUtility.ToAbsolute("~/Account/Confirm?confirmationCode=" + HttpUtility.UrlEncode(token));

                        WebMail.Send(
                            to: email,    
                            subject: "アカウントを確認してください",
                            body: "確認コード:  " + token + "。<a href=\"" + confirmationUrl + "\">" + confirmationUrl + "</a> にアクセスしてアカウントを有効にしてください。"
                        );
                    }

                    if (requireEmailConfirmation) {
                        //  ユーザーの登録に感謝し、間もなく電子メールが届くことを伝えます
                        Response.Redirect("~/Account/Thanks");
                    } else {
                        //  ホームページに移動して終了します
                        WebSecurity.Login(email, password);
                        Response.Redirect("~/");
                    }
                } catch (System.Web.Security.MembershipCreateUserException e) {
                    isValid = false;
                    accountCreationErrorMessage = e.ToString();
                }
            } else {
                //  ユーザーは既に存在します
                isValid = false;
                accountCreationErrorMessage = "電子メール アドレスは既に使用中です。";
            }
        }    

 
以上で終了です。
実行後にデータベースを確認すると、追加で設定されたされた氏名と年齢の情報も登録されていることがわかります。

まとめ

以上のように、ASP.NET Web PagesではWebSecurityクラスそしてWebSecurity.CreateUserAndAccountメソッドを利用することで簡単にユーザの登録が行え、さらに追加の属性情報も簡単に扱うことができます。
ちょっとしたWebアプリケーションを実装するレベルなら、これでもかなり実用的なものになるのではないかと思います。

参考:
CreateUserAndAccount Method(MSDN)


蛇足

ちなみに、WebSecurity.CreateUserAndAccountの第三引数には、匿名クラスで値を渡すこともできますので、わざわざエンティティを宣言しなくても

  var additionalUserInfo = new { FirstName = "ほげ", LastName = "ふが", Age = 20 };
  var token = WebSecurity.CreateUserAndAccount(email, password, additionalUserInfo, requireEmailConfirmation);

のようにすることもできます。

*1:Plain Old CLR Object、単純な.NETのクラス

クラシックASP to Rasor (2)

間が空きましたが、また少しずつ、フォローアップを書いてきたいと思います。

1.ファイルの粒度と構成は?

A.割とそのままいけると思います

「素直なASPの構成」をしていれば、おそらくはほぼ、そのままに近い粒度で移行できると思います。HTMLのFORMやJavascriptが許容する大抵の挙動と、フレームワークの思想がバッティングしません。WebFormのようにPOSTBACKを半ば強要*1されるようなこともありません。
.cshtml/vbhtml以外のファイルにすべきものについては、『3.サブルーチンをどうするか』『4.includeをどうするか?』あたりを参考にしてください。


重ねてになりますが、もともとのクラシックASPのファイル構成があまり良くない場合は、やはりある程度再構成をする方が良いという判断になるかもしれません。

2.コードの置き換えは?

A.注意点はあるものの、素直に書き換えできる部分が多そう

そもそも元がVBScriptですから*2、そんなにややこしいことはできません。VB.NETC#で実現に困るようなことは、そうそうできないと思います。

また言語ですが、元がVBScriptであるなら、VB.NETを選択する方が無難だと思います。文法もそうですが、StrConvやCInt等そのまま利用できます。これらのMicrosoft.VisualBasic名前空間の各種機能を「移植時の互換性維持のために使う」のは真っ当な使い方だと思います。


ただし、いくつか注意すべき点があるので、その点については補足しておきます。

(1) Response.Writeの置き換え

クラシックASPでResponse.Writeを利用している場合、ここは書き換える必要があります。一度、Response.Writeをそのまま実行してみると分かるのですが、恐らくは意図した場所に出力されていないはずです。HTML中に記述した行付近ではなく、ファイルの先頭付近に書き出されていることと思います。理由の詳細はここでは割愛しますが(Web FormsでのResponse.Writeと同じ挙動ですので)、クラシックASPと同じ目的ではResponse.Writeは大抵の場合、使えないと思います。

代わりに利用することになるのがWriteもしくはWriteLiteralメソッドになります。コード中からであれば、これで書き換えるのが自然だともいます。また、@やで書くのが見やすいようでしたら、そちらで書くのも良いと思います。

(2) VB6 → VB.NET移植時と同様の注意点

VB6からVB.NETの移植時にもよく問題になった点は、やはりここでも問題になってくる可能性があります。
代表的なものに、

  • 変数宣言の省略
  • Variant型の扱い
  • 引数の値渡しと参照渡し

があります。

前二つについては、資料での触れていますのでここでは割愛します。

『引数の値私と参照渡し』については資料で触れるのを忘れていましたので、ここで改めて補足させてください。

プロシージャを自分で定義する際、VBScriptでは、明示的に宣言しない限り、引数は参照私になります。つまり、VBScriptでは、

Sub SomeMethod(myValue)
End Sub

と、

Sub SomeMethod(byRef myValue)
End Sub

は等価です。


VB.NETでは、以下の二つが等価になります*3

Sub SomeMethod(myValue)
End Sub
Sub SomeMethod(byVal myValue)
End Sub

元々のコード『参照渡し』であることを意識しておらず、引数に値をセットして戻すようなことしていなければよいのですが、そうでない場合、挙動が変わってしまいます。しかも、これはコンパイルエラーは出ません。ですので、これが潜在していると、動かしてみるまで顕在化しないバグになる可能性があります。


なぜ、こんな大事なことを書き漏らしていたかというと…言い訳でしかないのですが、二つほど理由がありまして…。一つは、VB6からの移植をいろいろやっていた身としては、もう常識として体が勝手にこれを考慮してコードを移植する感じになっていていること。もう一つは、そもそもVBScriptでも、私が書いたコードだと極力明示的にbyValとbyRefを書くようにしていたことです。おかげで、移行テストをしていた時にまったくこの問題に引っかからず(^^;


この問題を含め、言語レベルの問題については先にも挙げた『Visual Basic 6.0 ユーザーのための Visual Basic .NET 移行ガイド』を参考にしてください。VB6系から.NETへの移行を経験されたことがない方でしたら、一度は目を通すことを強くお奨めしておきます。

*1:別に強要されるわけではなくほかの手段も使えますが、Web Formsのそもそもの思想と考えると、POSTBACKと完全に決別して実装するのが良い手段とは思えません。

*2:Javascriptで実装している方もいるかもしれませんが、大半はVBScriptだと思います

*3:Visual StudioのIDEを利用すると勝手に補完されます

クラシックASP to Rasor (1)


2011/08/27に開催されたTech Party 2011 広島会場にて、後援させていただいた『新しい「ASP.NET Web Pages」を触ってみた − Classic ASP to Razor !? −』のフォローアップです。こちらもご一読ください。


あらかじめお断りしておきますが、前エントリでも書かせていただいている通り、今回の本題は「Classi ASP to Razor」です。このフォローアップも、基本的にはその点のみに絞って書いていこうと思います。
ご了承ください*1


早速それぞれの話題に入っていきたい…ところですが、最初に一つだけ、当日のセッションでも触れるのを忘れていた点について、追加で補足します。

前提条件

クラシックASPからの移植が今回の主題です。移植の難易度や手段を検討する際、そのASPソースコードについて、やはりある程度の前提条件は必要だと思います。今回その前提条件として以下を想定しています。

  1. クラシックASPとして正しく動作している
  2. クラシックASPとして(それなりに)健全なソースである

一つ目は言わずもがな、当然ですね。問題は二つ目です。一口に健全といっても「何が健全で何が不健全か」は判断が難しいかもしれません。…かもしれませんが、例えば私が今まで実際に遭遇した例でいうと、『1つの.aspファイルに30画面分のHTMLが埋め込まれていてIf ~ ElseIF ~で切り分けられている』というものがあったのですが、個人的にはこれはあまり健全なソースではないと思っています。



ASPとしても『ソースコードがグチャグチャでどうしようもない』とか『構成が複雑怪奇でメンテナンスができない』ようなものを仮にそのままWeb Pagesに移植できたとしても、どれはそのままでしょう。決して『今後継続してメンテナンスしやすい状態』にはならないと思います。



恐らく多少ソースがグチャグチャしていたり、複雑怪奇な構成になっていたりするものであっても、ASPでできる範囲のことであれば、Web Pagesに移植は可能だとは思うのですが、やはり移植した後のことも考え、ある程度のリファクタリングは事前に行ったほうが無難かもしれません。



これはこの”クラシックASP to Razor”特有の話ではなく、近い筋の話だと”VB6 to VB.NET”でも同じようなことがあったと記憶しています。ですので、そういった点でつまずいた場合は、ひょっとしたら以下のドキュメントに参考になる部分があるかもしれません。
Visual Basic 6.0 ユーザーのための Visual Basic .NET 移行ガイド


さて、前置きはこの辺りにして本題に…と行きたいところですが、今日はここまで。次回から、各ポイントについて具体的に書いていきたいと思います。

*1:もちろん、誤りや誤解を招くような表現がある場合は補足・訂正させていただきますので、そのような点があればぜひ、ご指摘ください。よろしくお願いします

Classi ASPからRasorの移植ネタでお話しさせていただきました

2011/08/27に開催されたTech Party 2011*1の広島会場にて、『新しい「ASP.NET Web Pages」を触ってみた − Classic ASP to Razor !? −』というタイトルでお話しさせていただきました。
このようなタイトルですが、実は後半のサブタイトル「Classic ASP to Razor」というのが本題で、Web PagesやRazorの概要や基礎的なお話は相当はしょらせていただいています。


暫定版の資料(いくつか既知の誤字などがありますが修正されていません)とサンプルのソリューションを以下のOITECのSkyDriveにアップしてあります。



発表前は『こんなニッチなネタでよいのだろうか』とも思ったのですが、案外(?)私と同じような境遇なのか、このような求められている方もおられたようで、幸いでした。また同じ会場で参加されていた、日本マイクロソフトエバンジェリストの安納さんにもblogで取り上げていただき、光栄に思っています。


しかしながら、セッションでは限られた時間であったりまた私自身の時間配分やボリュームのコントロールが稚拙であったことから十分お話しすることができなかった部分があります*2。これらについてこのblogで複数回に分けたエントリでフォローアップしていこうと思います。


しばらく諸事情でバタバタする予定なので、どれくらいの頻度でエントリを上げれるかわかりませんが、興味がある方は是非、見てやっていただければと思います。

*1:http://techparty2011.iinaa.net/

*2:練習不足ですね。申し訳ありません