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