Skip to main content

C#

La structure des projets et le coding style employé pour les projets en C# est celui de la team ASP.<span>NET</span> :

Vous trouverez le détail dans les différentes sections :

🔧 Pratiques .NET Core

Architecture et Structure

Respecter la Clean Architecture

// ✅ CORRECT - Dépendances vers l'intérieur uniquement
namespace Kelios.WordSmartAssistant.Core.Interfaces
{
public interface IUserRepository
{
Task&lt;User&gt; GetByIdAsync(int id);
}
}

// ✅ CORRECT - Infrastructure implémente Core
namespace Kelios.WordSmartAssistant.Infrastructure.Repositories
{
public class UserRepository : Repository&lt;User&gt;, IUserRepository
{
// Implémentation...
}
}

// ❌ INCORRECT - Core ne doit pas référencer Infrastructure
using Kelios.WordSmartAssistant.Infrastructure; // NON !

Services et Injection de Dépendances

// ✅ CORRECT - Enregistrement dans DependencyInjection.cs
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
services.AddScoped&lt;IUserRepository, UserRepository&gt;();
services.AddScoped&lt;IUserService, UserService&gt;();
return services;
}
}

// ✅ CORRECT - Service avec interface
public interface IUserService
{
Task&lt;User&gt; CreateUserAsync(CreateUserRequest request);
}

public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
private readonly ILogger&lt;UserService&gt; _logger;

public UserService(IUserRepository userRepository, ILogger&lt;UserService&gt; logger)
{
_userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
}

// ✅ CORRECT - Utilisation du constructeur primaire (C# 12) - Identique à la version ci-dessus
public class UserService(IUserRepository userRepository, ILogger&lt;UserService&gt; logger) : IUserService
{
}

Gestion des Erreurs et Logging

public async Task&lt;User&gt; GetUserByIdAsync(int userId)
{
try
{
_logger.LogInformation("Fetching user with ID {UserId}", userId);
var user = await _userRepository.GetByIdAsync(userId);
if (user == null)
{
_logger.LogWarning("User with ID {UserId} not found", userId);
throw new NotFoundException($"User with ID {userId} not found");
}
return user;
}
catch (NotFoundException)
{
throw; // Re-throw business exceptions
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching user with ID {UserId}", userId);
throw new InternalServerException("An error occurred while fetching the user");
}
}

🗄️ Base de Données

Migrations Entity Framework

# ✅ CORRECT - Process de migration
cd apps/api/src/Kelios.WordSmartAssistant.API

# 1. Créer migration après modification d'entité
dotnet ef migrations add AddUserPreferencesTable

# 2. Vérifier le code de migration généré
# Fichier: Migrations/YYYYMMDD_AddUserPreferencesTable.cs

# 3. Appliquer en développement
dotnet ef database update

# 4. Tester la migration
./tools/scripts/dev-full.sh

# 5. Commit migration + entité ensemble
git add .
git commit -m "feat(entities): add UserPreferences table with migration"

Configuration des Entités

