Dans mon application Asp.Net Core, j'ai besoin d'un service singleton que je peux réutiliser pendant toute la durée de vie de l'application. Pour le construire, j'ai besoin d'un DbContext
(de EF Core), mais c'est un service de portée et non thread-safe.
Par conséquent, j'utilise le modèle suivant pour construire mon service singleton. Cela semble un peu hacky, donc je me demandais si cette approche est acceptable et ne posera pas de problèmes?
services.AddScoped<IPersistedConfigurationDbContext, PersistedConfigurationDbContext>(); services.AddSingleton<IPersistedConfigurationService>(s => { ConfigModel currentConfig; using (var scope = s.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<IPersistedConfigurationDbContext>(); currentConfig = dbContext.retrieveConfig(); } return new PersistedConfigurationService(currentConfig); }); ... public class ConfigModel { string configParam { get; set; } }
3 Réponses :
Bien que Injection de dépendances: durée de vie des services La documentation dans ASP.NET Core dit:
Il est dangereux de résoudre un service de portée à partir d'un singleton. Cela peut entraîner un état incorrect du service lors du traitement des demandes ultérieures.
Mais dans votre cas, ce n'est pas le problème. En fait, vous ne résolvez pas le service de portée à partir de singleton. Il s'agit simplement d'obtenir une instance de service à portée de singleton chaque fois qu'il le souhaite. Votre code doit donc fonctionner correctement sans aucune erreur de contexte supprimée!
Mais une autre solution potentielle peut être l'utilisation de IHostedService
. Voici les détails à ce sujet:
Utilisation d'un service de portée dans une tâche d'arrière-plan (IHostedService)
La documentation montre également comment procéder correctement
@PanagiotisKanavos Okay compris! Parlez-vous de IHostedService
?
IHostedService n'est qu'un exemple de service singleton.
@PanagiotisKanavos Oui! J'améliore ma réponse. Merci encore.
Ce que vous faites n'est pas bon et peut certainement entraîner des problèmes. Puisque cela est fait dans l'enregistrement du service, le service de portée va être récupéré une fois lorsque votre singleton est injecté pour la première fois. En d'autres termes, ce code ici ne s'exécutera qu'une fois pendant la durée de vie du service que vous enregistrez, ce qui, puisqu'il s'agit d'un singleton, signifie que cela ne se produira qu'une fois, point final. De plus, le contexte que vous injectez ici n'existe que dans la portée que vous avez créée, qui disparaît dès que l'instruction using se ferme. En tant que tel, au moment où vous essayez d'utiliser le contexte dans votre singleton, il aura été supprimé et vous obtiendrez une ObjectDisposedException
.
Si vous avez besoin d'utiliser un service de portée à l'intérieur un singleton, vous devez alors injecter IServiceProvider
dans le singleton. Ensuite, vous devez créer une portée et extraire votre contexte lorsque vous devez l'utiliser , et cela devra être fait chaque fois que vous en aurez besoin. Par exemple:
public class PersistedConfigurationService : IPersistedConfigurationService { private readonly IServiceProvider _serviceProvider; public PersistedConfigurationService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task Foo() { using (var scope = _serviceProvider.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService<IPersistedConfigurationDbContext>(); // do something with context } } }
Juste pour souligner, encore une fois, vous devrez le faire dans chaque méthode qui doit utiliser le service de portée (votre contexte). Vous ne pouvez pas persister ceci à un ivar ou quelque chose. Si vous êtes découragé par le code, vous devriez l'être, car c'est un anti-modèle. Si vous devez obtenir un service de portée dans un singleton, vous n’avez pas le choix, mais le plus souvent, c’est un signe de mauvaise conception. Si un service a besoin d'utiliser des services étendus, il doit presque invariablement être scopé lui-même, pas singleton. Il n'y a que quelques cas où vous avez vraiment besoin d'une durée de vie unique, et ceux-ci tournent principalement autour de la gestion des sémaphores ou d'autres états qui doivent être persistés tout au long de la vie de l'application. À moins qu'il y ait une très bonne raison de faire de votre service un singleton, vous devriez opter pour scoped dans tous les cas; scoped doit être la durée de vie par défaut, sauf si vous avez une raison de faire autrement.
Merci pour une réponse aussi détaillée, mais je pense que vous avez mal compris ma question. Mes excuses, pour ne pas être clair. J'ai mis à jour la question avec les parties que je pensais être implicitement claires.
Et je pense que votre suggestion est fondamentalement la même que celle que j'ai dans ma question :)
Non, je ne crois pas du tout avoir mal compris. Si vous avez besoin d'accéder à un service de portée dans un singleton, la méthode dans ma réponse ci-dessus est le seul moyen de le faire . Le code que vous avez est fondamentalement cassé et ne fonctionnera pas du tout. Donc, ce n'est même pas vraiment une question de savoir si c'est "acceptable" ou non, cela ne fonctionnera pas du tout.
Je ne passe pas un service à portée comme argument dans un constructeur d'un singleton. J'instancie un service de portée, je l'utilise pour récupérer des données, puis je le supprime et je ne vois plus jamais l'objet de la portée.
Donc retrieveConfig
renvoie un objet statique qui ne change jamais?
@ChrisPratt si IPersistedConfigurationDbContext
a d'autres dépendances et que ces dépendances sont définies comme Scoped, suffira-t-il de créer la portée ici? La portée sera-t-elle propagée aux dépendances?
En regardant le nom de ce service - je pense que vous avez besoin d'un fournisseur de configuration personnalisé qui charge la configuration à partir de la base de données au démarrage (une seule fois). Pourquoi ne faites-vous pas quelque chose comme suivre à la place? C'est une meilleure conception, une approche plus conforme au cadre et aussi quelque chose que vous pouvez créer en tant que bibliothèque partagée dont d'autres personnes peuvent également bénéficier (ou dont vous pouvez bénéficier dans plusieurs projets).
public static class ConfigurationBuilderExtensions { public static IConfigurationBuilder AddPersistentConfig(this IConfigurationBuilder configurationBuilder, string connectionString) { return configurationBuilder.Add(new PersistentConfigurationSource(connectionString)); } } class PersistentConfigurationSource : IConfigurationSource { public string ConnectionString { get; set; } public PersistentConfigurationSource(string connectionString) { ConnectionString = connectionString; } public IConfigurationProvider Build(IConfigurationBuilder builder) { return new PersistentConfigurationProvider(new DbContext(ConnectionString)); } } class PersistentConfigurationProvider : ConfigurationProvider { private readonly DbContext _context; public PersistentConfigurationProvider(DbContext context) { _context = context; } public override void Load() { // Using _dbContext // Load Configuration as valuesFromDb // Set Data // Data = valuesFromDb.ToDictionary<string, string>... } }
Ici - AddPersistentConfig
est une méthode d'extension construite comme une bibliothèque qui ressemble à ceci.
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .ConfigureAppConfiguration((context, config) => { var builtConfig = config.Build(); var persistentConfigBuilder = new ConfigurationBuilder(); var connectionString = builtConfig["ConnectionString"]; persistentStorageBuilder.AddPersistentConfig(connectionString); var persistentConfig = persistentConfigBuilder.Build(); config.AddConfiguration(persistentConfig); }); }
Il existe de nombreux doublons qui montrent comment procéder. Il en va de même pour la documentation des services d'arrière-plan.
PersistedConfigurationService
doit avoir un paramètreIServiceProvider
dans son costructeur. Il devrait créer la portée si nécessaire.Un
ServiceProvider
n'est rien de plus qu'une portée «racine»; par conséquent, vous pouvez simplement l'utiliser directement (c'est-à-dire sans avoir besoin de créer une portée).Vérifiez Utilisation d'un service de portée dans une tâche d'arrière-plan . Il montre que
IServiceProvider
est injecté dans le constructeur du service singleton. La méthode qui a réellement besoin du service de portée,DoWork
, crée la portée et ne demande le service que lorsqu'il est réellement nécessaire.@PanagiotisKanavos, merci d'avoir partagé le lien avec un exemple. La question qui me reste est de savoir si mon approche est bonne alors, car elle fait une chose similaire à la vôtre, mais dans un endroit différent (lorsque la construction de services se produit, et non à l'intérieur d'un service)
Je ne pense pas qu'il y ait vraiment quelque chose qui cloche dans cette approche. Vous ne transmettez pas un service de portée dans le
PersistedConfigurationService
- vous transmettez une configuration obtenue à partir d'un service de portée, ce qui est différent. @PanagiotisKanavos Êtes-vous d'accord? On pourrait soutenir quePersistedConfigurationService
ne devrait pas connaître ou se soucier deIPersistedConfigurationDbContext
ici.@KirkLarkin Votre commentaire précédent a du sens! En fait, il ne résout pas le service de portée de singleton. C'est juste obtenir une instance de service étendu. Ai-je raison?
@TanvirArjel Oui, c'est vrai. Il est facile de mal interpréter cela et de penser que nous avons un singleton qui tente de consommer un service de portée, mais nous ne le faisons pas.
@KirkLarkin J'ai édité le Q et me suis débarrassé d'un problème de
var
et d'initialisation. Veuillez transformer cela en réponse, je suis heureux de l'accepter, merci :)