ASP.NET Web API と DataAnnotations でモデルバリデーションする
ASP.NET MVC では DataAnnotations を使ってバリデーションを行うのが一般的ですが、 ASP.NET Web API ではどうやってやるのか調べてみました。 Visual Studio 2013 で確認しています。
要件としては以下のような感じとします。
- Controller の引数でクラスを受け取り、クラスに対して属性を付与してバリデーションする
- バリデーションエラーがある場合、エラー内容とともに Bad Request(400) を返す
API のパラメーターを受けるモデルはこんな感じ。
public class SampleModel { [Required] [Display(Name = "なにかの値")] public string Value { get; set; } }
コントローラーはこんな感じ。属性ルーティングを使っています。
[RoutePrefix("api")] public class SampleController : ApiController { [Route("test")] public IHttpActionResult PostTest(SampleModel model) { if (ModelState.IsValid == false) { return new ResponseMessageResult(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } // 略 } }
MVC のときと基本的に同じですね。 /api/test に Accept: application/json ヘッダーを付けて Value=
という感じの POST を投げると以下の様な結果が返ってきます。レスポンスを返すところは Request.CreateResponse
と間違えやすいので気をつけましょう(間違えてはまりかけた)。
{"Message":"The request is invalid.","ModelState":{"model.Value":["なにかの値 フィールドが必要です。"]}}
検証失敗時の動作はどのコントローラーでも同じとしたいので、処理を共通化したいところです。 ASP.NET MVC と同様、 ActionFilter が使えるので、これを使って共通化するのが良さそうですね。 MVC のものとは名前空間が違うので注意。
上記ページの例ほぼそのままですが、以下の様な感じで ActionFilter を作ります。
public class ApiValidationAttribute : ActionFilterAttribute { public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) { var modelState = actionContext.ModelState; if (modelState.IsValid == false) { actionContext.Response = actionContext.Request .CreateErrorResponse(HttpStatusCode.BadRequest, modelState); } base.OnActionExecuting(actionContext); } }
Controller のメソッドに属性を付けます。デフォルトで有効にしたいのであれば、 Global.asax
あたりで GlobalConfiguration
に設定するのが良いでしょう。
[Route("test")] [ApiValidation] public IHttpActionResult PostTest(SampleModel model)
パラメーターが空の場合の対応
モデルにバインドするパラメーターを何も指定しない場合、コントローラーのメソッド引数であるモデルクラスが null
になります。この場合、 ModelState.IsValid
が true
を返してくるので、入力値検証が働きません。この場合でも検証エラーとしたいところです。
ActionFilter の処理に以下の様な感じでチェックを追加することで対応できるようです。
if (actionContext.ActionArguments.Any(kv => kv.Value == null)) { actionContext.Response = actionContext.Request .CreateErrorResponse(HttpStatusCode.BadRequest, "パラメーターが指定されていません。"); }
これで以下の様な結果が返ります。
{"Message":"パラメーターが指定されていません。"}
以上で、 ASP.NET Web API でも DataAnnotations を使ってモデルバリデーションすることができました。結果データが .NET 風味あふれる感じになっているあたりが課題ですかね。少なくともキー名は自分でカスタマイズしたいところです。そのへんについては別途調査したいと思います。
JSF で認証情報を SessionScoped で保持する
JSF を使う場合に、 Java EE の SessionScoped
にログイン済みの認証情報を保持する方法を考えてみました。本来であれば Java EE 標準のレルムによる認証機構を使ったほうが良いんだと思いますが、要件的に適用が難しそうなのでここでは使用していません。 GlassFish 4.1 で確認しています。
認証情報
CDI 管理でセッションに保持する認証情報クラスをこんな感じで用意します。保持する内容はなんでもいいです。
@SessionScoped public class Auth implements Serializable { private String name; private boolean authenticated; public void login(String name) { this.name = name; this.authenticated = true; } public void logout() { this.name = null; this.authenticated = false; } // setter/getter 等略 }
ログイン、ログアウト処理を行う JSF 管理 Bean
@Named(value = "sample") @ViewScoped pubic class SampleBean implements Serializable { @Inject private Auth auth; public void login(String name, String password) { // ここで認証処理を行うとする // 認証処理に成功したら認証情報を保存 this.auth.login(name); } public void logout() { this.auth.logout(); } }
Session Fixation 対策(動かない)
上記のような感じで動くのですが、 Session Fixation 対策をしなくては、と思い、認証情報クラスのログイン処理を以下のように変更しました。セッション id を変更するために、セッションをいったん無効化しています。
public void login(String name) { // セッションを無効化 ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext(); externalContext.invalidateSession(); this.name = name; this.authenticated = true; }
しかし、こうすると以降のリクエストでは保存したはずの name などが保存されていません。 @Inject
でインジェクションされる認証情報が別インスタンスになってしまいます。
落ち着いて考えたらセッションを invalidate してるんだからセッションに保存されている SessionScoped
なインスタンスが破棄されるのは当たり前なんですが、最初ちょっと混乱してしまいました(;´Д`)
Session Fixation 対策 1 (動く)
苦し紛れに考えたのがこれ。認証情報クラスのログイン処理を以下のように変更します。新しい認証情報クラスのインスタンスを作成し、自前でセッションに突っ込みます。
public void login(String name) { // セッションを無効化 ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext(); externalContext.invalidateSession(); // セッションに突っ込む Auth temp = new Auth(); temp.name = name temp.authenticated = true; externalContext.getSessionMap().put(SESSION_KEY, temp); }
SessionScoped
で保存される際に使用されているキーがわかれば、そのキーでセッションに直接突っ込めばいいかとも思いましたが、セッションの中身を覗いてみるとちょっと触っちゃいけない感じのキー名だったので、自前でキーを決めて突っ込んでいます。きっとそのあたりはプログラマが触るべきものではないのでしょう。
自前のキーなので、当然 SessionScoped
でインジェクションされるインスタンスとは別になります。そこで、先ほど設定した認証情報を SessionScoped
なインスタンスにコピーするよう、認証情報クラスに以下の処理を追加します。セッションの invalidate
後にインスタンスが再生成されることを利用し、 @PostConstruct
でコピー処理を行っています。
@PostConstruct public void postConstruct() { ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext(); Auth temp = (Auth) externalContext.getSessionMap().get(SESSION_KEY); if (temp != null) { this.name = temp.name; this.authenticated = temp.authenticated; externalContext.getSessionMap().remove(SESSION_KEY); } }
一応動くことは動きますが、なんか負けた感があります。また、なんとなく危ないような気がしないでもないです。もうちょっと良い方法はないのか・・。
Session Fixation 対策 2 (動く)
ツイッターで教えていただいたのがこれ。 Servlet 3.1 で追加された HttpServletRequest.changeSessionId()
を使います。セッションを破棄することなくセッション id を変更することができます。
これを使うとログイン処理はこうなります。
public void login(String name) { // セッション id を変更 ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext(); HttpServletRequest request = (HttpServletRequest) externalContext.getRequest(); request.changeSessionId(); this.name = name; this.authenticated = true; }
生リクエストを触る必要があるのがちょっとアレですが、だいぶすっきりしました。
最初 changeSessionId
はセッションを invalidate
した上でセッション id を変更するものと思い込んでいたんですが、セッションは破棄されないんですね。リファレンスをちゃんと読んで実際に動かしてみることが大事ですね・・・。
結論
- Servlet 3.1 以降の環境であれば
HttpServletRequest.changeSessionId()
が使える - それ以前の環境では自前でセッション値をごにょごにょする必要がある・・・?
こんな感じです。
ASP.NET MVC で DataAnnotations のエラーメッセージをカスタマイズ
ASP.NET MVC で DataAnnotations の入力値検証エラーメッセージをカスタマイズしようとしたら微妙にめんどくさかったのでメモっておきます。 ASP.NET MVC 5 で確認しています。
方針
- リソースファイル( *.resx )にカスタムメッセージを記述し、それを使うようにする
- DataAnnotations の属性ごとにメッセージをカスタマイズできるようにする
調べると、わりと古いですが以下の日本語情報がひっかかります。
自前で標準の属性を継承したカスタム属性を作ってリソースを指定する方法と、属性のファクトリ?的なアダプターを作成する方法が紹介されていますが、前者は確かにいちいちカスタム属性使うよう変更するのも面倒だし間違えやすそうなので、後者の方法を使います。
属性の指定時にメッセージをベタ書きしてアドホックにカスタマイズすることもできますが、それだったらカスタム属性作る方がましなので選択肢から除外します。
実装
リソースファイル
プロジェクト直下に App_GlobalResources
フォルダを作ってその下に作成します。作成方法については以下の記事を参照。 Visual Studio のバージョンによって微妙に違いますが、基本的には同じ。ここではリソースファイルの名前を Messages とでもしておきます。
ここに「名前」と「値」を以下の様な感じで記述します。
名前 | 値 |
---|---|
PropertyValueRequired | {0} は必須項目やで |
PropertyValueStringLength | {0} は {1} 文字までしかあかんで |
アダプターの作成・登録
このへんは最初の記事そのままで OK 。
public class CustomRequiredAttributeAdapter : RequiredAttributeAdapter { public CustomRequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, RequiredAttribute attribute) : base(metadata, context, attribute) { // 作成したリソースファイルを指定 attribute.ErrorMessageResourceType = typeof(Messages); // リソースファイルに記述した名前を指定 attribute.ErrorMessageResourceName = "PropertyValueRequired"; } }
Global.asax
の Application_Start()
で登録します。 App_Start
以下になんとか Config みたいなの作ってそっちに書くのが今風かもしれません。
// 作成したリソース名を設定 DefaultModelBinder.ResourceClassKey = "Messages"; // アダプターの設定 DataAnnotationsModelValidatorProvider.RegisterAdapter( typeof(RequiredAttribute), typeof(CustomRequiredAttributeAdapter));
これでカスタムメッセージが使われるようになります。
その他
ここまでは RequiredAttribute
の例ですが、 StringLengthAttribute
なんかでもズバリ StringLengthAttributeAdapter
なんてクラスがあるので、これを継承して同じようにやれば動きます。
アダプターが用意されていない属性については、 DataAnnotationsModelValidator
を継承すれば動きました。ただちょっと注意があって、属性に対して ErrorMessage == null
しないと例外が発生します。
以下は EmailAddressAttribute
での実装例です。
public class CustomEmailAddressAttributeAdapter : DataAnnotationsModelValidator<EmailAddressAttribute> { public CustomEmailAddressAttributeAdapter(ModelMetadata metadata, ControllerContext context, EmailAddressAttribute attribute) : base(metadata, context, attribute) { attribute.ErrorMessageResourceType = typeof(Messages); attribute.ErrorMessageResourceName = "PropertyValueEmailAddress"; // これをしないと以下の例外 // 「*System.InvalidOperationException*System.InvalidOperationException: ErrorMessageString と ErrorMessageResourceName は、どちらか 1 つを設定する必要があります。両方は設定できません。」 attribute.ErrorMessage = null; } }
ValidationAttribute.ErrorMessageString Property (System.ComponentModel.DataAnnotations) | Microsoft Docs によると、以下にのように書かれているのでその辺が原因ですかね?
エラエラー メッセージ文字列は、ErrorMessage プロパティを評価する、または ErrorMessageResourceType および ErrorMessageResourceName プロパティを評価することにより取得されます。 2 つのケースは同時に指定できません。 後者のケースが、ローカライズされたエラー メッセージを表示する場合に使用されます。
「エラエラー」が気になる。
とりあえずやりたいことは出来ましたが、属性ごとにいちいちアダプターを作って登録みたいなことをやる必要があり、やりたいことに対してちょっと手順が多すぎる気がします。もうちょいましな方法はないんだろうか?
なお、型が一致しない場合*1のエラーメッセージは、特にアダプターとか作らなくても、リソースに PropertyValueInvalid
という名前で登録すれば使われるようです。
*1:int 型のプロパティに文字列突っ込もうとした場合とか
User.Identity.IsAuthenticated は同一リクエスト中ではサインアウトしても更新されない
はまりかけたのでメモ。環境は以下の通りです。
- Visual Studio 2013
- ASP.NET MVC 5.2
- ASP.NET Identity 2.1
.NET の認証機構を使用している場合、ユーザーが認証済みかどうかは以下の様なコードでチェックできます。
if (HttpContext.User.Identity.IsAuthenticated) { // 認証済みである }
ところが、サインアウト処理を行っても、同一リクエスト内では上記の値は更新されません。例えば ASP.NET Identity を使っている場合、以下の様な動きになります。
// サインアウト処理 var authentication = HttpContext.GetOwinContext().Authentication; authentication.SignOut(DefaultAuthenticationTypes.ApplicationCookie); if (HttpContext.User.Identity.IsAuthenticated) { // ここに入る! }
普通はサインアウト処理の後にリダイレクトすることが多いので、リダイレクト後には結果が false になるので問題にならないのですが、なんか特殊なチェック処理の結果、強制的にログアウト処理を行いたいような場合に困ります。
結論としては、 HttpContext.User
(実体は IPrincipal )を上書きしてしまえばいいようです。
先ほどのコードは以下のようにします。
var authentication = HttpContext.GetOwinContext().Authentication; authentication.SignOut(DefaultAuthenticationTypes.ApplicationCookie); // User を上書き HttpContext.User = new GenericPrincipal(new GenericIdentity(string.Empty), null); if (HttpContext.User.Identity.IsAuthenticated) { // ここに入らなくなった! }
User に渡すインスタンスはこれでいいのか?というあたりが不安な感じですが、とりあえずは意図したとおりに動きました。なお、デバッガで確認してみると、サインイン前には User に System.Security.Principal.WindowsPrincipal
のインスタンスが入ってくるようです。
まぁ、もしリダイレクトしてもよいのであればそうするのが無難かと思います。
NuGet で StyleCop を導入して Visual Studio のビルドプロセスに組み込む
メモ。 Visual Studio 2013 で確認しました。
手順としては NuGet で StyleCop と StyleCop.MSBuild をインストール*1するだけです。これだけで csproj ファイルが自動的に更新され、 Visual Studio からのビルド時に StyleCop が走るようになります。
ルールの修正
デフォルトのルールファイルは以下にあります。
- ソリューション直下/packages/StyleCop.MSBuild.X.X.XX.X/tools/Settings.StyleCop
ソリューション共通でよければ、これをソリューション直下にでもコピーしておきます。自動的に読まれるようになります。もし StyleCop を単体でインストールしているならダブルクリックで開いて GUI で編集できます。または上記フォルダーにある StyleCopSettingsEditor.exe の引数に渡して実行してもいいようです。デフォルトのルールは辛いので、どうでもよさそうなものは無効化しておくといいと思います。
ASP.NET MVC の ActionFilter でセッションの値を管理する
この記事は ASP.NET Advent Calendar 2014 - Qiita の 11 日目の記事です。なんか空いてたので登録してみました。昨日は KatsuYuzu さんの ASP.NET の customErrors、IISの httpErrors #aspnetjp - KatsuYuzuのブログ でした。明日もまだ空いているようなのでネタのある方はいかがでしょうか。
ActionFilter について
ActionFilter とは ASP.NET MVC で提供されている Attribute *1のひとつである、 System.Web.Mvc.ActionFilterAttribute
のことです。
開発者はこの ActionFilterAttribute
のサブクラスを実装することで、 Controller のアクションメソッドの処理前後に任意の処理を挟むことができます。また ActionFilterAttribute
を使用することで、以下のようなことができます。
- HttpContext にアクセスできる
- Controller のアクションメソッド引数に任意の値を注入できる
HttpContext にアクセスできるということは、セッションにもアクセスできるということです((実際には HttpContext.Current
を使えば別にここ以外でもセッションにアクセスできますが・・・))。つまりセッションから任意の値を取り出して Controller のメソッド引数にセットするなんてことができるわけで、これらを利用すれば Controller からセッション情報の管理処理を追い出すことができそうです。ユニットテストがはかどりますね。
実際にやってみます。
セッションに保持するモデルクラス
なんでもいいですが、簡単にこんなクラスを用意します。
[Serializable] public class SessionModel { public string Value { get; set; } }
普通に Controller からセッションを操作すると、アクションメソッドの実装は以下のような感じになります。
public class HomeController : Controller { public ActionResult Index() { // セッションから取得 var model = (SessionModel)HttpContext.Session[sessionKey]; if (model == null) { model = new SessionModel(); HttpContext.Session[sessionKey] = model; } // なんか処理 DoSomething(model); // セッションから削除 HttpContext.Session.Remove(sessionKey); return View(); } }
これを Attribute を使ってできるようにします。
Attribute
ActionFilterAttribute
を継承した、こんな感じの Attribute を作ります。 Controller のアクションメソッドの引数に値を注入するには、 filterContext.ActionParameters
に引数名を指定して値をセットします。セッションの値の取得・更新・削除については特筆すべき点はありませんね。
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] public class SessionModelParamAttribute : ActionFilterAttribute { private static readonly string SessionKey = "hogehoge"; /// <summary> /// セッションの値を設定する引数名。 /// </summary> public string ParamName { get; set; } /// <summary> /// 処理完了後にセッションの値を破棄するかどうか。 /// </summary public bool Destroy { get; set; } public override void OnActionExecuting(ActionExecutingContext filterContext) { // ここの処理はアクションメソッドの実行前に呼ばれる if (ParamName == null) { return; } // セッション値がセットされてなければ作成 var model = (SessionModel)filterContext.HttpContext.Session[SessionKey]; if (model == null) { model = new SessionModel(); filterContext.HttpContext.Session[SessionKey] = model; } // アクションメソッドの引数にセッション値を設定 filterContext.ActionParameters[ParamName] = model; } public override void OnActionExecuted(ActionExecutedContext filterContext) { // ここの処理はアクションメソッドの実行後に呼ばれる if (Destroy) { // セッション値を破棄 filterContext.HttpContext.Session.Remove(SessionKey); } } }
やらなくても動きますが、 Controller でセッション値を更新する場合は OnActionExecuted
でセッションに値を再セットしたほうがいいかもしれません*2。
Controller
こんな感じで Attribute を使えます。これで ParamName
で指定した名前の引数にセッションの値がセットされた状態でメソッドが実行されます。 ParamName
の指定を忘れるとリクエストパラメーターでモデルの値が指定できてしまうので注意しましょう。
public class HomeController : Controller { [SessionModelParam(ParamName = "model", Destroy = true)] public ActionResult Index(SessionModel model) // model にセッションの値が入っている { // なんか処理 DoSomething(model.Value); return View(); // 処理完了後、セッションの値は Attribute で破棄される } }
すっきりしましたね。
今回は単純なサンプルですが、 Attribute の実装を工夫すればいろいろ柔軟な処理ができそうですね。
Visual Studio + SVN の diff ツールに WinMerge を使う設定
メモ。 VS 標準の diff ツール?が微妙なので WinMerge で置き換える設定です。 AnkhSVN を使っています。
設定箇所は以下。 VS2010 、 2013 で同じです。
ツール -> オプション -> ソース管理 -> Subversion User Tools -> External Diff Tool
設定内容は以下のような具合です。
"C:\Program Files\WinMerge\WinMergeU.exe" -e -x -ub "%base" "%mine"