c# - tipo - toxina botulínica para que sirve
Validación: ¿Cómo inyectar un contenedor de estado modelo con Ninject? (2)
La solución dada por ese artículo mezcla la lógica de validación con la lógica del servicio. Estas son dos preocupaciones y deben separarse. Cuando la 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 permita que la capa de servicio genere una excepción cuando ocurra un error de validación. Esto lo haría mucho más explícito y mucho más difícil de olvidar para verificar si hay errores. Esto deja la forma en que se manejan los errores a 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 debe tener validación en sí misma, pero debe delegar eso en una clase especializada para la validación: 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
no debe validarse a sí mismo, sino delegar la validación en clases de validación especializadas en la validación de un tipo específico. Cuando un objeto (o conjunto de objetos) no es válido, el proveedor de validación debe emitir una ValidationException
, que puede capturarse más arriba en la pila de llamadas. 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);
}
}
ValidationProvider
depende de IValidator
instancias de IValidator
, que hacen la validación real. El proveedor en sí no sabe cómo crear esas instancias, pero utiliza el delegado Func<Type, IValidator>
para eso. Este método tendrá un código específico de contenedor, por ejemplo esto para Ninject:
var provider = new ValidationProvider(type =>
{
var valType = typeof(Validator<>).MakeGenericType(type);
return (IValidator)kernel.Get(valType);
});
Este fragmento muestra una clase Validator<T>
. Voy a mostrar esto en un segundo. Primero, 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 a nuestro contenedor IoC un poco, deberíamos 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 se 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 declaración de yield return
C # yield return
que hace que devolver errores de validación sea más fácil.
Lo último que debemos hacer para que todo 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>();
¿Estamos realmente terminados? Depende. La desventaja 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 de 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 de forma dinámica desde un ensamblaje determinado. 2. Podemos volver a una implementación predeterminada cuando no existe registro.
Tal implementación predeterminada podría verse así:
sealed class NullValidator<T> : Validator<T>
{
protected override IEnumerable<ValidationResult> Validate(T entity)
{
return Enumerable.Empty<ValidationResult>();
}
}
Podemos configurar este NullValidator<T>
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 registra ninguna implementación específica para él.
Lo último que falta ahora es el registro automático (o el registro por lotes). Esto le ahorrará tener que agregar un registro por implementación Validator<T>
y dejar que Ninject busque dinámicamente sus ensambles por usted. No pude encontrar ningún ejemplo de esto, pero supongo que Ninject puede hacer esto.
ACTUALIZACIÓN: consulte la respuesta de Kayess para aprender a registrar por lotes estos tipos.
Una última nota: para hacer esto, necesitas bastante plomería, así que si tu proyecto es (y se mantiene) bastante pequeño, este enfoque podría darte demasiados gastos. Sin embargo, cuando su proyecto crezca, estará muy contento cuando tenga un diseño tan flexible. Piense en lo que debe hacer si desea cambiar la validación para decir Validación Application Block o DataAnnotations. Lo único que tienes que hacer es escribir una implementación para NullValidator<T>
(en su caso, le cambiaría el nombre a DefaultValidator<T>
. Además de eso, aún es posible tener tus clases de validación personalizadas para validaciones adicionales que son difícil con VAB o DataAnnotations.
Tenga en cuenta que el uso de abstracciones como IProductService
e ICustomerService
viola los principios SOLID y puede beneficiarse al pasar de este patrón a un patrón que resuma los casos de uso .
Actualización: También eche un vistazo a este q / a ; discute una pregunta de seguimiento sobre el mismo artículo.
Estaba viendo este tutorial http://asp-umb.neudesic.com/mvc/tutorials/validating-with-a-service-layer--cs sobre cómo ajustar mis datos de validación en un contenedor.
Sin embargo, me gustaría usar la función de dependencia. Estoy usando ninject 2.0
namespace MvcApplication1.Models
{
public interface IValidationDictionary
{
void AddError(string key, string errorMessage);
bool IsValid { get; }
}
}
// envoltura
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;
}
Me gustaría extender la fantástica respuesta de Stevens donde escribió:
Lo último que falta ahora es el registro automático (o el registro por lotes). Esto le ahorrará tener que agregar un registro por implementación de Validator y dejar que Ninject busque dinámicamente sus ensamblajes por usted. No pude encontrar ningún ejemplo de esto, pero supongo que Ninject puede hacer esto.
Él refiere que este código no puede ser automático:
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 de una manera que he encontrado para hacerlo automático:
kernel.Bind(
x => x.FromAssembliesMatching("Fully.Qualified.AssemblyName*")
.SelectAllClasses()
.InheritedFrom(typeof(Validator<>))
.BindBase()
);
Donde puede reemplazar Fully.Qualified.AssemblyName con su nombre de ensamblado completo totalmente calificado, incluido su espacio de nombres.
ACTUALIZACIÓN: para que todo esto funcione, debe instalar el paquete NuGet y usar el espacio de nombres Ninject.Extensions.Conventions
y utilizar el método Ninject.Extensions.Conventions que acepta un delegado como parámetro.