Leistungsthemen für Fortgeschrittene

DbContext-Pooling

DbContext ist im Allgemeinen ein leichtes Objekt: Das Erstellen und Löschen eines Objekts umfasst keinen Datenbankvorgang, und die meisten Anwendungen können dies ohne spürbare Auswirkungen auf die Leistung tun. Jede Kontextinstanz richtet jedoch verschiedene interne Dienste und Objekte ein, die für die Erfüllung ihrer Aufgaben erforderlich sind, und der Aufwand dieser kontinuierlichen Ausführung kann in Hochleistungsszenarien maßgeblich sein. In diesen Fällen kann EF Core Ihre Kontextinstanzen poolen: Wenn Sie Ihren Kontext löschen, setzt EF Core den Status zurück und speichert ihn in einem internen Pool. Wenn als nächstes eine neue Instanz angefordert wird, wird diese poolierte Instanz zurückgegeben, anstatt eine neue Instanz einzurichten. Kontextpooling ermöglicht es Ihnen, kontextbezogene Einrichtungsaufwände nur einmal beim Programmstart zu leisten, statt kontinuierlich.

Beachten Sie, dass Kontextpooling orthogonal zum Datenbankverbindungspooling ist, was auf einer niedrigeren Ebene im Datenbanktreiber verwaltet wird.

Das typische Muster in einer ASP.NET Core-App mit EF Core umfasst das Registrieren eines benutzerdefinierten Typs DbContext in den Abhängigkeitseinfügungscontainer über AddDbContext. Anschließend werden Instanzen dieses Typs über Konstruktorparameter in Controllern oder Razor Pages abgerufen.

Um Kontextpooling zu aktivieren, ersetzen Sie AddDbContext einfach durch AddDbContextPool:

builder.Services.AddDbContextPool<WeatherForecastContext>(
    o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));

Der poolSize-Parameter der AddDbContextPool-Einstellung legt die maximale Anzahl von Instanzen fest, die vom Pool aufbewahrt werden (Standardwert ist 1024). Sobald poolSize überschritten wurde, werden neue Kontextinstanzen nicht mehr zwischengespeichert, und EF greift auf das Nicht-Pooling-Verhaltens zurück, bei dem Instanzen bei Bedarf erstellt werden.

Vergleichstests

Es folgen die Benchmarkergebnisse zum Abrufen einer einzelnen Zeile aus einer SQL Server-Datenbank, die lokal auf demselben Computer mit und ohne Kontextpooling ausgeführt werden. Wie immer ändern sich die Ergebnisse mit der Anzahl der Zeilen, der Latenz auf Dem Datenbankserver und anderen Faktoren. Wichtig ist, dass diese Benchmarks die Leistung des Einzelthreadpoolings darstellen, während ein reales Szenario unterschiedliche Ergebnisse aufweisen kann; führen Sie Benchmarking auf Ihrer Plattform durch, bevor Sie Entscheidungen treffen. Der Quellcode ist hier verfügbar, und Sie können ihn gerne als Grundlage für Ihre eigenen Messungen verwenden.

Methode NumBlogs Mittelwert Fehler StdDev Gen 0 Gen1 Gen2 Zugeordnet
OhneKontextpooling 1 701,6 us 26,62 us 78,48 us 11,7188 - - 50,38 KB
MitKontextpooling 1 350,1 us 6,80 us 14,64 us 0,9766 - - 4,63 KB

Verwaltung des Zustands in poolierten Kontexten

Kontextpooling funktioniert durch erneutes Verwenden derselben Kontextinstanz für verschiedene Anforderungen. Das bedeutet, dass sie als Singleton registriert ist und dieselbe Instanz für mehrere Anforderungen (oder DI-Bereiche) wiederverwendet wird. Das bedeutet, dass besondere Sorgfalt erforderlich ist, wenn der Kontext einen Zustand umfasst, der sich von Anforderung zu Anforderung ändern kann. Entscheidend ist, dass der Kontext OnConfiguring nur einmal aufgerufen wird - wenn der Instanzkontext zum ersten Mal erstellt wird - und daher nicht zum Festlegen eines Zustands verwendet werden kann, der variieren muss (z. B. eine Mandanten-ID).

