32

検証データをラッパーにラップする方法について、このチュートリアルhttp://asp-umb.neudesic.com/mvc/tutorials/validating-with-a-service-layer--csを見ていました。

ただし、依存性注入を使用したいと思います。ninject 2.0を使用しています

namespace MvcApplication1.Models
{
    public interface IValidationDictionary
    {
        void AddError(string key, string errorMessage);
        bool IsValid { get; }
    }
}

// ラッパー

using System.Web.Mvc;

namespace MvcApplication1.Models
{
    public class ModelStateWrapper : IValidationDictionary
    {

        private ModelStateDictionary _modelState;

        public ModelStateWrapper(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }

        #region IValidationDictionary Members

        public void AddError(string key, string errorMessage)
        {
            _modelState.AddModelError(key, errorMessage);
        }

        public bool IsValid
        {
            get { return _modelState.IsValid; }
        }

        #endregion
    }
}

// コントローラー

private IProductService _service;

public ProductController() 
{
    _service = new ProductService(new ModelStateWrapper(this.ModelState),
        new ProductRepository());
}

// サービス層

private IValidationDictionary _validatonDictionary;
private IProductRepository _repository;

public ProductService(IValidationDictionary validationDictionary,
    IProductRepository repository)
{
    _validatonDictionary = validationDictionary;
    _repository = repository;
}

public ProductController(IProductService service)
{
    _service = service;
}
4

2 に答える 2

65

その記事で提供されているソリューションでは、検証ロジックとサービス ロジックが混在しています。これらは 2 つの懸念事項であり、分離する必要があります。アプリケーションが成長すると、検証ロジックが複雑になり、サービス レイヤー全体で重複することがすぐにわかります。したがって、私は別のアプローチを提案したいと思います。

まず第一に、検証エラーが発生したときにサービス層に例外をスローさせる方がはるかに優れています。これにより、エラーのチェックがより明確になり、忘れにくくなります。これにより、エラーの処理方法はプレゼンテーション層に委ねられます。次のリストは、ProductControllerこのアプローチを使用する を示しています。

public class ProductController : Controller
{
    private readonly IProductService service;

    public ProductController(IProductService service) => this.service = service;

    public ActionResult Create(
        [Bind(Exclude = "Id")] Product productToCreate)
    {
        try
        {
            this.service.CreateProduct(productToCreate);
        }
        catch (ValidationException ex)
        {
            this.ModelState.AddModelErrors(ex);
            return View();
        }

        return RedirectToAction("Index");
    }
}

public static class MvcValidationExtension
{
    public static void AddModelErrors(
        this ModelStateDictionary state, ValidationException exception)
    {
        foreach (var error in exception.Errors)
        {
            state.AddModelError(error.Key, error.Message);
        }
    }
}

クラス自体に検証を含めるべきではProductServiceありませんが、それを検証に特化したクラスに委譲する必要がありIValidationProviderます。

public interface IValidationProvider
{
    void Validate(object entity);
    void ValidateAll(IEnumerable entities);
}

public class ProductService : IProductService
{
    private readonly IValidationProvider validationProvider;
    private readonly IProductRespository repository;

    public ProductService(
        IProductRespository repository,
        IValidationProvider validationProvider)
    {
        this.repository = repository;
        this.validationProvider = validationProvider;
    }

    // Does not return an error code anymore. Just throws an exception
    public void CreateProduct(Product productToCreate)
    {
        // Do validation here or perhaps even in the repository...
        this.validationProvider.Validate(productToCreate);

        // This call should also throw on failure.
        this.repository.CreateProduct(productToCreate);
    }
}

ただし、 thisIValidationProviderはそれ自体を検証するのではなく、特定のタイプの検証に特化した検証クラスに検証を委譲する必要があります。オブジェクト (またはオブジェクトのセット) が有効でない場合、検証プロバイダーは をスローする必要ValidationExceptionがあります。これは、コール スタックの上位でキャッチできます。プロバイダーの実装は次のようになります。

sealed class ValidationProvider : IValidationProvider
{
    private readonly Func<Type, IValidator> validatorFactory;

    public ValidationProvider(Func<Type, IValidator> validatorFactory)
    {
        this.validatorFactory = validatorFactory;
    }

    public void Validate(object entity)
    {
        IValidator validator = this.validatorFactory(entity.GetType());
        var results = validator.Validate(entity).ToArray();        

        if (results.Length > 0)
            throw new ValidationException(results);
    }

    public void ValidateAll(IEnumerable entities)
    {
        var results = (
            from entity in entities.Cast<object>()
            let validator = this.validatorFactory(entity.GetType())
            from result in validator.Validate(entity)
            select result)
            .ToArray();

        if (results.Length > 0)
            throw new ValidationException(results);
    }
}

実際の検証を行うインスタンスに依存ValidationProviderします。IValidatorプロバイダー自体は、これらのインスタンスを作成する方法を知りませんが、そのために注入されたFunc<Type, IValidator>デリゲートを使用します。このメソッドには、コンテナー固有のコードが含まれます。たとえば、Ninject の場合は次のようになります。

