🚨 15 Common Mistakes C#/.NET Developers Make (And How to Avoid Them)
I've reviewed many C#/.NET projects over the years—from small internal tools to large enterprise applications—and one thing is surprisingly consistent:
Most production issues aren't caused by C# itself.
They're caused by the way we write, structure, and maintain our code.
As developers, we all make mistakes. I've made them too. The important part is recognizing them early, learning from them, and continuously improving.
Here are some of the most common mistakes I see in C#/.NET development, along with practical solutions and examples.
1. Writing Classes That Do Everything
One of the biggest code smells is creating a class that handles multiple responsibilities.
Instead of focusing on one job, it manages database operations, business rules, logging, email notifications, validation, and more.
❌ Bad Example
public class OrderService
{
public void CreateOrder(Order order)
{
SaveToDatabase(order);
SendEmail(order);
GenerateInvoice(order);
UpdateInventory(order);
}
}
This class becomes difficult to test, maintain, and extend.
âś… Better Approach
Split responsibilities into dedicated services.
public interface IEmailService
{
Task SendConfirmationAsync(Order order);
}
public interface IInventoryService
{
Task UpdateStockAsync(Order order);
}
âś” How to Solve
- Follow the Single Responsibility Principle (SRP).
- Keep classes focused on one responsibility.
- Use Dependency Injection to compose services.
2. Blocking Asynchronous Code
Many developers accidentally destroy application performance by blocking asynchronous operations.
❌ Bad Example
var users = GetUsersAsync().Result;
or
GetUsersAsync().Wait();
These can cause deadlocks and reduce scalability.
âś… Better
var users = await GetUsersAsync();
âś” How to Solve
- Use
async and await consistently.
- Avoid mixing synchronous and asynchronous code.
3. Forgetting to Dispose Resources
Database connections, file streams, and network resources should always be released.
❌ Bad Example
var connection = new SqlConnection(connectionString);
connection.Open();
âś… Better
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
âś” How to Solve
Always use:
using
await using
IDisposable
This prevents memory leaks and resource exhaustion.
4. Swallowing Exceptions
Ignoring exceptions makes debugging nearly impossible.
❌ Bad Example
try
{
Save();
}
catch
{
}
The application fails silently.
âś… Better
try
{
Save();
}
catch(Exception ex)
{
logger.LogError(ex, "Unable to save order.");
throw;
}
âś” How to Solve
- Catch only expected exceptions.
- Log meaningful information.
- Never ignore errors.
5. Putting Business Logic Inside Controllers
Controllers should only receive requests and return responses.
❌ Bad Example
public IActionResult Create(Order order)
{
// Hundreds of lines of business logic
}
âś… Better
public IActionResult Create(Order order)
{
return Ok(_orderService.Create(order));
}
âś” How to Solve
Keep controllers thin.
Move business logic into dedicated services.
6. Loading Too Much Data
Many applications become slow because they retrieve entire tables when only a few records are needed.
❌ Bad Example
var users = context.Users.ToList();
âś… Better
var user = await context.Users
.FirstOrDefaultAsync(x => x.Id == id);
Or project only the required fields.
var users = await context.Users
.Select(x => new UserDto
{
Name = x.Name,
Email = x.Email
})
.ToListAsync();
âś” How to Solve
- Select only required columns.
- Use pagination.
- Avoid unnecessary queries.
7. Hardcoding Configuration
❌ Bad Example
string apiKey = "123456789";
âś… Better
var apiKey = builder.Configuration["ApiKey"];
âś” How to Solve
Store secrets in:
- Environment Variables
- Azure Key Vault
- AWS Secrets Manager
- .NET User Secrets
Never commit secrets to Git.
8. Returning Database Entities Directly
❌ Bad Example
return Ok(user);
Sensitive fields may accidentally be exposed.
âś… Better
return Ok(new UserDto
{
Name = user.Name,
Email = user.Email
});
âś” How to Solve
Always return DTOs instead of database entities.
9. Ignoring Nullable Reference Types
NullReferenceException remains one of the most common production errors.
❌ Bad Example
string name = GetName();
Console.WriteLine(name.Length);
âś… Better
string? name = GetName();
if(name != null)
{
Console.WriteLine(name.Length);
}
âś” How to Solve
Enable Nullable Reference Types and let the compiler help you catch potential null issues before deployment.
10. Poor Logging
❌ Bad Example
Console.WriteLine("Something failed");
âś… Better
logger.LogInformation(
"User {UserId} logged in",
userId);
âś” How to Solve
Use structured logging with ILogger, Serilog, or NLog.
Log useful context not just messages.
11. Skipping Unit Tests
Applications without automated tests become harder to maintain.
Example
Assert.Equal(expected, actual);
âś” How to Solve
Write unit tests for:
- Business logic
- Services
- Validation
- Critical workflows
Testing saves countless hours of debugging later.
12. Creating the N+1 Query Problem
❌ Bad Example
foreach(var order in orders)
{
Console.WriteLine(order.Customer.Name);
}
This may generate hundreds of SQL queries.
âś… Better
var orders = await context.Orders
.Include(x => x.Customer)
.ToListAsync();
âś” How to Solve
Understand:
- Lazy Loading
- Eager Loading
- Explicit Loading
Choose the right strategy for your scenario.
❌ Bad Example
public IActionResult Register(User user)
{
Save(user);
}
âś… Better
if(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
âś” How to Solve
Always validate:
- Input
- API requests
- Business rules
Never trust client-side validation alone.
14. Ignoring Dependency Injection
❌ Bad Example
public class ProductService
{
private SqlRepository repo = new SqlRepository();
}
âś… Better
public class ProductService
{
private readonly IRepository _repository;
public ProductService(IRepository repository)
{
_repository = repository;
}
}
âś” How to Solve
Depend on abstractions, not concrete implementations.
Dependency Injection improves testing, flexibility, and maintainability.
15. Optimizing Too Early
Some developers spend days optimizing code that isn't actually causing performance problems.
Optimization without measurement often wastes time.
âś” How to Solve
Measure first.
Use profiling tools like:
- Visual Studio Profiler
- dotTrace
- BenchmarkDotNet
Optimize only where data shows a real bottleneck.
Final Thoughts
Great C#/.NET developers aren't defined by never making mistakes they're defined by recognizing them, learning from them, and building better software with every project.
Clean architecture, meaningful testing, proper error handling, efficient database access, secure coding practices, and continuous learning are what separate production-ready applications from code that constantly creates maintenance headaches.
Every line of code is an opportunity to improve not just the software, but also the developer behind it.
What other C#/.NET mistakes have you encountered during development or code reviews? Share your experience in the comments. Let's help each other write cleaner, more reliable software.