Подтвердить что ты не робот

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

Я смотрел этот учебник 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; }
    }
}

//wrapper

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;
}
4b9b3361

Ответ 1

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

Прежде всего, 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> делегат 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 использует оператор yield return С# 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, и вам может быть полезно перейти от этого шаблона к шаблону, который абстрагирует варианты использования.

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

Ответ 2

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

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

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