Freigeben über


Globale Abfragefilter

Globale Abfragefilter ermöglichen das Anfügen eines Filters an einen Entitätstyp und das Anwenden dieses Filters, wenn eine Abfrage für diesen Entitätstyp ausgeführt wird; Stellen Sie sich diese als zusätzlichen LINQ-Operator Where vor, der immer dann hinzugefügt wird, wenn der Entitätstyp abgefragt wird. Solche Filter sind in einer Vielzahl von Fällen nützlich.

Tipp

Das in diesem Artikel verwendete Beispiel finden Sie auf GitHub.

Einfaches Beispiel – vorläufiges Löschen

In einigen Szenarien ist es vorzuziehen, statt eine Zeile aus der Datenbank zu löschen, stattdessen ein IsDeleted Kennzeichen festzulegen, um die Zeile als gelöscht zu markieren. Dieses Muster wird als vorläufiges Löschen bezeichnet. Bei einer vorläufigen („weichen“)-Löschung können Zeilen bei Bedarf wiederhergestellt werden, oder es wird ein Audit-Protokoll beibehalten, wobei auf gelöschte Zeilen weiterhin zugegriffen werden kann. Globale Abfragefilter können standardmäßig verwendet werden, um vorläufig gelöschte Zeilen herauszufiltern, während Sie dennoch an bestimmten Stellen darauf zugreifen können, indem Sie den Filter für eine bestimmte Abfrage deaktivieren.

Um das weiche Löschen zu aktivieren, fügen wir unserem Blog-Typ eine IsDeleted Eigenschaft hinzu.

public class Blog
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }

    public string Name { get; set; }
}

Wir richten jetzt einen globalen Abfragefilter ein, indem wir die HasQueryFilter API in OnModelCreating verwenden.

modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);

Wir können jetzt unsere Blog Entitäten wie gewohnt abfragen. Der konfigurierte Filter stellt sicher, dass alle Abfragen standardmäßig alle Instanzen herausfiltern, in denen IsDeleted "true" ist.

Beachten Sie, dass Sie zu diesem Zeitpunkt manuell festlegen IsDeleted müssen, um eine Entität vorläufig zu löschen. Für eine umfassendere End-to-End-Lösung können Sie die Methode des Kontexttyps SaveChangesAsync außer Kraft setzen, um Logik hinzuzufügen, die alle Entitäten überschreitet, die der Benutzer gelöscht hat, und sie stattdessen so ändert, dass sie geändert werden, indem Sie die IsDeleted Eigenschaft auf "true" festlegen:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    ChangeTracker.DetectChanges();

    foreach (var item in ChangeTracker.Entries<Blog>().Where(e => e.State == EntityState.Deleted))
    {
        item.State = EntityState.Modified;
        item.CurrentValues["IsDeleted"] = true;
    }

    return await base.SaveChangesAsync(cancellationToken);
}

Auf diese Weise können Sie EF-APIs verwenden, die eine Entitätsinstanz wie gewohnt löschen und sie stattdessen soft-gelöscht werden lassen.

Verwenden von Kontextdaten – Mehrinstanzenfähigkeit

Ein weiteres typisches Szenario für globale Abfragefilter ist eine Situation mit mehreren Mandanten, in der Ihre Anwendung Daten speichert, die zu verschiedenen Benutzern in derselben Tabelle gehören. In solchen Fällen gibt es in der Regel eine Mandanten-ID-Spalte , die die Zeile einem bestimmten Mandanten zuordnet, und globale Abfragefilter können verwendet werden, um automatisch nach den Zeilen des aktuellen Mandanten zu filtern. Dies bietet standardmäßig eine starke Mandantenisolation für Ihre Abfragen, wodurch die Notwendigkeit entfernt wird, die Filterung für den Mandanten in jeder und jeder Abfrage zu berücksichtigen.

Anders als beim vorläufigen Löschen erfordert eine Mehrmandantenumgebung die aktuelle Mandanten-ID; dieser Wert wird normalerweise bestimmt, wenn der Benutzer etwa über das Web authentifiziert wird. Für EF-Zwecke muss die Mandanten-ID in der Kontextinstanz verfügbar sein, damit der globale Abfragefilter darauf verweisen und bei der Abfrage verwendet werden kann. Nehmen wir einen tenantId Parameter im Konstruktor des Kontexttyps an, und verweisen wir auf diesen parameter aus unserem Filter:

public class MultitenancyContext(string tenantId) : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
    }
}

Dadurch wird erzwungen, dass jeder, der einen Kontext erstellt, seine zugeordnete Mandanten-ID angibt, und stellt sicher, dass nur Blog Entitäten mit dieser ID standardmäßig aus Abfragen zurückgegeben werden.

Hinweis

In diesem Beispiel wurden nur grundlegende Mehrinstanzenkonzepte gezeigt, die erforderlich sind, um globale Abfragefilter zu veranschaulichen. Weitere Informationen zu Mehrmandantenumgebungen und EF finden Sie unter Mehrere Mandanten in EF Core-Anwendungen.