var provider = new ValidationProvider(type =>
{
    var valType = typeof(Validator<>).MakeGenericType(type);
    return (IValidator)kernel.Get(valType);
});

このスニペットはValidator<T>クラスを示しています。このクラスはすぐに示します。まず、ValidationProviderは次のクラスに依存します。

public interface IValidator
{
    IEnumerable<ValidationResult> Validate(object entity);
}

public class ValidationResult
{
    public ValidationResult(string key, string message)
    {
        this.Key = key;
        this.Message = message; 
    }
    public string Key { get; }
    public string Message { get; }
}

public class ValidationException : Exception
{
    public ValidationException(ValidationResult[] r) : base(r[0].Message)
    {
        this.Errors = new ReadOnlyCollection<ValidationResult>(r);
    }

    public ReadOnlyCollection<ValidationResult> Errors { get; }            
}    

上記のコードはすべて、検証を行うために必要な配管です。検証するエンティティごとに検証クラスを定義できるようになりました。ただし、DI コンテナーを少しでも支援するには、バリデーターのジェネリック基本クラスを定義する必要があります。これにより、検証タイプを登録できます。

public abstract class Validator<T> : IValidator
{
    IEnumerable<ValidationResult> IValidator.Validate(object entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");

        return this.Validate((T)entity);
    }

    protected abstract IEnumerable<ValidationResult> Validate(T entity);
}

ご覧のとおり、この抽象クラスは を継承していIValidatorます。ProductValidatorから派生するクラスを定義できるようになりましたValidator<Product>

public sealed class ProductValidator : Validator<Product>
{
    protected override IEnumerable<ValidationResult> Validate(
        Product entity)
    {
        if (entity.Name.Trim().Length == 0)
            yield return new ValidationResult(
                nameof(Product.Name), "Name is required.");

        if (entity.Description.Trim().Length == 0)
            yield return new ValidationResult(
                nameof(Product.Description), "Description is required.");

        if (entity.UnitsInStock < 0)
            yield return new ValidationResult(
                nameof(Product.UnitsInStock), 
                "Units in stock cnnot be less than zero.");
    }
}

ご覧のとおり、このProductValidatorクラスは C#yield returnステートメントを使用しているため、検証エラーがよりスムーズに返されます。

これをすべて機能させるために最後に行うべきことは、Ninject 構成をセットアップすることです。

kernel.Bind<IProductService>().To<ProductService>();
kernel.Bind<IProductRepository>().To<L2SProductRepository>();

Func<Type, IValidator> validatorFactory = type =>
{
    var valType = typeof(Validator<>).MakeGenericType(type);
    return (IValidator)kernel.Get(valType);
};

kernel.Bind<IValidationProvider>()
    .ToConstant(new ValidationProvider(validatorFactory));

kernel.Bind<Validator<Product>>().To<ProductValidator>();

本当に終わりですか?場合によります。上記の構成の欠点は、ドメイン内のエンティティごとにValidator<T>実装が必要になることです。おそらくほとんどの実装が空になる場合でも。

この問題は、次の 2 つの方法で解決できます。

  1. 自動登録を使用して、特定のアセンブリから動的にすべての実装を自動的にロードできます。
  2. 登録が存在しない場合は、デフォルトの実装に戻すことができます。

このようなデフォルトの実装は次のようになります。

sealed class NullValidator<T> : Validator<T>
{
    protected override IEnumerable<ValidationResult> Validate(T entity)
    {
        return Enumerable.Empty<ValidationResult>();
    }
}

NullValidator<T>これは次のように構成できます。

kernel.Bind(typeof(Validator<>)).To(typeof(NullValidator<>));

これを行った後、Ninject はNullValidator<Customer>aValidator<Customer>が要求され、特定の実装が登録されていない場合に a を返します。

現在欠けている最後のものは、自動登録です。これにより、実装ごとに登録を追加する必要がValidator<T>なくなり、Ninject がアセンブリを動的に検索できるようになります。この例は見つかりませんでしたが、Ninject でこれができると思います。

更新:これらのタイプを自動登録する方法については、Kayess の回答を参照してください。

最後に 1 つ注意してください。これを行うには、かなりの量の配管が必要です。そのため、プロジェクトがかなり小さい (そしてそのままである) 場合、このアプローチではオーバーヘッドが大きくなりすぎる可能性があります。しかし、プロジェクトが大きくなると、このように柔軟な設計ができると非常に嬉しくなります。検証を変更したい場合に何をしなければならないかを考えてください (たとえば、Validation Application Block または DataAnnotations)。あなたがしなければならない唯一のことは、の実装を書くことですNullValidator<T>(その場合は名前を変更しDefaultValidator<T>ます。それ以外にも、他の検証テクノロジーでは実装が難しい追加の検証用にカスタム検証クラスを使用することも可能です.

IProductServiceやなどの抽象化の使用はICustomerServiceSOLID の原則に違反するため、このパターンからユース ケースを抽象化するパターンに移行することでメリットが得られる可能性があることに注意してください。

更新:こちらの q/a もご覧ください。同じ記事に関するフォローアップの質問について説明します。

于 2011-01-31T14:23:08.363 に答える