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

PitfallFix
.Result / .Wait() on asyncawait properly; never block async
async voidAlways async Task (except event handlers)
new HttpClient() per requestUse IHttpClientFactory
Missing CancellationToken propagationPass ct through the call chain
DateTime.Now in domain logicUse DateTimeOffset.UtcNow or inject IClock

Related

  • Rules: rules/dotnet/coding-style.md
  • Agent: language reviewers (general-purpose for .NET)