dotnet
Idiomatic .NET/C# patterns — LINQ, async/await, records, xUnit testing, dotnet format, and build commands. Covers minimal APIs, dependency injection, Entity Framework, and common pitfalls. Use when working on .NET projects.
.NET / C# Patterns — dog_stack
Idiomatic C# and .NET conventions covering modern C# features, ASP.NET Core, xUnit testing, and build toolchain.
Idiomatic C#
Records for value objects
// ✓ Records for immutable value objects (C# 9+)
public record User(Guid Id, string Name, string Email);
// ✓ With-expressions for immutable updates
var updated = user with { Name = "Bob" };
Nullable reference types
// ✓ Enable in project file
<Nullable>enable</Nullable>
// ✓ Explicit null handling
public User? FindUser(Guid id) => _users.TryGetValue(id, out var user) ? user : null;
// ✓ Null-coalescing
var name = user?.Name ?? "Unknown";
async/await
// ✓ async all the way (avoid .Result / .Wait())
public async Task<User> GetUserAsync(Guid id, CancellationToken ct = default)
{
return await _repository.FindAsync(id, ct)
?? throw new NotFoundException($"User {id} not found");
}
// ✓ ConfigureAwait(false) in library code
var data = await httpClient.GetStringAsync(url).ConfigureAwait(false);
LINQ
// ✓ Readable LINQ chains
var activeAdmins = users
.Where(u => u.IsActive && u.Role == Role.Admin)
.OrderBy(u => u.Name)
.Select(u => new UserDto(u.Id, u.Name))
.ToList();
Minimal APIs (ASP.NET Core 6+)
var app = WebApplication.Create(args);
app.MapGet("/users/{id}", async (Guid id, IUserService svc, CancellationToken ct) =>
{
var user = await svc.GetAsync(id, ct);
return user is null ? Results.NotFound() : Results.Ok(user);
});
app.Run();
Testing with xUnit
public class UserServiceTests
{
[Fact]
public async Task GetAsync_ReturnsUser_WhenExists()
{
// Arrange
var repo = Substitute.For<IUserRepository>();
var userId = Guid.NewGuid();
repo.FindAsync(userId, default).Returns(new User(userId, "Alice", "[email protected]"));
var svc = new UserService(repo);
// Act
var result = await svc.GetAsync(userId);
// Assert
Assert.NotNull(result);
Assert.Equal("Alice", result.Name);
}
[Theory]
[InlineData("")]
[InlineData(null)]
public void User_WithInvalidName_ThrowsArgumentException(string? name)
{
Assert.Throws<ArgumentException>(() => new User(Guid.NewGuid(), name!, "[email protected]"));
}
}
Run tests:
dotnet test # all tests
dotnet test --filter "Category=Unit" # filtered
dotnet test --logger "console;verbosity=detailed"
Build Commands
dotnet build # debug build
dotnet build -c Release # release build
dotnet run # run project
dotnet publish -c Release # publish for deployment
dotnet restore # restore NuGet packages
dotnet add package <name> # add package
dotnet format
dotnet format # format all files
dotnet format --verify-no-changes # CI check (non-destructive)
dotnet format style # style rules only
dotnet format analyzers # analyzer rules only
Common Pitfalls
| Pitfall | Fix |
|---|---|
.Result / .Wait() on async | await properly; never block async |
async void | Always async Task (except event handlers) |
new HttpClient() per request | Use IHttpClientFactory |
Missing CancellationToken propagation | Pass ct through the call chain |
DateTime.Now in domain logic | Use DateTimeOffset.UtcNow or inject IClock |
Related
- Rules:
rules/dotnet/coding-style.md - Agent: language reviewers (general-purpose for .NET)