Ein typisches Szenario mit Kontextstatus wäre eine mehrinstanzenfähige ASP.NET Core-Anwendung, bei der die Kontextinstanz über eine Mandanten-ID verfügt, die von Abfragen berücksichtigt wird (weitere Details finden Sie unter globalen Abfragefilter). Da sich die Mandanten-ID bei jeder Webanforderung ändern muss, müssen wir einige zusätzliche Schritte durchlaufen, damit sie alle mit Kontextpooling funktionieren.

Angenommen, Ihre Anwendung registriert einen bereichsbezogenen ITenant-Dienst, der die Mandanten-ID und alle anderen mandantenbezogenen Informationen umschließt:

// 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;
});

Wie oben beschrieben, sollten Sie besonders darauf achten, wo Sie die Mandanten-ID erhalten – dies ist ein wichtiger Aspekt der Sicherheit Ihrer Anwendung.

Sobald wir unseren bereichsbezogenen Dienst ITenant haben, registrieren Sie eine Poolkontextfactory wie gewohnt als Singleton-Dienst:

builder.Services.AddPooledDbContextFactory<WeatherForecastContext>(
    o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));

Schreiben Sie als nächstes eine benutzerdefinierte Kontextfactory, die einen poolierten Kontext aus der von uns registrierten Singleton-Factory abruft, und die Mandanten-ID in Kontextinstanzen, die sie aushändigt, einfügt:

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;
    }
}

Sobald wir über unsere benutzerdefinierte Kontextfactory verfügen, registrieren Sie sie als Bereichsdienst:

builder.Services.AddScoped<WeatherForecastScopedFactory>();

Ordnen Sie schließlich einen Kontext an, der von unserer Bereichsfactory gefüllt wird:

builder.Services.AddScoped(
    sp => sp.GetRequiredService<WeatherForecastScopedFactory>().CreateDbContext());

An diesem Punkt werden Ihre Controller automatisch mit einer Kontextinstanz gefüllt, die über die richtige Mandanten-ID verfügt, ohne etwas über sie wissen zu müssen.

Der vollständige Quellcode für dieses Beispiel ist hierverfügbar.

Hinweis

Obwohl EF Core sich um das Zurücksetzen des internen Zustands für DbContext und der zugehörigen Dienste kümmert, wird der Zustand in der Regel nicht im zugrunde liegenden Datenbanktreiber, der sich außerhalb von EF befindet, zurückgesetzt. Wenn Sie beispielsweise DbConnection öffnen und verwenden oder anderweitig den ADO.NET-Zustand bearbeiten, müssen Sie diesen Zustand wiederherstellen, z. B. durch Schließen der Verbindung, bevor Sie die Kontextinstanz an den Pool zurückgeben. Das Unterlassen kann dazu führen, dass der Zustand über nicht zusammenhängende Anforderungen hinweg verloren geht.

Kompilierte Abfragen

Wenn EF eine LINQ-Abfragestruktur für die Ausführung empfängt, muss es zuerst diese Struktur "kompilieren", z. B. daraus SQL-Code erstellen. Da diese Aufgabe ein schwerer Prozess ist, speichert EF Abfragen mit Abfragestrukturform zwischen, sodass Abfragen mit derselben Struktur intern zwischengespeicherte Kompilierungsausgaben wiederverwenden. Diese Zwischenspeicherung stellt sicher, dass die mehrmalige Ausführung derselben LINQ-Abfrage sehr schnell ist, auch wenn die Parameterwerte unterschiedlich sind.

EF muss jedoch weiterhin bestimmte Aufgaben ausführen, bevor der interne Abfragecache verwendet werden kann. Beispielsweise muss die Ausdrucksstruktur Ihrer Abfrage rekursiv mit den Ausdrucksstrukturen zwischengespeicherter Abfragen verglichen werden, um die richtige zwischengespeicherte Abfrage zu finden. Der Aufwand für diese anfängliche Verarbeitung ist in den meisten EF-Anwendungen gering, insbesondere im Vergleich zu anderen Aufwände für die Abfrageausführung (Netzwerk-E/A, tatsächliche Abfrageverarbeitung und Datenträger-E/A in der Datenbank...). In bestimmten Hochleistungsszenarien kann es jedoch wünschenswert sein, sie zu beseitigen.

