DbEntityValidationException: ¿cómo puedo saber fácilmente qué causó el error?

217

Tengo un proyecto que usa Entity Framework. Al llamar SaveChangesa mi DbContext, obtengo la siguiente excepción:

System.Data.Entity.Validation.DbEntityValidationException: la validación falló para una o más entidades. Consulte la propiedad 'EntityValidationErrors' para obtener más detalles.

Todo esto está muy bien, pero no quiero adjuntar un depurador cada vez que se produce esta excepción. Además, en entornos de producción no puedo conectar fácilmente un depurador, por lo que debo hacer todo lo posible para reproducir estos errores.

¿Cómo puedo ver los detalles ocultos en el DbEntityValidationException?

Martin Devillers
fuente

Respuestas:

429

La solución más fácil es anular SaveChangesen su clase de entidades. Puede capturar DbEntityValidationException, desenvolver los errores reales y crear uno nuevo DbEntityValidationExceptioncon el mensaje mejorado.

  1. Cree una clase parcial al lado de su archivo SomethingSomething.Context.cs.
  2. Usa el código al final de esta publicación.
  3. Eso es. Su implementación utilizará automáticamente los SaveChanges reemplazados sin ningún trabajo de refactorización.

Su mensaje de excepción ahora se verá así:

System.Data.Entity.Validation.DbEntityValidationException: la validación falló para una o más entidades. Consulte la propiedad 'EntityValidationErrors' para obtener más detalles. Los errores de validación son: El campo PhoneNumber debe ser una cadena o tipo de matriz con una longitud máxima de '12'; El campo Apellido es obligatorio.

Puede descartar los SaveChanges anulados en cualquier clase que herede de DbContext:

public partial class SomethingSomethingEntities
{
    public override int SaveChanges()
    {
        try
        {
            return base.SaveChanges();
        }
        catch (DbEntityValidationException ex)
        {
            // Retrieve the error messages as a list of strings.
            var errorMessages = ex.EntityValidationErrors
                    .SelectMany(x => x.ValidationErrors)
                    .Select(x => x.ErrorMessage);
    
            // Join the list to a single string.
            var fullErrorMessage = string.Join("; ", errorMessages);
    
            // Combine the original exception message with the new one.
            var exceptionMessage = string.Concat(ex.Message, " The validation errors are: ", fullErrorMessage);
    
            // Throw a new DbEntityValidationException with the improved exception message.
            throw new DbEntityValidationException(exceptionMessage, ex.EntityValidationErrors);
        }
    }
}

El DbEntityValidationExceptiontambién contiene las entidades que causaron los errores de validación. Entonces, si necesita aún más información, puede cambiar el código anterior para generar información sobre estas entidades.

Ver también: http://devillers.nl/improving-dbentityvalidationexception/

Martin Devillers
fuente
66
La clase Entidades generada ya hereda de DbContext, por lo que no tiene que agregarla nuevamente en la clase parcial. No romperá ni cambiará nada al agregarlo a la clase parcial. De hecho, si agrega la herencia de DbContext, Resharper le sugerirá que la elimine: "El tipo base 'DbContext' ya está especificado en otras partes".
Martin Devillers
15
¿Por qué no es este el comportamiento predeterminado de SaveChanges?
John Shedletsky
44
"¿Por qué no es este el comportamiento predeterminado de SaveChanges?" - Esa es una muy buena pregunta. Esta fue una solución increíble, ¡me ahorró horas! Tuve que tirarusing System.Linq;
John August
1
Mis errores de vista de creación en base.SaveChanges () que se encuentra en el bloque de prueba. Nunca salta al bloque de captura. Obtuve su código para anular SaveChanges pero nunca entra en el Bloqueo de captura por error.
JustJohn
77
Debe establecer la excepción interna para preservar el seguimiento de la pila.
dotjoe
48

Como Martin indicó, hay más información en el DbEntityValidationResult. Me pareció útil obtener mi nombre de clase POCO y el nombre de la propiedad en cada mensaje, y quería evitar tener que escribir ErrorMessageatributos personalizados en todas mis [Required]etiquetas solo por esto.

El siguiente ajuste al código de Martin se ocupó de estos detalles por mí:

// Retrieve the error messages as a list of strings.
List<string> errorMessages = new List<string>();
foreach (DbEntityValidationResult validationResult in ex.EntityValidationErrors)
{
    string entityName = validationResult.Entry.Entity.GetType().Name;
    foreach (DbValidationError error in validationResult.ValidationErrors)
    {
        errorMessages.Add(entityName + "." + error.PropertyName + ": " + error.ErrorMessage);
    }
}
Eric Hirst
fuente
1
Utilizando SelectMany and Aggregateen github por Daring
Coders
43

Para ver la EntityValidationErrorscolección, agregue la siguiente expresión Watch a la ventana Watch.

