Filtros de consulta global

Los filtros de consulta global son predicados de consulta LINQ que se aplican a los tipos de entidad en el modelo de metadatos (normalmente en OnModelCreating). Un predicado de consulta es una expresión booleana que se pasa normalmente al operador de consulta Where de LINQ. EF Core aplica estos filtros automáticamente a cualquier consulta LINQ que implique esos tipos de entidad. EF Core también los aplica a los tipos de entidad, a los que se hace referencia de forma indirecta mediante el uso de la propiedad de navegación o Include. Algunas aplicaciones comunes de esta característica son:

  • Eliminación temporal: un tipo de entidad define una propiedad IsDeleted.
  • Servicios multiinquilino: un tipo de entidad define una propiedad TenantId.

Ejemplo

En el ejemplo siguiente se muestra cómo usar los filtros de consulta global para implementar los comportamientos de consulta de multiinquilino y eliminación temporal en un modelo sencillo de creación de blogs.

Sugerencia

Puede ver un ejemplo de este artículo en GitHub.

Nota:

La configuración multiinquilino se usa aquí como ejemplo sencillo. También hay un artículo con instrucciones completas para la configuración multiinquilino en aplicaciones de EF Core.

En primer lugar, defina las entidades:

public class Blog
{
#pragma warning disable IDE0051, CS0169 // Remove unused private members
    private string _tenantId;
#pragma warning restore IDE0051, CS0169 // Remove unused private members

    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public bool IsDeleted { get; set; }

    public Blog Blog { get; set; }
}

Anote la declaración de un campo _tenantId en la entidad Blog. Este campo se usará para asociar cada instancia de blog a un inquilino específico. También se define una propiedad IsDeleted en el tipo de entidad Post. Se usa para llevar un seguimiento de si una instancia post se ha eliminado de manera temporal. Es decir, la instancia se marca como eliminada sin quitar físicamente los datos subyacentes.

A continuación, configure los filtros de consulta en OnModelCreating mediante la API HasQueryFilter.

modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);

Las expresiones de predicado pasadas a las llamadas de HasQueryFilter ahora se aplicarán automáticamente a cualquier consulta LINQ para esos tipos.

Sugerencia

Tenga en cuenta el uso de un campo en el nivel de instancia de DbContext: _tenantId se usa para establecer el inquilino actual. Los filtros de nivel de modelo usan el valor de la instancia de contexto correcta (es decir, la instancia que está ejecutando la consulta).

Nota:

Actualmente no es posible definir varios filtros de consulta en la misma entidad. Solo se aplicará el último. Sin embargo, puede definir un solo filtro con varias condiciones mediante el operador lógico AND (&& en C#).

Uso de navegaciones

También se pueden usar las navegaciones en la definición de filtros de consulta global. El uso de navegaciones en el filtro de consulta hará que los filtros de consulta se apliquen de forma recursiva. Cuando EF Core expande las navegaciones que se usan en los filtros de consulta, también aplica los filtros de consulta definidos en las entidades a las que se hace referencia.

Para ilustrar esto, configure los filtros de consulta en OnModelCreating de la siguiente manera:

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Posts.Count > 0);
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Title.Contains("fish"));

A continuación, consulte todas las entidades Blog:

var filteredBlogs = db.Blogs.ToList();

Esta consulta genera el siguiente código SQL, que aplica los filtros de consulta definidos para las entidades Blog y Post:

SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE (
    SELECT COUNT(*)
    FROM [Posts] AS [p]
    WHERE ([p].[Title] LIKE N'%fish%') AND ([b].[BlogId] = [p].[BlogId])) > 0

Nota:

Actualmente EF Core no detecta ciclos en las definiciones de filtros de consulta global, por lo que debe tener cuidado al definirlas. Si se especifican incorrectamente, los ciclos pueden provocar bucles infinitos durante la traslación de consultas.

Acceso a una entidad con filtro de consultas mediante la navegación necesaria

Precaución

El uso de la navegación necesaria para acceder a la entidad que tiene definido un filtro de consulta global puede producir resultados inesperados.

La navegación necesaria espera que la entidad relacionada esté siempre presente. Si el filtro de consulta filtra la entidad relacionada necesaria, es posible que la entidad primaria no esté en el resultado. Por lo tanto, puede que en el resultado obtenga menos elementos de los esperados.

Para ilustrar el problema, podemos usar las entidades Blog y Post especificadas anteriormente y el siguiente método OnModelCreating:

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));

El modelo se puede inicializar con los siguientes datos:

db.Blogs.Add(
    new Blog
    {
        Url = "http://sample.com/blogs/fish",
        Posts = new List<Post>
        {
            new Post { Title = "Fish care 101" },
            new Post { Title = "Caring for tropical fish" },
            new Post { Title = "Types of ornamental fish" }
        }
    });

db.Blogs.Add(
    new Blog
    {
        Url = "http://sample.com/blogs/cats",
        Posts = new List<Post>
        {
            new Post { Title = "Cat care 101" },
            new Post { Title = "Caring for tropical cats" },
            new Post { Title = "Types of ornamental cats" }
        }
    });

Se puede observar el problema al ejecutar dos consultas:

var allPosts = db.Posts.ToList();
var allPostsWithBlogsIncluded = db.Posts.Include(p => p.Blog).ToList();

Con la configuración anterior, la primera consulta devuelve las 6 Post; sin embargo, la segunda consulta solo devuelve 3. Esta falta de coincidencia se produce porque el método Include de la segunda consulta carga las entidades Blog relacionadas. Dado que se requiere la navegación entre Blog y Post, EF Core utiliza INNER JOIN al construir la consulta:

SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[IsDeleted], [p].[Title], [t].[BlogId], [t].[Name], [t].[Url]
FROM [Posts] AS [p]
INNER JOIN (
    SELECT [b].[BlogId], [b].[Name], [b].[Url]
    FROM [Blogs] AS [b]
    WHERE [b].[Url] LIKE N'%fish%'
) AS [t] ON [p].[BlogId] = [t].[BlogId]

El uso de INNER JOIN filtra todos los valores Post cuyos Blog relacionados se han quitado mediante un filtro de consulta global.

Se puede solucionar mediante el uso de la navegación opcional, en lugar de la necesaria. De este modo, la primera consulta se mantiene igual que antes, pero la segunda consulta generará LEFT JOIN y devolverá 6 resultados.

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));

Un enfoque alternativo consiste en especificar filtros coherentes en las entidades Blog y Post. De este modo, los filtros coincidentes se aplican tanto a Blog como a Post. Las Post que podrían acabar en un estado inesperado se quitan y ambas consultas devuelven 3 resultados.

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));

Deshabilitación de filtros

Los filtros se pueden deshabilitar para consultas LINQ individuales mediante el operador IgnoreQueryFilters.

blogs = db.Blogs
    .Include(b => b.Posts)
    .IgnoreQueryFilters()
    .ToList();

Limitaciones

Los filtros de consulta global tienen las limitaciones siguientes:

  • Solo se pueden definir filtros para el tipo de entidad raíz de una jerarquía de herencia.