EF unterstützt kompilierte Abfragen, welche die explizite Kompilierung einer LINQ-Abfrage in einen .NET-Delegaten ermöglichen. Sobald diese Stellvertretung erworben wurde, kann sie direkt aufgerufen werden, um die Abfrage auszuführen, ohne die LINQ-Ausdrucksstruktur bereitzustellen. Auf diesem Weg wird die Cachesuche umgangen und die beste Methode zum Ausführen einer Abfrage in EF Core geboten. Es folgen einige Benchmarkergebnisse, die kompilierte und nicht kompilierte Abfrageleistung vergleichen; Sie sollten Benchmarking auf Ihrer Plattform durchführen, bevor Sie Entscheidungen treffen. Der Quellcode ist hier verfügbar, und Sie können ihn gerne als Grundlage für Ihre eigenen Messungen verwenden.

Methode NumBlogs Mittelwert Fehler StdDev Gen 0 Zugeordnet
MitKompilierterAbfrage 1 564,2 us 6,75 us 5,99 us 1,9531 9 KB
OhneKompilierteAbfrage 1 671,6 uns 12,72 uns 16,54 uns 2,9297 13 KB
MitKompilierterAbfrage 10 645,3 us 10,00 us 9,35 us 2,9297 13 KB
OhneKompilierteAbfrage 10 709,8 us 25,20 us 73,10 us 3,9063 18 KB

Kompilieren Sie zunächst eine Abfrage mit EF.CompileAsyncQuery wie folgt, um kompilierte Abfragen zu verwenden (für synchrone Abfragen EF.CompileQuery verwenden):

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 diesem Codebeispiel stellen wir EF eine Lambda-Funktion bereit, die eine DbContext-Instanz und einen beliebigen Parameter, der an die Abfrage übergeben werden soll, akzeptiert. Sie können diese Stellvertretung jetzt aufrufen, wenn Sie die Abfrage ausführen möchten:

await foreach (var blog in _compiledQuery(context, 8))
{
    // Do something with the results
}

Beachten Sie, dass die Stellvertretung threadsicher ist und gleichzeitig für verschiedene Kontextinstanzen aufgerufen werden kann.

Begrenzungen

  • Kompilierte Abfragen können nur für ein einzelnes EF Core-Modell verwendet werden. Verschiedene Kontextinstanzen desselben Typs können manchmal für die Verwendung verschiedener Modelle konfiguriert werden; das Ausführen kompilierter Abfragen für dieses Szenario wird nicht unterstützt.
  • Nehmen Sie bei Verwendung von Parametern in kompilierten Abfragen einfache, skalare Parameter. Komplexere Parameterausdrücke, z. B. Member-/Methodenzugriffe auf Instanzen, werden nicht unterstützt.

Zwischenspeicherung und Parametrisierung von Abfragen

Wenn EF eine LINQ-Abfragestruktur für die Ausführung empfängt, muss es zuerst diese Struktur "kompilieren", z. B. daraus SQL-Code erstellen. Da diese Aufgabe ein schwerer Prozess ist, speichert EF Abfragen mit Abfragestrukturform zwischen, sodass Abfragen mit derselben Struktur intern zwischengespeicherte Kompilierungsausgaben wiederverwenden. Diese Zwischenspeicherung stellt sicher, dass die mehrmalige Ausführung derselben LINQ-Abfrage sehr schnell ist, auch wenn die Parameterwerte unterschiedlich sind.

Betrachten Sie diese beiden Entwürfe:

var post1 = context.Posts.FirstOrDefault(p => p.Title == "post1");
var post2 = context.Posts.FirstOrDefault(p => p.Title == "post2");

