Validación: ¿Cómo inyectar un envoltorio de Estado de Modelo con Ninject?


Estaba mirando este tutorial http://asp-umb.neudesic.com/mvc/tutorials/validating-with-a-service-layer--cs sobre cómo envolver mis datos de validación alrededor de un contenedor.

Me gustaría usar dependency inject sin embargo. Estoy usando 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
    }
}

/ / controlador

private IProductService _service;

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

/ / capa de servicio

private IValidationDictionary _validatonDictionary;
private IProductRepository _repository;

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

public ProductController(IProductService service)
{
    _service = service;
}
Author: Steven, 2011-01-23

2 answers

La solución dada por ese artículo mezcla la lógica de validación con la lógica de servicio. Estas son dos preocupaciones y deben separarse. Cuando su aplicación crezca, descubrirá rápidamente que la lógica de validación se complica y se duplica en toda la capa de servicio.

Por lo tanto, me gusta sugerir un enfoque diferente.

En primer lugar, sería mucho mejor que IMO permitiera que la capa de servicio lanzara una excepción cuando ocurriera un error de validación. Esto haría es mucho más explícito y mucho más difícil olvidar comprobar si hay errores. Esto deja la forma en que se manejan los errores en la capa de presentación. El ProductController se verá así:

public class ProductController : Controller
{
    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);
    }
}

La clase ProductService no debería tener ninguna validación en sí misma, sino que debería delegarla a una clase especializada en validación: la 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);
    }
}

El IValidationProvider no debe validarse a sí mismo, sino delegar la validación a clases de validación especializadas en la validación de un tipo específico. Cuando un object (or set of objects) is not valid, the validation provider should throw a ValidationException, that can be caught higher up the call stack. La implementación del proveedor podría verse así:

sealed class ValidationProvider : IValidationProvider
{
    private readonly Func<Type, IValidator> validatorFactory;

    public ValidationProvider(Func<Type, IValidator> validatorFactory)
    {
        this.validatorFactory = validatorFactory;
    }

    public void Validate(object entity)
    {
        var results = this.validatorFactory(entity.GetType())
            .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);
    }
}

El ValidationProvider depende de las instancias IValidator, que hacen la validación real. El propio proveedor no sabe cómo crear esas instancias, pero utiliza el delegado inyectado Func<Type, IValidator> para ello. Este método tendrá código específico del contenedor, por ejemplo este para Ninject:

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

Esto el fragmento muestra una clase Validator<T>. Voy a mostrar esto en un segundo. Primero, el ValidationProvider depende de las siguientes clases:

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; private set; }
    public string Message { get; private set; }
}

public class ValidationException : Exception
{
    public ValidationException(IEnumerable<ValidationResult> r)
        : base(GetFirstErrorMessage(r))
    {
        this.Errors = 
            new ReadOnlyCollection<ValidationResult>(r.ToArray());
    }

    public ReadOnlyCollection<ValidationResult> Errors { get; private set; }

    private static string GetFirstErrorMessage(
        IEnumerable<ValidationResult> errors)
    {
        return errors.First().Message;
    }
}    

Todo el código anterior es la plomería necesaria para obtener la validación en su lugar. Ahora podemos definir una clase de validación por entidad que queremos validar. Sin embargo, para ayudar un poco a nuestro contenedor IoC, debemos definir una clase base genérica para los validadores. Esto nos permitirá registrar los tipos de validación:

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

Como puede ver, esta clase abstracta hereda de IValidator. Ahora podemos definir una clase ProductValidator que deriva de 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("Name", 
                "Name is required.");

        if (entity.Description.Trim().Length == 0)
            yield return new ValidationResult("Description",
                "Description is required.");

        if (entity.UnitsInStock < 0)
            yield return new ValidationResult("UnitsInStock", 
                "Units in stock cnnot be less than zero.");
    }
}

