Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
236 views
in Technique[技术] by (71.8m points)

c# - Validation: How to inject A Model State wrapper with Ninject?

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.

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

// wrapper

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
    }
}

// controller

private IProductService _service;

public ProductController() 
{
    _service = new ProductService(new ModelStateWrapper(this.ModelState),
        new ProductRepository());
}

// service layer

private IValidationDictionary _validatonDictionary;
private IProductRepository _repository;

public ProductService(IValidationDictionary validationDictionary,
    IProductRepository repository)
{
    _validatonDictionary = validationDictionary;
    _repository = repository;
}

public ProductController(IProductService service)
{
    _service = service;
}
Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.

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

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

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

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

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

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

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.");
    }
}

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

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

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. 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>();
    }
}

You can configure this NullValidator<T> as follows:

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

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

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.

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

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.

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.

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...