Cursor Rules for C#: 6 Rules That Make AI Write Enterprise C# Code
If you use Cursor or Claude Code for C# development, you've watched the AI generate code that compiles but makes senior .NET engineers cringe. Async methods that return void. Services new-ing up dependencies instead of injecting them. LINQ queries rewritten as manual foreach loops. string concatenation where interpolation belongs.
The fix isn't better prompting. It's better rules.
Here are 6 cursor rules for C# that make your AI assistant write code that follows .NET conventions and passes code review the first time. Each one includes a before/after example so you can see exactly what changes.
1. Enforce async/await Correctly — Ban async void and .Result
Without this rule, AI generates async void methods (which swallow exceptions and crash your process) and calls .Result or .Wait() on tasks (which deadlock in ASP.NET). These are the two most common async bugs in C#.
The rule:
All async methods must return Task or Task<T>, never void.
The only exception is event handlers in UI frameworks.
Never call .Result or .Wait() on a Task — always await.
Suffix async method names with Async.
Use CancellationToken as the last parameter for all async methods.
Bad — what the AI generates without the rule:
public async void ProcessOrder(Order order)
{
var user = _userRepository.GetByIdAsync(order.UserId).Result;
await _paymentService.ChargeAsync(user, order.Total);
_notificationService.SendAsync(user.Email, "Order processed").Wait();
}
Good — what the AI generates with the rule:
public async Task ProcessOrderAsync(Order order, CancellationToken ct = default)
{
var user = await _userRepository.GetByIdAsync(order.UserId, ct);
await _paymentService.ChargeAsync(user, order.Total, ct);
await _notificationService.SendAsync(user.Email, "Order processed", ct);
}
No deadlocks. No swallowed exceptions. Cancellation propagates through the entire chain. When a client disconnects, every downstream operation respects it.
2. Enforce Constructor Injection with Interfaces — Ban new for Services
AI instantiates dependencies with new inside methods. This couples your classes together, makes unit testing require real implementations, and defeats the entire DI container that ASP.NET gives you for free.
The rule:
All service dependencies must be injected through the constructor as interfaces.
Never instantiate services with new inside a class.
Register services in Program.cs or a dedicated extension method.
Use primary constructors (C# 12) or readonly fields assigned in the constructor.
Bad — hard-coded dependencies:
public class OrderService
{
public async Task<Order> CreateOrderAsync(OrderRequest request)
{
var repo = new SqlOrderRepository(new DbContext());
var gateway = new StripePaymentGateway();
var notifier = new EmailNotificationService();
var order = new Order(request.UserId, request.Total);
await repo.SaveAsync(order);
await gateway.ChargeAsync(order);
await notifier.NotifyAsync(order.UserId, "Order created");
return order;
}
}
Good — constructor injection with interfaces:
public class OrderService(
IOrderRepository orderRepository,
IPaymentGateway paymentGateway,
INotificationService notificationService)
{
public async Task<Order> CreateOrderAsync(OrderRequest request, CancellationToken ct = default)
{
var order = new Order(request.UserId, request.Total);
await orderRepository.SaveAsync(order, ct);
await paymentGateway.ChargeAsync(order, ct);
await notificationService.NotifyAsync(order.UserId, "Order created", ct);
return order;
}
}
// Registration in Program.cs
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
Unit tests inject mocks. The DI container manages lifetimes. Swapping from Stripe to another provider is one line in Program.cs.
3. Enforce LINQ Over Manual Loops — Ban foreach for Transformations
Without this rule, AI writes foreach loops with manual list building for every collection operation. You end up with 15 lines of mutable state where 1 LINQ expression does the job.
The rule:
Use LINQ for filtering, transforming, and aggregating collections.
Reserve foreach for side effects only (logging, sending notifications, mutating external state).
Prefer method syntax (Where, Select, GroupBy) over query syntax.
Never call ToList() in the middle of a LINQ chain — materialize only at the end.
Bad — manual loops for everything:
public List<OrderSummary> GetActiveOrderSummaries(List<Order> orders)
{
var result = new List<OrderSummary>();
foreach (var order in orders)
{
if (order.Status == OrderStatus.Active)
{
if (order.Total > 0)
{
var summary = new OrderSummary
{
OrderId = order.Id,
CustomerName = order.Customer.Name,
Total = order.Total
};
result.Add(summary);
}
}
}
result.Sort((a, b) => b.Total.CompareTo(a.Total));
return result;
}
Good — LINQ pipeline:
public List<OrderSummary> GetActiveOrderSummaries(List<Order> orders) =>
orders
.Where(o => o.Status == OrderStatus.Active && o.Total > 0)
.OrderByDescending(o => o.Total)
.Select(o => new OrderSummary
{
OrderId = o.Id,
CustomerName = o.Customer.Name,
Total = o.Total,
})
.ToList();
One expression. No mutable list. The intent is clear: filter, sort, project. Adding a new filter condition is one line, not a nested if block.
4. Enforce Result Pattern — Ban Throwing Exceptions for Business Logic
AI throws exceptions for validation failures, not-found cases, and business rule violations. Exceptions are for exceptional situations — a database going down, not a user entering an invalid email.
The rule:
Use a Result<T> pattern for operations that can fail due to business rules.
Reserve exceptions for truly exceptional situations (infrastructure failures, bugs).
Never use exceptions for control flow (validation, not-found, permission denied).
Return a discriminated result that the caller must handle explicitly.
Bad — exceptions for control flow:
public User GetUser(long id)
{
var user = _dbContext.Users.Find(id);
if (user == null)
throw new NotFoundException($"User {id} not found");
if (!user.IsActive)
throw new BusinessException("User is deactivated");
return user;
}
Good — result pattern:
public abstract record GetUserResult
{
public record Success(User User) : GetUserResult;
public record NotFound(long UserId) : GetUserResult;
public record Deactivated(long UserId) : GetUserResult;
}
public GetUserResult GetUser(long id)
{
var user = _dbContext.Users.Find(id);
if (user is null)
return new GetUserResult.NotFound(id);
if (!user.IsActive)
return new GetUserResult.Deactivated(id);
return new GetUserResult.Success(user);
}
// Caller — compiler warns about unhandled cases with pattern matching
var response = _userService.GetUser(id) switch
{
GetUserResult.Success s => Ok(s.User),
GetUserResult.NotFound nf => NotFound($"User {nf.UserId} not found"),
GetUserResult.Deactivated d => BadRequest($"User {d.UserId} is deactivated"),
_ => StatusCode(500),
};
Every failure mode is a type. Pattern matching forces the caller to handle each case. No more missing catch blocks in production.
5. Enforce Naming Conventions — Ban Java/Python Style Names
AI trained on multi-language codebases mixes naming conventions. You see getUserById (Java) and get_user_by_id (Python) in the same codebase. C# has its own clear conventions and deviation looks wrong to every .NET developer.
The rule:
PascalCase for public members, types, methods, properties, and namespaces.
camelCase for private fields, local variables, and parameters.
Prefix private fields with underscore: _userRepository.
Prefix interfaces with I: IUserService.
Use Async suffix for async methods: GetUserAsync.
Never use Hungarian notation or abbreviations (no strName, no btn, no svc).
Bad — mixed conventions:
public class user_service
{
private IUserRepository userRepo;
public async Task<User> getUserById(int user_id)
{
var db_result = await userRepo.find(user_id);
return db_result;
}
}
Good — idiomatic C# naming:
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<User> GetUserByIdAsync(int userId)
{
var user = await _userRepository.FindAsync(userId);
return user;
}
}
Every .NET developer reads this instantly. Tooling supports it. Code analysis rules enforce it. It looks like it belongs in the ecosystem.
6. Enforce Record Types for Immutable Data — Ban Mutable DTOs
AI generates classes with public setters for API requests and responses. The data gets mutated somewhere mid-pipeline, and you spend an hour figuring out where order.Status changed from "pending" to "cancelled" without a trace.
The rule:
Use record types for DTOs, API contracts, and value objects.
Use required properties for mandatory fields.
Use init-only setters when a record needs incremental construction.
Reserve classes for entities with mutable state and behavior.
Bad — mutable DTO:
public class CreateOrderRequest
{
public long UserId { get; set; }
public decimal Total { get; set; }
public List<string> Items { get; set; }
}
Good — immutable record:
public record CreateOrderRequest(
long UserId,
decimal Total,
IReadOnlyList<string> Items
);
// For responses with optional fields
public record OrderResponse
{
public required long OrderId { get; init; }
public required string Status { get; init; }
public required decimal Total { get; init; }
public DateTimeOffset? ShippedAt { get; init; }
}
Records give you value equality, immutability, and with-expressions for creating modified copies. The data flows through your pipeline without surprise mutations.
Put These Rules to Work
These 6 rules cover the patterns where AI coding assistants fail most often in C# projects. Add them to your .cursorrules or CLAUDE.md and the difference is immediate — fewer review comments, idiomatic code from the first generation, and less time rewriting AI output.
I've packaged these rules (plus 44 more covering ASP.NET, Entity Framework, Blazor, and testing patterns) into a ready-to-use rules pack: Cursor Rules Pack v2
Drop it into your project directory and stop fighting your AI assistant.













