Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Pooling del DbContext
Un DbContext è in genere un oggetto chiaro: la creazione e l'eliminazione di uno non comporta un'operazione di database e la maggior parte delle applicazioni può farlo senza alcun impatto notevole sulle prestazioni. Tuttavia, ogni istanza di contesto configura vari servizi e oggetti interni necessari per l'esecuzione dei compiti e il sovraccarico di questa operazione può essere significativo in scenari a prestazioni elevate. Per questi casi, EF Core può raggruppare le istanze di contesto: quando si elimina il contesto, EF Core reimposta lo stato e lo archivia in un pool interno. Quando viene richiesta una nuova istanza, tale istanza in pool viene restituita anziché impostarne una nuova. Il pool di contesti consente di pagare i costi di configurazione del contesto solo una volta all'avvio del programma, anziché continuamente.
Si noti che il pool di contesti è ortogonale al pool di connessioni al database, che viene gestito a un livello inferiore nel driver di database.
Il modello tipico in un'app ASP.NET Core che usa EF Core comporta la registrazione di un tipo personalizzato DbContext nel contenitore di inserimento delle dipendenze tramite AddDbContext. Quindi, le istanze di quel tipo vengono ottenute tramite i parametri del costruttore nei controller o in Razor Pages.
Per abilitare il pool di contesti, sostituire AddDbContext semplicemente con AddDbContextPool:
builder.Services.AddDbContextPool<WeatherForecastContext>(
o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));
Il poolSize parametro di AddDbContextPool imposta il numero massimo di istanze mantenute dal pool (il valore predefinito è 1024). Una volta superato poolSize, le nuove istanze di contesto non vengono memorizzate nella cache e EF esegue il fallback al comportamento di non pooling della creazione di istanze su richiesta.
Benchmark
Di seguito sono riportati i risultati del benchmark per il recupero di una singola riga da un database di SQL Server in esecuzione localmente nello stesso computer, con e senza pool di contesto. Come sempre, i risultati cambieranno con il numero di righe, la latenza per il server di database e altri fattori. In particolare, questo benchmark consente di ottenere prestazioni del pooling a thread singolo, mentre uno scenario conteso reale può avere risultati diversi; esegui il benchmark sulla tua piattaforma prima di prendere qualsiasi decisione. Il codice sorgente è disponibile qui, è possibile usarlo come base per le misurazioni personalizzate.
| metodo | NumBlogs | Media aritmetica | Errore | StdDev | Generazione 0 | Gen1 | Gen2 | Assegnato |
|---|---|---|---|---|---|---|---|---|
| WithoutContextPooling | 1 | 701,6 us | 26.62 noi | 78.48 | 11.7188 | - | - | 50,38 KB |
| WithContextPooling | 1 | 350,1 microsecondi | 6.80 noi | 14.64 noi | 0.9766 | - | - | 4,63 KB |
Gestione dello stato nei contesti condivisi
Il pooling dei contesti funziona riutilizzando la stessa istanza di contesto tra le diverse richieste; ciò significa che viene effettivamente registrato come Singleton e la stessa istanza viene riutilizzata tra più richieste (o ambiti DI). Ciò significa che è necessario prestare particolare attenzione quando il contesto implica qualsiasi stato che può cambiare tra le richieste. In modo cruciale, il contesto viene richiamato una sola volta, quando il contesto dell'istanza OnConfiguring viene creato per la prima volta e quindi non può essere usato per impostare lo stato che deve variare (ad esempio, un ID tenant).
Uno scenario tipico che coinvolge lo stato del contesto è un'applicazione multi-tenant ASP.NET Core, in cui l'istanza di contesto ha un ID tenant che viene preso in considerazione dalle query (vedere Filtri di query globali per altri dettagli). Poiché l'ID tenant deve cambiare con ogni richiesta Web, è necessario eseguire alcuni passaggi aggiuntivi per far sì che tutto funzioni con il pool di contesto.
Supponiamo che l'applicazione registri un servizio con ambito ITenant, che incapsula l'ID del tenant e altre informazioni correlate al tenant:
// 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;
});
Come scritto in precedenza, prestare particolare attenzione alla posizione da cui si ottiene l'ID tenant. Si tratta di un aspetto importante della sicurezza dell'applicazione.
Dopo aver creato il servizio con ambito definito ITenant, registrare una factory per il pooling del contesto come servizio Singleton, come di consueto.
builder.Services.AddPooledDbContextFactory<WeatherForecastContext>(
o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));
Scrivere quindi una factory di contesto personalizzata che recupera un contesto in pool dalla factory Singleton registrata e inserisce l'ID del tenant nelle istanze di contesto che fornisce:
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;
}
}
Dopo aver creato la factory del contesto personalizzata, registrarla come servizio Scoped:
builder.Services.AddScoped<WeatherForecastScopedFactory>();
Infine, predisporre un contesto per l'iniezione da parte della factory con ambito.
builder.Services.AddScoped(
sp => sp.GetRequiredService<WeatherForecastScopedFactory>().CreateDbContext());
A questo punto, i controller vengono iniettati automaticamente con un'istanza di contesto che ha l'ID tenant corretto, senza dover sapere nulla al riguardo.
Il codice sorgente completo per questo esempio è disponibile qui.
Nota
Anche se EF Core si occupa della reimpostazione dello stato interno per DbContext e dei servizi correlati, in genere non reimposta lo stato nel driver di database sottostante, che si trova all'esterno di ENTITY. Ad esempio, se apri manualmente e usi uno stato DbConnection o manipoli lo stato ADO.NET, è necessario restaurare tale stato prima di restituire l'istanza del contesto al pool, ad esempio, chiudendo la connessione. In caso contrario, potrebbe verificarsi una perdita di stato tra richieste non correlate.
Considerazioni sul pool di connessioni
Con la maggior parte dei database, è necessaria una connessione di lunga durata per l'esecuzione di operazioni di database e tali connessioni possono essere costose da aprire e chiudere. EF non implementa il pool di connessioni autonomamente, ma si affida ai driver di database sottostanti (ad esempio il driver ADO.NET) per la gestione delle connessioni di database. Il pool di connessioni è un meccanismo lato client che riutilizza le connessioni di database esistenti per ridurre l'onere dell'apertura e chiusura ripetuta delle connessioni. Questo meccanismo è in genere coerente tra i database supportati da Entity Framework, ad esempio database SQL di Azure, PostgreSQL e altri. Anche se fattori specifici per il database o l'ambiente, ad esempio i limiti delle risorse o le configurazioni del servizio, possono influire sull'efficienza del pool. Il pooling delle connessioni è generalmente abilitato per impostazione predefinita, e qualsiasi configurazione di pooling deve essere eseguita a livello di driver a basso livello, come documentato da tale driver. Ad esempio, quando si usa ADO.NET, i parametri come le dimensioni minime o massime del pool vengono generalmente configurati tramite la stringa di connessione.
Il pool di connessioni è completamente ortogonale al pooling di DbContext di Entity Framework, descritto in precedenza: mentre il driver di database a basso livello gestisce il pool di connessioni del database (per evitare il sovraccarico dell'apertura/chiusura delle connessioni), EF può raggruppare istanze di contesto (per evitare il sovraccarico di allocazione e inizializzazione della memoria del contesto). Indipendentemente dal fatto che un'istanza di contesto sia in pool o meno, Ef apre in genere le connessioni subito prima di ogni operazione (ad esempio, query) e la chiude subito dopo, causandone la restituzione al pool. questa operazione viene eseguita per evitare di mantenere le connessioni fuori dal pool più a lungo di quanto necessario.
Query precompilate
Quando EF riceve un albero di query LINQ per l'esecuzione, deve prima "compilare" tale albero, ad esempio produrre SQL da esso. Poiché questa attività è un processo pesante, EF memorizza nella cache le query dalla forma dell'albero delle query, in modo che le query con la stessa struttura riutilizzino gli output di compilazione memorizzati internamente nella cache. Questa memorizzazione nella cache garantisce che l'esecuzione della stessa query LINQ più volte sia molto veloce, anche se i valori dei parametri sono diversi.
Tuttavia, EF deve comunque eseguire determinate attività prima di poter usare la cache di query interna. Ad esempio, l'albero delle espressioni della query deve essere confrontato in modo ricorsivo con gli alberi delle espressioni delle query memorizzate nella cache per trovare la query memorizzata nella cache corretta. Il sovraccarico per questa elaborazione iniziale è trascurabile nella maggior parte delle applicazioni EF, soprattutto rispetto ad altri costi associati all'esecuzione di query (I/O di rete, elaborazione effettiva delle query e I/O su disco nel database...). Tuttavia, in alcuni scenari ad alte prestazioni potrebbe essere preferibile eliminarlo.
EF supporta query compilate, che consentono la compilazione esplicita di una query LINQ in un delegato .NET. Una volta acquisito, questo delegato può essere richiamato direttamente per eseguire la query, senza fornire l'albero delle espressioni LINQ. Questa tecnica ignora la ricerca della cache e offre il modo più ottimizzato per eseguire una query in EF Core. Di seguito sono riportati alcuni risultati del benchmark che confrontano le prestazioni delle query compilate e non compilate; benchmark sulla piattaforma prima di prendere decisioni. Il codice sorgente è disponibile qui, è possibile usarlo come base per le misurazioni personalizzate.
| metodo | NumBlogs | Media aritmetica | Errore | StdDev | Generazione 0 | Assegnato |
|---|---|---|---|---|---|---|
| WithCompiledQuery | 1 | 564,2 µs | 6.75 noi | 5,99 dollari USA | 1.9531 | 9 KB |
| WithoutCompiledQuery | 1 | 671,6 us | 12,72 µs | 16.54 noi | 2.9297 | 13 KB |
| WithCompiledQuery | 10 | 645,3 μs | 10.00 USD | 9.35 | 2.9297 | 13 KB |
| WithoutCompiledQuery | 10 | 709.8 µs | 25,20 USD | 73,10 µs | 3,9063 | 18 KB |
Per usare le query compilate, compilare prima di tutto una query con EF.CompileAsyncQuery come indicato di seguito (usare EF.CompileQuery per le query sincrone):
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 questo esempio di codice viene fornito ef con un'espressione lambda che accetta un'istanza DbContext e un parametro arbitrario da passare alla query. È ora possibile richiamare tale delegato ogni volta che si vuole eseguire la query:
await foreach (var blog in _compiledQuery(context, 8))
{
// Do something with the results
}
Si noti che il delegato è thread-safe e può essere chiamato simultaneamente in istanze di contesto diverse.
Limiti
- Le query compilate possono essere usate solo su un singolo modello di EF Core. A volte è possibile configurare istanze di contesto diverse dello stesso tipo per l'uso di modelli diversi; L'esecuzione di query compilate in questo scenario non è supportata.
- Quando si usano parametri nelle query compilate, usare parametri scalari semplici. Le espressioni di parametro più complesse, ad esempio gli accessi a membro/metodo nelle istanze, non sono supportate.
Memorizzazione nella cache delle query e parametrizzazione
Quando EF riceve un albero di query LINQ per l'esecuzione, deve prima "compilare" tale albero, ad esempio produrre SQL da esso. Poiché questa attività è un processo pesante, EF memorizza nella cache le query dalla forma dell'albero delle query, in modo che le query con la stessa struttura riutilizzino gli output di compilazione memorizzati internamente nella cache. Questa memorizzazione nella cache garantisce che l'esecuzione della stessa query LINQ più volte sia molto veloce, anche se i valori dei parametri sono diversi.
Valutare le due query seguenti:
var post1 = await context.Posts.FirstOrDefaultAsync(p => p.Title == "post1");
var post2 = await context.Posts.FirstOrDefaultAsync(p => p.Title == "post2");
Poiché gli alberi delle espressioni contengono costanti diverse, l'albero delle espressioni è diverso e ognuna di queste query verrà compilata separatamente da EF Core. Inoltre, ogni query produce un comando SQL leggermente diverso:
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'
Poiché SQL è diverso, è probabile che anche il server di database debba produrre un piano di query per entrambe le query, anziché riutilizzare lo stesso piano.
Una piccola modifica alle query può cambiare notevolmente le cose:
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);
Poiché il nome del blog è ora parametrizzato, entrambe le query hanno la stessa forma ad albero e EF viene compilato una sola volta. Il codice SQL generato viene parametrizzato, consentendo al database di riutilizzare lo stesso piano di query:
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Posts] AS [b]
WHERE [b].[Name] = @__postTitle_0
Si noti che non è necessario parametrizzare ogni singola query: è perfettamente corretto avere alcune query con costanti e, in effetti, i database (e EF) possono talvolta eseguire determinate ottimizzazioni sulle costanti che non sono possibili quando la query è parametrizzata. Vedere la sezione sulle query costruite dinamicamente per un esempio in cui la corretta parametrizzazione è fondamentale.
Nota
Le metriche di EF Core segnalano la frequenza di riscontri nella cache delle query. In un'applicazione normale, questa metrica raggiunge il 100% poco dopo l'avvio del programma, una volta che la maggior parte delle query è stata eseguita almeno una volta. Se questa metrica rimane stabile al di sotto del 100%, si tratta di un'indicazione che l'applicazione potrebbe eseguire un'operazione che sconfigge la cache delle query. È consigliabile esaminarla.
Suggerimento
EF Core usa IMemoryCache per la memorizzazione nella cache interna di query e modelli compilati. Se necessario, è possibile configurare il limite delle dimensioni della cache. Per altre informazioni, vedere Integrazione della cache di memoria .
Nota
Il modo in cui il database gestisce i piani di query della cache dipende dal database. Ad esempio, SQL Server gestisce in modo implicito una cache del piano di query LRU, mentre PostgreSQL non (ma le istruzioni preparate possono produrre un effetto finale molto simile). Per maggiori dettagli, consultare la documentazione del database.
Query costruite dinamicamente
In alcune situazioni, è necessario costruire in modo dinamico query LINQ anziché specificarle correttamente nel codice sorgente. Ciò può verificarsi, ad esempio, in un sito Web che riceve dettagli di query arbitrari da un client, con operatori di query aperti (ordinamento, filtro, paging... ). In linea di principio, se eseguita correttamente, le query create in modo dinamico possono essere altrettanto efficienti di quelle normali (anche se non è possibile usare l'ottimizzazione delle query compilate con query dinamiche). In pratica, tuttavia, sono spesso la fonte di problemi di prestazioni, poiché è facile produrre accidentalmente alberi delle espressioni con forme che differiscono ogni volta.
Nell'esempio seguente vengono usate tre tecniche per costruire l'espressione lambda di Where una query:
- API di espressione con costante: compilare dinamicamente l'espressione con l'API Espressione, usando un nodo costante. Si tratta di un errore frequente durante la compilazione dinamica degli alberi delle espressioni e fa in modo che EF ricompila la query ogni volta che viene richiamata con un valore costante diverso (causa anche l'inquinamento della cache dei piani nel server di database).
- API di espressione con parametro: versione migliore, che sostituisce la costante con un parametro . In questo modo si garantisce che la query venga compilata solo una volta indipendentemente dal valore specificato e che venga generato lo stesso SQL con parametri.
- Semplice con il parametro: versione che non usa l'API Expression, per il confronto, che crea lo stesso albero del metodo precedente, ma è molto più semplice. In molti casi, è possibile costruire dinamicamente l'albero delle espressioni senza ricorrere all'API Expression, con cui è facile commettere errori.
Si aggiunge un Where operatore alla query solo se il parametro specificato non è Null. Si noti che questo non è un buon caso d'uso per la creazione dinamica di una query, ma viene usato per semplicità:
[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();
}
Il benchmarking di queste due tecniche offre i risultati seguenti:
| metodo | Media aritmetica | Errore | StdDev | Generazione 0 | Prima generazione | Assegnato |
|---|---|---|---|---|---|---|
| ExpressionApiWithConstant | 1.665,8 | 56,99 USD | 163,5 microsecondi | 15,6250 | - | 109,92 KB |
| ExpressioneApiConParametro | 757.1 microsecondi | 35,14 µs | 103,6 microsecondi | 12,6953 | 0.9766 | 54,95 KB |
| SimpleWithParameter | 760,3 µs | 37,99 USD | 112,0 us | 12,6953 | - | 55,03 KB |
Anche se la differenza del sotto millisecondo sembra piccola, tenere presente che la versione costante inquina continuamente la cache e fa sì che altre query vengano ricompilata, rallentandole e avendo un impatto negativo generale sulle prestazioni complessive. È consigliabile evitare la ricompilazione costante delle query.
Nota
Evitare di costruire query con l'API dell'albero delle espressioni, a meno che non sia effettivamente necessario. Oltre alla complessità dell'API, è molto facile causare inavvertitamente problemi di prestazioni significativi quando vengono usati.
Modelli compilati
I modelli compilati possono migliorare il tempo di avvio di EF Core per le applicazioni con modelli di grandi dimensioni. Un modello di grandi dimensioni significa in genere centinaia a migliaia di tipi di entità e relazioni. Il tempo di avvio è il tempo necessario per eseguire la prima operazione su DbContext quando tale tipo DbContext viene usato per la prima volta nell'applicazione. Si noti che solo la creazione di un'istanza DbContext non comporta l'inizializzazione del modello di Entity Framework. Al contrario, le prime operazioni tipiche che causano l'inizializzazione del modello includono la chiamata DbContext.Add o l'esecuzione della prima query.
I modelli compilati vengono creati usando lo strumento da riga di comando dotnet ef. Assicurarsi di aver installato la versione più recente dello strumento prima di continuare.
Viene usato un nuovo comando dbcontext optimize per generare il modello compilato. Ad esempio:
dotnet ef dbcontext optimize
Le opzioni --output-dir e --namespace possono essere usate per specificare la directory e lo spazio dei nomi in cui verrà generato il modello compilato. Ad esempio:
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>
- Per altre informazioni, vedere
dotnet ef dbcontext optimize. - Se si è più comodi all'interno di Visual Studio, è anche possibile usare Optimize-DbContext
L'output dell'esecuzione di questo comando include una parte di codice da copiare e incollare nella configurazione DbContext per fare in modo che EF Core usi il modello compilato. Ad esempio:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseModel(MyCompiledModels.BlogsContextModel.Instance)
.UseSqlite(@"Data Source=test.db");
Avvio del modello compilato
In genere non è necessario esaminare il codice di bootstrap generato. Tuttavia, a volte può essere utile personalizzare il modello o il relativo caricamento. Il codice bootstrap ha un aspetto simile a questo:
[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();
}
Si tratta di una classe parziale con metodi parziali che possono essere implementati per personalizzare il modello in base alle esigenze.
Inoltre, è possibile generare più modelli compilati per i tipi DbContext che possono usare modelli diversi a seconda di una configurazione di runtime. Questi elementi devono essere collocati in cartelle e namespace diversi, come illustrato in precedenza. Le informazioni di runtime, ad esempio il stringa di connessione, possono quindi essere esaminate e il modello corretto restituito in base alle esigenze. Ad esempio:
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.");
});
}
Limiti
I modelli compilati presentano alcune limitazioni:
- I filtri di query globali non sono supportati.
- I proxy di caricamento ritardato e rilevamento delle modifiche non sono supportati.
- I convertitori di valori che fanno riferimento a metodi privati non sono supportati. Rendere invece pubblici o interni i metodi a cui si fa riferimento.
- Il modello deve essere sincronizzato manualmente rigenerandolo ogni volta che la definizione o la configurazione del modello cambiano.
- Le implementazioni di IModelCacheKeyFactory personalizzate non sono supportate. Tuttavia, è possibile compilare più modelli e caricarne uno appropriato in base alle esigenze.
A causa di queste limitazioni, è consigliabile usare modelli compilati solo se il tempo di avvio di EF Core è troppo lento. La compilazione di modelli di piccole dimensioni in genere non ne vale la pena.
Se il supporto di una di queste funzionalità è fondamentale per il vostro successo, si prega di votare per le questioni appropriate menzionate sopra.
Gestione degli errori di compilazione dovuti a riferimenti di tipo ambigui
Quando si compilano modelli con tipi con lo stesso nome ma esistono in spazi dei nomi diversi, il codice generato può generare errori di compilazione a causa di riferimenti di tipo ambigui. Per risolvere questo problema, è possibile personalizzare la generazione del codice in modo da usare nomi di tipo completi eseguendo l'override CSharpHelper.ShouldUseFullName per restituire true. Per informazioni su come eseguire l'override dei servizi in fase di progettazione, ad esempio ICSharpHelper.
Riduzione dell'overhead di runtime
Come per qualsiasi livello, EF Core aggiunge un po' di sovraccarico di runtime rispetto alla codifica diretta rispetto alle API di database di livello inferiore. Questo sovraccarico di runtime non influisce in modo significativo sulla maggior parte delle applicazioni reali; gli altri argomenti di questa guida alle prestazioni, ad esempio efficienza delle query, utilizzo degli indici e riduzione al minimo dei round trip, sono molto più importanti. Inoltre, anche per applicazioni altamente ottimizzate, la latenza di rete e I/O del database in genere dominano qualsiasi tempo dedicato all'interno di EF Core stesso. Tuttavia, per applicazioni a bassa latenza e prestazioni elevate in cui ogni bit di prestazioni è importante, è possibile usare le raccomandazioni seguenti per ridurre il sovraccarico di EF Core al minimo:
- Attivare il pooling DbContext; i benchmark mostrano che questa funzionalità può avere un impatto decisivo sulle applicazioni ad alte prestazioni e bassa latenza.
- Assicurati che
maxPoolSizecorrisponda al tuo scenario di utilizzo; se è troppo basso, le istanze diDbContextverranno create ed eliminate costantemente, riducendo le prestazioni. L'impostazione di un valore troppo elevato potrebbe consumare inutilmente memoria poiché le istanze inutilizzateDbContextvengono mantenute nel pool. - Per un ulteriore miglioramento delle prestazioni, è consigliabile usare
PooledDbContextFactoryinvece di fare in modo che DI inietti direttamente le istanze del contesto. La gestione dell'ID del poolDbContextcomporta un leggero sovraccarico.
- Assicurati che
- Usare query precompilate per le query frequenti.
- Più complessa è la query LINQ, maggiore è il numero di operatori contenuti e più grande è l'albero delle espressioni risultante. È possibile ottenere maggiori vantaggi dall'uso di query compilate.
- Valutare la possibilità di disabilitare i controlli di sicurezza del thread impostando
EnableThreadSafetyCheckssu false nella configurazione del contesto.- L'uso simultaneo della stessa istanza
DbContextda thread diversi non è supportato. EF Core dispone di una funzionalità di sicurezza che rileva questo bug di programmazione in molti casi (ma non tutti) e genera immediatamente un'eccezione informativa. Tuttavia, questa funzionalità di sicurezza comporta un sovraccarico di runtime. - AVVISO: disabilitare i controlli di thread safety solo dopo aver verificato accuratamente che l'applicazione non presenti bug di concorrenza.
- L'uso simultaneo della stessa istanza
Integrazione della cache di memoria
EF Core usa IMemoryCache per operazioni di memorizzazione nella cache interne, ad esempio la compilazione di query e la compilazione di modelli. Per impostazione predefinita, EF Core configura il proprio IMemoryCache con un limite di dimensioni pari a 10240. Per riferimento, una query compilata ha una dimensione della cache pari a 10, mentre il modello compilato ha una dimensione della cache pari a 100.
Se è necessario modificare il limite di dimensioni della cache predefinite, usare UseMemoryCache per fornire un'istanza personalizzata IMemoryCache :
var memoryCache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 20480 });
services.AddSingleton<IDisposable>(memoryCache);
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseMemoryCache(memoryCache);
options.UseSqlServer(connectionString);
});
L'istanza MemoryCache viene registrata in IDisposable modo che venga eliminata quando il provider di servizi viene eliminato, senza sostituire l'oggetto a livello IMemoryCache di app.
In alternativa, se si registra un personalizzato IMemoryCache tramite AddMemoryCache l'inserimento delle dipendenze, è possibile risolverlo dal provider di servizi. Si noti che questa condivisione della cache tra EF Core e altri servizi che utilizzano IMemoryCache, pertanto il limite di dimensioni deve tenere conto di tutti i consumatori. Quando un SizeLimit è impostato, tutte le voci della cache di ogni consumatore devono specificare una dimensione; in caso contrario, IMemoryCache lancerà un'eccezione quando vengono aggiunte voci.
services.AddMemoryCache(options => options.SizeLimit = 20480);
services.AddDbContext<ApplicationDbContext>((serviceProvider, options) =>
{
options.UseMemoryCache(serviceProvider.GetRequiredService<IMemoryCache>());
options.UseSqlServer(connectionString);
});