Onderwerpen over geavanceerde prestaties
DbContext-pooling
Een DbContext
is over het algemeen een licht object: het maken en verwijderen van een object omvat geen databasebewerking en de meeste toepassingen kunnen dit doen zonder merkbare invloed op de prestaties. Elk contextexemplaren stelt echter verschillende interne services en objecten in die nodig zijn voor het uitvoeren van zijn taken, en de overhead om dit continu te doen, kan aanzienlijk zijn in scenario's met hoge prestaties. In deze gevallen kan EF Core pool uw contextexemplaren: wanneer u uw context verwijdert, wordt de status van EF Core opnieuw ingesteld en opgeslagen in een interne pool; wanneer een nieuw exemplaar vervolgens wordt aangevraagd, wordt dat poolexemplaren geretourneerd in plaats van een nieuw exemplaar in te stellen. Met contextpooling kunt u slechts eenmaal kosten betalen voor het instellen van context bij het opstarten van het programma, in plaats van continu.
Houd er rekening mee dat contextpooling orthogonaal is voor pooling van databaseverbindingen, die op een lager niveau in het databasestuurprogramma wordt beheerd.
Het typische patroon in een ASP.NET Core-app met EF Core omvat het registreren van een aangepast DbContext type in de afhankelijkheidsinjectie container via AddDbContext. Vervolgens worden objecten van dat type gemaakt via constructorparameters in controllers of Razor Pages.
Als u contextpooling wilt inschakelen, vervangt u AddDbContext
door AddDbContextPool:
builder.Services.AddDbContextPool<WeatherForecastContext>(
o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));
Met de parameter poolSize
van AddDbContextPool wordt het maximum aantal exemplaren ingesteld dat door de groep wordt bewaard (standaard ingesteld op 1024). Zodra poolSize
is overschreden, worden nieuwe contextexemplaren niet in de cache opgeslagen en valt EF terug op het gedrag van het niet-poolen van het maken van exemplaren op aanvraag.
Referentiepunten
Hieronder volgen de benchmarkresultaten voor het ophalen van één rij uit een SQL Server-database die lokaal op dezelfde computer wordt uitgevoerd, met en zonder contextpooling. Zoals altijd veranderen de resultaten met het aantal rijen, de latentie van uw databaseserver en andere factoren. Het is belangrijk dat deze benchmarks de prestaties van één-thread pooling evalueren, terwijl een praktijkscenario andere resultaten kan opleveren; benchmark op uw platform voordat u beslissingen neemt. De broncode is hier beschikbaar, kunt u deze gebruiken als basis voor uw eigen metingen.
Methode | NumBlogs | Bedoelen | Fout | StdDev | Gen 0 | Gen 1 | Gen 2 | Toegewezen |
---|---|---|---|---|---|---|---|---|
WithoutContextPooling | 1 | 701.6 ons | 26.62 ons | 78.48 ons | 11.7188 | - | - | 50,38 KB |
WithContextPooling | 1 | 350.1 ons | 6.80 ons | 14.64 ons | 0.9766 | - | - | 4,63 KB |
Status beheren in gegroepeerde contexten
Contextpooling werkt door dezelfde contextinstantie opnieuw te gebruiken in aanvragen; dit betekent dat het effectief wordt geregistreerd als een Singletonen dezelfde instantie wordt hergebruikt voor verschillende aanvragen (of DI-scopes). Dit betekent dat er speciale zorg moet worden besteed wanneer de context elke status omvat die tussen aanvragen kan veranderen. Cruciaal is dat de OnConfiguring
van de context slechts eenmaal wordt aangeroepen - wanneer de instantiecontext voor het eerst wordt gemaakt - en dus niet kan worden gebruikt om de status in te stellen die moet variëren (bijvoorbeeld een tenant-id).
Een typisch scenario met contextstatus is een multitenant ASP.NET Core-toepassing, waarbij het contextexemplaren een tenant-id heeft die in aanmerking wordt genomen door query's (zie Global Query Filters voor meer informatie). Omdat de tenant-id moet worden gewijzigd bij elke webaanvraag, moeten we een aantal extra stappen uitvoeren om deze allemaal te laten werken met contextpooling.
Stel dat uw toepassing een scoped ITenant
-service registreert, waarmee de tenant-id en eventuele andere tenantgerelateerde informatie worden verpakt:
// Below is a minimal tenant resolution strategy, which registers a scoped ITenant service in DI.
// In this sample, we simply accept the tenant ID as a request query, which means that a client can impersonate any
// tenant. In a real application, the tenant ID would be set based on secure authentication data.
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenant>(sp =>
{
var tenantIdString = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request.Query["TenantId"];
return tenantIdString != StringValues.Empty && int.TryParse(tenantIdString, out var tenantId)
? new Tenant(tenantId)
: null;
});
Zoals hierboven is geschreven, moet u speciale aandacht besteden aan waar u de tenant-id vandaan haalt. Dit is een belangrijk aspect van de beveiliging van uw toepassing.
Zodra we onze scoped ITenant
-service hebben, registreert u zoals gebruikelijk een poolingcontext factory als singleton-service:
builder.Services.AddPooledDbContextFactory<WeatherForecastContext>(
o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));
Schrijf vervolgens een aangepaste contextfactory die een poolcontext ophaalt uit de Singleton-factory die we hebben geregistreerd en injecteert de tenant-id in contextinstanties die het uitgeeft:
public class WeatherForecastScopedFactory : IDbContextFactory<WeatherForecastContext>
{
private const int DefaultTenantId = -1;
private readonly IDbContextFactory<WeatherForecastContext> _pooledFactory;
private readonly int _tenantId;
public WeatherForecastScopedFactory(
IDbContextFactory<WeatherForecastContext> pooledFactory,
ITenant tenant)
{
_pooledFactory = pooledFactory;
_tenantId = tenant?.TenantId ?? DefaultTenantId;
}
public WeatherForecastContext CreateDbContext()
{
var context = _pooledFactory.CreateDbContext();
context.TenantId = _tenantId;
return context;
}
}
Zodra we onze aangepaste contextfactory hebben, registreert u deze als een Scoped-service:
builder.Services.AddScoped<WeatherForecastScopedFactory>();
Zorg er ten slotte voor dat een context wordt geïnjecteerd vanuit onze Scoped Factory:
builder.Services.AddScoped(
sp => sp.GetRequiredService<WeatherForecastScopedFactory>().CreateDbContext());
Op dit punt worden uw controllers automatisch geïnjecteerd met een context-exemplaar met de juiste tenant-ID, zonder dat u daar iets van hoeft te weten.
De volledige broncode voor dit voorbeeld is hier beschikbaar .
Notitie
Hoewel EF Core zorgt voor het opnieuw instellen van de interne status voor DbContext
en de bijbehorende services, wordt de status over het algemeen niet opnieuw ingesteld in het onderliggende databasestuurprogramma, dat zich buiten EF bevindt. Als u bijvoorbeeld handmatig een DbConnection
opent en gebruikt of op een andere manier de status ADO.NET bewerkt, is het aan u om die status te herstellen voordat u het contextexemplaren naar de pool retourneert, bijvoorbeeld door de verbinding te sluiten. Als u dit niet doet, kan de status uitlekken over niet-gerelateerde verzoeken.
Overwegingen voor groepsgewijze verbindingen
Bij de meeste databases is een langdurige verbinding vereist voor het uitvoeren van databasebewerkingen. Dergelijke verbindingen kunnen duur zijn om te openen en te sluiten. EF implementeert geen groepsgewijze verbindingen zelf, maar is afhankelijk van het onderliggende databasestuurprogramma (bijvoorbeeld ADO.NET stuurprogramma) voor het beheren van databaseverbindingen. Verbindingspooling is een mechanisme aan de clientzijde dat bestaande databaseverbindingen hergebruikt om de overhead van het openen en sluiten van verbindingen herhaaldelijk te verminderen. Dit mechanisme is over het algemeen consistent voor databases die worden ondersteund door EF, zoals Azure SQL Database, PostgreSQL en andere., hoewel factoren die specifiek zijn voor de database of omgeving, zoals resourcelimieten of serviceconfiguraties, van invloed kunnen zijn op poolefficiëntie. Groepsgewijze verbindingen worden meestal standaard ingeschakeld en elke poolconfiguratie moet worden uitgevoerd op het niveau van het stuurprogramma op laag niveau, zoals beschreven door dat stuurprogramma; Wanneer u bijvoorbeeld ADO.NET gebruikt, worden parameters zoals minimale of maximale poolgrootten meestal geconfigureerd via de verbindingsreeks.
Verbindingspooling is volledig onafhankelijk van de DbContext
pooling van EF, zoals hierboven wordt beschreven: terwijl laag-niveau databasestuurprogramma's databaseverbindingen poolen (om de overhead van het openen/sluiten van verbindingen te voorkomen), kan EF contextexemplaren poolen (om contextgeheugentoewijzing en overhead bij initialisatie te voorkomen). Ongeacht of een contextexemplaren zijn gegroepeerd of niet, opent EF doorgaans verbindingen vlak voor elke bewerking (bijvoorbeeld query) en sluit deze direct daarna, waardoor deze wordt geretourneerd naar de pool; Dit wordt gedaan om te voorkomen dat verbindingen langer buiten de pool blijven dan nodig is.
Gecompileerde query's
Wanneer EF een LINQ-querystructuur voor uitvoering ontvangt, moet deze eerst die boomstructuur compileren, bijvoorbeeld SQL hiervan produceren. Omdat deze taak een intensief proces is, slaat EF query's op volgens de vorm van de queryboom, zodat query's met dezelfde structuur de interne compilatie-uitvoer opnieuw gebruiken. Deze caching zorgt ervoor dat het meerdere keren uitvoeren van dezelfde LINQ-query zeer snel is, zelfs als parameterwaarden verschillen.
EF moet echter nog steeds bepaalde taken uitvoeren voordat deze de interne querycache kan gebruiken. De expressiestructuur van uw query moet bijvoorbeeld recursief worden vergeleken met de expressiestructuren van query's in de cache om de juiste query in de cache te vinden. De overhead voor deze initiële verwerking is in de meeste EF-toepassingen te verwaarlozen, vooral in vergelijking met andere kosten die zijn gekoppeld aan het uitvoeren van query's (netwerk-I/O, werkelijke queryverwerking en schijf-I/O in de database...). In bepaalde scenario's met hoge prestaties kan het echter wenselijk zijn om deze te elimineren.
EF ondersteunt gecompileerde query's, waardoor de expliciete compilatie van een LINQ-query in een .NET-gemachtigde mogelijk is. Zodra deze gemachtigde is verkregen, kan deze rechtstreeks worden aangeroepen om de query uit te voeren, zonder de LINQ-expressiestructuur op te geven. Deze techniek omzeilt de cachezoekfunctie en biedt de meest geoptimaliseerde manier om een query uit te voeren in EF Core. Hieronder volgen enkele benchmarkresultaten die de prestaties van gecompileerde en niet-gecompileerde query's vergelijken; benchmark op uw platform voordat u beslissingen neemt. De broncode is hier beschikbaar, kunt u deze gebruiken als basis voor uw eigen metingen.
Methode | NumBlogs | Bedoelen | Fout | StdDev | Gen 0 | Toegewezen |
---|---|---|---|---|---|---|
MetCompiledQuery | 1 | 564.2 ons | 6.75 ons | 5.99 ons | 1.9531 | 9 KB |
ZonderCompiledQuery | 1 | 671.6 ons | 12.72 us | 16.54 ons | 2.9297 | 13 KB |
Met SamengesteldeQuery | 10 | 645.3 ons | 10.00 ons | 9.35 ons | 2.9297 | 13 KB |
ZonderCompiledQuery | 10 | 709.8 ons | 25.20 ons | 73.10 ons | 3,9063 | 18 KB |
Als u gecompileerde query's wilt gebruiken, compileert u eerst als volgt een query met EF.CompileAsyncQuery (gebruik EF.CompileQuery voor synchrone query's):
private static readonly Func<BloggingContext, int, IAsyncEnumerable<Blog>> _compiledQuery
= EF.CompileAsyncQuery(
(BloggingContext context, int length) => context.Blogs.Where(b => b.Url.StartsWith("http://") && b.Url.Length == length));
In dit codevoorbeeld bieden we EF een lambda die een DbContext
exemplaar accepteert en een willekeurige parameter die moet worden doorgegeven aan de query. U kunt deze gemachtigde nu aanroepen wanneer u de query wilt uitvoeren:
await foreach (var blog in _compiledQuery(context, 8))
{
// Do something with the results
}
Houd er rekening mee dat de delegeaat thread-veilig is en tegelijkertijd kan worden aangeroepen op verschillende contextinstanties.
Beperkingen
- Gecompileerde query's kunnen alleen worden gebruikt voor één EF Core-model. Verschillende contextexemplaren van hetzelfde type kunnen soms worden geconfigureerd voor het gebruik van verschillende modellen; het uitvoeren van gecompileerde query's in dit scenario wordt niet ondersteund.
- Wanneer u parameters in gecompileerde query's gebruikt, gebruikt u eenvoudige, scalaire parameters. Complexere parameterexpressies, zoals lid- en methodeaanroepen op instanties, worden niet ondersteund.
Query's opslaan in cache en parameterisatie
Wanneer EF een LINQ-querystructuur voor uitvoering ontvangt, moet deze eerst die boomstructuur compileren, bijvoorbeeld SQL hiervan produceren. Omdat deze taak een intensief proces is, slaat EF query's op door de vorm van de queryboom, zodat query's met dezelfde structuur de eerder in de cache opgeslagen compilatie-uitvoer opnieuw gebruiken. Deze caching zorgt ervoor dat het meerdere keren uitvoeren van dezelfde LINQ-query zeer snel is, zelfs als parameterwaarden verschillen.
Houd rekening met de volgende twee query's:
var post1 = await context.Posts.FirstOrDefaultAsync(p => p.Title == "post1");
var post2 = await context.Posts.FirstOrDefaultAsync(p => p.Title == "post2");
Omdat de expressiestructuren verschillende constanten bevatten, verschilt de expressiestructuur en wordt elk van deze query's afzonderlijk gecompileerd door EF Core. Bovendien produceert elke query een iets andere SQL-opdracht:
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = N'post1'
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = N'post2'
Omdat de SQL verschilt, moet uw databaseserver waarschijnlijk ook een queryplan maken voor beide query's, in plaats van hetzelfde plan opnieuw te gebruiken.
Een kleine wijziging van uw query's kan de volgende zaken aanzienlijk wijzigen:
var postTitle = "post1";
var post1 = await context.Posts.FirstOrDefaultAsync(p => p.Title == postTitle);
postTitle = "post2";
var post2 = await context.Posts.FirstOrDefaultAsync(p => p.Title == postTitle);
Omdat de blognaam nu is geparameteriseerd, hebben beide query's dezelfde structuur en hoeft EF slechts eenmaal te worden gecompileerd. De geproduceerde SQL wordt ook geparameteriseerd, zodat de database hetzelfde queryplan opnieuw kan gebruiken:
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = @__postTitle_0
Houd er rekening mee dat u elke en elke query niet hoeft te parameteriseren: het is perfect om een aantal query's met constanten te hebben, en databases (en EF) kunnen soms bepaalde optimalisaties uitvoeren rond constanten die niet mogelijk zijn wanneer de query wordt geparameteriseerd. Zie de sectie over dynamisch samengestelde query's voor een voorbeeld waarin de juiste parameterisatie cruciaal is.
Notitie
EF Core's -gegevens rapporteren het trefferpercentage van de querycache. In een normale toepassing bereikt deze metrische waarde 100% kort na het opstarten van het programma, zodra de meeste query's ten minste één keer zijn uitgevoerd. Als deze metrische waarde stabiel blijft onder de 100%, is dat een indicatie dat uw toepassing iets doet wat de querycache verslaat. Het is een goed idee om dat te onderzoeken.
Notitie
Hoe de database queryplannen voor caches beheert, is databaseafhankelijk. SQL Server onderhoudt bijvoorbeeld impliciet een cache van een LRU-queryplan, terwijl PostgreSQL dat niet doet (maar voorbereide instructies kunnen een zeer vergelijkbaar eindeffect opleveren). Raadpleeg uw databasedocumentatie voor meer informatie.
Dynamisch samengestelde queries
In sommige situaties is het nodig om LINQ-query's dynamisch samen te stellen in plaats van ze in broncode op te geven. Dit kan bijvoorbeeld gebeuren in een website die willekeurige querygegevens van een client ontvangt, met open-ended queryoperators (sorteren, filteren, pagineren...). Indien correct uitgevoerd, kunnen dynamisch samengestelde query's in principe net zo efficiënt zijn als gewone query's (hoewel het niet mogelijk is om de gecompileerde queryoptimalisatie te gebruiken met dynamische query's). In de praktijk zijn ze echter vaak de bron van prestatieproblemen, omdat het gemakkelijk is om per ongeluk expressiestructuren te produceren met shapes die elke keer verschillen.
In het volgende voorbeeld worden drie technieken gebruikt om de Where
lambda-expressie van een query te maken:
- Expressie-API met constante: bouw de expressie dynamisch samen met de Expressie-API met behulp van een constant knooppunt. Dit is een veelvoorkomende fout bij het dynamisch bouwen van expressiestructuren en zorgt ervoor dat EF de query telkens opnieuw compileert wanneer deze wordt aangeroepen met een andere constante waarde (dit veroorzaakt meestal ook plancachevervuiling op de databaseserver).
- Expressie-API met parameter: een betere versie, die de constante vervangt door een parameter. Dit zorgt ervoor dat de query slechts eenmaal wordt gecompileerd, ongeacht de opgegeven waarde en dat dezelfde (geparameteriseerde) SQL wordt gegenereerd.
- Simple met parameter: een versie die niet gebruikmaakt van de Expressie-API, ter vergelijking, waarmee dezelfde structuur wordt gemaakt als de bovenstaande methode, maar veel eenvoudiger is. In veel gevallen is het mogelijk om uw expressiestructuur dynamisch te bouwen zonder gebruik te maken van de Expressie-API. Dit is eenvoudig om fout te gaan.
We voegen alleen een Where
-operator toe aan de query als de opgegeven parameter niet null is. Houd er rekening mee dat dit geen goed gebruiksvoorbeeld is voor het dynamisch samenstellen van een query, maar we gebruiken deze om het eenvoudig te houden:
[Benchmark]
public async Task<int> ExpressionApiWithConstant()
{
var url = "blog" + Interlocked.Increment(ref _blogNumber);
using var context = new BloggingContext();
IQueryable<Blog> query = context.Blogs;
if (_addWhereClause)
{
var blogParam = Expression.Parameter(typeof(Blog), "b");
var whereLambda = Expression.Lambda<Func<Blog, bool>>(
Expression.Equal(
Expression.MakeMemberAccess(
blogParam,
typeof(Blog).GetMember(nameof(Blog.Url)).Single()),
Expression.Constant(url)),
blogParam);
query = query.Where(whereLambda);
}
return await query.CountAsync();
}
Het benchmarken van deze twee technieken geeft de volgende resultaten:
Methode | Bedoelen | Fout | StdDev | Gen0 | Gen1 | Toegewezen |
---|---|---|---|---|---|---|
ExpressionApiWithConstant | 1,665,8 us | 56.99 ons | 163.5 ons | 15.6250 | - | 109,92 KB |
ExpressionApiWithParameter | 757.1 ons | 35.14 ons | 103.6 ons | 12.6953 | 0.9766 | 54,95 KB |
EenvoudigMetParameter | 760.3 ons | 37.99 ons | 112.0 ons | 12,6953 | - | 55,03 KB |
Zelfs als het verschil in sub milliseconden klein lijkt, moet u er rekening mee houden dat de constante versie de cache continu vervuilt en ervoor zorgt dat andere query's opnieuw worden gecompileerd, ze ook vertragen en een algemene negatieve invloed hebben op uw algehele prestaties. Het wordt ten zeerste aanbevolen om constante hercompilatie van query's te voorkomen.
Notitie
Vermijd het maken van query's met de API voor de expressiestructuur, tenzij u dat echt nodig hebt. Afgezien van de complexiteit van de API, is het heel eenvoudig om per ongeluk aanzienlijke prestatieproblemen te veroorzaken bij het gebruik ervan.
Gecompileerde modellen
Gecompileerde modellen kunnen de opstarttijd van EF Core voor toepassingen met grote modellen verbeteren. Een groot model betekent doorgaans honderden tot duizenden entiteitstypen en -relaties. Opstarttijd hier is het moment om de eerste bewerking uit te voeren op een DbContext
wanneer dat DbContext
type voor het eerst in de toepassing wordt gebruikt. Houd er rekening mee dat alleen het maken van een DbContext
-exemplaar ervoor zorgt dat het EF-model niet wordt geïnitialiseerd. In plaats daarvan zijn typische eerste bewerkingen die ervoor zorgen dat het model wordt geïnitialiseerd, het aanroepen van DbContext.Add
of het uitvoeren van de eerste query.
Gecompileerde modellen worden gemaakt met behulp van het opdrachtregelprogramma dotnet ef
. Zorg ervoor dat u de nieuwste versie van het hulpprogramma hebt geïnstalleerd voordat u doorgaat.
Er wordt een nieuwe dbcontext optimize
-opdracht gebruikt om het gecompileerde model te genereren. Bijvoorbeeld:
dotnet ef dbcontext optimize
De opties --output-dir
en --namespace
kunnen worden gebruikt om de map en naamruimte op te geven waarin het gecompileerde model wordt gegenereerd. Bijvoorbeeld:
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>
- Zie
dotnet ef dbcontext optimize
voor meer informatie. - Als u meer vertrouwd bent met werken in Visual Studio, kunt u ook Optimize-DbContext gebruiken.
De uitvoer van het uitvoeren van deze opdracht bevat een stukje code om het gecompileerde model te kopiëren en in uw DbContext
-configuratie te plakken, zodat EF Core het gecompileerde model gebruikt. Bijvoorbeeld:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseModel(MyCompiledModels.BlogsContextModel.Instance)
.UseSqlite(@"Data Source=test.db");
Gecompileerd model opstarten
Het is doorgaans niet nodig om de gegenereerde bootstrapping-code te bekijken. Soms kan het echter handig zijn om het model of het laden ervan aan te passen. De bootstrapping-code ziet er ongeveer als volgt uit:
[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
private static BlogsContextModel _instance;
public static IModel Instance
{
get
{
if (_instance == null)
{
_instance = new BlogsContextModel();
_instance.Initialize();
_instance.Customize();
}
return _instance;
}
}
partial void Initialize();
partial void Customize();
}
Dit is een gedeeltelijke klasse met gedeeltelijke methoden die kunnen worden geïmplementeerd om het model naar behoefte aan te passen.
Daarnaast kunnen er meerdere gecompileerde modellen worden gegenereerd voor DbContext
typen die verschillende modellen kunnen gebruiken, afhankelijk van een bepaalde runtimeconfiguratie. Deze moeten in verschillende mappen en naamruimten worden geplaatst, zoals hierboven wordt weergegeven. Runtime-informatie, zoals de verbindingsreeks, kan vervolgens worden onderzocht, en indien nodig kan het juiste model worden geretourneerd. Bijvoorbeeld:
public static class RuntimeModelCache
{
private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
= new();
public static IModel GetOrCreateModel(string connectionString)
=> _runtimeModels.GetOrAdd(
connectionString, cs =>
{
if (cs.Contains("X"))
{
return BlogsContextModel1.Instance;
}
if (cs.Contains("Y"))
{
return BlogsContextModel2.Instance;
}
throw new InvalidOperationException("No appropriate compiled model found.");
});
}
Beperkingen
Gecompileerde modellen hebben enkele beperkingen:
- Globale queryfilters worden niet ondersteund.
- luie laad- en wijzigingsproxy's worden niet ondersteund.
- Het model moet handmatig worden gesynchroniseerd door het opnieuw te genereren wanneer de modeldefinitie of configuratie verandert.
- Aangepaste IModelCacheKeyFactory-implementaties worden niet ondersteund. U kunt echter meerdere modellen compileren en de juiste zo nodig laden.
Vanwege deze beperkingen moet u alleen gecompileerde modellen gebruiken als uw EF Core-opstarttijd te traag is. Het compileren van kleine modellen is meestal niet de moeite waard.
Als het ondersteunen van een van deze functies cruciaal is voor uw succes, stem dan op de juiste kwesties die hierboven zijn genoemd.
Runtime-overhead verminderen
Net als bij elke laag voegt EF Core een beetje runtime-overhead toe vergeleken met het rechtstreeks coderen van database-API's op lager niveau. Deze runtime-overhead is waarschijnlijk niet van invloed op de meeste echte toepassingen op een aanzienlijke manier; de andere onderwerpen in deze prestatiehandleiding, zoals de efficiëntie van query's, het indexgebruik en het minimaliseren van roundtrips, zijn veel belangrijker. Bovendien zullen zelfs voor zeer geoptimaliseerde toepassingen netwerklatentie en database-I/O meestal de tijd die in EF Core zelf wordt doorgebracht, domineren. Voor toepassingen met een hoge prestaties met lage latentie waarbij elk stukje prestatie belangrijk is, kunnen de volgende aanbevelingen worden gebruikt om de OVERHEAD van EF Core tot een minimum te beperken:
- Schakel DbContext-pooling in; onze benchmarks laten zien dat deze functie een beslissende invloed kan hebben op toepassingen met hoge prestaties en lage latentie.
- Zorg ervoor dat de
maxPoolSize
overeenkomt met uw gebruiksscenario; als deze te laag is, wordenDbContext
exemplaren voortdurend gemaakt en verwijderd, waardoor de prestaties afnemen. Het instellen van een te hoge waarde kan onnodig geheugen verbruiken omdat ongebruikteDbContext
exemplaren in de pool worden behouden. - Voor een extra kleine prestatieverbetering kunt u overwegen
PooledDbContextFactory
te gebruiken in plaats van DI contextinstanties rechtstreeks te gebruiken. Het beheer van DI bijDbContext
pooling brengt een kleine overhead met zich mee.
- Zorg ervoor dat de
- Gebruik vooraf gecompileerde query's voor dynamische query's.
- Hoe complexer de LINQ-query - hoe meer operators deze bevat en hoe groter de resulterende expressiestructuur - hoe meer winst kan worden verwacht van het gebruik van gecompileerde query's.
- Overweeg om threadveiligheidscontroles uit te schakelen door
EnableThreadSafetyChecks
in te stellen op false in uw contextconfiguratie.- Het gelijktijdig gebruiken van hetzelfde
DbContext
exemplaar van verschillende threads wordt niet ondersteund. EF Core heeft een veiligheidsfunctie die deze programmeerfout in veel gevallen (maar niet alle) detecteert en onmiddellijk een informatieve uitzondering genereert. Deze veiligheidsfunctie voegt echter enige runtime-overhead toe. - WAARSCHUWING: Schakel veiligheidscontroles voor threads alleen uit nadat u hebt getest dat uw toepassing geen dergelijke gelijktijdigheidsfouten bevat.
- Het gelijktijdig gebruiken van hetzelfde