((System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors

Estoy usando visual studio 2013

Shehab Fawzy
fuente
$ excepción es brillante! eso significa que en la ventana inmediata puedo hacer $ exception.EntityValidationErrors.SelectMany (x => x.ValidationErrors) .Select (x => x.ErrorMessage);
chrispepper1989
13

Mientras está en modo de depuración dentro del catch {...}bloque, abra la ventana "QuickWatch" ( ctrl+ alt+ q) y pegue allí:

((System.Data.Entity.Validation.DbEntityValidationException)ex).EntityValidationErrors

Esto le permitirá profundizar en el ValidationErrorsárbol. Es la forma más fácil que he encontrado para obtener información instantánea sobre estos errores.

Para los usuarios de Visual 2012+ que solo se preocupan por el primer error y pueden no tener un catchbloqueo, incluso pueden hacer:

((System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors.First().ValidationErrors.First().ErrorMessage
GONeale
fuente
9

Para encontrar rápidamente un mensaje de error significativo inspeccionando el error durante la depuración:

  • Agregue un reloj rápido para:

    ((System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors
  • Profundice en EntityValidationErrors como este:

    (elemento de colección, por ejemplo, [0])> ValidationErrors> (elemento de colección, por ejemplo, [0])> Mensaje de error

Chris Halcrow
fuente
5

En realidad, este es solo el problema de validación, EF validará primero las propiedades de la entidad antes de realizar cualquier cambio en la base de datos. Entonces, EF verificará si el valor de la propiedad está fuera de rango, como cuando diseñó la tabla. Table_Column_UserName es varchar (20). Pero, en EF, ingresó un valor superior a 20. O, en otros casos, si la columna no permite ser Nulo. Por lo tanto, en el proceso de validación, debe establecer un valor en la columna no nula, sin importar si va a realizar el cambio en ella. Personalmente, me gusta la respuesta de Leniel Macaferi. Puede mostrarle los detalles de los problemas de validación.

Calvin
fuente
4

Creo que "Los errores de validación reales" pueden contener información confidencial, y esta podría ser la razón por la cual Microsoft decidió colocarlos en otro lugar (propiedades). La solución marcada aquí es práctica, pero debe tomarse con precaución.

Preferiría crear un método de extensión. Más razones para esto:

  • Mantener el seguimiento de la pila original
  • Siga el principio abierto / cerrado (es decir: puedo usar diferentes mensajes para diferentes tipos de registros)
  • En entornos de producción podría haber otros lugares (es decir: otro dbcontext) donde se podría lanzar una DbEntityValidationException.
Luis Toapanta
fuente
1

Para Azure Functions, usamos esta extensión simple para Microsoft.Extensions.Logging.ILogger

public static class LoggerExtensions
{
    public static void Error(this ILogger logger, string message, Exception exception)
    {
        if (exception is DbEntityValidationException dbException)
        {
            message += "\nValidation Errors: ";
            foreach (var error in dbException.EntityValidationErrors.SelectMany(entity => entity.ValidationErrors))
            {
                message += $"\n * Field name: {error.PropertyName}, Error message: {error.ErrorMessage}";
            }
        }

        logger.LogError(default(EventId), exception, message);
    }
}

y ejemplo de uso:

try
{
    do something with request and EF
}
catch (Exception e)
{
    log.Error($"Failed to create customer due to an exception: {e.Message}", e);
    return await StringResponseUtil.CreateResponse(HttpStatusCode.InternalServerError, e.Message);
}
Juri
fuente
0

Use try block en su código como

try
{
    // Your code...
    // Could also be before try if you know the exception occurs in SaveChanges

    context.SaveChanges();
}
catch (DbEntityValidationException e)
{
    foreach (var eve in e.EntityValidationErrors)
    {
        Console.WriteLine("Entity of type \"{0}\" in state \"{1}\" has the following validation errors:",
            eve.Entry.Entity.GetType().Name, eve.Entry.State);
        foreach (var ve in eve.ValidationErrors)
        {
            Console.WriteLine("- Property: \"{0}\", Error: \"{1}\"",
                ve.PropertyName, ve.ErrorMessage);
        }
    }
    throw;
}

Puedes consultar los detalles aquí también

  1. http://mattrandle.me/viewing-entityvalidationerrors-in-visual-studio/

  2. La validación falló para una o más entidades. Vea la propiedad 'EntityValidationErrors' para más detalles

  3. http://blogs.infosupport.com/improving-dbentityvalidationexception/

Atta H.
fuente
Su tercer enlace es una copia del blog de la respuesta aceptada, pero en un sitio diferente. El segundo enlace es una pregunta de desbordamiento de pila que ya hace referencia a su primer enlace.
Eris
Entonces, ¿tratar de ayudar a alguien con la referencia adecuada es algún problema aquí?
Atta H.
Sí, su respuesta no debe contener solo enlaces. Haga un resumen que responda la pregunta y luego publique el enlace al final para cualquier lectura adicional.
ChrisO