Créer une API en ASP.NET Core avec Swagger, SimpleInjector et xUnit

# ASPNETCore# xUnit# SimpleInjector# Swagger
écrit par julien.plateau publié le vendredi 27 octobre 2017
Dans cet article nous allons voir comment créer une Web API avec ASP.NET Core avec toutes les technologies pour avoir une application modulaire et robuste. Afin de bien comprendre les possibilités de modularité de l'injection de dépendance, nous allons créer une API permettant de consulter les articles de plusieurs journaux. Les journaux devront être séparé de l'API afin d'avoir le moins de code possible à modifier si ont souhaite en ajouter ou en supprimer. Voici les étapes que nous allons réaliser:
  1. Création du projet Web API
  2. Ajout de l'injection de dépendance simple
  3. Ajout de Swagger
  4. Ajout de SimpleInjector
  5. Mise en place de tests unitaires et d'intégrations avec xUnit
Les pré-requis:
  1. Visual Studio 2017
  2. .NET Core 2.0

1 - Création du projet Web API

Commençons par créer la solution et le projet. Dans la catégorie de gauche, choisir .NET Core et ensuite Application web ASP.NET Core. Une nouvelle fenêtre apparaît, vérifier tout d'abord bien que vous êtes en ASP.NET Core 2.0. Sélectionner API web et mettre Aucune authentification. Votre projet est maintenant créer, bravo ! Mais ce n'est que le début :). Un controlleur ValuesController à été créer automatiquement pour avoir un exemple. Comme vous pouvez le voir la classe et les méthodes possèdent chacun un attribut. Celui de la classe définit la route pour accéder à ce contrôleur et ceux des méthodes définissent quel type de requête (GET, POST, PUT, DELETE) peut y accéder et les différents paramètres attendus. Lançons l'application pour constater que tout fonctionne, vous devriez avoir ce résultat: Ajoutons une classe News:
    public class News
    {
        public string Title { get; set; }
        public string Content { get; set; }
        public DateTimeOffset Date { get; set; }
        public string NewsPaper { get; set; }
    }
Modifions maintenant ce contrôleur pour récupérer des articles de journaux.
[Route("api/[controller]")]
    public class NewsController : Controller
    {
        private IEnumerable<News> _news;

        public NewsController()
        {
            _news = new List<News> {
                new News()
                {
                    Title = "Incendie volontaire à la gendarmerie de Grenoble : la section de recherches saisie de l'enquête",
                    Content = "Le feu a pris dans un entrepôt technique vaste de 2 000 mètres carrés et a touché plusieurs garages et bureaux mais aucun logement de la caserne n'a été touché. Un gendarme a néanmoins été légèrement blessé en tentant d'intervenir en premier lieu. Une vingtaine de véhicules sérigraphiés et civils de la gendarmerie ont été touchés.",
                    Date = new DateTimeOffset(new DateTime(2017, 09, 21, 08, 07, 00)),
                    NewsPaper = "Le dauphiné"
                },
                new News()
                {
                    Title = "A New York, le sort de l’accord nucléaire iranien dans l’impasse",
                    Content = "Les signataires de l’accord conclu en 2015 se sont retrouvés mercredi à l’ONU dans une ambiance tendue, alors que Donald Trump menace toujours de dénoncer le texte.",
                    Date = new DateTimeOffset(new DateTime(2017, 09, 21, 06, 44, 00)),
                    NewsPaper = "Le Monde"
                },
                new News()
                {
                    Title = "Avec Airlab, Airparif veut développer des projets concrets pour lutter contre la pollution",
                    Content = "L’organisme indépendant qui surveille la qualité de l’air en Ile-de-France met en place une structure favorisant l’émergence et l’accompagnement de projets innovants.",
                    Date = new DateTimeOffset(new DateTime(2017, 09, 20, 18, 55, 00)),
                    NewsPaper = "Le Monde"
                }
            };
        }

        [HttpGet]
        public IEnumerable<News> Get()
        {
            return _news;
        }

        [HttpGet("{newspaper}")]
        public IEnumerable<News> Get(string newspaper)
        {
            return _news.Where(p => p.NewsPaper.Equals(newspaper, StringComparison.OrdinalIgnoreCase)).ToList();
        }
    }
J'ai créer trois articles dont un provenant du Dauphiné et les deux autres du Monde. Il faut maintenant les propriétés du projet pour pointer sur la bonne url. Vous pouvez maintenant lancer l'application. Deux urls sont disponibles: api/news et api/news/{journal} (ex: api/news/le dauphine). Notre API est maintenant fonctionnelle. Voici les sources du projet : NewsPaperAPI - Part 1

