web-dev-qa-db-fra.com

Comment valider le conteneur DI dans ASP.NET Core?

Dans ma classe Startup, j'utilise la méthode ConfigureServices(IServiceCollection services) pour configurer mon conteneur de service, en utilisant le conteneur DI intégré de Microsoft.Extensions.DependencyInjection.

Je veux valider le graphique de dépendance dans un test unitaire pour vérifier que tous les services peuvent être construits, afin de pouvoir réparer tous les services manquants pendant le test unitaire au lieu de faire planter l'application au moment de l'exécution. Dans les projets précédents, j'ai utilisé Simple Injector, qui a une méthode .Verify() pour le conteneur. Mais je n'ai pas pu trouver quelque chose de similaire pour ASP.NET Core.

Existe-t-il un moyen intégré (ou au moins recommandé) de vérifier que le graphe de dépendance entier peut être construit?

(La façon la plus stupide à laquelle je peux penser est quelque chose comme ça, mais cela échouera toujours à cause des génériques ouverts qui sont injectés par le framework lui-même):

startup.ConfigureServices(serviceCollection);
var provider = serviceCollection.BuildServiceProvider();
foreach (var serviceDescriptor in serviceCollection)
{
    provider.GetService(serviceDescriptor.ServiceType);
}
23
Martin Wedvich

Une validation de conteneur DI intégrée a été ajoutée dans ASP.NET Core 3 et elle n'est activée que dans l'environnement Development par défaut. Si quelque chose manque, le conteneur lève une exception fatale au démarrage.

Gardez à l'esprit que les contrôleurs ne sont pas créés par défaut dans le conteneur DI, donc une application web typique ne tirera pas grand-chose de cette vérification tant que les contrôleurs ne seront pas enregistrés dans la DI:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddControllersAsServices();
}

Pour désactiver/personnaliser la validation, ajoutez un IHostBuilder.UseDefaultServiceProvider appel:

public class Program
{
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            //...
            .UseDefaultServiceProvider((context, options) =>
            {
                options.ValidateOnBuild = false;
            });

Cette fonctionnalité de validation a plusieurs limitations, lisez plus ici: https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/

2
Ilya Chumakov

En fait, je viens d'utiliser l'exemple de votre question avec quelques modifications et cela a plutôt bien fonctionné pour moi. La théorie derrière le filtrage par classes dans mon espace de noms est que ceux-ci finiront par demander tout ce qui m'importe.

Mon test ressemblait beaucoup à ceci:

[Test or Fact or Whatever]
public void AllDependenciesPresentAndAccountedFor()
{
    // Arrange
    var startup = new Startup();

    // Act
    startup.ConfigureServices(serviceCollection);

    // Assert
    var exceptions = new List<InvalidOperationException>();
    var provider = serviceCollection.BuildServiceProvider();
    foreach (var serviceDescriptor in services)
    {
        var serviceType = serviceDescriptor.ServiceType;
        if (serviceType.Namespace.StartsWith("my.namespace.here"))
        {
            try
            {
                provider.GetService(serviceType);
            }
            catch (InvalidOperationException e)
            {
                exceptions.Add(e);
            }
        }
    }

    if (exceptions.Any())
    {
        throw new AggregateException("Some services are missing", exceptions);
    }
}
1
Benrobot

Pour vous assurer que votre application ASP.NET Core fonctionne comme prévu et que toutes les dépendances sont correctement injectées, vous devez utiliser test d'intégration dans ASP.NET Core . De cette façon, vous pouvez initialiser TestServer avec la même classe Startup, afin que toutes les dépendances soient injectées (pas de mocks ou de stubs similaires) et tester votre application à l'aide des routes/URL/chemins exposés.

Le test d'intégration pour l'URL racine par défaut pourrait ressembler à ceci:

public class PrimeWebDefaultRequestShould
{
    private readonly TestServer _server;
    private readonly HttpClient _client;
    public PrimeWebDefaultRequestShould()
    {
        // Arrange
        _server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
        _client = _server.CreateClient();
    }

    [Fact]
    public async Task ReturnHelloWorld()
    {
        // Act
        var response = await _client.GetAsync("/");
        response.EnsureSuccessStatusCode();

        var responseString = await response.Content.ReadAsStringAsync();

        // Assert
        Assert.Equal("Hello World!", responseString);
    }
}

Si vous devez récupérer des services spécifiques injectés via DI vous pouvez toujours le faire de cette façon:

var service = _server.Host.Services.GetService(typeof(IYourService));
0
Dmitry Pavlov

J'ai eu le même problème dans l'un de mes projets. Ma résolution:

  1. ajoutez des méthodes telles que AddScopedService, AddTransientService et AddSingletonService, qui ajoutent le service à DI, puis l'ajoutent à une liste. Utilisez ces méthodes au lieu de AddScoped, AddSingleton et AddTransient

  2. au démarrage de la première application, j'itère cette liste et appelle GetRequiredService. Si aucun service ne peut être résolu, l'application ne démarre pas

  3. J'ai eu CI: construction automatique et déploiement sur commit pour développer la branche. Donc, si quelqu'un fusionne des modifications qui ont cassé DI, l'application échoue et nous le savons tous.

  4. Si vous voulez le faire plus rapidement, vous pouvez utiliser TestServer dans la réponse de Dmitry Pavlov avec ma solution ensemble

0
Timur Lemeshko