Проверка: как ввести оболочку 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; } } } 

// обертка

 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 } } 

// controller

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

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

Поэтому я хотел бы предложить другой подход.

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

 sealed class ValidationProvider : IValidationProvider { private readonly Func validatorFactory; public ValidationProvider(Func 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() 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 делегат Func . Этот метод будет иметь конкретный код контейнера, например, для Ninject:

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

Этот fragment показывает class Validator . Я покажу это через секунду. Во-первых, ValidationProvider зависит от следующих classов:

 public interface IValidator { IEnumerable 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 r) : base(GetFirstErrorMessage(r)) { this.Errors = new ReadOnlyCollection(r.ToArray()); } public ReadOnlyCollection Errors { get; private set; } private static string GetFirstErrorMessage( IEnumerable errors) { return errors.First().Message; } } 

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

 public abstract class Validator : IValidator { IEnumerable IValidator.Validate(object entity) { if (entity == null) throw new ArgumentNullException("entity"); return this.Validate((T)entity); } protected abstract IEnumerable Validate(T entity); } 

Как вы можете видеть, этот абстрактный class наследуется от IValidator . Теперь мы можем определить class ProductValidator который выводится из Validator :

 public sealed class ProductValidator : Validator { protected override IEnumerable 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."); } } 

Как вы можете видеть, class ProductValidator использует оператор yield return C #, который упрощает возврат ошибок проверки.

Последнее, что мы должны сделать, чтобы все это работало, - это настройка конфигурации Ninject:

 kernel.Bind().To(); kernel.Bind().To(); Func validatorFactory = type => { var valType = typeof(Validator<>).MakeGenericType(type); return (IValidator)kernel.Get(valType); }; kernel.Bind() .ToConstant(new ValidationProvider(validatorFactory)); kernel.Bind>().To(); 

Мы действительно сделали? Это зависит. Нижняя сторона конфигурации выше - это то, что для каждого объекта в нашем домене нам понадобится реализация Validator . Даже когда, возможно, большинство реализаций будут пустыми.

Мы можем решить эту проблему, выполнив две вещи: 1. Мы можем использовать пакетную регистрацию для автоматической загрузки всех реализаций динамически из данной сборки. 2. Мы можем вернуться к реализации по умолчанию, если регистрация не существует.

Такая реализация по умолчанию может выглядеть так:

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

Мы можем настроить этот NullValidator следующим образом:

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

После этого Ninject вернет NullValidator когда запрашивается Validator и для него не зарегистрирована определенная реализация.

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

ОБНОВЛЕНИЕ: см. Ответ Kayess, чтобы узнать, как регистрировать эти типы.

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

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

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

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

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

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

 kernel.Bind>().To(); 

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

 ... kernel.Bind>().To(); kernel.Bind>().To(); kernel.Bind>().To(); ... 

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

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

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

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

Давайте будем гением компьютера.