ASP.NET MVCでのPOSTの整理

このトピックでは、フォームアクションのPOSTハンドラーで繰り返されるコードの数を大幅に削減できるASP.NET MVCアプリケーションを開発するための専門的な手法の1つについて説明しますASP.NET MVC In Actionの初版と最初のmvcConfでさえそれについて学んだという事実にもかかわらず、 ジミー・ボガードのプレゼンテーションのスタイルは私にとって非常にシンプルに思えたので、まだこのアプローチを実際に使用していない人のために無料の翻訳を公開することにしました。





多くの人々は、なぜAutoMapperにリバースマッピング( DTO- >永続オブジェクトモデル)の組み込み機能がほとんどないのかを尋ねます。 実際、既存の機会はドメインモデルを大きく制限し、それらを貧弱にすることを余儀なくされているため、POSTリクエストの複雑さに対処する別の方法を見つけました。



中/大規模なASP.NET MVCサイト、複雑さ、またはサイズを見てください。実装にいくつかのパターンが現れていることがわかります。 GETアクションとPOSTアクションの外観には大きな違いがあります。 これは予想されることです GETはクエリであり、POSTはコマンドです(それらを正しく実装した場合、まさにそうです)。 フォームタグとPOSTアクションの比率は必ずしも1対1であるとは限りません。 フォームを使用してリクエストを送信することもできます(検索フォームなど)。



GETアクションについては、私の意見では、問題はすでに解決されています。 GETアクションは、ViewModelを作成し、それをビューに送信し、任意の数の最適化/抽象化(AutoMapper、モデルバインディング、規則の投影など)を使用します。



POSTはまったく別の獣です。 情報の変更とコマンドの受信/検証の複雑さのベクトルは、GETと完全に直交しているため、以前の決定をすべて破棄します。 通常、次のようなものが表示されます。

[HttpPost]

public ActionResult Edit(ConferenceEditModel form)

{

if (!ModelState.IsValid)

{

return View(form);

}



var conf = _repository.GetById(form.Id);



conf.ChangeName(form.Name);



foreach ( var attendeeEditModel in form.Attendees)

{

var attendee = conf.GetAttendee(attendeeEditModel.Id);



attendee.ChangeName(attendeeEditModel.FirstName, attendeeEditModel.LastName);

attendee.Email = attendeeEditModel.Email;

}



return this .RedirectToAction(c => c.Index( null ), "Default" );

}



* This source code was highlighted with Source Code Highlighter .






何度も何度も目にするのは、次のようなパターンです。

[HttpPost]

public ActionResult Edit( SomeEditModel form)

{

if ( IsNotValid )

{

return ShowAView (form);

}



DoActualWork ();



return RedirectToSuccessPage ();

}




* This source code was highlighted with Source Code Highlighter .






赤でマークされているものはすべて、POSTアクションからPOSTアクションに変わります。



では、なぜこれらのアクションを心配する必要があるのでしょうか? ここで見たものに基づいて共通の実行パスを形成してみませんか? ここに私たちが直面しているいくつかの理由があります:



回避策として、技術の組み合わせを使用しました。



これらの抽象化を常に作成する必要はありませんが、POSTの複雑さを管理するのに役立ちます。 始めるために、アクション結果を作成しましょう。



全体的な実行フローの決定



アクションの結果を作成するパスに沿って進む前に、上記の一般的なテンプレートを見てみましょう。 コントローラーアクションで定義する必要があるものもあれば、ランダムなものもあります。 たとえば、「DoActualWork」ブロックは、受信したフォームに基づいて決定できます。 フォームのアクションを処理する2つの異なる方法はありませんので、このフォームを処理するためのインターフェイスを定義しましょう。

public interface IFormHandler<T>

{

void Handle(T form);

}



* This source code was highlighted with Source Code Highlighter .






Action(T) 」を表すクラス、またはCommandパターンの実装は非常に簡単です。 実際、メッセージに精通している場合は、メッセージハンドラのように見えます。 フォームはメッセージであり、ハンドラーはそのようなメッセージをどう処理するかを知っています。



上記の抽象化は、DoActualWorkブロックに対して行う必要があることを表しており、残りはアクションの全体的な結果にドラッグできます。

public class FormActionResult<T> : ActionResult

{

public ViewResult Failure { get ; private set ; }

public ActionResult Success { get ; private set ; }

public T Form { get ; private set ; }



public FormActionResult(T form, ActionResult success, ViewResult failure)

{

Form = form;

Success = success;

Failure = failure;

}



public override void ExecuteResult(ControllerContext context)

{

if (!context.Controller.ViewData.ModelState.IsValid)

{

Failure.ExecuteResult(context);



return ;

}



var handler = ObjectFactory.GetInstance<IFormHandler<T>>();



handler.Handle(Form);



Success.ExecuteResult(context);

}

}



