循環参照は、サービスレイヤーがコントローラーのModelStateに依存し、コントローラーがサービスレイヤーに依存しているという事実に関係しているようです。
これを機能させるには、検証レイヤーを書き直す必要がありました。これが私がしたことです。
以下のような汎用バリデーターインターフェースを定義します。
public interface IValidator<TEntity>
{
ValidationState Validate(TEntity entity);
}
明らかに、検証の状態を定義するValidationStateのインスタンスを返すことができるようにする必要があります。
public class ValidationState
{
private readonly ValidationErrorCollection _errors;
public ValidationErrorCollection Errors
{
get
{
return _errors;
}
}
public bool IsValid
{
get
{
return Errors.Count == 0;
}
}
public ValidationState()
{
_errors = new ValidationErrorCollection();
}
}
強く型付けされたエラーコレクションがあり、これも定義する必要があることに注意してください。コレクションは、検証するエンティティのプロパティ名とそれに関連付けられたエラーメッセージを含むValidationErrorオブジェクトで構成されます。これは、標準のModelStateインターフェイスに従います。
public class ValidationErrorCollection : Collection<ValidationError>
{
public void Add(string property, string message)
{
Add(new ValidationError(property, message));
}
}
そして、ValidationErrorは次のようになります。
public class ValidationError
{
private string _property;
private string _message;
public string Property
{
get
{
return _property;
}
private set
{
_property = value;
}
}
public string Message
{
get
{
return _message;
}
private set
{
_message = value;
}
}
public ValidationError(string property, string message)
{
Property = property;
Message = message;
}
}
これの残りはStructureMapの魔法です。検証オブジェクトを見つけてエンティティを検証する検証サービスレイヤーを作成する必要があります。検証サービスを使用している人は誰でもStructureMapの存在を完全に知らないようにしたいので、このためのインターフェイスを定義したいと思います。その上、ブートストラッパーロジック以外の場所にObjectFactory.GetInstance()を振りかけるのは悪い考えだと思います。一元化しておくことは、優れた保守性を保証するための良い方法です。とにかく、私はここでデコレータパターンを使用します:
public interface IValidationService
{
ValidationState Validate<TEntity>(TEntity entity);
}
そしてついにそれを実装します:
public class ValidationService : IValidationService
{
#region IValidationService Members
public IValidator<TEntity> GetValidatorFor<TEntity>(TEntity entity)
{
return ObjectFactory.GetInstance<IValidator<TEntity>>();
}
public ValidationState Validate<TEntity>(TEntity entity)
{
IValidator<TEntity> validator = GetValidatorFor(entity);
if (validator == null)
{
throw new Exception("Cannot locate validator");
}
return validator.Validate(entity);
}
#endregion
}
コントローラで検証サービスを使用します。これをサービスレイヤーに移動し、StructureMapにプロパティインジェクションを使用してコントローラーのModelStateのインスタンスをサービスレイヤーにインジェクトさせることもできますが、サービスレイヤーをModelStateと結合させたくありません。別の検証手法を使用することにした場合はどうなりますか?これが私がむしろそれをコントローラーに入れたい理由です。これが私のコントローラーの外観です:
public class PostController : Controller
{
private IEntityService<Post> _service = null;
private IValidationService _validationService = null;
public PostController(IEntityService<Post> service, IValidationService validationService)
{
_service = service;
_validationService = validationService;
}
}
ここでは、StructureMapを使用してサービスレイヤーと検証サービスインスタンスを注入しています。したがって、両方をStructureMapレジストリに登録する必要があります。
ForRequestedType<IValidationService>()
.TheDefaultIsConcreteType<ValidationService>();
ForRequestedType<IValidator<Post>>()
.TheDefaultIsConcreteType<PostValidator>();
それでおしまい。PostValidatorを実装する方法は示していませんが、単にIValidatorインターフェイスを実装し、Validate()メソッドで検証ロジックを定義しているだけです。あとは、検証サービスインスタンスを呼び出してバリデーターを取得し、エンティティのvalidateメソッドを呼び出して、エラーをModelStateに書き込むだけです。
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude = "PostId")] Post post)
{
ValidationState vst = _validationService.Validate<Post>(post);
if (!vst.IsValid)
{
foreach (ValidationError error in vst.Errors)
{
this.ModelState.AddModelError(error.Property, error.Message);
}
return View(post);
}
...
}
私がこれで誰かを助けたことを願っています:)