Aller au contenu principal

Rules: csharp

// 2. Private readonly fields private readonly IUserRepository _userRepository; private readonly ILogger<UserService> _logger;

Affected files

These rules apply to files matching the following patterns:

  • **/*.cs
  • **/*.csproj
  • **/*.sln

Detailed rules

C# Rules

Code Conventions

Naming

ElementConventionExample
ClassesPascalCaseUserService
InterfacesPascalCase with I prefixIUserRepository
MethodsPascalCaseGetUserById
PropertiesPascalCaseFirstName
Local variablescamelCaseuserName
ParameterscamelCaseuserId
ConstantsPascalCaseMaxRetryCount
Private fields_camelCase_userRepository

Class Structure

public class UserService : IUserService
{
// 1. Constants
private const int MaxRetries = 3;

// 2. Private readonly fields
private readonly IUserRepository _userRepository;
private readonly ILogger<UserService> _logger;

// 3. Constructor
public UserService(IUserRepository userRepository, ILogger<UserService> logger)
{
_userRepository = userRepository;
_logger = logger;
}

// 4. Properties
public int RetryCount { get; private set; }

// 5. Public methods
public async Task<User?> GetByIdAsync(int id) { ... }

// 6. Private methods
private void ValidateUser(User user) { ... }
}

Best Practices

Nullable reference types

// Enable in the project
<Nullable>enable</Nullable>

// Use correctly
public User? FindById(int id) // May return null
public User GetById(int id) // Never returns null

// Pattern matching
if (user is { Name: var name, Email: var email })
{
Console.WriteLine($"{name}: {email}");
}

Records (C# 9+)

// Immutable record
public record User(int Id, string Name, string Email);

// Record with mutable properties if needed
public record User
{
public int Id { get; init; }
public string Name { get; init; } = "";
public string Email { get; init; } = "";
}

Async/Await

// Always suffix with Async
public async Task<User> GetUserAsync(int id)
{
return await _repository.FindByIdAsync(id)
?? throw new UserNotFoundException(id);
}

// Avoid .Result and .Wait()
// Use ConfigureAwait(false) in libraries
public async Task<User> GetUserAsync(int id)
{
return await _repository.FindByIdAsync(id).ConfigureAwait(false);
}

LINQ

// Fluent syntax preferred
var activeUsers = users
.Where(u => u.IsActive)
.OrderBy(u => u.Name)
.Select(u => new UserDto(u.Id, u.Name))
.ToList();

// Use Any() instead of Count() > 0
if (users.Any(u => u.IsAdmin))
{
// ...
}

Pattern Matching

// Switch expressions (C# 8+)
var message = status switch
{
Status.Active => "User is active",
Status.Pending => "User is pending",
Status.Inactive => "User is inactive",
_ => throw new ArgumentOutOfRangeException(nameof(status))
};

// Pattern matching with types
if (result is Success<User> { Value: var user })
{
return user;
}

ASP.NET Core

Controllers

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;

public UsersController(IUserService userService)
{
_userService = userService;
}

[HttpGet("{id}")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserDto>> GetById(int id)
{
var user = await _userService.GetByIdAsync(id);
return user is null ? NotFound(): Ok(user);
}

[HttpPost]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)]
public async Task<ActionResult<UserDto>> Create([FromBody] CreateUserRequest request)
{
var user = await _userService.CreateAsync(request);
return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
}
}

Dependency Injection

// Program.cs
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IUserService, UserService>();

// Options pattern
builder.Services.Configure<EmailOptions>(
builder.Configuration.GetSection("Email"));

Validation

public class CreateUserRequest
{
[Required]
[StringLength(100, MinimumLength = 2)]
public string Name { get; init; } = "";

[Required]
[EmailAddress]
public string Email { get; init; } = "";

[Required]
[MinLength(8)]
public string Password { get; init; } = "";
}

Tests

xUnit + FluentAssertions

public class UserServiceTests
{
private readonly Mock<IUserRepository> _repositoryMock;
private readonly UserService _sut;

public UserServiceTests()
{
_repositoryMock = new Mock<IUserRepository>();
_sut = new UserService(_repositoryMock.Object);
}

[Fact]
public async Task GetByIdAsync_WhenUserExists_ReturnsUser()
{
// Arrange
var user = new User(1, "John", "john@example.com");
_repositoryMock
.Setup(r => r.FindByIdAsync(1))
.ReturnsAsync(user);

// Act
var result = await _sut.GetByIdAsync(1);

// Assert
result.Should().NotBeNull();
result!.Name.Should().Be("John");
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Validate_WithInvalidName_ThrowsException(string? name)
{
// Act & Assert
var act = () => _sut.Validate(new User(1, name!, "email@test.com"));
act.Should().Throw<ValidationException>();
}
}

To Avoid

  • dynamic except in very specific cases
  • goto
  • Exceptions for flow control
  • Public fields (use properties)
  • async void except for event handlers

Automatic application

These rules are automatically applied by Claude during:

  • Reading the matching files
  • Modifying code
  • Suggestions and fixes

See also