Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Met globale queryfilters kan een filter worden gekoppeld aan een entiteitstype en dat filter wordt toegepast wanneer een query op dat entiteitstype wordt uitgevoerd; U kunt deze zien als een extra LINQ-operator Where die wordt toegevoegd wanneer het entiteitstype wordt opgevraagd. Dergelijke filters zijn handig in verschillende gevallen.
Aanbeveling
U kunt het voorbeeld van dit artikel bekijken op GitHub.
Basisvoorbeeld : voorlopig verwijderen
In sommige scenario's, in plaats van een rij uit de database te verwijderen, is het beter om in plaats daarvan een IsDeleted vlag in te stellen om de rij te markeren als verwijderd. Dit patroon wordt voorlopig verwijderen genoemd. Met soft deletion kunnen rijen indien nodig worden hersteld, of een audittrail behouden blijven waarbij verwijderde rijen nog steeds toegankelijk zijn. Globale queryfilters kunnen standaard worden gebruikt om zacht verwijderde rijen te filteren, terwijl ze nog steeds op specifieke locaties toegankelijk zijn door het filter voor een specifieke query uit te schakelen.
Om soft deletion in te schakelen, voegen we een IsDeleted eigenschap toe aan onze Blog-type.
public class Blog
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
public string Name { get; set; }
}
We hebben nu een globaal queryfilter ingesteld met behulp van de HasQueryFilter API in OnModelCreating:
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
We kunnen nu onze Blog entiteiten op de gebruikelijke manier opvragen. Het geconfigureerde filter zorgt ervoor dat alle query's standaard alle exemplaren uitfilteren waar IsDeleted waar is.
Houd er rekening mee dat u op dit moment handmatig moet instellen IsDeleted om een entiteit voorlopig te verwijderen. Voor een end-to-end-oplossing kunt u de SaveChangesAsync-methode van uw contexttype overschrijven om logica toe te voegen die alle entiteiten doorloopt die de gebruiker heeft verwijderd, en om deze in plaats daarvan te wijzigen zodat de IsDeleted-eigenschap op true wordt gezet.
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);
}
Hiermee kunt u EF-API's gebruiken die een entiteitsexemplaar zoals gebruikelijk verwijderen en deze in plaats daarvan zacht laten verwijderen.
Contextgegevens gebruiken - multitenancy
Een ander algemeen scenario voor globale queryfilters is multitenancy, waarbij uw toepassing gegevens opslaat die behoren tot verschillende gebruikers in dezelfde tabel. In dergelijke gevallen is er meestal een tenant-id-kolom die de rij koppelt aan een specifieke tenant en globale queryfilters kunnen worden gebruikt om automatisch te filteren op de rijen van de huidige tenant. Dit biedt standaard een sterke tenantisolatie voor uw query's, zodat u niet hoeft te denken aan filteren voor de tenant in elke en elke query.
In tegenstelling tot zachte verwijdering vereist multitenancy de huidige tenant-id; deze waarde wordt bijvoorbeeld meestal bepaald wanneer de gebruiker zich authenticeert via het web. Voor EF-doeleinden moet de tenant-id beschikbaar zijn in het contextexemplaren, zodat het globale queryfilter ernaar kan verwijzen en deze kan gebruiken bij het uitvoeren van query's. Laten we een tenantId parameter in de constructor van het contexttype accepteren en ernaar verwijzen vanuit ons filter:
public class MultitenancyContext(string tenantId) : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
}
}
Dit dwingt iedereen die een context maakt om de bijbehorende tenant-id op te geven en zorgt ervoor dat alleen Blog entiteiten met die id standaard worden geretourneerd uit query's.
Opmerking
In dit voorbeeld werden alleen basisprincipes van multitenancy getoond die nodig zijn om globale query-filters te demonstreren. Zie multitenancy en EF voor meer informatie over multitenancy en EF in EF Core-toepassingen.
Meerdere queryfilters gebruiken
Als u een eenvoudig filter aanroept HasQueryFilter , wordt een eerder filter overschreven, zodat er op deze manier niet meerdere filters kunnen worden gedefinieerd voor hetzelfde entiteitstype:
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
// The following overwrites the previous query filter:
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
Opmerking
Deze functie wordt geïntroduceerd in EF Core 10.0 (in preview).
Als u meerdere queryfilters voor hetzelfde entiteitstype wilt definiëren, moet deze de naam hebben:
modelBuilder.Entity<Blog>()
.HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted)
.HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);
Hiermee kunt u elk filter afzonderlijk beheren, inclusief selectief uitschakelen van een filter, maar niet van de andere.
Filters uitschakelen
Filters kunnen worden uitgeschakeld voor afzonderlijke LINQ-query's met behulp van de IgnoreQueryFilters operator:
var allBlogs = await context.Blogs.IgnoreQueryFilters().ToListAsync();
Als er meerdere benoemde filters zijn geconfigureerd, worden ze allemaal uitgeschakeld. Als u specifieke filters selectief wilt uitschakelen (te beginnen met EF 10), geeft u de lijst met filternamen door die moeten worden uitgeschakeld:
var allBlogs = await context.Blogs.IgnoreQueryFilters(["SoftDeletionFilter"]).ToListAsync();
Queryfilters en vereiste navigatie
Waarschuwing
Het gebruik van de vereiste navigatie voor toegang tot de entiteit waarvoor een globaal queryfilter is gedefinieerd, kan leiden tot onverwachte resultaten.
Vereiste navigatie in EF impliceert dat de gerelateerde entiteit altijd aanwezig is. Omdat inner joins kunnen worden gebruikt om gerelateerde entiteiten op te halen, kan ook de bovenliggende entiteit worden gefilterd als een vereiste gerelateerde entiteit door het queryfilter wordt uitgesloten. Dit kan ertoe leiden dat er onverwacht minder elementen worden opgehaald dan verwacht.
Ter illustratie van het probleem kunnen we entiteiten gebruiken Blog en Post configureren als volgt:
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Het model kan worden gezaaid met de volgende gegevens:
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" }
]
});
Het probleem kan worden waargenomen bij het uitvoeren van de volgende twee query's:
var allPosts = await db.Posts.ToListAsync();
var allPostsWithBlogsIncluded = await db.Posts.Include(p => p.Blog).ToListAsync();
Met de bovenstaande installatie retourneert de eerste query alle 6 Post exemplaren, maar de tweede query retourneert slechts 3. Deze overeenkomst komt niet overeen omdat de Include methode in de tweede query de gerelateerde Blog entiteiten laadt. Omdat de navigatie tussen Blog en Post is vereist, gebruikt INNER JOIN EF Core bij het samenstellen van de query:
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]
Gebruik van de INNER JOIN filtert alle Post rijen eruit waarvan de gerelateerde Blog rijen door een queryfilter zijn verwijderd. Dit probleem kan worden opgelost door de navigatie te configureren als optionele navigatie in plaats van als verplicht, waardoor EF een LEFT JOIN in plaats van een INNER JOIN genereert.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Een alternatieve benadering is om consistente filters voor zowel Blog als Post entiteitstypen op te geven; zodra overeenkomende filters op zowel Blog als Post zijn toegepast, worden Post rijen die in een onverwachte staat terecht kunnen komen, verwijderd en beide query's retourneren drie resultaten.
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"));
Queryfilters en IEntityTypeConfiguration
Als uw queryfilter toegang nodig heeft tot een tenant-id of vergelijkbare contextuele informatie, IEntityTypeConfiguration<TEntity> kan dit een extra complicatie vormen, in tegenstelling tot bij OnModelCreating, is er geen exemplaar van uw contexttype beschikbaar om te verwijzen vanuit het queryfilter. Als tijdelijke oplossing voegt u een dummycontext toe aan uw configuratietype en verwijst u ernaar als volgt:
private sealed class CustomerEntityConfiguration : IEntityTypeConfiguration<Customer>
{
private readonly SomeDbContext _context = null!;
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.HasQueryFilter(d => d.TenantId == _context.TenantId);
}
}
Beperkingen
Globale queryfilters hebben de volgende beperkingen:
- Filters kunnen alleen worden gedefinieerd voor het hoofdentiteitstype van een overnamehiërarchie.
- Ef Core detecteert momenteel geen cycli in globale queryfilterdefinities, dus wees voorzichtig bij het definiëren ervan. Indien onjuist opgegeven, kunnen cycli leiden tot oneindige lussen tijdens het vertalen van query's.