💻 Développement

dev-dotnet-csharp-advisor

Développement .NET/C# avec ASP.NET Core, EF Core et patterns modernes.

⚡ Installation & lancement en 1 commande

Copiez-collez dans votre terminal : le skill s'installe dans ~/.claude/skills et Claude Code se lance directement dessus.

macOS / Linux
curl -fsSL https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.sh | sh -s -- dev-dotnet-csharp-advisor --launch
Windows (PowerShell)
iex "& { $(iwr -useb https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.ps1) } dev-dotnet-csharp-advisor -Launch"

🚀 Déjà installé ?

claude "/dev-dotnet-csharp-advisor"

Ou tapez /dev-dotnet-csharp-advisor dans une session Claude Code, ou décrivez simplement votre besoin — le skill se déclenche automatiquement via le skill-router.

🔑 Déclencheurs automatiques

Le skill s'active automatiquement quand votre demande contient :

.NETC#ASP.NETEF CoreEntity FrameworkMinimal APIBlazorLINQNuGetdotnet

📦 Installation manuelle

git clone https://github.com/khalilbenaz/claude-skills-collection.git cp -r claude-skills-collection/skills/dev-dotnet-csharp-advisor ~/.claude/skills/

Payload du plugin : skills/dev-dotnet-csharp-advisor · source éditable : dev-skills/dotnet-csharp-advisor

📖 Manuel

.NET/C# Advisor

Workflow

1. Créer et structurer le projet