Da die Ausdrucksstrukturen unterschiedliche Konstanten enthalten, unterscheidet sich jede dieser Abfragen wird separat von EF Core kompiliert. Darüber hinaus erzeugt jede Abfrage einen etwas anderen SQL-Befehl:

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] = N'blog1'

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] = N'blog2'

Da sich der SQL-Code unterscheidet, muss Ihr Datenbankserver wahrscheinlich auch einen Abfrageplan für beide Abfragen erstellen, statt denselben Plan wiederzuverwenden.

Eine kleine Änderung ihrer Abfragen kann dies erheblich ändern:

var postTitle = "post1";
var post1 = context.Posts.FirstOrDefault(p => p.Title == postTitle);
postTitle = "post2";
var post2 = context.Posts.FirstOrDefault(p => p.Title == postTitle);

Da der Blogname jetzt parametrisiert ist, haben beide Abfragen die gleiche Strukturform, und EF muss nur einmal kompiliert werden. Der erstellte SQL-Code wird ebenfalls parametriert, sodass die Datenbank denselben Abfrageplan wiederverwenden kann:

SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] = @__blogName_0

Beachten Sie, dass es nicht erforderlich ist, jede Abfrage zu parametrisieren: Es ist vollkommen in Ordnung, einige Abfragen mit Konstanten zu haben, und in der Tat können Datenbanken (und EF) manchmal bestimmte Optimierungen mit Konstanten durchführen, die beim Parametrieren der Abfrage nicht möglich sind. Im Abschnitt zu dynamisch konstruierten Abfragen finden Sie ein Beispiel, in dem die richtige Parametrierung von entscheidender Bedeutung ist.

Hinweis

Die Ereigniszählervon EF Core melden die Trefferrate des Abfragecaches. In einer normalen Anwendung erreicht dieser Zähler bald nach dem Programmstart 100 %, sobald die meisten Abfragen mindestens einmal ausgeführt wurden. Wenn dieser Leistungsindikator unter 100 % stabil bleibt, ist dies ein Hinweis darauf, dass Ihre Anwendung etwas ausführt, das den Abfragecache bezwingt. Es empfiehlt sich, das zu untersuchen.

Hinweis

Wie die Datenbank Abfragepläne zwischengespeichert werden, ist datenbankabhängig. Beispielsweise verwaltet SQL Server implizit einen LRU-Abfrageplancache, während PostgreSQL dies nicht tut (aber vorbereitete Anweisungen können einen sehr ähnlichen Endeffekt erzeugen). Nähere Informationen dazu finden Sie in der Dokumentation zu Ihrer Datenbank.

Dynamisch erstellte Abfragen

In einigen Fällen ist es notwendig, LINQ-Abfragen dynamisch zu erstellen, anstatt sie im Quellcode direkt anzugeben. Dies kann beispielsweise bei einer Website geschehen, die beliebige Abfragedetails von einem Client mit geöffneten Abfrageoperatoren (Sortieren, Filtern, Paging...) empfängt. Im Prinzip können dynamisch erstellte Abfragen genauso effizient wie reguläre Abfragen sein (obwohl es nicht möglich ist, die kompilierte Abfrageoptimierung mit dynamischen Abfragen zu verwenden). In der Praxis sind sie jedoch häufig die Quelle von Leistungsproblemen, da es leicht passiert, dass versehentlich Ausdrucksstrukturen mit Formen erzeugt werden, die sich jedes Mal unterscheiden.

Im folgenden Beispiel werden drei Techniken zum Erstellen des Lambda-Ausdrucks einer Abfrage verwendet Where:

  1. Ausdrucks-API mit Konstante: Dynamisches Erstellen des Ausdrucks mit der Ausdrucks-API mithilfe eines Konstantenknotens. Dies ist ein häufiger Fehler beim dynamischen Erstellen von Ausdrucksstrukturen und bewirkt, dass EF die Abfrage jedes Mal neu kompiliert, wenn sie mit einem anderen Konstantenwert aufgerufen wird (es verursacht normalerweise auch eine Belastung des Plancache auf dem Datenbankserver).
  2. Ausdrucks-API mit Parameter: Eine bessere Version, welche die Konstante durch einen Parameter ersetzt. Dadurch wird sichergestellt, dass die Abfrage unabhängig vom bereitgestellten Wert nur einmal kompiliert und die gleiche (parametrierte) SQL-Datei generiert wird.
  3. Einfach mit Parameter: Eine Version, welche die Ausdrucks-API zum Vergleich nicht verwendet und dieselbe Struktur wie die oben die beschriebene Methode erstellt, aber viel einfacher ist. In vielen Fällen ist es möglich, Ihre Ausdrucksstruktur dynamisch zu erstellen, ohne auf die Ausdrucks-API zurückgreifen zu müssen, was leicht zu Fehlern führt.