Préparons la suite

Nous allons apporter quelques modifications à la solution pour préparer les prochaines étapes. On va créer un projet d'abstraction contenant une interface INewsPaper et la classe News précédemment créer, ainsi que deux projets LeDauphine et LeMonde. Pour tous ces projets, j'ai choisi de les créer en .NET Standard 2.0. Voici à quoi ressemble la solution maintenant: Et la class NewsController:
[Route("api/[controller]")]
    public class NewsController : Controller
    {
        private IEnumerable<INewsPaper> _newsPapers;

        public NewsController()
        {
            _newsPapers = new List<INewsPaper>()
            {
                new LeDauphine(),
                new LeMonde()
            };
        }

        [HttpGet]
        public IEnumerable<News> Get()
        {
            return _newsPapers.SelectMany(np => np.GetAllNews());
        }

        [HttpGet("{newspaper}")]
        public IEnumerable<News> Get(string newspaper)
        {
            return _newsPapers.Where(np => np.GetNewsPaperName().Equals(newspaper, StringComparison.OrdinalIgnoreCase)).FirstOrDefault()?.GetAllNews();
        }
Voici les sources du projet : NewsPaperAPI - Part 1.5

2 - Ajout de l'injection de dépendance simple

Avec le .NET Core, nous avons la possibilité d'utiliser l'injection de dépendance nativement dans notre projet. Mais avant de continuer, il est important d'expliquer ce qu'est l'injection de dépendance. Quand une classe A instancie une classe B, on dit qu'il y a un couplage fort entre ces deux classes. L'un des problèmes de ce couplage est qu'il est impossible de tester unitairement la classe A sans la classe B. Afin de supprimer ce couplage, on passe par une couche d'abstraction de la classe B, ainsi la classe A ne se préoccupe pas de comment est implémenté la classe B et il est facile de passer un mock de la classe B à la classe A pour faire des tests unitaires. En pratique comment ça ce concrétise ? Actuellement, notre NewsController crée lui-même les instance de LeMonde et LeDauphine. L'injection de dépendance permet de passer des données aux contrôleur via le constructeur, il suffit de mettre quel type d'interface on souhaite et l'application passera la bonne instance à l'exécution. Modifions le constructeur notre classe NewsController afin de mieux visualiser.
 [Route("api/[controller]")]
    public class NewsController : Controller
    {
        private IEnumerable<INewsPaper> _newsPapers;

        public NewsController(IEnumerable<INewsPaper> newsPapers)
        {
            _newsPapers = newsPapers;
        }

        [HttpGet]
        public IEnumerable<News> Get()
        {
            return _newsPapers.SelectMany(np => np.GetAllNews());
        }

        [HttpGet("{newspaper}")]
        public IEnumerable<News> Get(string newspaper)
        {
            return _newsPapers.Where(np => np.GetNewsPaperName().Equals(newspaper, StringComparison.OrdinalIgnoreCase)).FirstOrDefault()?.GetAllNews();
        }
    }
Il faut maintenant dire à l'API quelles instances de INewsPaper il faut créer. Ca se passe dans le Startup.cs
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddSingleton(typeof(IEnumerable<INewsPaper>), new List<INewsPaper>() { new LeMonde(), new LeDauphine() });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }
    }
Il y a trois type d'instanciation possible: Singleton, Transient et Scoped. Singleton signifie que l'instance crée sera unique à toute l'application. Transient signifie que l'application va fournir une instance différente de l'interface à chaque objet qui la demande. Scoped signifie que l'instance est crée au début de la requête et détruite à la fin de celle-ci. Pour notre cas, nous allons utiliser le Singleton car nos journaux ne changent pas en fonction de l'instanciation. Lancer à nouveau l'application et vous devriez toujours voir les articles apparaître. Voici les sources du projet : NewsPaperAPI - Part 2

3 - Ajout de Swagger