dotnet new sln -n MonProjet
dotnet new webapi -n MonProjet.Api --use-minimal-apis
dotnet new classlib -n MonProjet.Domain
dotnet new classlib -n MonProjet.Infrastructure
dotnet new xunit -n MonProjet.Tests
dotnet sln add **/*.csproj

Directory.Build.props à la racine (partagé par tous les projets) :

<Project>
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
  </PropertyGroup>
</Project>

Directory.Packages.props pour la gestion centralisée des versions NuGet :

<Project>
  <PropertyGroup><ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally></PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
    <PackageVersion Include="xunit" Version="2.9.0" />
  </ItemGroup>
</Project>

2. Utiliser C# moderne (C# 12/13)

Records pour DTOs et Value Objects immuables :

public record CreateUserCommand(string Name, string Email);
public record UserId(Guid Value);

Primary constructors (C# 12) pour les services :

public class UserService(IUserRepository repo, ILogger<UserService> logger)
{
    public async Task<User?> GetAsync(Guid id) => await repo.FindAsync(id);
}

Pattern matching exhaustif :

var label = order.Status switch
{
    OrderStatus.Pending => "En attente",
    OrderStatus.Shipped => "Expédié",
    OrderStatus.Cancelled => "Annulé",
    _ => throw new UnreachableException()
};

Raw string literals pour JSON/SQL embarqués :

var json = """
    {
      "key": "value"
    }
    """;

Critère de décision C# 12 vs 13 : cibler .NET 9 + C# 13 pour les nouveaux projets ; .NET 8 LTS si contrainte de stabilité long terme.


3. ASP.NET Core — Minimal APIs vs Controllers

CritèreMinimal APIsControllers
Microservice / API simple
CQRS, projet complexe
Filters globaux, conventionsLimité
Performance (cold start)MeilleureStandard

Minimal API avec endpoint group :

var app = builder.Build();
app.MapGroup("/users").MapUsersEndpoints();
app.Run();

// UsersEndpoints.cs
public static class UsersEndpoints
{
    public static RouteGroupBuilder MapUsersEndpoints(this RouteGroupBuilder group)
    {
        group.MapGet("/{id:guid}", async (Guid id, UserService svc) =>
            await svc.GetAsync(id) is { } user ? Results.Ok(user) : Results.NotFound());
        return group;
    }
}

Ordre du middleware pipeline (critique) :

app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers(); // ou app.MapGroup(...)

4. Entity Framework Core

DbContext :

public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
    public DbSet<User> Users => Set<User>();

    protected override void OnModelCreating(ModelBuilder model)
        => model.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}

Configuration Fluent API séparée :

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.HasKey(u => u.Id);
        builder.Property(u => u.Email).HasMaxLength(256).IsRequired();
        builder.HasIndex(u => u.Email).IsUnique();
    }
}

Migrations :

dotnet ef migrations add InitialCreate --project MonProjet.Infrastructure --startup-project MonProjet.Api
dotnet ef database update --startup-project MonProjet.Api

Optimisations clés :

// Lecture seule — désactive le change tracking
var users = await db.Users.AsNoTracking().ToListAsync();

// Bulk update EF Core 7+ — pas de chargement en mémoire
await db.Users.Where(u => u.IsInactive)
    .ExecuteUpdateAsync(s => s.SetProperty(u => u.ArchivedAt, DateTime.UtcNow));

// Éviter N+1 avec Include explicite
var orders = await db.Orders.Include(o => o.Lines).ThenInclude(l => l.Product).ToListAsync();

5. Authentication & Security

JWT Bearer :

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opts =>
    {
        opts.TokenValidationParameters = new()
        {
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidateAudience = true,
            ValidAudience = builder.Configuration["Jwt:Audience"],
            ValidateLifetime = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!))
        };
    });

Authorization policy :

builder.Services.AddAuthorization(opts =>
    opts.AddPolicy("AdminOnly", p => p.RequireRole("Admin").RequireClaim("department", "IT")));

// Usage Minimal API
group.MapDelete("/{id}", handler).RequireAuthorization("AdminOnly");

6. Background processing

BackgroundService (tâche périodique) :

public class CleanupWorker(IServiceScopeFactory factory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            using var scope = factory.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            await db.Logs.Where(l => l.CreatedAt < DateTime.UtcNow.AddDays(-30))
                         .ExecuteDeleteAsync(ct);
            await Task.Delay(TimeSpan.FromHours(1), ct);
        }
    }
}

Critère Hangfire vs BackgroundService : Hangfire si besoin de dashboard, retry configurable, jobs récurrents Cron persistés ; BackgroundService si tâche simple sans dépendance supplémentaire.


7. Testing

Test unitaire avec xUnit + NSubstitute :

public class UserServiceTests
{
    [Fact]
    public async Task GetAsync_ExistingId_ReturnsUser()
    {
        var repo = Substitute.For<IUserRepository>();
        repo.FindAsync(Arg.Any<Guid>()).Returns(new User { Id = Guid.NewGuid() });
        var svc = new UserService(repo, NullLogger<UserService>.Instance);

        var result = await svc.GetAsync(Guid.NewGuid());

        Assert.NotNull(result);
    }
}

Test d'intégration avec WebApplicationFactory :

public class UsersApiTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task GetUser_Returns200()
    {
        var client = factory.CreateClient();
        var response = await client.GetAsync("/users/some-id");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}

Testcontainers pour SQL Server réel :

var container = new MsSqlBuilder().Build();
await container.StartAsync();
// utiliser container.GetConnectionString() dans les options EF

8. Performance

// Source generator JSON — zéro reflection en production
[JsonSerializable(typeof(UserDto))]
public partial class AppJsonContext : JsonSerializerContext { }

// Regex compilée au build
[GeneratedRegex(@"^\d{8}$")]
private static partial Regex CinRegex();

// Span<T> — pas d'allocation heap
public static bool StartsWithCode(ReadOnlySpan<char> input)
    => input.StartsWith("B3G", StringComparison.OrdinalIgnoreCase);

Benchmarker avant d'optimiser :

dotnet add package BenchmarkDotNet
dotnet run -c Release -- --filter *MyBenchmark*

Garde-fous & Anti-patterns

Anti-patternRisqueCorrection
.Result / .Wait() sur TaskDeadlock ASP.NETawait partout
DbContext SingletonConcurrence, data staleToujours Scoped
.ToList() avant .Where()Charge toute la table en mémoireFiltrer avant ToList
Include en cascade sans limiteQuery CartésienneExplosionProjeter avec Select
Secrets dans appsettings.jsonFuite credentialsdotnet user-secrets / Azure Key Vault
Ignorer les warnings nullableNullReferenceException prodTreatWarningsAsErrors + corriger
Scoped dans SingletonObjectDisposedExceptionInjecter IServiceScopeFactory

Bonnes pratiques 2026