Решение, данное в этой статье, смешивает логику валидации с сервисной логикой. Это две проблемы, и они должны быть отделены. Когда ваше приложение будет расти, вы быстро обнаружите, что логика проверки усложняется и дублируется на уровне обслуживания. Поэтому я хотел бы предложить другой подход.
Прежде всего, 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>
. Даже когда, возможно, большинство реализаций будут пустыми.
Вы можете решить эту проблему, выполнив две вещи:
- Вы можете использовать Авторегистрация , чтобы автоматически загружать все реализации динамически из данной сборки.
- Вы можете вернуться к реализации по умолчанию, если не существует никакой регистрации.
Такая реализация по умолчанию может выглядеть так:
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 *, в нем обсуждается дополнительный вопрос по той же статье.