* This source code was highlighted with Source Code Highlighter .






メインの実行パイプラインを調べて、変化する部分を見つけました。 これらは、結果が成功した場合の実行用のActionResultと、結果が失敗した場合の実行用のActionResultであることは注目に値します。 実行用の特定のフォームハンドラーは、フォームの種類に基づいて既に定義されているため、一般的なIoCコンテナーを使用して、実行用の特定のフォームハンドラーを見つけます(私の場合はStructureMap )。 実装に基づいてIFormHandler実装を見つけるようにStructureMapに指示しましょう。これは1行のコードです。

Scan(scanner =>

{

scanner.TheCallingAssembly();

scanner.ConnectImplementationsToTypesClosing( typeof (IFormHandler<>));

});



* This source code was highlighted with Source Code Highlighter .






次に、「DoActualWork」ブロックをクラスにドラッグします。これは、フォームの処理のみを処理し、トラフィックUIの追跡は行いません。

public class ConferenceEditModelFormHandler

: IFormHandler<ConferenceEditModel>

{

private readonly IConferenceRepository _repository;



public ConferenceEditModelFormHandler(

IConferenceRepository repository)

{

_repository = repository;

}



public void Handle(ConferenceEditModel form)

{

Conference conf = _repository.GetById(form.Id);



conf.ChangeName(form.Name);



foreach ( var attendeeEditModel in GetAttendeeForms(form))

{

Attendee attendee = conf.GetAttendee(attendeeEditModel.Id);



attendee.ChangeName(attendeeEditModel.FirstName,

attendeeEditModel.LastName);

attendee.Email = attendeeEditModel.Email;

}

}



private ConferenceEditModel.AttendeeEditModel[] GetAttendeeForms(ConferenceEditModel form)

{

return form.Attendees ??

new ConferenceEditModel.AttendeeEditModel[0];

}

}



* This source code was highlighted with Source Code Highlighter .






現在、このクラスはフォーム処理の成功のみを目的として設計されています。 つまり、私のドメインオブジェクトに戻り、それに応じて変更します。 なぜなら 私はビヘイビアドメインモデルを持っています。「逆マッピング」の可能性はありません。 これは意図的に行われます。



本当に興味深いのは、これらの問題をすべて分離し、特定のASP.NETアクションの結果に依存しなくなったことです。 現時点では、作業の問題を​​直接作業から効果的に分離しています。



コントローラーに適用



アクション結果を作成したので、最後の問題が残っています-このアクション結果をコントローラーアクションに適用します。 ほとんどの人と同様に、コントローラークラスの階層にレイヤーを挿入して、すべてのコントローラーでヘルパーメソッドを使用できるようにすることがよくあります。 このクラスでは、ヘルパーメソッドを追加して、カスタムアクション結果を作成します。

public class DefaultController : Controller

{

protected FormActionResult<TForm> Form<TForm>(

TForm form,

ActionResult success)

{

var failure = View(form);



return new FormActionResult<TForm>(form, success, failure);

}



* This source code was highlighted with Source Code Highlighter .






しばしば定義するデフォルトのパスを単にラップします。 たとえば、処理エラーはほとんどの場合、到着したばかりのビューを示します。 最後に、元のPOSTコントローラーアクションを変更できます。

public class ConferenceController : DefaultController

{

[HttpPost]

public ActionResult Edit(ConferenceEditModel form)

{

var successResult =

this .RedirectToAction(c => c.Index( null ), "Default" );



return Form(form, successResult);

}



* This source code was highlighted with Source Code Highlighter .






コントローラーのアクションを減らして、実際に私たちがやっていることの説明であり、それを行う方法として、より低いレベルに移動しました。 これは、実際のオブジェクト指向構成のアプリケーションの典型的な例です。アクションの結果でPOSTフォームを実行するさまざまな方法を組み合わせて、フォームハンドラを実装しました。 実際、私たちは書くことを余儀なくされたコードを減らさず、ただ移動し、推論するのが少し簡単になりました。



もう1つの興味深い副作用は、フォームハンドラーの単体/統合テストを作成していますが、コントローラーアクションの作成ではないということです。 そして、チェックするものは何ですか? 私たちには、テストを書くインセンティブがありません。 ロジックが少なすぎます。



大規模なパターンの使用を観察する場合、主にルートの結合を調査することが重要です。 これにより、継承されたルートの場合よりも、パーツをまとめる際の柔軟性が少し高くなります。



これはやや複雑な例ですが、次の記事では、検証が単純な要素を超え、POSTハンドラーがさらに複雑になったときに、より複雑なPOSTアクションがどのように見えるかを見ていきます。



All Articles