update remove net last initial framework eliminar asp entity-framework asynchronous dbcontext asp.net-core-webapi

entity-framework - remove - seed entity framework core



¿Cuál es la mejor práctica en EF Core para usar llamadas asíncronas paralelas con un DbContext inyectado? (2)

Tengo una API .NET Core 1.1 con EF Core 1.1 y utilizo la configuración de Microsoft de vainilla de usar Dependency Injection para proporcionar el DbContext a mis servicios. (Referencia: https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro#register-the-context-with-dependency-injection )

Ahora, estoy buscando paralelizar las lecturas de bases de datos como una optimización utilizando WhenAll

Así que en lugar de:

var result1 = await _dbContext.TableModel1.FirstOrDefaultAsync(x => x.SomeId == AnId); var result2 = await _dbContext.TableModel2.FirstOrDefaultAsync(x => x.SomeOtherProp == AProp);

Yo suelo:

var repositoryTask1 = _dbContext.TableModel1.FirstOrDefaultAsync(x => x.SomeId == AnId); var repositoryTask2 = _dbContext.TableModel2.FirstOrDefaultAsync(x => x.SomeOtherProp == AProp); (var result1, var result2) = await (repositoryTask1, repositoryTask2 ).WhenAll();

Todo esto está muy bien, hasta que use la misma estrategia fuera de estas clases de acceso al Repositorio de DB y llame a estos mismos métodos con WhenAll en mi controlador a través de múltiples servicios:

var serviceTask1 = _service1.GetSomethingsFromDb(Id); var serviceTask2 = _service2.GetSomeMoreThingsFromDb(Id); (var dataForController1, var dataForController2) = await (serviceTask1, serviceTask2).WhenAll();

Ahora, cuando lo llame desde mi controlador, obtendré de forma aleatoria errores de concurrencia como:

System.InvalidOperationException: ExecuteReader requiere una conexión abierta y disponible. El estado actual de la conexión está cerrado.

La razón por la que creo es porque a veces estos hilos intentan acceder a las mismas tablas al mismo tiempo. Sé que esto es por diseño en EF Core y si quisiera podría crear un nuevo dbContext cada vez, pero estoy tratando de ver si hay una solución alternativa. Fue entonces cuando encontré este buen post de Mehdi El Gueddari: http://mehdi.me/ambient-dbcontext-in-ef6/

En el que reconoce esta limitación:

un DbContext inyectado le impide poder introducir subprocesos múltiples o cualquier tipo de flujos de ejecución paralelos en sus servicios.

Y ofrece una solución personalizada con DbContextScope .

Sin embargo, presenta una advertencia incluso con DbContextScope en que no funcionará en paralelo (lo que estoy tratando de hacer arriba):

Si intenta iniciar múltiples tareas paralelas dentro del contexto de un DbContextScope (por ejemplo, creando múltiples subprocesos o múltiples tareas de TPL), tendrá un gran problema. Esto se debe a que el DbContextScope ambiental fluirá a través de todos los subprocesos que están utilizando sus tareas paralelas.

Su último punto aquí me lleva a mi pregunta:

En general, el acceso paralelo a la base de datos dentro de una sola transacción comercial tiene pocos o ningún beneficio y solo agrega una complejidad significativa. Cualquier operación paralela realizada dentro del contexto de una transacción comercial no debe acceder a la base de datos.

¿No debería estar utilizando WhenAll en este caso en mis controladores y seguir usando esperar a uno por uno? ¿O es la inyección de dependencia de DbContext el problema más fundamental aquí, por lo tanto, un nuevo tipo debería ser creado / suministrado cada vez por algún tipo de fábrica?


El uso de cualquier método context.XyzAsync() solo es útil si await el método llamado o devuelve el control a un subproceso de llamada que no tiene context en su ámbito .

Una instancia de DbContext no es segura para subprocesos: nunca debe usarla en subprocesos paralelos. Lo que significa que, de seguro, nunca lo utilices en varios subprocesos de todos modos, incluso si no se ejecutan en paralelo. No trates de evitarlo.

Si por alguna razón desea ejecutar operaciones de base de datos paralelas (y cree que puede evitar puntos muertos, conflictos de concurrencia, etc. ), asegúrese de que cada una tenga su propia instancia de DbContext . Sin embargo, tenga en cuenta que la paralelización es principalmente útil para procesos ligados a la CPU, no procesos ligados a IO como la interacción de la base de datos. Tal vez pueda beneficiarse de las operaciones de lectura independientes en paralelo, pero ciertamente nunca ejecutaría procesos de escritura en paralelo. Aparte de los puntos muertos, etc., también hace que sea mucho más difícil ejecutar todas las operaciones en una transacción.

En el núcleo de ASP.Net, generalmente usaría el patrón de contexto por solicitud ( ServiceLifetime.Scoped , vea here ), pero incluso eso no puede evitar que transfiera el contexto a múltiples hilos. Al final, solo el programador puede evitarlo.

Si está preocupado por los costos de rendimiento de crear nuevos contextos todo el tiempo: no lo haga. Crear un contexto es una operación liviana, porque el modelo subyacente (modelo de tienda, modelo conceptual + asignaciones entre ellos) se crea una vez y luego se almacena en el dominio de la aplicación. Además, un nuevo contexto no crea una conexión física a la base de datos. Todas las operaciones de la base de datos de ASP.Net se ejecutan a través del conjunto de conexiones que administra un conjunto de conexiones físicas.

Si todo esto implica que tiene que reconfigurar su DI para alinearse con las mejores prácticas, que así sea. Si su configuración actual pasa los contextos a varios subprocesos, ha habido una mala decisión de diseño en el pasado. Resista la tentación de posponer la refactorización inevitable por los arreglos temporales. La única solución es des-paralizar su código, por lo que al final puede ser más lento que si rediseñara su DI y el código para adherirse al contexto por hilo.


Llegó al punto en el que realmente la única forma de responder al debate era hacer una prueba de rendimiento / carga para obtener pruebas estadísticas empíricas, comparables, de modo que pudiera resolver esto de una vez por todas.

Esto es lo que he probado:

Prueba de carga en la nube con usuarios de VSTS @ 200 como máximo durante 4 minutos en una aplicación web estándar de Azure.

Prueba # 1: 1 llamada a la API con inyección de dependencia de DbContext y async / await para cada servicio.

Resultados para la Prueba # 1:

Prueba # 2: 1 llamada a API con nueva creación de DbContext dentro de cada llamada de método de servicio y usando ejecución de subprocesos paralelos con WhenAll.

Resultados para la Prueba # 2:

Conclusión:

Para aquellos que dudan de los resultados, realicé estas pruebas varias veces con diferentes cargas de usuarios, y los promedios fueron básicamente los mismos cada vez.

El aumento de rendimiento con el procesamiento paralelo en mi opinión es insignificante, y esto no justifica la necesidad de abandonar la inyección de dependencia, lo que crearía gastos de desarrollo / deuda de mantenimiento, el potencial de errores si se maneja mal, y una desviación de las recomendaciones oficiales de Microsoft.

Una cosa más a tener en cuenta: como puede ver, en realidad hubo algunas solicitudes fallidas con la estrategia WhenAll, incluso cuando se asegura que se crea un nuevo contexto cada vez . No estoy seguro de la razón de esto, pero preferiría que no haya 500 errores en un aumento de rendimiento de 10 ms.