Wir fügen der Abfrage nur dann einen Where-Operator hinzu, wenn der angegebene Parameter nicht NULL ist. Beachten Sie, dass dies kein guter Anwendungsfall für die dynamische Erstellung einer Abfrage ist – aber wir verwenden ihn zur Einfachheit:

[Benchmark]
public 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 query.Count();
}

Das Benchmarking dieser beiden Techniken führt zu den folgenden Ergebnissen:

Methode Mittelwert Fehler StdDev Gen0 Gen1 Zugeordnet
AusdruckApiMitKonstante 1.665,8 us 56,99 us 163,5 us 15,6250 - 109,92 KB
AusdruckApiMitParameter 757,1 us 35,14 us 103,6 us 12,6953 0,9766 54,95 KB
EinfachMitParameter 760,3 us 37,99 us 112,0 us 12,6953 - 55,03 KB

Auch wenn der Unterschied von unter einer Millisekunde klein erscheint, denken Sie daran, dass die konstante Version den Cache kontinuierlich belastet und bewirkt, dass andere Abfragen neu kompiliert werden, was sie verlangsamt und eine allgemeine negative Auswirkung auf die Gesamtleistung hat. Es wird dringend empfohlen, eine erneute Kompilierung von Konstantenabfragen zu vermeiden.

Hinweis

Vermeiden Sie das Erstellen von Abfragen mit der Ausdrucksstruktur-API, es sei denn, es gibt wirklich keinen anderen Weg. Abgesehen von der Komplexität der API passiert es sehr leicht, versehentlich erhebliche Leistungsprobleme bei der Verwendung zu verursachen.

Kompilierte Modelle

Kompilierte Modelle können die EF Core-Startzeit für Anwendungen mit großen Modellen verkürzen. Ein großes Modell bedeutet in der Regel hunderte bis tausende von Entitätstypen und Beziehungen. Die Startzeit hier ist die Zeit den ersten Vorgang auf einem DbContext auszuführen, bei dem der Typ DbContext zum ersten Mal in der Anwendung verwendet wird. Beachten Sie, dass das Erstellen einer DbContext-Instanz allein noch nicht dazu führt, dass das EF-Modell initialisiert wird. Typische erste Operationen, die das Modell initialisieren, sind dagegen das Aufrufen von DbContext.Add oder das Ausführen der ersten Abfrage.

Kompilierte Modelle werden mit dem dotnet ef-Befehlszeilenwerkzeug erstellt. Beachten Sie, dass Sie die aktuelle Version des Tools installiert haben müssen, bevor Sie fortfahren.

Um das kompilierte Modell zu erstellen, wird ein neuer dbcontext optimize-Befehl verwendet. Beispiel:

dotnet ef dbcontext optimize

Mit den Optionen --output-dir und --namespace kann das Verzeichnis und der Namespace angegeben werden, in denen das kompilierte Modell generiert wird. Beispiel:

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>

Die Ausgabe der Ausführung dieses Befehls enthält einen Codeabschnitt zum Kopieren und Einfügen in Ihre DbContext-Konfiguration, damit EF Core das kompilierte Modell verwendet. Beispiel:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

Bootstrapping des kompilierten Modells

Es ist in der Regel nicht erforderlich, den generierten Bootstrappingcode zu beachten. Manchmal kann es jedoch hilfreich sein, das Modell oder dessen Ladevorgang anzupassen. Der Bootstrappingcode sieht in etwa wie folgt aus:

[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();
}