Como puede ver, la clase ProductValidator usa la instrucción C# yield return que hace que devolver errores de validación sea más fácil.

Lo último que debemos hacer para que todo esto funcione, es configurar la configuración de 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>();

¿Hemos terminado realmente? Depende. El inconveniente de la configuración anterior es que para cada entidad en nuestro dominio necesitaremos una implementación Validator<T>. Incluso cuando quizás la mayoría las implementaciones estarán vacías.

Podemos resolver este problema haciendo dos cosas: 1. Podemos usar el registro por lotes para cargar automáticamente todas las implementaciones dinámicamente desde un ensamblaje dado. 2. Podemos volver a una implementación predeterminada cuando no existe registro.

Tal implementación por defecto podría tener este aspecto:

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

Podemos configurar esto NullValidator<T> de la siguiente manera:

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

Después de hacer esto, Ninject devolverá un NullValidator<Customer> cuando se solicite un Validator<Customer> y no se ha registrado ninguna aplicación específica al respecto.

Lo último que falta ahora es el registro automático (o registro por lotes). Esto le ahorrará tener que agregar un registro por implementación Validator<T> y le permitirá a Ninject buscar sus ensamblados dinámicamente por usted. No pude encontrar ningún ejemplo de esto, pero asumo que Ninject puede hacer esto.

ACTUALIZAR: Ver Respuesta de Kayess para aprender cómo registrar por lotes estos tipos.

Una última nota: usted necesita un montón de plomería, por lo que si su proyecto es (y se mantiene) bastante poco, este enfoque podría darle demasiada sobrecarga. Sin embargo, cuando su proyecto crezca, estará muy contento cuando tenga un diseño tan flexible. Piense en lo que tiene que hacer si desea cambiar la validación para decir Bloque de aplicación de validación o Anotaciones de datos. Lo único que tienes que hacer es escribir una implementación para el NullValidator<T> (lo cambiaría el nombre a DefaultValidator<T> en ese caso. Además de eso, todavía es posible tener sus clases de validación personalizadas para validaciones adicionales que son difíciles con VAB o DataAnnotations.

Tenga en cuenta que el uso de abstracciones como IProductService y ICustomerService viola los principios SÓLIDOS y podría beneficiarse de pasar de este patrón a un patrón que abstrae casos de uso.

Actualización: También echa un vistazo a esta q/a; discute una pregunta de seguimiento sobre el mismo artículo.

 60
Author: Steven,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-07 07:19:43

Me gustaría extender la respuesta fantástica de Stevens donde escribió:

Lo último que falta ahora es el registro automático (o lote registro). Esto le ahorrará tener que agregar un registro por implementación de validador y deje que Ninject busque sus ensamblados dinámicamente para ti. No pude encontrar ningún ejemplo de esto, pero asume que Ninject puede hacer esto.

Él se refiere a este código no puede ser automagic:

kernel.Bind<Validator<Product>>().To<ProductValidator>();

Ahora imagina si tienes decenas de esto como:

...
kernel.Bind<Validator<Product>>().To<ProductValidator>();
kernel.Bind<Validator<Acme>>().To<AcmeValidator>();
kernel.Bind<Validator<JohnDoe>>().To<JohnDoeValidator>();
...

Así que para superar esto he encontrado una manera de hacerlo automático:

kernel.Bind(
    x => x.FromAssembliesMatching("Fully.Qualified.AssemblyName*")
    .SelectAllClasses()
    .InheritedFrom(typeof(Validator<>))
    .BindBase()
);

Donde puedes reemplazar completamente.Calificado.AssemblyName con su nombre de ensamblado real completamente calificado incluyendo su espacio de nombres.

UPDATE: para que todo esto funcione necesitas instalar el paquete NuGet y usar el Ninject.Extensions.Conventions espacio de nombres y utilizar el Bind() método que acepta un delegado como parámetro.

 4
Author: kayess,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-07 09:02:38