Verwenden mehrerer Abfragefilter

Durch Aufrufen HasQueryFilter mit einem einfachen Filter werden alle vorherigen Filter überschrieben, sodass auf diese Weise nicht mehrere Filter für denselben Entitätstyp definiert werden können :

modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
// The following overwrites the previous query filter:
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);

Hinweis

Dieses Feature wird in EF Core 10.0 (in der Vorschau) eingeführt.

Um mehrere Abfragefilter für denselben Entitätstyp zu definieren, müssen sie benannt werden:

modelBuilder.Entity<Blog>()
    .HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted)
    .HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);

Auf diese Weise können Sie jeden Filter separat verwalten, einschließlich selektiver Deaktivierung eines, aber nicht des anderen.

Deaktivieren von Filtern

Filter können für einzelne LINQ-Abfragen mithilfe des IgnoreQueryFilters Operators deaktiviert werden:

var allBlogs = await context.Blogs.IgnoreQueryFilters().ToListAsync();

Wenn mehrere benannte Filter konfiguriert sind, werden alle filter deaktiviert. Um bestimmte Filter selektiv zu deaktivieren (beginnend mit EF 10), übergeben Sie die Liste der zu deaktivierenden Filternamen:

var allBlogs = await context.Blogs.IgnoreQueryFilters(["SoftDeletionFilter"]).ToListAsync();

Abfragefilter und erforderliche Navigationen

Vorsicht

Die Verwendung der erforderlichen Navigation für den Zugriff auf Entitäten, für die ein globaler Abfragefilter definiert ist, kann zu unerwarteten Ergebnissen führen.

Erforderliche Navigationen in EF implizieren, dass die zugehörige Entität immer vorhanden ist. Da innere Verknüpfungen zum Abrufen verwandter Entitäten verwendet werden können, wenn eine erforderliche verwandte Entität vom Abfragefilter gefiltert wird, wird die übergeordnete Entität möglicherweise auch herausgefiltert. Dies kann dazu führen, dass weniger Elemente als erwartet abgerufen werden.

Um das Problem zu veranschaulichen, können wir die Entitäten Blog und Post wie folgt konfigurieren:

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

Das Modell kann mit den folgenden Daten initialisiert werden.

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

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

Das Problem kann beim Ausführen der folgenden beiden Abfragen beobachtet werden:

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

Mit dem obigen Setup gibt die erste Abfrage alle 6 Post Instanzen zurück, aber die zweite Abfrage gibt nur 3 zurück. Dieser Konflikt tritt auf, da die Include Methode in der zweiten Abfrage die zugehörigen Blog Entitäten lädt. Da die Navigation zwischen Blog und Post erforderlich ist, verwendet INNER JOIN EF Core beim Erstellen der Abfrage:

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]

Die Verwendung von INNER JOIN filtert alle Post Zeilen aus, deren verwandte Blog Zeilen durch einen Abfragefilter herausgefiltert wurden. Dieses Problem kann behoben werden, indem die Navigation als optionale Navigation konfiguriert wird, anstatt als erforderliche. Dadurch erzeugt EF ein LEFT JOIN anstelle eines INNER JOIN.

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

Ein alternativer Ansatz besteht darin, konsistente Filter sowohl für die Blog- als auch für die Post-Entitätstypen anzugeben. Sobald übereinstimmende Filter sowohl auf Blog als auch auf Post angewendet werden, werden Post-Zeilen, die in einem unerwarteten Zustand enden könnten, entfernt, und beide Abfragen geben 3 Ergebnisse zurück.

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"));

Abfragefilter und IEntityTypeConfiguration

Wenn Ihr Abfragefilter auf eine Mandanten-ID oder ähnliche kontextbezogene Informationen zugreifen muss, kann IEntityTypeConfiguration<TEntity> eine zusätzliche Komplikation darstellen, da im Gegensatz zu OnModelCreating keine Instanz Ihres Kontexttyps vorhanden ist, auf die direkt aus dem Abfragefilter verwiesen werden kann. Fügen Sie als Problemumgehung Ihrem Konfigurationstyp einen Dummykontext hinzu, und verweisen Sie wie folgt darauf:

private sealed class CustomerEntityConfiguration : IEntityTypeConfiguration<Customer>
{
    private readonly SomeDbContext _context = null!;

    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.HasQueryFilter(d => d.TenantId == _context.TenantId);
    }
}

Einschränkungen

Globale Abfragefilter haben die folgenden Einschränkungen:

  • Filter können nur für den Stammentitätstyp einer Vererbungshierarchie definiert werden.
  • Derzeit erkennt EF Core keine Zyklen in globalen Abfragefilterdefinitionen, daher sollten Sie beim Definieren vorsichtig sein. Wenn dies falsch angegeben ist, können Zyklen während der Abfrageübersetzung zu unendlichen Schleifen führen.