Non sono sicuro se lo sto affrontando correttamente.
Contesto: ho un'azione del controller GET foo () come esempio. Questo foo () deve andare e chiamare bar (), e bar () potrebbe richiedere molto tempo. Quindi, ho bisogno di foo () per rispondere con "OK" prima (o indipendentemente da quando) la barra () completa .
Una leggera complessità è che bar () deve accedere a DBContext e recuperare alcuni dati dal DB. Con la mia attuale implementazione, ottengo un'eccezione "DBContext System.ObjectDisposed" quando provo ad accedere al db tramite barra. Qualche idea sul perché e come posso aggirare questo per favore? Sono davvero nuovo ai thread e ai compiti, quindi potrei sbagliare completamente!
Uso l'iniezione di dipendenza per fornire il contesto DB all'avvio
services.AddEntityFrameworkNpgsql()
.AddDbContext<MyDBContext>()
.BuildServiceProvider();
Quindi faccio call a foo () che a sua volta chiama bar () usando un nuovo thread (forse sto sbagliando?):
public async Task<string> foo(string msg)
{
Thread x = new Thread(async () =>
{
await bar(msg);
});
x.IsBackground = true;
x.Start();
return "OK.";
}
Quindi la barra tenta immediatamente di accedere a DBContext per afferrare alcune entità e genera l'eccezione!
Eccezione non gestita: System.ObjectDisposedException: impossibile accedere a un oggetto eliminato. Una causa comune di questo errore è l'eliminazione di un contesto che è stato risolto dall'iniezione delle dipendenze e successivamente il tentativo di utilizzare la stessa istanza di contesto altrove nell'applicazione. Ciò può verificarsi se si chiama Dispose () sul contesto o si avvolge il contesto in un'istruzione using. Se si utilizza l'iniezione di dipendenza, è necessario lasciare che il contenitore di iniezione di dipendenza si occupi di eliminare le istanze di contesto. Nome oggetto: "MyDBContext".
Se tolgo bar () dal thread, va bene ma ovviamente "OK" non viene restituito fino a quando la barra non ha terminato il suo processo molto lungo, che è il problema che devo risolvere.
Mille grazie per qualche consiglio, per favore.
MODIFICA con il codice in esecuzione MA è ancora in attesa del completamento di Task.Run prima di restituire "OK". (quasi lì?)
public async Task<string> SendBigFile(byte[] fileBytes)
{
ServerLogger.Info("SendBigFile called.");
var task = Task.Run(async () =>
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var someProvider = scope.ServiceProvider.GetService<ISomeProvider>();
var fileProvider = scope.ServiceProvider.GetService<IFileProvider>();
await GoOffAndSend(someProvider, fileProvider, fileBytes);
}
});
ServerLogger.Info("Hello World, this should print and not wait for Task.Run."); //Unfortunately this is waiting even though GoOffAndSend takes at least 1 minute.
return "Ok"; //This is not returned until Task.Run finishes unfortunately... how do I "skip" to this immediately?
}
private async Task GoOffAndSend(ISomeProvider s, IFileProvider f, byte[] bytes)
{
// Some really long task that can take up to 1 minute that involves finding the file, doing weird stuff to it and then
using (var client = new HttpClient())
{
var response = await client.PostAsync("http://abcdef/somewhere", someContent);
}
}
AddDbContext<>()
registra MyDBContext
come servizio con ServiceLifetime.Scoped
che significa che DbContext
viene creato per richiesta web. Viene eliminato al completamento della richiesta.
Il modo più semplice per evitare tale eccezione è iniettare IServiceScopeFactory
nel controller, quindi creare un nuovo ambito utilizzando CreateScope()
e richiedere il servizio MyDBContext
da tale ambito (non è necessario preoccuparsi di DbContextOptions
). Al termine del lavoro, eliminare DbContext
che successivamente DbContext
. Invece di Thread
è meglio usare Task
. Task
API dell'attività è più potente e di solito ha prestazioni migliori. Sarà simile a questo:
public class ValuesController : ControllerBase
{
IServiceScopeFactory _serviceScopeFactory
public ValuesController(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public async Task<string> foo(string msg)
{
var task = Task.Run(async () =>
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetService<MyDBContext>();
await bar(db, msg);
}
});
// you may wait or not when task completes
return "OK.";
}
}
Inoltre, dovresti sapere che Asp.Net
non è il posto migliore per le attività in background (ad es. Quando l'app è ospitata in IIS, può essere chiusa a causa del riciclo del pool di app)
Your ServerLogger.Info("SendBigFile finished.");
non aspetta al termine client.PostAsync
. Si registra immediatamente dopo Task.Run
avviato una nuova attività. Per registrarlo al termine di client.PostAsync
è necessario inserire ServerLogger.Info("SendBigFile finished.");
subito dopo await GoOffAndSend(someProvider, fileProvider, fileBytes);
:
...
await GoOffAndSend(someProvider, fileProvider, fileBytes);
ServerLogger.Info("SendBigFile finished.");
...
In Asp.net, la durata degli articoli iniettati dipende dal framework. Una volta che foo () ritorna, Asp non sa che hai creato un thread che ha ancora bisogno del DbContext che ti ha dato, quindi Asp elimina il contesto e si verifica un problema.
Puoi creare tu stesso un nuovo DbContext nel tuo thread e puoi decidere quando disporlo. Questo non è bello perché se la configurazione del database cambia, ora ci sono due punti che potrebbero dover essere aggiornati. Ecco un esempio:
var optionsBuilder = new DbContextOptionsBuilder<MyDBContext>();
optionsBuilder.UseNpgsql(ConnectionString);
using(var dataContext = new MyDBContext(optionsBuilder.Options)){
//Do stuff here
}
Come seconda opzione, Asp.net core ha anche la possibilità di utilizzare IHostedService
per creare servizi in background e registrarli nell'avvio, completi di injection dependency. È possibile creare un servizio in background che esegua attività in background e quindi foo () potrebbe aggiungere l'attività alla coda del servizio. Ecco un esempio .