32

我正在查看本教程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;
}
4

2 回答 2

65

那篇文章给出的解决方案将验证逻辑与服务逻辑混合在一起。这是两个问题,应该分开。当您的应用程序增长时,您会很快发现验证逻辑变得复杂并在整个服务层中重复出现。因此,我想提出一种不同的方法。

首先,IMO 在发生验证错误时让服务层抛出异常会更好。这使得忘记检查错误变得更加明确和困难。这将处理错误的方式留给了表示层。以下清单显示了ProductController使用此方法的 a:

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>实现。即使大多数实现可能是空的。

你可以通过做两件事来解决这个问题:

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

完成此操作后,NullValidator<Customer>Validator<Customer>请求 a 并且没有为其注册特定实现时,Ninject 将返回 a。

现在缺少的最后一件事是自动注册。这将使您不必为每个Validator<T>实现添加注册,并让 Ninject 为您动态搜索您的程序集。我找不到任何这样的例子,但我认为 Ninject 可以做到这一点。

更新:请参阅Kayess 的回答以了解如何自动注册这些类型。

最后一点:要完成这项工作,您需要大量的管道,因此如果您的项目(并且保持)很少,这种方法可能会给您带来太多开销。但是,当您的项目发展壮大时,您会很高兴拥有如此灵活的设计。想想如果你想改变验证(比如验证应用程序块或数据注释)你必须做什么。您唯一需要做的就是为NullValidator<T>(在这种情况下我会将其重命名为)编写一个实现DefaultValidator<T>。除此之外,您仍然可以使用自定义验证类来进行其他验证技术难以实现的额外验证。

请注意,使用诸如IProductService和之类的抽象ICustomerService违反了 SOLID 原则,您可能会从这种模式转移到抽象用例的模式中受益。

更新:也看看这个 q/a;它讨论了关于同一篇文章的后续问题。

于 2011-01-31T14:23:08.363 回答
4

我想在史蒂文斯写的地方扩展他的精彩回答:

现在缺少的最后一件事是自动注册(或批量注册)。这将使您不必为每个验证器实现添加注册,并让 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()接受委托作为参数的方法。

于 2016-10-27T12:06:02.773 回答