The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
C# Projects Workflow
Tool Grid
| Task | Tool | Command |
|---|---|---|
| Lint | Roslynator | dotnet roslynator analyze |
| Format | dotnet format | dotnet format |
| Build | dotnet | dotnet build |
| Test | dotnet | dotnet test |
| Publish | dotnet | dotnet publish |
| Watch | dotnet | dotnet watch run |
| NuGet restore | dotnet | dotnet restore |
| Add package | dotnet | dotnet add package <name> |
C# 12+ Features
Primary Constructors
SHOULD use primary constructors for dependency injection and simple initialization:
public class UserService(IUserRepository repository, ILogger<UserService> logger)
{
public async Task<User?> GetByIdAsync(int id) => await repository.FindAsync(id);
}
Collection Expressions
SHOULD use collection expressions for collection initialization:
// Preferred int[] numbers = [1, 2, 3, 4, 5]; List<string> names = ["Alice", "Bob", "Charlie"]; // Spread operator int[] combined = [..firstArray, ..secondArray];
Raw String Literals
SHOULD use raw string literals for multi-line strings and strings containing quotes:
var json = """
{
"name": "Example",
"value": 42
}
""";
File-Scoped Namespaces
MUST use file-scoped namespaces to reduce nesting:
namespace MyApp.Services;
public class MyService { }
.NET 8+ Patterns
Minimal APIs
SHOULD use minimal APIs for microservices and simple endpoints:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/api/users/{id}", async (int id, IUserService service) =>
await service.GetByIdAsync(id) is User user
? Results.Ok(user)
: Results.NotFound());
app.Run();
AOT Compilation
SHOULD design for AOT compatibility when targeting native deployment:
- •MUST NOT use reflection-heavy patterns
- •SHOULD use source generators instead of runtime reflection
- •MUST use
[JsonSerializable]for System.Text.Json
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
internal partial class AppJsonContext : JsonSerializerContext { }
Keyed Services
MAY use keyed services for named dependency resolution:
builder.Services.AddKeyedSingleton<ICache, RedisCache>("redis");
builder.Services.AddKeyedSingleton<ICache, MemoryCache>("memory");
Naming Conventions
General Rules
| Element | Convention | Example |
|---|---|---|
| Public members | PascalCase | GetUserAsync() |
| Private fields | _camelCase | _userRepository |
| Local variables | camelCase | userName |
| Constants | PascalCase | MaxRetryCount |
| Interfaces | IPascalCase | IUserService |
| Type parameters | TPascalCase | TEntity |
| Async methods | Async suffix | GetUserAsync() |
MUST Follow
- •MUST use
Iprefix for interfaces - •MUST use
Asyncsuffix for async methods - •MUST NOT use Hungarian notation
- •MUST NOT use underscores in public identifiers
Nullable Reference Types
Configuration
MUST enable nullable reference types in all projects:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
Usage Rules
- •MUST annotate all reference types explicitly
- •MUST handle null appropriately with null-conditional operators
- •SHOULD use
requiredkeyword for required properties - •MUST NOT use
!(null-forgiving operator) except when absolutely necessary
public class User
{
public required string Name { get; init; }
public string? Email { get; set; }
}
Async/Await Best Practices
Rules
- •MUST use async all the way (no sync-over-async)
- •MUST use
ConfigureAwait(false)in library code - •MUST use
CancellationTokenfor cancellable operations - •SHOULD prefer
ValueTaskfor hot paths with frequent sync completion - •MUST NOT use
async voidexcept for event handlers
public async Task<User?> GetUserAsync(int id, CancellationToken ct = default)
{
return await _repository.FindAsync(id, ct).ConfigureAwait(false);
}
Exception Handling
- •MUST catch specific exceptions, not
Exception - •SHOULD use
whenclause for conditional catches - •MUST NOT swallow exceptions silently
Dependency Injection
Rules
- •MUST use constructor injection for required dependencies
- •SHOULD use primary constructors for DI
- •MUST NOT use service locator pattern
- •MUST register services with appropriate lifetime
// Registration builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddSingleton<ICacheService, CacheService>(); builder.Services.AddTransient<IEmailService, EmailService>();
Lifetime Guidelines
| Lifetime | Use Case |
|---|---|
| Singleton | Stateless services, caches |
| Scoped | Per-request services, DbContext |
| Transient | Lightweight, stateless operations |
Record Types
DTOs and Value Objects
MUST use records for DTOs and immutable data:
public record UserDto(int Id, string Name, string? Email); public record CreateUserRequest(string Name, string Email); public record ApiResponse<T>(T Data, bool Success, string? Error = null);
Rules
- •MUST use positional records for simple DTOs
- •MAY use record classes with properties for complex objects
- •SHOULD use
withexpressions for immutable updates
LINQ Best Practices
Method Syntax
SHOULD prefer method syntax over query syntax:
// Preferred
var adults = users
.Where(u => u.Age >= 18)
.OrderBy(u => u.Name)
.Select(u => new UserDto(u.Id, u.Name, u.Email));
// Avoid query syntax for simple queries
var adults = from u in users
where u.Age >= 18
select u;
Performance
- •MUST materialize queries when iterating multiple times
- •SHOULD use
Any()instead ofCount() > 0 - •SHOULD use
FirstOrDefault()with predicate - •MUST NOT use LINQ in hot paths without benchmarking
Testing
Framework Options
| Framework | Command |
|---|---|
| xUnit | dotnet test |
| NUnit | dotnet test |
| MSTest | dotnet test |
xUnit Patterns (RECOMMENDED)
public class UserServiceTests
{
private readonly Mock<IUserRepository> _mockRepo = new();
private readonly UserService _sut;
public UserServiceTests()
{
_sut = new UserService(_mockRepo.Object);
}
[Fact]
public async Task GetByIdAsync_WhenUserExists_ReturnsUser()
{
// Arrange
var expected = new User { Id = 1, Name = "Test" };
_mockRepo.Setup(r => r.FindAsync(1, default))
.ReturnsAsync(expected);
// Act
var result = await _sut.GetByIdAsync(1);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public async Task GetByIdAsync_WhenIdInvalid_ThrowsArgumentException(int id)
{
await Assert.ThrowsAsync<ArgumentException>(() => _sut.GetByIdAsync(id));
}
}
Rules
- •MUST follow Arrange-Act-Assert pattern
- •MUST use meaningful test names
- •SHOULD use
[Theory]for parameterized tests - •MUST mock external dependencies
Project Structure
RECOMMENDED Layout
MySolution/ ├── src/ │ ├── MyApp.Api/ │ │ ├── Controllers/ │ │ ├── Endpoints/ │ │ └── Program.cs │ ├── MyApp.Application/ │ │ ├── Services/ │ │ └── DTOs/ │ ├── MyApp.Domain/ │ │ ├── Entities/ │ │ └── Interfaces/ │ └── MyApp.Infrastructure/ │ ├── Data/ │ └── Services/ ├── tests/ │ ├── MyApp.UnitTests/ │ └── MyApp.IntegrationTests/ ├── Directory.Build.props ├── Directory.Packages.props └── MySolution.sln
Directory.Build.props
SHOULD use centralized build configuration:
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
Error Handling
Result Pattern
SHOULD use Result pattern for expected failures:
public record Result<T>(T? Value, bool IsSuccess, string? Error = null)
{
public static Result<T> Success(T value) => new(value, true);
public static Result<T> Failure(string error) => new(default, false, error);
}
Exceptions
- •MUST use exceptions for exceptional conditions only
- •SHOULD create custom exceptions for domain errors
- •MUST include relevant context in exception messages
Configuration
Options Pattern
MUST use Options pattern for configuration:
public class DatabaseOptions
{
public const string SectionName = "Database";
public required string ConnectionString { get; init; }
public int MaxRetryCount { get; init; } = 3;
}
// Registration
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection(DatabaseOptions.SectionName));
Code Quality
Analyzers
SHOULD enable code analysis:
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
EditorConfig
MUST include .editorconfig for consistent style across team.