C# and .NET project workflow guidelines. Activate when working with C# files (.cs), .csproj, .NET projects, or C#-specific tooling.
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.
| 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> |
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);
}
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];
SHOULD use raw string literals for multi-line strings and strings containing quotes:
var json = """
{
"name": "Example",
"value": 42
}
""";
MUST use file-scoped namespaces to reduce nesting:
namespace MyApp.Services;
public class MyService { }
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();
SHOULD design for AOT compatibility when targeting native deployment:
[JsonSerializable] for System.Text.Json[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
internal partial class AppJsonContext : JsonSerializerContext { }
MAY use keyed services for named dependency resolution:
builder.Services.AddKeyedSingleton<ICache, RedisCache>("redis");
builder.Services.AddKeyedSingleton<ICache, MemoryCache>("memory");
| 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() |
I prefix for interfacesAsync suffix for async methodsMUST enable nullable reference types in all projects:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
required keyword for required properties! (null-forgiving operator) except when absolutely necessarypublic class User
{
public required string Name { get; init; }
public string? Email { get; set; }
}
ConfigureAwait(false) in library codeCancellationToken for cancellable operationsValueTask for hot paths with frequent sync completionasync void except for event handlerspublic async Task<User?> GetUserAsync(int id, CancellationToken ct = default)
{
return await _repository.FindAsync(id, ct).ConfigureAwait(false);
}
Exceptionwhen clause for conditional catches// Registration
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddTransient<IEmailService, EmailService>();
| Lifetime | Use Case |
|---|---|
| Singleton | Stateless services, caches |
| Scoped | Per-request services, DbContext |
| Transient | Lightweight, stateless operations |
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);
with expressions for immutable updatesSHOULD 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;
Any() instead of Count() > 0FirstOrDefault() with predicate| Framework | Command |
|---|---|
| xUnit | dotnet test |
| NUnit | dotnet test |
| MSTest | dotnet test |
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));
}
}
[Theory] for parameterized testsMySolution/
āāā 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
SHOULD use centralized build configuration:
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
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);
}
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));
SHOULD enable code analysis:
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
MUST include .editorconfig for consistent style across team.