Проверка: как внедрить оболочку состояния модели с помощью Ninject?


Я смотрел этот учебник http://asp-umb.neudesic.com/mvc/tutorials/validating-with-a-service-layer--cs о том, как обернуть мои данные проверки в оболочку.

Однако я хотел бы использовать dependency inject. Я использую 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;
}
2 31

2 ответа:

Решение, данное в этой статье, смешивает логику проверки с логикой обслуживания. Это две проблемы, и они должны быть разделены. Когда ваше приложение вырастет, вы быстро обнаружите, что логика проверки усложняется и дублируется на уровне сервиса.

Таким образом, я хотел бы предложить другой подход.

Во-первых, было бы гораздо лучше, если бы ИМО позволял сервисному уровню выдавать исключение при возникновении ошибки проверки. Это сделало бы он гораздо более явный и гораздо сложнее забыть проверить на наличие ошибок. Это оставляет способ обработки ошибок на уровне представления. ProductController будет выглядеть так:

public class ProductController : Controller
{
    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);
    }
}

IValidationProvider не должен проверять сам себя, но делегировать проверку классам проверки, которые специализируются на проверке одного конкретного типа. Когда объект (или набор объектов) не является допустимым, поставщик проверки должен бросить 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)
    {
        var results = this.validatorFactory(entity.GetType())
            .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; private set; }
    public string Message { get; private set; }
}

public class ValidationException : Exception
{
    public ValidationException(IEnumerable<ValidationResult> r)
        : base(GetFirstErrorMessage(r))
    {
        this.Errors = 
            new ReadOnlyCollection<ValidationResult>(r.ToArray());
    }

    public ReadOnlyCollection<ValidationResult> Errors { get; private set; }

    private static string GetFirstErrorMessage(
        IEnumerable<ValidationResult> errors)
    {
        return errors.First().Message;
    }
}    

Весь вышеприведенный код-это сантехника, необходимая для проведения валидации. Теперь мы можем определить класс проверки для каждой сущности, которую мы хотим проверить. Однако, чтобы немного помочь нашему контейнеру IoC, мы должны определить универсальный базовый класс для валидаторов. Это позволит нам зарегистрировать типы валидации:

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("Name", 
                "Name is required.");

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

        if (entity.UnitsInStock < 0)
            yield return new ValidationResult("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>. Даже когда возможно большинство реализации будут пустыми.

Мы можем решить эту проблему, сделав две вещи: 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>, Когда Validator<Customer> и для него не зарегистрировано никакой конкретной реализации.

Последнее, чего сейчас не хватает, - это автоматическая регистрация (или пакетная Регистрация). Это избавит вас от необходимости добавлять регистрацию для реализации Validator<T> и позволит Ninject динамически искать ваши сборки. Я не смог найти никаких примеров этого, но я предполагаю, что Ninject может это сделать.

Обновление: смотрите ответ Кайесса, чтобы узнать, как пакетная регистрация этих типов.

Последнее замечание: чтобы это сделать, вам нужно довольно много сантехники, поэтому, если ваш проект (и остается) довольно небольшим, этот подход может дать вам слишком много накладных расходов. Однако, когда ваш проект вырастет, вы будете очень рады, когда у вас будет такой гибкий дизайн. Подумайте о том, что вам нужно сделать, если вы хотите изменить проверку, чтобы сказать блок приложений проверки или DataAnnotations. Единственное, что вам нужно сделать, это написать реализацию для NullValidator<T> (я бы переименовал ее в DefaultValidator<T> в этом случае. Кроме того, это по-прежнему возможно иметь свои собственные классы проверки для дополнительных проверок, которые являются трудными с VAB или DataAnnotations.

Обратите внимание, что использование абстракций, таких как IProductService и ICustomerService, нарушает твердые принципы, и вы можете извлечь выгоду из перехода от этого шаблона к шаблону , который абстрагирует варианты использования.

Обновление: также взгляните на этот q/a; он обсуждает последующий вопрос о той же статье.

Я хотел бы расширить фантастический ответ Стивенса, где он написал:

Последнее, чего сейчас не хватает, - это автоматическая регистрация (или пакетная Регистрация). регистрация). Это избавит вас от необходимости добавлять регистрацию за реализацию валидатора и пусть Ninject ищет ваши сборки динамично для вас. Я не мог найти ни одного примера этого, но я предположим, что Ninject может это сделать.

Он ссылается на то, что этот код не может быть автомагическим:

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

Теперь представьте, что у вас есть десятки таких, как:

...
kernel.Bind<Validator<Product>>().To<ProductValidator>();
kernel.Bind<Validator<Acme>>().To<AcmeValidator>();
kernel.Bind<Validator<JohnDoe>>().To<JohnDoeValidator>();
...

Итак, чтобы преодолеть этот путь, я нашел способ сделать его автоматическим:

kernel.Bind(
    x => x.FromAssembliesMatching("Fully.Qualified.AssemblyName*")
    .SelectAllClasses()
    .InheritedFrom(typeof(Validator<>))
    .BindBase()
);

, где вы можете полностью заменить .Квалифицированный.AssemblyName с полным указанием вашего фактического имени сборки, включая пространство имен.

UPDATE: чтобы все это работало, вам нужно установить пакет NuGet и использовать Ninject.Extensions.Conventions пространство имен и использование Bind() метод, который принимает делегат в качестве параметра.