Socle .NET Core : API REST

December 19, 2019 .Net Core 3 minutes, 57 secondes

Définition d'un socle .NET Core 3.x pour une API REST nécessitant une couche de persistance.

Le Socle

Les packages NuGet

Voici les composants utilisés pour ce socle.

  • Entity Framework Core (Microsoft.EntityFrameworkCore) : ORM .Net (version 3.x)
  • Swashbuckle.AspNetCore : outil swagger pour documenter les APIs (version 4.0.1+)
  • NLog & NLog.Config : outil de log (version 4.6.7+)
  • Microsoft.AspNetCore.ResponseCompression : optimisation des performances (version 2.x+)
  • Microsoft.AspNetCore.Authentication.JwtBearer : gestoin d'un jeton OpenID Connect

L'injection de dépendance

Le Framework .NET Core intègre une mécanique d'injection de dépendance incluant trois configurations de durée de vie pour les objets.

  • AddTransient : Les objets transitoires sont toujours différents; une nouvelle instance est fournie à chaque contrôleur et à chaque service.
  • AddTransient : Les objets étendus sont les mêmes dans une demande, mais différents selon les demandes.
  • AddSingleton : Les objets singleton sont les mêmes pour chaque objet et chaque demande.

Le Code

Program.cs

    public class Program
    {
        public static void Main(string[] args)
        {
            // NLog: setup the logger first to catch all errors
            var logger = NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger();
            try
            {
                logger.Debug("init main");
                BuildWebHost(args).Build().Run();
            }
            catch (Exception e)
            {
                //NLog: catch setup errors
                logger.Error(e, "Stopped program because of exception");
                throw;
            }
        }

        public static IWebHostBuilder BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((context, builder) =>
                {
                    var env = context.HostingEnvironment;

                    builder.AddJsonFile("appsetting.json", true, true)
                        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);
                })
                .UseNLog()
                .UseStartup<Startup>();
    }

Startup.cs


        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)
        {
            AddRepositories(services);
            AddServices(services);

            services.Configure<GzipCompressionProviderOptions>(options =>
            {
                options.Level = CompressionLevel.Optimal;
            });

            services.AddResponseCompression(options =>
            {
                options.EnableForHttps = true;
                options.Providers.Add<GzipCompressionProvider>();
            });

            ConfigureJwtAuthService(services, Configuration);

            AddSwagger(services);

            services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
            {
                builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader();
            }));
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            var loggerFactory = LoggerFactory.Create(builder =>
            {
                builder
                    .AddFilter("Microsoft", LogLevel.Warning)
                    .AddFilter("System", LogLevel.Warning)
                    .AddFilter("MonProjet.ServiceMetier.Startup", LogLevel.Debug)
                    .AddConsole()
                    .AddEventLog()
                    .AddDebug();
            });
            ILogger logger = loggerFactory.CreateLogger<Program>();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "MON-API");
            });

            app.UseCors("CorsPolicy");
            app.UseHttpsRedirection();
            app.UseAuthentication();
            app.UseMiddleware(typeof(ErrorHandlingMiddleware));
            app.UseResponseCompression();
            app.UseMvc();
        }

        /// <summary>
        /// Injection des repositories et des repositories wrappers
        /// </summary>
        /// <param name="services"></param>
        public void AddRepositories(IServiceCollection services)
        {
            // Utilisation du context par défaut pour SQL SERVER
            string connectionString = Configuration.GetConnectionString("sqlConnection");
            services.AddDbContext<DefaultRepositoryContext>(options => options.UseSqlServer(connectionString), ServiceLifetime.Singleton);

            // Patterns UOW & Repository utilisés ou injection des interfaces repositories
            services.AddSingleton<ITodoRepository, TodoRepository>();
            // services.AddSingleton<IUnitOfWork, DefaultUnitOfWork>();
        }

        /// <summary>
        /// Injection des service
        /// </summary>
        /// <param name="services"></param>
        private void AddServices(IServiceCollection services)
        {
            services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
            services.AddSingleton<IQueryToDoService, QueryToDoService>();
            services.AddSingleton<IQueryUserService, QueryUserService>();
        }

        /// <summary>
        /// Injection de la configuration swagger
        /// </summary>
        /// <param name="services"></param>
        private void AddSwagger(IServiceCollection services)
        {
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Version = "v1",
                    Title = "MON API METIER",
                    Description = "Api exposant les services métiers.",
                    TermsOfService = "OpenSource"
                });
            });
        }
    }