Comme vous avez pu le voir précédemment, pour tester l'API, nous avons besoin connaitre les url et les paramètre par coeur ou d'aller à chaque fois chercher dans le code pour trouver ces urls. Swagger est une librairie permettant d'exposer une page web avec l'ensemble des méthodes d'api disponible. Voici le site officiel : https://swagger.io/ Pour commencer, il faut installer le package nuget: Swashbuckle.AspNetCore 1.0.0 Ensuite il faut ajouter le fichier index.html du lien ci-après dans le dossier wwwroothttps://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerUI/Template/index.html Il ne reste plus qu'à modifier le Startup. Ajouter les lignes suivantes dans la méthode ConfigureServices:
services.AddSwaggerGen(c =>
{
     c.SwaggerDoc("v1", new Info { Title = "NewsPaperAPI", Version = "v1" });
});
Et ajouter celles-ci dans Configure:
app.UseSwagger();

app.UseSwaggerUI(c =>
{
	c.RoutePrefix = "api-docs";
	c.SwaggerEndpoint("/swagger/v1/swagger.json", "NewsPaperAPI V1");
});
Vous pouvez maintenant modifier les propriétés du projet pour pointer sur l'url du Swagger: api-docs Lancer l'application, vous devriez avoir ce visuel: Les différentes méthodes sont maintenant listées et peuvent être testées facilement.

4 - Ajouter SimpleInjector

Avec les 3 étapes précédentes, vous avez déjà une API fonctionnelle. Les suivantes seront mises en place pour les tests unitaires et d'intégrations. Voici le site officiel: https://simpleinjector.org/index.html SimpleInjector permet d'améliorer l'injection de dépendance du .NET Core et de pouvoir remplacer des instances enregistrées par d'autres. Il faut tout d'abord installer les packages nuget suivants:
  • SimpleInjector 4.0.11
  • SimpleInjector.Integration.AspNetCore.Mvc 4.0.11
Ensuite tout se passe dans le Startup.
public class Startup
    {
        private Container _container;
        public Container Container => _container;

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info { Title = "NewsPaperAPI", Version = "v1" });
            });

            _container = new Container();

            IntegrateSimpleInjector(services);
        }

        private void IntegrateSimpleInjector(IServiceCollection services)
        {
            _container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            services.AddSingleton<IControllerActivator>(
                new SimpleInjectorControllerActivator(_container));
            services.AddSingleton<IViewComponentActivator>(
                new SimpleInjectorViewComponentActivator(_container));

            services.EnableSimpleInjectorCrossWiring(_container);
            services.UseSimpleInjectorAspNetRequestScoping(_container);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            InitializeContainer(app);

            app.UseMvc();

            app.UseSwagger();

            app.UseSwaggerUI(c =>
            {
                c.RoutePrefix = "api-docs";
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "NewsPaperAPI V1");
            });
        }

        private void InitializeContainer(IApplicationBuilder app)
        {
            // Add application presentation components:
            _container.RegisterMvcControllers(app);
            _container.RegisterMvcViewComponents(app);

            // Add application services. For instance:
            _container.Register(typeof(IEnumerable<INewsPaper>), () => new List<INewsPaper>() { new LeMonde(), new LeDauphine() },
                Lifestyle.Singleton);

            // Cross-wire ASP.NET services (if any). For instance:
            _container.CrossWire<ILoggerFactory>(app);

            // NOTE: Do prevent cross-wired instances as much as possible.
            // See: https://simpleinjector.org/blog/2016/07/
        }
    }
C'est tout !

5 - Mise en place de tests unitaires et d'intégrations avec xUnit

Pour les tests unitaires nous pourrions utiliser les fonctionnalités natives du .NET, mais xUnit permet de facilement multiplier les tests. Nous allons tout d'abord créer un projet de tests nommé NewsPaper.Tests de type Projet de test xUnit (.NET Core).

Tests unitaires

Pour une méthode de test unitaire vous pouvez mettre deux attributs Fact ou Theory. Fact signifie que votre méthode est un test simple et qu'elle peut être exécutée.
[Fact]
public void Get_GetAllNews_Success()
{
	var newspaper = new LeDauphine();

	var news = newspaper.GetAllNews();

	Assert.NotNull(news);
}
Theory s'utilise avec une méthode qui à des paramètres en entrée. Il suffit d'ajouter des InlineData en attributs pour définir plusieurs tests à partir d'une seule méthode de test.
[Theory]
[InlineData("p1", 0, 0)]
[InlineData("p2", 1, 0)]
[InlineData(null, 1, 2)]
public void Theory_Success(string param1, int param2, int result)
{
	//Test de la méthode

	Assert.Equal(result, 0);
}

Tests d'intégrations

Les tests unitaires c'est bien, mais tester l'appel à une méthode de l'API via son url et vérifier son retour, c'est encore mieux. Disponible prochainement !