Dónde ejecutar un cheque duplicado para una entidad

Estoy buscando asesoramiento sobre el “mejor” lugar para poner la lógica de validación, como un cheque duplicado para una entidad, cuando se utiliza Entity Framework Code-First, en una aplicación MVC.

Para usar un ejemplo simple:

public class JobRole { public int Id { get; set; } public string Name { get; set; } } 

La regla es que el campo “Nombre” debe ser único.

Cuando agrego una nueva JobRole, es fácil ejecutar una verificación en el Repositorio de roles de trabajo de que el Nombre aún no existe.

Pero si un usuario edita una JobRole existente y accidentalmente establece el Nombre en uno que ya existe, ¿cómo puedo verificar esto?

El problema es que no es necesario que haya un método de “actualización” en el repository, ya que la entidad de función de tarea detectará automáticamente los cambios, por lo que no es un lugar lógico para hacer esta comprobación antes de intentar guardar.

He considerado dos opciones hasta el momento:

  1. Sustituya el método ValidateEntry en DbContext, y luego cuando la entidad JobRole se esté guardando con EntityState.Modified, ejecute la comprobación duplicada.
  2. Cree algún tipo de servicio de verificación de duplicados, llamado desde el Controlador, antes de intentar guardarlo.

Ninguno de los dos parece realmente ideal. El uso de ValidateEntry parece bastante tarde (justo antes de guardar) y es difícil de probar. El uso de un Servicio deja la posibilidad de que alguien olvide llamarlo desde un Controlador, permitiendo el paso de datos duplicados.

¿Hay una mejor manera?

