entity framework - Cómo burlarse de las limitaciones de la implementación de IQueryable de EntityFramework
entity-framework mocking (1)
Creo que es muy difícil, si es posible, simular el comportamiento de Entity Framework. En primer lugar, porque requeriría un profundo conocimiento de todas las peculiaridades y casos extremos en los que linq-to-entites difiere de linq-to-objects. Como dices: el verdadero desafío es encontrarlos. Permítanme señalar tres áreas principales sin pretender ser siquiera exhaustivas:
Casos donde Linq-to-Objects tiene éxito y Linq-to-Entities falla:
-
.Select(x => x.Property1.ToString()
. LINQ to Entities no reconoce el método ''System.String ToString ()'' método ... Esto se aplica a casi todos los métodos en clases nativas .Net y por supuesto a propios métodos. Solo unos pocos métodos .Net se traducirán a SQL. Consulte el Método CLR para la Asignación de funciones canónicas . A partir de EF 6.1,ToString
es compatible por cierto, pero solo la sobrecarga sin parámetros. -
Skip()
sin precederOrderBy
. -
Except
eIntersect
: puede producir consultas monstruosas que arrojen Alguna parte de su declaración SQL está anidada demasiado profundamente. Reescribe la consulta o divídela en consultas más pequeñas. -
Select(x => x.Date1 - x.Date2)
: los argumentos de DbArithmeticExpression deben tener un tipo común numérico. - (su caso).
.Where(p => p.Category == category)
: En este contexto, solo se admiten tipos primitivos o tipos de enumeración. -
Nodes.Where(n => n.ParentNodes.First().Id == 1)
: El método ''Primero'' solo puede usarse como una operación de consulta final. -
context.Nodes.Last()
: LINQ to Entities no reconoce el método ''... Last ...'' . Esto se aplica a muchos otros métodos de extensiónIQueryable
. Ver Métodos LINQ soportados y no soportados . - (Ver el comentario de Slauma a continuación):
.Select(x => new A { Property1 = (x.BoolProperty ? new B { BProp1 = x.Prop1, BProp2 = x.Prop2 } : new B { BProp1 = x.Prop1 }) })
: El tipo ''B'' aparece en dos inicializaciones estructuralmente incompatibles dentro de una sola consulta LINQ a Entidades ... desde here . -
context.Entities.Cast<IEntity>()
: no se puede convertir el tipo ''Entity'' para escribir ''IEntity''. LINQ to Entities solo admite la conversión de primitiva EDM o tipos de enumeración. -
.Select(p => p.Category?.Name)
. El uso de propagación nula en una expresión arroja CS8072 Un árbol de expresiones lambda puede no contener un operador de propagación nulo. Esto puede arreglarse un día . - Esta pregunta: ¿Por qué esta combinación de Select, Where y GroupBy causa una excepción? me hizo consciente del hecho de que incluso hay construcciones de consulta completas que no son compatibles con EF, mientras que L2O no tendría ningún problema con ellas.
Casos en los que Linq-to-Objects falla y Linq-to-Entities tiene éxito:
-
.Select(p => p.Category.Name)
: cuandop.Category
es nulo, L2E devuelve null, pero L2O lanza Object reference no establecido en una instancia de un objeto. Esto no se puede solucionar usando la propagación nula (ver arriba). -
Nodes.Max(n => n.ParentId.Value)
con algunos valores nulos paran.ParentId
. L2E devuelve un valor máximo, L2O arroja objetos Nullable debe tener un valor. - Usar
EntityFunctions
(DbFunctions
deDbFunctions
partir de EF 6) oSqlFunctions
.
Casos donde ambos tienen éxito / fallan pero se comportan de manera diferente:
-
Nodes.Include("ParentNodes")
: L2O no tiene implementación de incluir. Ejecutará y devolverá nodos (si losNodes
sonIQueryable
), pero sin los nodos principales. -
Nodes.Select(n => n.ParentNodes.Max(p => p.Id))
con algunas coleccionesParentNodes
vacías: ambas fallan pero con diferentes excepciones. -
Nodes.Where(n => n.Name.Contains("par"))
: L2O distingue entre mayúsculas y minúsculas, L2E depende de la intercalación de la base de datos (a menudo no es sensible a mayúsculas y minúsculas). -
node.ParentNode = parentNode
: con una relación bidireccional, en L2E esto también agregará el nodo a la colección de nodos del padre ( corrección de relación ). No en L2O. (Ver Unidad probando una relación EF bidireccional ). -
.Select(p => p.Category == null ? string.Empty : p.Category.Name)
de problemas para la propagación nula.Select(p => p.Category == null ? string.Empty : p.Category.Name)
:.Select(p => p.Category == null ? string.Empty : p.Category.Name)
: el resultado es el mismo, pero la consulta SQL generada también contiene la verificación nula y puede ser más difícil de optimizar -
Nodes.AsNoTracking().Select(n => n.ParentNode
. ¡Este es muy complicado! ConAsNoTracking
EF crea nuevos objetosParentNode
para cadaNode
, por lo que puede haber duplicados. SinAsNoTracking
EF reutiliza losParentNodes
existentes, porque ahora la entidad el administrador de estado y las claves de entidad están involucradas.AsNoTracking()
se puede llamar en L2O, pero no hace nada, por lo que nunca habrá una diferencia con o sin él.
¿Y qué hay sobre la burla de la carga floja / ansiosa y el efecto del ciclo de vida del contexto en las excepciones de carga diferida? O el efecto de algunas construcciones de consulta en el rendimiento (como construcciones que desencadenan N + 1 consultas SQL). ¿O excepciones debido a llaves de entidad duplicadas o faltantes? ¿O corrección de relación?
Mi opinión: nadie va a fingir eso. El área más alarmante es donde L2O tiene éxito y L2E falla. Ahora, ¿cuál es el valor de las pruebas de unidades verdes? Se ha dicho antes que EF solo puede probarse confiablemente en pruebas de integración (por ejemplo, here ) y estoy de acuerdo.
Sin embargo, eso no significa que debamos olvidarnos de las pruebas unitarias en proyectos con EF como capa de datos. Hay formas de hacerlo , pero, creo, no sin pruebas de integración.
Actualmente estoy escribiendo pruebas unitarias para la implementación de mi repositorio en una aplicación MVC4. Para burlar el contexto de los datos, comencé adoptando algunas ideas de esta publicación , pero ahora he descubierto algunas limitaciones que me hacen cuestionar si es posible incluso IQueryable
adecuadamente IQueryable
.
En particular, he visto algunas situaciones donde las pruebas pasan pero el código falla en la producción y no he podido encontrar ninguna forma de burlarme del comportamiento que causa esta falla.
Por ejemplo, el siguiente fragmento de código se utiliza para seleccionar entidades de Post
que se encuentran dentro de una lista predefinida de categorías:
var posts = repository.GetEntities<Post>(); // Returns IQueryable<Post>
var categories = GetCategoriesInGroup("Post"); // Returns a fixed list of type Category
var filtered = posts.Where(p => categories.Any(c => c.Name == p.Category)).ToList();
En mi entorno de prueba, he intentado burlar las posts
usando la implementación falsa de DbSet
mencionada anteriormente, y también al crear una List
de instancias de Post
y convertirlas a IQueryable
usando el método de extensión AsQueryable()
. Ambos enfoques funcionan en condiciones de prueba, pero el código realmente falla en la producción, con la siguiente excepción:
System.NotSupportedException : Unable to create a constant value of type ''Category''. Only primitive types or enumeration types are supported in this context.
Aunque los problemas de LINQ como este son fáciles de solucionar, el verdadero desafío es encontrarlos, dado que no se revelan en el entorno de prueba.
¿Estoy siendo poco realista al esperar que pueda burlarme del comportamiento de la implementación de IQueryable
de Entity Framework?
Gracias por tus ideas,
Tim.