Socle .NET Core : API REST
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>
