C#
La structure des projets et le coding style employé pour les projets en C# est celui de la team ASP.<span>NET</span> :
- https://github.com/aspnet/AspNetCore/wiki/Engineering-guidelines
- https://github.com/hassanhabib/CSharpCodingStandard
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<User> GetByIdAsync(int id);
}
}
// ✅ CORRECT - Infrastructure implémente Core
namespace Kelios.WordSmartAssistant.Infrastructure.Repositories
{
public class UserRepository : Repository<User>, 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<IUserRepository, UserRepository>();
services.AddScoped<IUserService, UserService>();
return services;
}
}
// ✅ CORRECT - Service avec interface
public interface IUserService
{
Task<User> CreateUserAsync(CreateUserRequest request);
}
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
private readonly ILogger<UserService> _logger;
public UserService(IUserRepository userRepository, ILogger<UserService> 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<UserService> logger) : IUserService
{
}
Gestion des Erreurs et Logging
public async Task<User> 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<User>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Email)
.IsRequired()
.HasMaxLength(255);
entity.HasIndex(e => e.Email)
.IsUnique()
.HasDatabaseName("IX_Users_Email");
// Relation avec ModelConfiguration
entity.HasMany(e => e.ModelConfigurations)
.WithOne(e => e.User)
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
// Configuration pour JSON (PostgreSQL)
modelBuilder.Entity<McpServerConfiguration>(entity =>
{
entity.Property(e => e.Configuration)
.HasColumnType("jsonb");
});
}
🧪 Tests
Tests Unitaires (.NET)
// ✅ CORRECT - Test unitaire avec mocking
public class UserServiceTests
{
private readonly Mock<IUserRepository> _mockUserRepository;
private readonly Mock<ILogger<UserService>> _mockLogger;
private readonly UserService _userService;
public UserServiceTests()
{
_mockUserRepository = new Mock<IUserRepository>();
_mockLogger = new Mock<ILogger<UserService>>();
_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 => r.GetByEmailAsync(request.Email))
.ReturnsAsync((User)null);
_mockUserRepository
.Setup(r => r.AddAsync(It.IsAny<User>()))
.ReturnsAsync((User user) => { 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 > 0);
_mockUserRepository.Verify(r => r.AddAsync(It.IsAny<User>()), 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 => r.GetByEmailAsync(request.Email))
.ReturnsAsync(existingUser);
// Act & Assert
var exception = await Assert.ThrowsAsync<ValidationException>(
() => _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<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public UserEndpointsTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
// Remplacer par base de données en mémoire
services.RemoveAll<DbContextOptions<WordSmartAssistantDbContext>>();
services.AddDbContext<WordSmartAssistantDbContext>(options =>
{
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<User>();
Assert.NotNull(user);
Assert.Equal(request.Email, user.Email);
Assert.True(user.Id > 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<User> 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 =>
{
options.AddDefaultPolicy(policy =>
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (environment == "Development")
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}
else
{
var allowedOrigins = configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()
?? 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<IEnumerable<ModelConfigurationSummaryDto>> GetUserConfigurationSummariesAsync(int userId)
{
return await _context.ModelConfigurations
.Where(mc => mc.UserId == userId)
.Select(mc => 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<PagedResult<ModelConfiguration>> GetUserConfigurationsPagedAsync(
int userId,
int page = 1,
int pageSize = 10)
{
var query = _context.ModelConfigurations
.Where(mc => mc.UserId == userId)
.OrderByDescending(mc => mc.CreatedAt);
var totalCount = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();
return new PagedResult<ModelConfiguration>
{
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<CachedModelConfigurationService> _logger;
private const int CacheDurationMinutes = 30;
public async Task<ModelConfiguration?> 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<ModelConfiguration> 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