// ✅ CORRECT - Configuration EF dans OnModelCreating
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configuration User
modelBuilder.Entity&lt;User&gt;(entity =&gt;
{
entity.HasKey(e =&gt; e.Id);
entity.Property(e =&gt; e.Email)
.IsRequired()
.HasMaxLength(255);
entity.HasIndex(e =&gt; e.Email)
.IsUnique()
.HasDatabaseName("IX_Users_Email");
// Relation avec ModelConfiguration
entity.HasMany(e =&gt; e.ModelConfigurations)
.WithOne(e =&gt; e.User)
.HasForeignKey(e =&gt; e.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
// Configuration pour JSON (PostgreSQL)
modelBuilder.Entity&lt;McpServerConfiguration&gt;(entity =&gt;
{
entity.Property(e =&gt; e.Configuration)
.HasColumnType("jsonb");
});
}

🧪 Tests

Tests Unitaires (.NET)

// ✅ CORRECT - Test unitaire avec mocking
public class UserServiceTests
{
private readonly Mock&lt;IUserRepository&gt; _mockUserRepository;
private readonly Mock&lt;ILogger&lt;UserService&gt;&gt; _mockLogger;
private readonly UserService _userService;

public UserServiceTests()
{
_mockUserRepository = new Mock&lt;IUserRepository&gt;();
_mockLogger = new Mock&lt;ILogger&lt;UserService&gt;&gt;();
_userService = new UserService(_mockUserRepository.Object, _mockLogger.Object);
}

[Fact]
public async Task CreateUserAsync_WithValidRequest_ShouldReturnUser()
{
// Arrange
var request = new CreateUserRequest
{
Email = "[email protected]",
Password = "SecurePassword123!"
};
_mockUserRepository
.Setup(r =&gt; r.GetByEmailAsync(request.Email))
.ReturnsAsync((User)null);

_mockUserRepository
.Setup(r =&gt; r.AddAsync(It.IsAny&lt;User&gt;()))
.ReturnsAsync((User user) =&gt; { user.Id = 1; return user; });

// Act
var result = await _userService.CreateUserAsync(request);

// Assert
Assert.NotNull(result);
Assert.Equal(request.Email, result.Email);
Assert.True(result.Id &gt; 0);
_mockUserRepository.Verify(r =&gt; r.AddAsync(It.IsAny&lt;User&gt;()), Times.Once);
}

[Fact]
public async Task CreateUserAsync_WithExistingEmail_ShouldThrowValidationException()
{
// Arrange
var request = new CreateUserRequest { Email = "[email protected]" };
var existingUser = new User { Id = 1, Email = request.Email };
_mockUserRepository
.Setup(r =&gt; r.GetByEmailAsync(request.Email))
.ReturnsAsync(existingUser);

// Act & Assert
var exception = await Assert.ThrowsAsync&lt;ValidationException&gt;(
() =&gt; _userService.CreateUserAsync(request));
Assert.Contains("email already exists", exception.Message);
}
}

Tests d'Intégration

// ✅ CORRECT - Test d'intégration avec WebApplicationFactory
public class UserEndpointsTests : IClassFixture&lt;WebApplicationFactory&lt;Program&gt;&gt;
{
private readonly WebApplicationFactory&lt;Program&gt; _factory;
private readonly HttpClient _client;

public UserEndpointsTests(WebApplicationFactory&lt;Program&gt; factory)
{
_factory = factory.WithWebHostBuilder(builder =&gt;
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =&gt;
{
// Remplacer par base de données en mémoire
services.RemoveAll&lt;DbContextOptions&lt;WordSmartAssistantDbContext&gt;&gt;();
services.AddDbContext&lt;WordSmartAssistantDbContext&gt;(options =&gt;
{
options.UseInMemoryDatabase("TestDb");
});
});
});
_client = _factory.CreateClient();
}

[Fact]
public async Task POST_CreateUser_ReturnsCreatedUser()
{
// Arrange
var request = new CreateUserRequest
{
Email = "[email protected]",
Password = "TestPassword123!"
};

// Act
var response = await _client.PostAsJsonAsync("/api/users", request);

// Assert
response.EnsureSuccessStatusCode();
var user = await response.Content.ReadFromJsonAsync&lt;User&gt;();
Assert.NotNull(user);
Assert.Equal(request.Email, user.Email);
Assert.True(user.Id &gt; 0);
}
}

🔒 Sécurité

Authentification et Autorisation

// ✅ CORRECT - Hash de mot de passe sécurisé, Préférence pour l'algorythme BCrypt si possible
public class PasswordHashingService
{
private const int SaltSize = 32;
private const int HashSize = 32;
private const int Iterations = 10000;

public string HashPassword(string password)
{
// Générer un salt unique
var salt = new byte[SaltSize];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}

// Hash avec PBKDF2
var hash = Rfc2898DeriveBytes.Pbkdf2(
Encoding.UTF8.GetBytes(password),
salt,
Iterations,
HashAlgorithmName.SHA256,
HashSize);

// Combiner salt + hash
var hashBytes = new byte[SaltSize + HashSize];
Array.Copy(salt, 0, hashBytes, 0, SaltSize);
Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);

return Convert.ToBase64String(hashBytes);
}

public bool VerifyPassword(string password, string hashedPassword)
{
var hashBytes = Convert.FromBase64String(hashedPassword);
var salt = new byte[SaltSize];
Array.Copy(hashBytes, 0, salt, 0, SaltSize);

var hash = Rfc2898DeriveBytes.Pbkdf2(
Encoding.UTF8.GetBytes(password),
salt,
Iterations,
HashAlgorithmName.SHA256,
HashSize);

var storedHash = new byte[HashSize];
Array.Copy(hashBytes, SaltSize, storedHash, 0, HashSize);

return CryptographicOperations.FixedTimeEquals(hash, storedHash);
}
}

Validation des Entrées

