ASP.NET MVC4 beta面白いですねー。
特にRESTfulなサービス実装を容易に実現するために導入されたApiControllerは強烈。
WCFのプロダクトとして開発が進められていたWCF Web APIが、名前を変えてASP.NET Web APIとして生まれ変わったのがその実態です。
そもそもWeb APIの出所がWCFで、その系譜もなかなか歴史のあるものだったわけですが、RESTfulならMVC、SOAPならWCFという住み分けを明確にするという意味も込めてのASP.NET Web APIなんじゃないかと勘ぐってるところです。
ホントのところは本国の開発チームしか知らないんだろーし、そんな理由はDeveloperには関係なかったりもするんだけど、気になって仕方ない。その辺はこっちに書いてみました。まるで根拠のない話ですから!フィクションですからね!
で、ですね、ApiControllerですけど。これまでのMVCだとIController実装のControllerBase、それを派生させたControllerを利用してましたね。そう、この名前が重要で、ApiControllerもControllerという名前。でも、その実態はIControlerではなくIHttpController実装。
はい~?何が違うの~?と、なりましょうね。そりゃーそうです。どっちも名前はControllerってなってるし、使い方もだいたい一緒で、見た目の違いはRouteCollectionへの登録時にMapRouteなのかMapHttpRouteなのか、くらいですからね。
だけど、だからといって、ApiControllerがIControllerの仲間だと思うのは大間違い!MapHttpRouteと、あえて違う拡張メソッドでの登録にしてるのには訳がある。
今のところソースも無いし、詳細を確認するにはなかなか厳しいところです。が、そんな時はJustDecompile!ベータも取れて若干の不具合はあるものの、大変便利なデコンパイラ。困ったらコレ。リバースエンジニアリングで黒判定?でも、知りたいし~。
まずはMVCの仕組み。
An Introduction to ASP.NET MVC Extensibility
↑こちらのPDFがかなり正確な感じです。
PDFをダウンロードしてみてみてください。
スタートはRouteの登録からはじまり、Controller→ModelBind→Action Invoke→ActionResult→Result Invokeとひと通りの流れと、Resolver使ってる部分が書かれてます。素晴らしいですね。
コレとMVC3のソース(公開されてるから見ましょう!)を、見つつApiControllerを使った場合の比較をしてみます。分かってる範囲で。
Route
MVC
System.Web.Mvc.RouteCollectionExtensions.MapRoute(System.Web.Mvc.dll)
RouteCollection.Add(
name,
System.Web.Routing.Route(MvcRouteHandler : IRouteHandler)
)
どうってこと無いですね。MvcRouteHandlerをRouteクラスに渡してます。MvcRouteHandlerにはMvcHandlerを渡します。
Web API
System.Web.Http.RouteCollectionExtensions.MapHttpRoute(System.Web.Http.WebHost.dll)
RouteCollection.Add(
name,
System.Web.Http.WebHost.Routing.HttpWebRoute(HttpControllerRouteHandler.Instance : IRouteHandler)
)
こっちはいきなり違います。HttpWebRouteクラスを登録です。そこへはHttpControllerHandlerを渡すんです。
パンチ効いてますねー。
IRouteHandler.GetHttpHandlerがRouteに紐づいてるIHttpHandlerを取得するんですけど、ココからすでに別物です。つまり、Web APIで提供されるApiControllerっていうのはMVCで提供されているControllerとはまるで別物ということです!
それが、何を意味するかというと、ActionSelector、ModelBinder、ActionFilterなどなど、MVCで提供されていたものがすべてWeb APIでは別のアセンブリとして提供されてるということです!!
というのも、そもそもWCF Web APIで実装を進めていたものだしね。今のところ、双方で依存関係もないです。どこかで一本化するのかどうかは微妙ですね。
System.Web.HttpとSystem.Web.Mvcはそれぞれの道を歩みそうな気もする。もちろんSystem.Web.RoutingやSystem.ComponentModel.DataAnnotationsなんかは共通なんだけど。それらを利用する部分はまるで別物。
それを踏まえて、いま、出回ってる情報を見ると、なんでDataAnnotationsを使ったバインドやAuthorizationFilterとかのFilterが使えるんだよ、とことさら強調してるのかがわかると思います。新たに実装したんだから、自慢したい!っていうね。違うか...。
リクエストとレスポンス
MVC
Requestは基本的にASP.NETの仕組みと同じで最終的にActionResultを返す。
Web API
RequestはHttpMessageHandlerが仲介してHttpRequestMessageになって、HttpResponseMessageを返す。
普通の事書いてる感じがするけど、大違いなんです。Web APIではこのMessageっていうのが重要でそれに対して各種Pipelineが介入していく。この設計、まさにWCFって感じですね。よくできてるなー、と感心せざるを得ない。
ここで分離し、かつWebサーバーの存在をHttpServerというクラスで抽象化することで、APIの提供をWeb サーバーに限定せず、Self Hostへとつなげることが、あたかも自明な流れとして受け入れられる。ん?そんなことない?
HTTP Message Handlers: The Official Microsoft ASP.NET Site
IHttpHandler
MVC
ControllerFactoryから対象Controllerインスタンスを取得してExecute。Controller.ExecuteCoreに入った時の流れ。
PossiblyLoadTempData
IActionInvoker.InvokeAction(ControllerActionInvoker)
ControllerDescriptor <= ReflectedControllerDescriptor
ActionDesctiptor <= ControllerDescriptor.FindAction
FilterInfo <= GetFilters
InvokeAuthorizationFilters - IList<IAuthorizationFilter>
AuthorizationContext
? InvokeAuthorizationFilters
: GetParameterValues
IModelBinder.BindModel : ParameterDescriptor
DefaultModelBinder.SetProperty
ModelValidator <= ModelValidatorProviders.Providers.GetValidators
DataAnnotationsModelValidatorProvider
DataErrorInfoModelValidatorProvider
ClientDataTypeModelValidatorProvider
InvokeActionMethodWithFilters : IList<IActionFilter>
InvokeActionMethod
IActionDescriptor.Execute
InvokeActionResultWithFilters : IList<IResultFilter>
InvokeActionResult
ActionResult.ExecuteResult
PossiblySaveTempData
わかりにく!メモってことで。そもそも素直な実装なので、コード見たほうが早い。
Web API
RequestからHttpRequestMessageを作成、ConfigurationとDispatcherを指定してHttpServeを作成。
HttpServer.SubmitRequestAsyncにHttpRequestMessageを渡して処理開始!HttpMessageHandler、HttpControllerDispatcherが大活躍になるのがここから。
HttpControllerDispatcher
SendAsync
SendAsyncInternal
Initialize
IHttpControllerActivator <= ServiceResolver.GetService || Activator.CreateInstance
IHttpActionSelector <= ServiceResolver.GetService || Activator.CreateInstance
IHttpActionInvoker <= ServiceResolver.GetService || Activator.CreateInstance
IHttpControllerFactory <= ServiceResolver.GetControllerFactory
IHttpControllerFactory.CreateController
IHttpController <= DefaultHttpContollerFactory.CreateInstance
ControllerDescriptor.HttpControllerActivator.Create
TypeActivator.Create
Expression.New
IHttpController.ExecuteAsync
HttpControllerDescriptor <= HttpControllerContext.Descriptor
HttpActionDescriptor <= HttpControllerDescriptor.HttpActionSelector.SelectAction
IHttpActionSelector : ApiControllerActionSelector
HttpActionContext
FilterInfo <= HttpActionDescriptor.GetFilterPipeline
IEnumerable<IActionFilter> <= FilterInfo.ActionFilters
IEnumerable<IAuthorizationFilter> <= FilterInfo.AuthorizationFilters
IEnumerable<IExceptionFilter> <= FilterInfo.ExceptionFilters
InvokeActionWithExceptionFilters(taks)
InvokeActionWithAuthorizationFilters
IActionValueBinder <= ServiceResolver.GetActionValueBinder : DefaultActionValueBinder
IActionValueBinder.BindValueAsync
HttpActionBinding
CreateParameterBindings
BindToBody
ValidationModelBinder.BindModel
ModelValidationNode.Validate
DefaultActionValueBinder.BindParameterValue
IModelBinder.BindModel
MutableObjectModelBinder.SetProperty?
HttpActionContextExtensions.GetValidators?
DataAnnotationsModelValidatorProvider
ClientDataTypeModelValidatorProvider
InvokeActionWithActionFilters
IHttpActionInvoker.HttpActionInvoker.InvokeActionAcyns : ApiControllerActionInvoker
ActionDescriptor.Execute(ControllerContext,ActionArguments)
HttpActionExecuteContext
動かしながらの確認じゃないのでイイカゲン。だし、相変わらずメモで読みにくし。どんまい。Resolverがいろんな所で使われてるのと、ほとんどがTaskになってる。
今のところよくわかってないのがMutableObjectModelBinderがValidationするModelBinderなんだけど、こいつが発動するのはいつなのかというところ。この中のSetPropertyでModelValidatorがワサワサ動いてるっぽいから、コレを利用したバインドにならないと検証が動かないじゃないっすか。動いてることは間違いないんだけど(DataAnnotatinosでの検証がかかってるのはサンプルで確認できてるし)なー。今後の調査課題。
ここでMVCと明確に違うのがDataErrorInfoModelValidatorProviderが存在してないところ。なくてもいいと思うけど、IDataErrorInfo使ってるのを動かすのは自分でしこまないとダメってことですね。こういう所ではちょっと差がある。
Validating your models in ASP.NET Web API - Pablo M. Cibraro (aka Cibrax) ASP.NET MVC 4 public beta including ASP.NET Web API
↑この書き方のサンプルをオンラインでよく見かけますよね。MVCならAction内でModelState.IsValid見てたと思うけど、Web APIだとFilterにしちゃうのがいいの?ときになるところだと思いますが、これはAPIが返す結果が何なのかを考えれば妥当というかコレしかないね、と思えるところです。
どういうことかというとHTMLを返して、その中にエラーメッセージを含んでたりModelStateによる入力値を復元させることを前提にしてるアプリケーションとしてのMVC(どこのViewにModelを渡すのかをControllerが指定)。それに対してエラーですよ、ということを返せばいいだけのAPI(ControllerがModelを返すけど、APIから返るデータをViewというならViewはModelをFormatしたXML/JSON固定で不変)。何がどのようにエラーだったのかを判定して表示を制御するのはAPIを利用したアプリケーションの責任で表示としてのViewを制御してるのはAPIじゃない。なので、一律Filterでエラーの時の処理を決めてレスポンスすれば良い。MVCでも同じようにFilterでModelStateをみてエラーをレスポンスすることは可能ですが、その時のViewをFilterが判断することになって、う~ん、まいった、ですね。
んじゃ、ApiControllerのActionの中でModelStateにエラーを入れることができないの?っていうとそんなことはなくて、入れればいいです。で、OnActionExecutingじゃなくてOnActionExecutedでResponse変えるとか、HttpMessageHandlerで書き換えるとか、事後にエラーとしてしまう方法はイロイロです。
DefaultServiceResolver
ちょっと脱線。最初から登録されてるインスタンスたちはコレらでした。
- IBuildManager
DefaultBuildManager - IHttpControllerFactory
DefaultHttpControllerFactory - IHttpControllerActivator
DefaultHttpControllerActivator - IHttpActionSelector
ApiControllerActionSelector - IHttpActionInvoker
ApiControllerActionInvoker - ModelMetadataProvider
EmptyModelMetadataProvider - IFormatterSelector
FormatterSelector - IActionValueBinder
DefaultActionValueBinder - IRequestContentReadPolicy
DefaultRequestContentReadPolicy - IFilterProvider
ConfigurationFilterProvider
ActionDescriptorFilterProvider
EnumerableEvaluatorFilterProvider
QueryCompositionFilterProvider - ModelBinderProvider
TypeMatchModelBinderProvider
BinaryDataModelBinderProvider
KeyValuePairModelBinderProvider
ComplexModelDtoModelBinderProvider
ArrayModelBinderProvider
DictionaryModelBinderProvider
CollectionModelBinderProvider
TypeConverterModelBinderProvider
MutableObjectModelBinderProvider
CompositeModelBinderProvider - ModelValidatorProvider
DataAnnotationsModelValidatorProvider
ClientDataTypeModelValidatorProvider - ValueProviderFactory
RouteDataValueProviderFactory
QueryStringValueProviderFactory - ModelMetadataProvider
CachedDataAnnotationsModelMetadataProvider - ILogger
DiagnosticLogger
いっぱいあるねー。
System.Web.Http.Services.DefaultServiceResolver。System.Web.Http.GlobalConfiguration.Configuration.ServiceResolverらへんです。
MVCそのもの(System.Web.Mvc)も楽しいんだけど、今後はASP.NET Web API(System.Web.Http)に対しても、目を光らせておこうと思う次第です。