Dies ist eine partielle Klasse mit partiellen Methoden, die implementiert werden können, um das Modell nach Bedarf anzupassen.

Darüber hinaus können mehrere kompilierte Modelle für DbContext-Typen generiert werden, die je nach Laufzeitkonfiguration unterschiedliche Modelle verwenden können. Diese sollten wie oben gezeigt in verschiedenen Ordnern und Namespaces platziert werden. Laufzeitinformationen, z. B. die Verbindungszeichenfolge, können dann untersucht und das richtige Modell bei Bedarf zurückgegeben werden. Beispiel:

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

Begrenzungen

Für kompilierte Modelle gelten einige Einschränkungen:

Aufgrund dieser Einschränkungen sollten Sie kompilierte Modelle nur verwenden, wenn Ihre EF Core-Startzeit zu langsam ist. Das Kompilieren kleiner Modelle lohnt sich in der Regel nicht.

Wenn die Unterstützung eines dieser Features für Ihren Erfolg entscheidend ist, entscheiden Sie sich für die oben verlinkten Punkte.

Reduktion des Laufzeitaufwands

Wie bei jeder Ebene fügt EF Core ein wenig Laufzeitaufwand hinzu, verglichen mit der direkten Codierung mit Datenbank-APIs auf niedrigerer Ebene. Es ist unwahrscheinlich, dass dieser Laufzeitaufwand die meisten realen Anwendungen erheblich beeinträchtigt; die anderen Themen in diesem Leistungshandbuch, z. B. Abfrageeffizienz, Indexnutzung und Minimierung von Roundtrips, sind viel wichtiger. Darüber hinaus dominieren auch bei hochoptimierten Anwendungen in der Regel die Netzwerklatenz und die Datenbank-E/A die Zeit, die man mit EF Core verbringt. Bei leistungsstarken Anwendungen mit geringer Latenz, bei denen jedes Bit an Perf wichtig ist, können jedoch die folgenden Empfehlungen verwendet werden, um den EF Core-Aufwand auf ein Minimum zu reduzieren:

  • Aktivieren Sie dbContext-Pooling; unsere Benchmarks zeigen, dass sich dieses Feature entscheidend auf Anwendungen mit hoher Perf und geringer Latenz auswirken kann.
    • Stellen Sie sicher, dass maxPoolSize Ihrem Nutzungsszenario entspricht. Wenn sie zu niedrig ist, werden DbContext-Instanzen ständig erstellt und verworfen, was die Leistung beeinträchtigt. Wenn Sie es zu hoch festlegen, wird möglicherweise unnötigerweise Arbeitsspeicher verbraucht, da nicht verwendete DbContext Instanzen im Pool verwaltet werden.
    • Für eine zusätzliche kleine Perf-Verstärkung sollten Sie die Verwendung von PooledDbContextFactory Betracht ziehen, anstatt DI-Kontextinstanzen direkt einzugeben. Die DI-Verwaltung von DbContext-Pooling verursacht einen leichten Mehraufwand.
  • Verwenden Sie vorkompilierte Abfragen für heiße Abfragen.
    • Je komplexer die LINQ-Abfrage - je mehr Operatoren sie enthält sie und je größer ist die resultierende Ausdrucksstruktur – desto mehr Vorteile können von der Verwendung kompilierter Abfragen erwartet werden.
  • Erwägen Sie das Deaktivieren von Threadsicherheitsprüfungen, indem Sie EnableThreadSafetyChecks in Ihrer Kontextkonfiguration auf falsch festlegen.
    • Die gleichzeitige Verwendung derselben DbContext-Instanz aus verschiedenen Threads wird nicht unterstützt. EF Core verfügt über ein Sicherheitsfeature, das diesen Programmierfehler in vielen Fällen (aber nicht allen) erkennt, und löst sofort eine informative Ausnahme aus. Dieses Sicherheitsfeature benötigt jedoch einen zusätzlichen Laufzeitaufwand.
    • WARNUNG: Deaktivieren Sie Threadsicherheitsprüfungen nur nach gründlichen Tests, dass Ihre Anwendung solche Parallelitätsfehler nicht enthält.