csharp-guide
SKILL.md
C# Guide
Applies to: C# 12+, .NET 8+, ASP.NET Core, Console Apps, Libraries
Core Principles
- Type Safety: Enable nullable reference types project-wide; treat warnings as errors
- Immutability First: Prefer records,
readonly, andinitproperties for data types - Async All The Way: Use
async/awaitend-to-end; never block on async code - Dependency Injection: Constructor injection via
IServiceCollection; no service locator - Fail Fast: Validate inputs at boundaries; use guard clauses and
ArgumentException
Guardrails
Version & Dependencies
- Target .NET 8+ with C# 12+ language features
- Use
<Nullable>enable</Nullable>and<ImplicitUsings>enable</ImplicitUsings>in.csproj - Pin package versions explicitly in
.csproj(avoid floating*versions) - Run
dotnet restorebefore committing after dependency changes - Audit new packages with
dotnet list package --vulnerablebefore adding
Code Style
- Follow .NET naming conventions
- Public members:
PascalCase| Private fields:_camelCasewith underscore prefix - Interfaces:
IServiceName| Async methods: suffix withAsync - Use
varwhen the type is obvious from the right side; explicit types otherwise - One class per file; filename matches class name
- Use file-scoped namespaces (
namespace MyApp.Services;) - Configure
.editorconfigfor consistent formatting across the team - Run
dotnet formatbefore every commit
Nullable Reference Types
- Enable globally:
<Nullable>enable</Nullable>in every.csproj - Never suppress warnings with
#pragma warning disableunless documented - Use
[NotNullWhen],[MaybeNullWhen],[NotNull]attributes for complex nullability - Use the null-forgiving operator
!sparingly and only with a justifying comment - Prefer
is not nullover!= nullfor null checks - Use
??and??=for default values;?.for conditional access
// Good: explicit nullability contract
public User? FindByEmail(string email)
{
ArgumentException.ThrowIfNullOrWhiteSpace(email);
return _users.FirstOrDefault(u => u.Email == email);
}
// Good: guard clause with null-coalescing
public void Process(Order order)
{
var customer = order.Customer
?? throw new InvalidOperationException("Order must have a customer.");
// ...
}
Async/Await
- Use
async/awaitend-to-end; never call.Resultor.Wait()(deadlock risk) - Suffix all async methods with
Async:GetUserAsync,SaveOrderAsync - Use
ValueTask<T>for hot paths that frequently complete synchronously - Use
ConfigureAwait(false)in library code (not in ASP.NET Core controllers) - Use
CancellationTokenin all async method signatures that perform I/O - Use
IAsyncEnumerable<T>for streaming large result sets - Set timeouts on all external calls with
CancellationTokenSource
public async Task<User> GetUserAsync(
int id, CancellationToken cancellationToken = default)
{
return await _dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == id, cancellationToken)
?? throw new NotFoundException(nameof(User), id);
}
LINQ
- Prefer method syntax over query syntax for consistency
- Never put side effects inside LINQ queries (no mutations, no I/O)
- Use
AsNoTracking()for read-only EF Core queries - Materialize queries with
ToListAsync()/ToArrayAsync()before returning - Avoid
Count()whenAny()suffices - Use
Selectto project only needed fields (avoid loading full entities)
// Good: projection, no-tracking, materialized
var activeEmails = await _dbContext.Users
.AsNoTracking()
.Where(u => u.IsActive)
.Select(u => u.Email)
.ToListAsync(cancellationToken);
// Bad: side effect in LINQ
var results = items.Select(i => { i.Processed = true; return i; }); // Don't do this
Project Structure
MySolution/
├── MySolution.sln
├── src/
│ ├── MySolution.Api/ # ASP.NET Core host / entry point
│ │ ├── Controllers/
│ │ ├── Middleware/
│ │ ├── Program.cs
│ │ └── MySolution.Api.csproj
│ ├── MySolution.Application/ # Use cases, commands, queries (CQRS)
│ │ ├── Commands/
│ │ ├── Queries/
│ │ ├── Interfaces/
│ │ └── MySolution.Application.csproj
│ ├── MySolution.Domain/ # Entities, value objects, domain events
│ │ ├── Entities/
│ │ ├── ValueObjects/
│ │ ├── Exceptions/
│ │ └── MySolution.Domain.csproj
│ └── MySolution.Infrastructure/ # EF Core, external services, file I/O
│ ├── Persistence/
│ ├── Services/
│ └── MySolution.Infrastructure.csproj
├── tests/
│ ├── MySolution.UnitTests/
│ │ └── MySolution.UnitTests.csproj
│ ├── MySolution.IntegrationTests/
│ │ └── MySolution.IntegrationTests.csproj
│ └── MySolution.ArchTests/ # Architecture rule tests (optional)
│ └── MySolution.ArchTests.csproj
├── .editorconfig
├── Directory.Build.props # Shared build properties
└── README.md
- Domain project has zero external dependencies (pure C#)
- Application depends only on Domain
- Infrastructure depends on Application and Domain
- Api depends on all projects (composition root)
- Test projects mirror
src/structure
Key Patterns
Nullable Reference Types & Guard Clauses
public sealed class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public async Task<OrderSummary> GetSummaryAsync(
int orderId, CancellationToken ct = default)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(orderId);
var order = await _repository.GetByIdAsync(orderId, ct)
?? throw new NotFoundException(nameof(Order), orderId);
return new OrderSummary(order.Id, order.Total, order.Status);
}
}
Pattern Matching
// Switch expression with property patterns
public decimal CalculateDiscount(Customer customer) => customer switch
{
{ MembershipLevel: "Gold", YearsActive: > 5 } => 0.20m,
{ MembershipLevel: "Gold" } => 0.15m,
{ MembershipLevel: "Silver" } => 0.10m,
{ TotalOrders: > 100 } => 0.05m,
_ => 0m,
};
// Relational and logical patterns
public string ClassifyTemperature(double temp) => temp switch
{
< 0 => "Freezing",
>= 0 and < 15 => "Cold",
>= 15 and < 25 => "Moderate",
>= 25 and < 35 => "Warm",
>= 35 => "Hot",
};
// Type pattern in is-expression
public static string Describe(object value) => value switch
{
int n when n < 0 => $"Negative integer: {n}",
int n => $"Positive integer: {n}",
string { Length: 0 } => "Empty string",
string s => $"String of length {s.Length}",
null => "null",
_ => $"Unknown: {value.GetType().Name}",
};
Records & Immutable Data
// Record for DTOs and value objects (value equality, immutable)
public sealed record OrderSummary(int Id, decimal Total, OrderStatus Status);
// Record with validation
public sealed record EmailAddress
{
public string Value { get; }
public EmailAddress(string value)
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
if (!value.Contains('@'))
throw new ArgumentException("Invalid email format.", nameof(value));
Value = value;
}
}
// Record struct for high-performance value types (no heap allocation)
public readonly record struct Coordinate(double Latitude, double Longitude);
// Nondestructive mutation with `with`
var updated = original with { Status = OrderStatus.Shipped };
Async Streams
// Producing an async stream
public async IAsyncEnumerable<LogEntry> StreamLogsAsync(
DateTime since,
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var batch in _logSource.ReadBatchesAsync(since, ct))
{
foreach (var entry in batch.Entries)
{
ct.ThrowIfCancellationRequested();
yield return entry;
}
}
}
// Consuming an async stream
await foreach (var log in StreamLogsAsync(DateTime.UtcNow.AddHours(-1), ct))
{
Console.WriteLine($"[{log.Timestamp}] {log.Message}");
}
Dependency Injection
// Registration in Program.cs (or a ServiceCollectionExtensions class)
public static IServiceCollection AddApplicationServices(
this IServiceCollection services)
{
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderService, OrderService>();
services.AddSingleton<IClock, SystemClock>();
services.AddHttpClient<IPaymentGateway, StripePaymentGateway>(client =>
{
client.BaseAddress = new Uri("https://api.stripe.com/");
client.Timeout = TimeSpan.FromSeconds(10);
});
return services;
}
// Constructor injection (no service locator, no static access)
public sealed class OrderService : IOrderService
{
private readonly IOrderRepository _repository;
private readonly IClock _clock;
public OrderService(IOrderRepository repository, IClock clock)
{
_repository = repository;
_clock = clock;
}
}
Testing
Standards
- Framework: xUnit (preferred), with
[Fact]and[Theory] - Mocking: NSubstitute or Moq (pick one per project, stay consistent)
- Assertions: FluentAssertions for readable assertion syntax
- Test naming:
MethodName_Scenario_ExpectedResult - One assertion concept per test (multiple
Shouldcalls for same concept OK) - Use
[Theory]with[InlineData]for parameterized tests - Coverage target: >80% for business logic, >60% overall
Unit Test Example
public sealed class OrderServiceTests
{
private readonly IOrderRepository _repository = Substitute.For<IOrderRepository>();
private readonly IClock _clock = Substitute.For<IClock>();
private readonly OrderService _sut;
public OrderServiceTests()
{
_sut = new OrderService(_repository, _clock);
}
[Fact]
public async Task GetSummaryAsync_ExistingOrder_ReturnsSummary()
{
// Arrange
var order = new Order { Id = 1, Total = 99.99m, Status = OrderStatus.Pending };
_repository.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(order);
// Act
var result = await _sut.GetSummaryAsync(1);
// Assert
result.Should().NotBeNull();
result.Id.Should().Be(1);
result.Total.Should().Be(99.99m);
result.Status.Should().Be(OrderStatus.Pending);
}
[Fact]
public async Task GetSummaryAsync_MissingOrder_ThrowsNotFoundException()
{
// Arrange
_repository.GetByIdAsync(99, Arg.Any<CancellationToken>())
.Returns((Order?)null);
// Act
var act = () => _sut.GetSummaryAsync(99);
// Assert
await act.Should().ThrowAsync<NotFoundException>()
.WithMessage("*Order*99*");
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public async Task GetSummaryAsync_InvalidId_ThrowsArgumentException(int invalidId)
{
var act = () => _sut.GetSummaryAsync(invalidId);
await act.Should().ThrowAsync<ArgumentOutOfRangeException>();
}
}
Integration Test Example
public sealed class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public OrdersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace real DB with in-memory for tests
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(opts =>
opts.UseInMemoryDatabase("TestDb"));
});
}).CreateClient();
}
[Fact]
public async Task GetOrder_ReturnsOk_WhenOrderExists()
{
var response = await _client.GetAsync("/api/orders/1");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<OrderSummary>();
body.Should().NotBeNull();
body!.Id.Should().Be(1);
}
}
Tooling
Essential Commands
dotnet new sln # Create solution
dotnet new webapi -n MyApp.Api # New Web API project
dotnet sln add src/MyApp.Api # Add project to solution
dotnet restore # Restore packages
dotnet build --no-restore # Build
dotnet test --no-build --verbosity normal # Run tests
dotnet test --collect:"XPlat Code Coverage" # With coverage
dotnet format # Format code
dotnet publish -c Release -o ./publish # Publish for deployment
Analyzers & EditorConfig
<!-- Directory.Build.props (shared across all projects) -->
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers"
Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="SonarAnalyzer.CSharp"
Version="9.32.0" PrivateAssets="all" />
</ItemGroup>
</Project>
# .editorconfig (key settings)
[*.cs]
indent_style = space
indent_size = 4
dotnet_sort_system_directives_first = true
csharp_style_namespace_declarations = file_scoped:warning
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion
csharp_style_prefer_pattern_matching = true:suggestion
csharp_prefer_simple_using_statement = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
References
For detailed patterns and examples, see:
- references/patterns.md -- Async patterns, DI registration, LINQ examples
External References
Weekly Installs
5
Repository
ar4mirez/samuelGitHub Stars
3
First Seen
Mar 1, 2026
Security Audits
Installed on
gemini-cli5
opencode5
codebuddy5
github-copilot5
codex5
kimi-cli5