| name | csharp |
| description | Load when writing, reviewing, or debugging C# code — ASP.NET Core APIs, background services, console apps, or libraries. Covers the patterns Claude consistently gets wrong in production C#: async/await pitfalls, nullable reference types, DI patterns, cancellation tokens, and LINQ vs. loops. Do not load for Python, Go, or Rust.
|
C#: What Claude Gets Wrong in Production Code
This skill is not a C# tutorial. It covers the specific patterns that tend to be
wrong or dangerous in AI-generated C#. Read this before writing or reviewing any
C# in this project.
Async/Await: The Deadlock Traps
Generated C# async code frequently causes deadlocks or degrades performance through
a handful of consistent mistakes.
public IActionResult GetUser(Guid id)
{
var user = _userService.GetByIdAsync(id).Result;
return Ok(user);
}
public async Task<IActionResult> GetUser(Guid id)
{
var user = await _userService.GetByIdAsync(id);
return Ok(user);
}
public async Task<User> GetByIdAsync(Guid id)
{
var result = await _dbContext.Users.FindAsync(id);
return result ?? throw new NotFoundException(id);
}
public async Task<User> GetByIdAsync(Guid id)
{
var result = await _dbContext.Users.FindAsync(id).ConfigureAwait(false);
return result ?? throw new NotFoundException(id);
}
public async void OnButtonClick(object sender, EventArgs e)
{
await DoSomethingAsync();
}
public async Task HandleClickAsync()
{
await DoSomethingAsync().ConfigureAwait(false);
}
public async void OnButtonClick(object sender, EventArgs e)
{
try { await HandleClickAsync(); }
catch (Exception ex) { _logger.LogError(ex, "Click handler failed"); }
}
CancellationToken: Thread It Through Everything
Every async method that does I/O should accept a CancellationToken. ASP.NET Core
passes one automatically to action methods via HttpContext.RequestAborted — use it.
public async Task<IActionResult> GetOrders()
{
var orders = await _orderService.ListAsync();
return Ok(orders);
}
public async Task<IActionResult> GetOrders(CancellationToken cancellationToken)
{
var orders = await _orderService.ListAsync(cancellationToken);
return Ok(orders);
}
public async Task<List<Order>> ListAsync(CancellationToken ct = default)
{
return await _dbContext.Orders
.Where(o => o.IsActive)
.ToListAsync(ct)
.ConfigureAwait(false);
}
The pattern: CancellationToken ct = default in all async methods so callers
that don't care about cancellation don't have to pass anything.
Nullable Reference Types: Enable Them and Mean It
Nullable reference types (#nullable enable) are enabled project-wide in new .NET
projects. Never disable them, and don't paper over warnings with ! (the null-forgiving
operator) without a comment explaining why it's safe.
var user = await _repo.GetByIdAsync(id);
return user!.Email;
var user = await _repo.GetByIdAsync(id)
?? throw new NotFoundException($"User {id} not found");
return user.Email;
public class Order
{
public string CustomerName { get; set; }
}
public class Order
{
public required string CustomerName { get; init; }
public string? Notes { get; init; }
}
Dependency Injection: Constructor Injection, IOptions for Config
public class OrderService
{
public async Task<Order> CreateAsync(OrderRequest request)
{
var repo = ServiceLocator.Get<IOrderRepository>();
var email = ServiceLocator.Get<IEmailService>();
...
}
}
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly IEmailService _emailService;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository repo,
IEmailService emailService,
ILogger<OrderService> logger)
{
_repo = repo;
_emailService = emailService;
_logger = logger;
}
}
public class SnowflakeService
{
public SnowflakeService()
{
var account = Environment.GetEnvironmentVariable("SNOWFLAKE_ACCOUNT");
}
}
public class SnowflakeOptions
{
public const string Section = "Snowflake";
[Required] public string Account { get; init; } = "";
[Required] public string Warehouse { get; init; } = "";
public int StatementTimeoutSeconds { get; init; } = 300;
}
builder.Services.AddOptions<SnowflakeOptions>()
.BindConfiguration(SnowflakeOptions.Section)
.ValidateDataAnnotations()
.ValidateOnStart();
public class SnowflakeService
{
private readonly SnowflakeOptions _options;
public SnowflakeService(IOptions<SnowflakeOptions> options)
{
_options = options.Value;
}
}
Records vs. Classes: Use Records for Data
public record OrderCreated(Guid OrderId, Guid UserId, decimal Total, DateTime CreatedAt);
public class Order
{
public Guid Id { get; private set; }
public OrderStatus Status { get; private set; }
public void Cancel()
{
if (Status == OrderStatus.Shipped)
throw new InvalidOperationException("Cannot cancel a shipped order");
Status = OrderStatus.Cancelled;
}
}
var updated = original with { Status = OrderStatus.Confirmed };
LINQ: Readable Over Clever
Prefer method syntax for most LINQ, query syntax for complex multi-table joins.
Don't chain 8 LINQ operators when a loop is clearer.
var activeOrders = orders
.Where(o => o.IsActive && o.Total > 0)
.OrderByDescending(o => o.CreatedAt)
.Take(50)
.Select(o => new OrderSummary(o.Id, o.Total, o.CreatedAt))
.ToList();
var expensive = await _dbContext.Orders
.ToListAsync()
.ContinueWith(t => t.Result.Where(o => o.Total > 1000).ToList());
var expensive = await _dbContext.Orders
.Where(o => o.Total > 1000)
.ToListAsync(ct);
N+1 with EF Core — the same problem as SQLAlchemy:
var orders = await _dbContext.Orders.ToListAsync(ct);
foreach (var order in orders)
{
Console.WriteLine(order.Customer.Name);
}
var orders = await _dbContext.Orders
.Include(o => o.Customer)
.Include(o => o.Items).ThenInclude(i => i.Product)
.ToListAsync(ct);
Disposal: using Declarations Over using Statements
using (var connection = new SqlConnection(connectionString))
{
await connection.OpenAsync(ct);
}
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(ct);
await using var stream = new FileStream(path, FileMode.Open);
Logging: Structured, Not Interpolated
_logger.LogInformation($"Order {orderId} created by user {userId} for ${total}");
_logger.LogInformation(
"Order {OrderId} created by {UserId} for {Total}",
orderId, userId, total);
_logger.LogDebug($"Processing item {item.ToDetailedString()}");
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Processing item {@Item}", item);