Валидация: Как добавить оболочку модельного состояния с помощью Ninject? - PullRequest
32 голосов
/ 23 января 2011

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

Я хотел бы использовать инъекцию зависимости, хотя.Я использую 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 ]

63 голосов
/ 31 января 2011

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

Прежде всего, IMO было бы намного лучше, если бы сервисный уровень генерировал исключение при возникновении ошибки проверки. Это делает его более явным и труднее забыть проверять ошибки. Это оставляет способ обработки ошибок на уровне представления. Следующий листинг показывает 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);
    }
}

Этот 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)
    {
        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>. Даже когда, возможно, большинство реализаций будут пустыми.

Вы можете решить эту проблему, выполнив две вещи:

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

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

Обновление: также взгляните на этот вопрос / 1090 *, в нем обсуждается дополнительный вопрос по той же статье.

4 голосов
/ 27 октября 2016

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

Последнее, чего сейчас не хватает, это автоматическая регистрация (или регистрация партии).Это избавит вас от необходимости добавлять регистрацию для каждой реализации Validator и позволит 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()
);

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

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

...