Gestion du JWT : StartupJWT.cs


public partial class Startup
    {
        private void ConfigureJwtAuthService(IServiceCollection services, IConfiguration configuration)
        {
            var audienceConfig = configuration.GetSection("AppSetting").GetSection("JwtSettings");
            var symmetricKeyAsBase64 = audienceConfig["JwtKeySecret"];
            var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
            var signingKey = new SymmetricSecurityKey(keyByteArray);

            var tokenValidationParameters = new TokenValidationParameters
            {
                // The signing key must match
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,

                // Validate the JWT issuer (Iss) claim
                ValidateIssuer = true,
                ValidIssuer = audienceConfig["JwtIssuer"],

                // Validate the JWT audience (Aud) claim
                ValidateAudience = true,
                ValidAudience = audienceConfig["JwtAudience"],

                // Validate token expiration
                ValidateLifetime = true,

                ClockSkew = TimeSpan.Zero
            };

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

            })
                .AddJwtBearer(o => { o.TokenValidationParameters = tokenValidationParameters; });

            services.AddAuthorization(options =>
            {
                options.AddPolicy(AuthorizationConstant.POLICY_REQUIRE_ROLE, policy =>
                    policy.RequireRole(AuthorizationConstant.ROLE_ADMIN));

                options.AddPolicy(AuthorizationConstant.POLICY_REQUIRE_ADMIN,
                    policy => policy.RequireRole(AuthorizationConstant.ROLE_ADMIN));
            });
        }
    }

Configuration de l'API : appsettings.json


{
  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Trace",
        "Microsoft": "Information"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  },
  "ConnectionStrings": {
    "sqlConnection": "server=monserveur; database=DbTest; Integrated Security=true"
  },
  "AppSetting": {
    "Version": "0.0.1",
    "AppName": "MONAPI",

    "JwtSettings": {
      "JwtKeySecret": "VOTRESECRET",
      "JwtExpireTime": 30,
      "JwtAudience": "MonServiceApi",
      "JwtIssuer": "http://localhost:63888/"
    }
  }
}

Configuration des Logs : NLog.config


<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="info">

  <!-- the targets to write to -->
  <targets>
    <!-- write logs to file  -->
    <target xsi:type="File" name="allfile" fileName="logs/socle-api-all-${shortdate}.log"
            layout="[${longdate}] [${uppercase:${level}}] [${logger}] ${message} ${exception}" />

    <!-- another file log, only own logs. Uses some ASP.NET core renderers -->
    <target xsi:type="File" name="ownFile-web" fileName="logs/socle-api-info-${shortdate}.log"
            layout="[${longdate}] [${uppercase:${level}}] [${logger}] [${aspnet-request-url}] [${aspnet-mvc-action}] ${message} ${exception}" />

    <target xsi:type="File" name="error-web" fileName="logs/socle-api-error-${shortdate}.log"
            layout="[${longdate}] [${uppercase:${level}}] [${logger}] [${aspnet-request-url}] [${aspnet-mvc-action}] ${message} ${exception}" />

    <!-- write to the void aka just remove -->
    <target xsi:type="Null" name="blackhole" />
  </targets>

  <!-- rules to map from logger name to target -->
  <rules>
    <!--All logs, including from Microsoft-->
    <logger name="*" minlevel="Trace" writeTo="allfile" />

    <!--Skip Microsoft logs and so log only own logs-->
    <logger name="Microsoft.*" minlevel="Trace" writeTo="blackhole" final="true" />
    <logger name="*" minlevel="Trace" writeTo="ownFile-web" />
    <logger name="*" minlevel="Error" writeTo="error-web" />
  </rules>
</nlog>

alt