多くの人々は、なぜ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アクションにはGET以外の依存関係が必要であり、これらの種の間の二分法はコントローラーの膨張につながります。
- ロギング、検証、認可、イベント通知などの追加など、すべての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アクションがどのように見えるかを見ていきます。