Su problema con ValidateEntity parece ser que la validación ocurre en SaveChanges y esto es demasiado tarde para usted. Pero en Entity Framework 5.0 puede llamar a la validación antes si desea usar DbContext.GetValidationErrors . Y, por supuesto, también puede simplemente llamar a DbContext.ValidateEntity directamente. Así es como lo hago:

  1. Reemplace el método ValidateEntity en el DbContext :

     protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary items) { //base validation for Data Annotations, IValidatableObject var result = base.ValidateEntity(entityEntry, items); //You can choose to bail out before custom validation //if (result.IsValid) // return result; CustomValidate(result); return result; } private void CustomValidate(DbEntityValidationResult result) { ValidateOrganisation(result); ValidateUserProfile(result); } private void ValidateOrganisation(DbEntityValidationResult result) { var organisation = result.Entry.Entity as Organisation; if (organisation == null) return; if (Organisations.Any(o => o.Name == organisation.Name && o.ID != organisation.ID)) result.ValidationErrors .Add(new DbValidationError("Name", "Name already exists")); } private void ValidateUserProfile(DbEntityValidationResult result) { var userProfile = result.Entry.Entity as UserProfile; if (userProfile == null) return; if (UserProfiles.Any(a => a.UserName == userProfile.UserName && a.ID != userProfile.ID)) result.ValidationErrors.Add(new DbValidationError("UserName", "Username already exists")); } 
  2. Context.SaveChanges en un try catch y cree un método para acceder a Context.GetValidationErrors( ). Esto está en mi clase UnitOfWork :

     public Dictionary GetValidationErrors() { return _context.GetValidationErrors() .SelectMany(x => x.ValidationErrors) .ToDictionary(x => x.PropertyName, x => x.ErrorMessage); } public int Save() { try { return _context.SaveChanges(); } catch (DbEntityValidationException e) { //http://blogs.infosupport.com/improving-dbentityvalidationexception/ var errors = e.EntityValidationErrors .SelectMany(x => x.ValidationErrors) .Select(x => x.ErrorMessage); string message = String.Join("; ", errors); throw new DataException(message); } } 
  3. En mi controlador, llame a GetValidationErrors() después de agregar la entidad al contexto pero antes de SaveChanges() :

     [HttpPost] public ActionResult Create(Organisation organisation, string returnUrl = null) { _uow.OrganisationRepository.InsertOrUpdate(organisation); foreach (var error in _uow.GetValidationErrors()) ModelState.AddModelError(error.Key, error.Value); if (!ModelState.IsValid) return View(); _uow.Save(); if (string.IsNullOrEmpty(returnUrl)) return RedirectToAction("Index"); return Redirect(returnUrl); } 

Mi clase de repository base implementa InsertOrUpdate esta manera:

  protected virtual void InsertOrUpdate(T e, int id) { if (id == default(int)) { // New entity context.Set().Add(e); } else { // Existing entity context.Entry(e).State = EntityState.Modified; } } 

Aún así, recomiendo agregar una restricción única a la base de datos, ya que eso garantizará la integridad de los datos y proporcionará un índice que puede mejorar la eficiencia, pero la invalidación de ValidateEntry brinda gran control sobre cómo y cuándo ocurre la validación.

El lugar más confiable para realizar esta lógica sería la base de datos misma declarando una restricción de campo única en la columna de nombre. Cuando alguien intenta insertar o actualizar una entidad existente e intenta establecer su nombre a un nombre existente, se lanzará una excepción de violación de restricciones que podría capturar e interpretar en su capa de acceso a datos.

En primer lugar , su primera opción anterior es aceptable . El hecho de que otro desarrollador no pueda atrapar una falla no es un gran inconveniente. Este es siempre el caso con la validación: lo captas lo más temprano y elegantemente que puedes, pero lo principal es mantener la integridad de tus datos. Más simple que simplemente tener una restricción en la base de datos y trampa para eso, ¿verdad? Pero sí, quieres atraparlo lo antes posible.

Si tiene el scope y el tiempo, sería mejor tener un objeto más inteligente que pudiera manejar el guardado , y tal vez otras cosas que necesita manejar de manera consistente. Hay muchas formas de hacer esto. Puede ser un envoltorio para su entidad. (Consulte el patrón Decorator , aunque prefiero que mi objeto tenga de forma consistente una propiedad de Data para acceder a la entidad). Puede requerir una entidad relevante para ser instanciado. Su controlador le daría a la entidad a este objeto inteligente para que lo guarde (de nuevo, quizás creando una instancia de este objeto inteligente con su entidad). Este objeto inteligente conocería toda la lógica de validación necesaria y se aseguraría de que suceda.

Como ejemplo, puede crear un objeto comercial JobRole. (“busJobRole”, prefijo de bus para “negocios”). Podría tener una colección DataExceptions . Su controlador toma la entidad JobRole publicada, crea una instancia de busJobRole y llama a un método SaveIfValid , que devolvería true si el elemento se guardó correctamente y falso si hubiera problemas de validación. A continuación, comprueba la propiedad DataExceptions datos de DataExceptions para conocer los problemas exactos, y completa tu estado de modelo, etc. Tal vez así:

 // Check ModelState first for more basic errors, like email invalid format, etc., and react accordingly. var jr = new busJobRole(modelJobRole); if (jr.SaveIfValid == false) { ModelState.AddModelError(jr.DataExceptions.First.GetKey(0), jr.DataExceptions.First.Get(0)) } 

Seguimos este modelo de manera coherente, e hice un método de extensión para que ModelState aceptara una colección NameValue (devuelta por objetos comerciales) (versión vb.net):

  _ Public Sub AddModelErrorsFromNameValueCollection( ByVal theModelState As ModelStateDictionary, ByVal collectionOfIssues As NameValueCollection, Optional ByRef prefix As String = "") If String.IsNullOrEmpty(prefix) Then prefix = "" Else prefix = prefix & "." End If For i = 0 To CollectionOfIssues.Count - 1 theModelState.AddModelError(prefix & CollectionOfIssues.GetKey(i), CollectionOfIssues.Get(i)) Next End Sub 

esto permite la adición rápida y elegante de excepciones (determinadas por objetos comerciales) a ModelState:

 ModelState.AddModelErrorsFromNameValueCollection(NewApp.RuleExceptions, "TrainingRequest") 

Su preocupación de que otros desarrolladores no sigan un plan que configure es muy válida y buena. Es por eso que su esquema debe ser consistente . Por ejemplo, en mi proyecto actual, tengo dos categorías de clases que actúan como he descrito. Si son muy livianos y solo tratan con el almacenamiento en caché y la validación, son clases de “administrador de datos” (por ejemplo, BureauDataManager ). Algunos son verdaderos objetos de dominio empresarial, muy completos, que prefijo con “bus” (por ejemplo, busTrainingRequest). Los primeros heredan de una clase base común, para garantizar la coherencia (y reducir el código, por supuesto). La consistencia permite una encapsulación verdadera, para la detección del código, para que el código correcto esté en el lugar correcto (único).