3
votes

Comment créer des requêtes asynchrones parallèles dans EF Core 3.0 avec un modèle de référentiel?

J'ai un référentiel comme

services.AddDbContext<EmployeeDbContext>(ServiceLifetime.Transient);

Constructeur de référentiel (injection DbContext):

var settingsTask = _employeeRepository
    .GetEmployeeSettings(employeeId.Value);

var workPositionsTask = _employeeRepository
    .GetWorkPositions(employeeId.Value);

await Task.WhenAll(settingsTask, workPositionsTask);

// do other things...

Et appelez-le dans EF Core 2.0 comme

public EmployeeRepository(EmployeeDbContext dbContext)
{
  _dbContext = dbContext;
}

Problème:

Avec EF Core 3.0, il y a InvalidOperationException: une deuxième opération a démarré sur ce contexte avant qu'une opération précédente ne soit terminée ...

DbContext est enregistré dans ConfigureServices comme

public interface IEmployeeRepository
{
  Task<EmployeeSettings> GetEmployeeSettings(int employeeId);
  Task<ICollection<DepartmentWorkPosition>> GetWorkPositions(int employeeId);
}

Le didacticiel indique ce qui suit: Entity Framework Core ne prend pas en charge plusieurs opérations parallèles exécutées sur la même instance de DbContext.

Mais! Comment l'utiliser avec des référentiels en async?


0 commentaires

3 Réponses :


1
votes

Comment l'utiliser avec des référentiels en async?

Vous ne pouvez avoir qu'une seule requête asynchrone simultanée par référentiel. Si vous devez en avoir plus d'un à la fois, vous avez besoin de plusieurs référentiels. Cela peut vous obliger à injecter une fabrique de référentiels dans vos types.


6 commentaires

Mais pourquoi? Quel est le problème avec deux requêtes parallèles qui attendent des données du serveur? L'utilisation de contextes différents implique des performances optimales, car les contextes initialisent certains éléments lors de leur création - je suppose que la mise en commun résoudrait ce problème.


@MihaMarkic: Je peux me tromper, mais je pense que la raison en est qu'une seule connexion db ne peut être utilisée que pour diffuser un ensemble de résultats à la fois. Pour en diffuser un autre, une autre connexion serait nécessaire. Il existe un moyen d'avoir plusieurs ensembles de résultats simultanés par connexion, mais je ne pense pas qu'EF le prend en charge.


Je pense que cela aussi. Cependant, la bonne vieille pratique ado.net est d'ouvrir une connexion, de faire ce que vous avez à faire et de la supprimer immédiatement après - elle retourne donc à la piscine. Je me demande quelles décisions ont poussé l'équipe EF à ne pas publier du tout une seule connexion (deviner).


Le fournisseur EF détermine la façon dont les connexions sont gérées, et je crois que tous les grands fournisseurs utilisent le regroupement de connexions.


Plusieurs ensembles de résultats actifs devraient gérer plus d'un résultat à la fois, je suppose que la matérialisation côté client de chaque résultat ne peut pas être multithread ou mise en file d'attente pour une exécution à la fois .... ce qui est dommage car cela a fonctionné dans 2.2 et a rendu très facile le lancement simultané de plusieurs requêtes potentiellement longues.


Une autre façon possible est donc de configurer DbContextPool pour louer / libérer DbContext par requête.



-1
votes

Ecrivez:

var settings = await _employeeRepository.GetEmployeeSettings(employeeId.Value);
var workPositions = await _employeeRepository.GetWorkPositions(employeeId.Value);

Ouais. EF Core ne prend pas en charge plusieurs opérations parallèles exécutées sur la même instance de contexte. Vous devez toujours attendre la fin d'une opération avant de commencer l'opération suivante. Cela se fait généralement en utilisant le mot clé await sur chaque opération asynchrone. Regardez https://docs.microsoft.com/en-us/ef/core/querying/async


2 commentaires

Vote négatif: la question n'est pas de savoir comment exécuter ces deux requêtes, mais comment les exécuter en parallèle


il n'y a aucun moyen d'utiliser en parallèle. s'il vous plaît lire mon commentaire.



0
votes

Utilisez la fabrique et instanciez explicitement le contexte.

Startup.cs

    //instantiate dbcontext for each call, so we can parallellize
    [TestMethod]
    public async Task TestMultiple()
    { 
        //test1 and test2 starts in parallel without test2 that need to wait the end of test1. For each one a Task in returned
        var test1 = _testService.TestMultiple(1,true);
        var test2 = _testService.TestMultiple(2,true);

        //wait test1 and test2 return
        string code1 = await test1;
        string code2 = await test2;

    }
    
    //use request dbcontext
    [TestMethod]
    public async Task TestClassic()
    {
        string code = await _testService.TestMultiple(3);

    }

Classe de service

public class TestService
{
    private readonly TestDB _testDb;
    private readonly Func<TestDB> _testDbfunct;

    public TestService(TestDB testDb, Func<TestDB> testDbfunct)
    {
        _testDb = testDb;
        _testDbfunct = testDbfunct;
    }

    //mixed classical request dbcontext and factory approaches
    public async Task<string> TestMultiple(int id, bool newConnection = false) //we need to add optional newConnection parameter and the end of other parameters
    {
        //use request connection (_testDb) if newconnection is false, otherwise instantiate a new connection using factory. null inside "using" means that "using" is not used
        //use newconnection = true if you want run parallel queries, so you need different connection for each one
        TestDB testDb = _testDb;
        using (newConnection ? testDb = _testDbfunct() : null)
        {
            return await (from t in testDb.Table where t.id == id select t.code).FirstOrDefaultAsync();

        }
    }
}

Classe d'essai

//classical dbcontext registration
services.AddDbContext<TestDB>(
            options => options.UseSqlServer(
                Configuration.GetConnectionString("Test")));

//factory
//in case we want parallellize more queries at the same request, we can't use the same connection. So, because dbcontext is instantiate at request time this would  generate exception, so we need to use factory and explicit "using" to explicitly manage dbcontext lifetime
var optionsBuilder = new DbContextOptionsBuilder<TestDB>();
        optionsBuilder.UseSqlServer(Configuration.GetConnectionString("Test"));
        services.AddSingleton(s => new Func<TestDB>(() => new TestDB(optionsBuilder.Options)));

NB: dans le nouveau .net core 5, vous pouvez utiliser buildin AddDbContextFactory au lieu de créer une fabrique personnalisée comme dans mon exemple


2 commentaires

Pas de code parallèle ici. Vous attendez les deux méthodes de test.


Salut. J'attends qu'ils commencent tous les deux (le second à commencer n'attend pas le premier qui finit). Vous pouvez également écrire: wait Task.WhenAll (test1, test2) et vous n'avez pas d'exception car sont utilisés 2 contextes différents;