验证:如何使用 Ninject 注入模型状态包装器? [英] Validation: How to inject A Model State wrapper with Ninject?

查看:23
本文介绍了验证:如何使用 Ninject 注入模型状态包装器?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在看这个教程 http://asp-umb.neudesic.com/mvc/tutorials/validating-with-a-service-layer--cs 关于如何将我的验证数据包装在包装器周围.

I was looking at this tutorial http://asp-umb.neudesic.com/mvc/tutorials/validating-with-a-service-layer--cs on how to wrap my validation data around a wrapper.

不过我想使用依赖注入.我正在使用 ninject 2.0

I would like to use dependency inject though. I am using 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;
}

推荐答案

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

The solution given by that article mixes validation logic with the service logic. These are two concerns and they should be separated. When your application grows you will quickly find out that validation logic gets complicated and gets duplicated throughout the service layer. I, therefore, like to suggest a different approach.

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

First of all, it would IMO be much better to let the service layer throw an exception when a validation error occurred. This makes it more explicit and harder to forget to check for errors. This leaves the way the errors are handled to the presentation layer. The following listing shows a ProductController that uses this approach:

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:

The ProductService class should not itself have any validation in it, but should delegate that to a class specialized to validation—i.e. the 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,它可以在调用堆栈的上层被捕获.提供程序的实现可能如下所示:

This IValidationProvider, however, should not validate itself, but should rather delegate the validation to validation classes that are specialized in validation one specific type. When an object (or set of objects) is not valid, the validation provider should throw a ValidationException, that can be caught higher up the call stack. The implementation of the provider could look like this:

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 委托.此方法将具有特定于容器的代码,例如 Ninject 的代码:

The ValidationProvider depends on IValidator instances, that do the actual validation. The provider itself doesn't know how to create those instances, but uses the injected Func<Type, IValidator> delegate for that. This method will have container specific code, for instance this for Ninject:

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

这个片段显示了一个 Validator 类——我将在稍后展示这个类.首先,ValidationProvider 依赖于以下类:

This snippet shows a Validator<T> class—I will show this class in a second. First, the ValidationProvider depends on the following classes:

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 容器,您应该为验证器定义一个通用基类.这将允许您注册验证类型:

All the above code is the plumbing needed to get the validation in place. You can now define a validation class per entity you want to validate. However, to help your DI Container out a bit, you should define a generic base class for the validators. This will allow you to register the validation types:

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.现在您可以定义一个从 Validator 派生的 ProductValidator 类:

As you can see, this abstract class inherits from IValidator. Now you can define a ProductValidator class that derives from 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 语句,这使得返回验证错误更加流畅.

As you can see the ProductValidator class uses the C# yield return statement which makes returning validation errors more fluent.

要让这一切正常运行,您应该做的最后一件事是设置 Ninject 配置:

The last thing you should do to get this all working, is setting up the Ninject configuration:

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

Are we really done? It depends. Downside of the configuration above is that for each entity in our domain you will need a Validator<T> implementation. Even when perhaps most implementations will be empty.

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

You can solve this problem by doing two things:

  1. 您可以使用自动注册从给定的程序集中动态加载所有实现.
  2. 您可以在不存在注册时恢复为默认实现.
  1. You can use Auto-Registration to automatically load all implementations dynamically from a given assembly.
  2. You can revert to a default implementation when no registration exists.

这样的默认实现可能如下所示:

Such a default implementation could look like this:

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

您可以按如下方式配置此NullValidator:

You can configure this NullValidator<T> as follows:

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

执行此操作后,当请求 Validator 并且没有为其注册特定实现时,Ninject 将返回一个 NullValidator.

After doing this, Ninject will return a NullValidator<Customer> when a Validator<Customer> is requested and no specific implementation is registered for it.

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

The last thing that's missing now is auto-registration. This will save you from having to add a registration per Validator<T> implementation and let Ninject search your assemblies dynamically for you. I couldn't find any examples of this, but I assume Ninject can do this.

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

UPDATE: See Kayess' answer to learn how to auto-register these types.

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

One last note: To get this done you need quite a lot of plumbing, so if your project is (and stays) fairly little, this approach might give you too much overhead. When your project grows, however, you will be very glad when you have such a flexible design. Think about what you have to do if you want to change the validation (to say Validation Application Block or DataAnnotations). The only thing you have to do is to write an implementation for the NullValidator<T> (I would rename it to DefaultValidator<T> in that case. Besides that, it is still possible to have your custom validation classes for extra validations that are hard to implement with other validation technologies.

请注意,诸如 IProductServiceICustomerService 之类的抽象的使用违反了 SOLID 原则,您可能会从这种模式转向 一种抽象用例的模式.

Note that the use of abstractions such as IProductService and ICustomerService violates the SOLID principles and you might benefit from moving from this pattern to a pattern that abstracts use cases.

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

Update: Also take a look at this q/a; it discusses a follow-up question about the same article.

这篇关于验证:如何使用 Ninject 注入模型状态包装器?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