Валидация: Как внедрить оболочку модельного состояния с помощью 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
}
}
// контроллер
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 ответа
Решение, данное в этой статье, смешивает логику валидации с сервисной логикой. Это две проблемы, и они должны быть отделены. Когда ваше приложение будет расти, вы быстро обнаружите, что логика проверки усложняется и дублируется на уровне обслуживания.
Поэтому я хотел бы предложить другой подход.
Прежде всего, было бы гораздо лучше, если бы 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
Класс сам по себе не должен иметь никакой проверки, но должен делегировать это классу, специализированному для проверки: 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 может сделать это.
ОБНОВЛЕНИЕ: См. Ответ Кейсса, чтобы узнать, как пакетно зарегистрировать эти типы.
Последнее замечание: чтобы сделать это, вам нужно много работы, поэтому, если ваш проект (и остается) довольно мало, такой подход может привести к чрезмерным накладным расходам. Однако, когда ваш проект растет, вы будете очень рады, когда у вас будет такой гибкий дизайн. Подумайте о том, что вам нужно сделать, если вы хотите изменить валидацию на "Блок приложения валидации" или "Аннотации данных". Единственное, что вам нужно сделать, это написать реализацию для NullValidator<T>
(Я бы переименовал его в DefaultValidator<T>
в таком случае. Кроме того, все еще возможно иметь свои собственные классы проверки для дополнительных проверок, которые трудны с VAB или DataAnnotations.
Обратите внимание, что использование абстракций, таких как IProductService
а также ICustomerService
нарушает принципы SOLID, и вы могли бы выиграть от перехода от этого шаблона к шаблону, который абстрагирует варианты использования.
Обновление: также взгляните на этот вопрос; в нем обсуждается дополнительный вопрос о той же статье.
Я хотел бы расширить фантастический ответ Стивенса, где он написал:
Последнее, чего сейчас не хватает, - это автоматической регистрации (или пакетной регистрации). Это избавит вас от необходимости добавлять регистрацию для каждой реализации 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()
метод, который принимает делегата в качестве параметра.