// ✅ CORRECT - Validation stricte des DTOs
public record CreateUserRequest
{
[Required]
[EmailAddress]
[StringLength(255)]
public string Email { get; init; } = string.Empty;

[Required]
[StringLength(100, MinimumLength = 8)]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\da-zA-Z]).{8,}$",
ErrorMessage = "Password must contain uppercase, lowercase, digit and special character")]
public string Password { get; init; } = string.Empty;
}

// ✅ CORRECT - Validation côté service
public async Task&lt;User&gt; CreateUserAsync(CreateUserRequest request)
{
// Validation métier supplémentaire
if (await _userRepository.GetByEmailAsync(request.Email) != null)
{
throw new ValidationException("A user with this email already exists");
}

// Sanitization
var sanitizedEmail = request.Email.Trim().ToLowerInvariant();
var user = new User
{
Email = sanitizedEmail,
PasswordHash = _passwordHashingService.HashPassword(request.Password),
CreatedAt = DateTime.UtcNow
};

return await _userRepository.AddAsync(user);
}

Configuration CORS Sécurisée

// ✅ CORRECT - CORS par environnement
public static void ConfigureCors(this IServiceCollection services, IConfiguration configuration)
{
services.AddCors(options =&gt;
{
options.AddDefaultPolicy(policy =&gt;
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (environment == "Development")
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}
else
{
var allowedOrigins = configuration.GetSection("Cors:AllowedOrigins").Get&lt;string[]&gt;()
?? throw new InvalidOperationException("Cors:AllowedOrigins must be configured for production");
policy.WithOrigins(allowedOrigins)
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization");
}
});
});
}

⚡ Performance

Optimisations EF Core

// ✅ CORRECT - Requêtes optimisées avec projections
public async Task&lt;IEnumerable&lt;ModelConfigurationSummaryDto&gt;&gt; GetUserConfigurationSummariesAsync(int userId)
{
return await _context.ModelConfigurations
.Where(mc =&gt; mc.UserId == userId)
.Select(mc =&gt; new ModelConfigurationSummaryDto
{
Id = mc.Id,
Name = mc.Name,
EditorName = mc.EditorName,
ModelName = mc.ModelName,
IsActive = mc.IsActive,
McpServerCount = mc.McpServerConfigurations.Count
})
.AsNoTracking()
.ToListAsync();
}

// ✅ CORRECT - Pagination
public async Task&lt;PagedResult&lt;ModelConfiguration&gt;&gt; GetUserConfigurationsPagedAsync(
int userId,
int page = 1,
int pageSize = 10)
{
var query = _context.ModelConfigurations
.Where(mc =&gt; mc.UserId == userId)
.OrderByDescending(mc =&gt; mc.CreatedAt);

var totalCount = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();

return new PagedResult&lt;ModelConfiguration&gt;
{
Items = items,
TotalCount = totalCount,
Page = page,
PageSize = pageSize,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
};
}

Cache en Mémoire

// ✅ CORRECT - Cache des données fréquemment accédées
public class CachedModelConfigurationService : IModelConfigurationService
{
private readonly IModelConfigurationService _inner;
private readonly IMemoryCache _cache;
private readonly ILogger&lt;CachedModelConfigurationService&gt; _logger;
private const int CacheDurationMinutes = 30;

public async Task&lt;ModelConfiguration?&gt; GetByIdAsync(int configurationId, int userId)
{
var cacheKey = $"model_config_{configurationId}_{userId}";
if (_cache.TryGetValue(cacheKey, out ModelConfiguration? cachedConfig))
{
_logger.LogDebug("Cache hit for configuration {ConfigurationId}", configurationId);
return cachedConfig;
}

var configuration = await _inner.GetByIdAsync(configurationId, userId);
if (configuration != null)
{
_cache.Set(cacheKey, configuration, TimeSpan.FromMinutes(CacheDurationMinutes));
_logger.LogDebug("Cached configuration {ConfigurationId}", configurationId);
}

return configuration;
}

public async Task&lt;ModelConfiguration&gt; UpdateAsync(int configurationId, int userId, UpdateModelConfigurationRequest request)
{
var result = await _inner.UpdateAsync(configurationId, userId, request);
// Invalider le cache
var cacheKey = $"model_config_{configurationId}_{userId}";
_cache.Remove(cacheKey);
_logger.LogDebug("Invalidated cache for configuration {ConfigurationId}", configurationId);
return result;
}
}

🆘 Aide et Ressources

Outils et Extensions Recommandés

  • VS Code: C# Dev Kit, REST Client
  • Visual Studio: ReSharper, SonarLint
  • Git: GitLens, Git Graph