C# Best Practices and Common Mistakes
A closing checklist of habits that separate solid, idiomatic C# from code that merely compiles.
3 min read
Code that compiles and runs correctly can still be fragile, hard to maintain, or quietly wasteful. A few habits make the difference between C# that merely works and C# a team can build on.
Follow .NET naming conventions
// Avoid
public class user_account
{
public string user_name;
public void get_data() { }
}
// Prefer
public class UserAccount
{
public string UserName { get; set; }
public void GetData() { }
}.NET convention is PascalCase for classes, methods, and public properties, and camelCase for local variables and parameters (with a leading underscore, _fieldName, being a common convention for private fields). Consistency here isn't cosmetic — every C# developer's tooling and instincts are built around these conventions, so deviating makes code measurably harder for others to scan.
Don't swallow exceptions
// Avoid: the error vanishes with no trace
try
{
ProcessOrder(order);
}
catch (Exception)
{
}
// Prefer: handle it meaningfully, or let it propagate
try
{
ProcessOrder(order);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
throw;
}An empty catch block is one of the most damaging patterns in any language — the program keeps running as if nothing happened, and the actual bug surfaces somewhere completely unrelated, far harder to trace back.
Prefer immutability where practical
// Avoid: mutable state that any code can reach in and change
public class Order
{
public List<Item> Items = new();
}
// Prefer: an immutable snapshot, changed only through explicit methods
public record Order(IReadOnlyList<Item> Items);Immutable data (records, readonly fields, init-only properties) is easier to reason about because it can't change out from under you after it's handed off — especially valuable once async code or multiple threads are involved.
Use LINQ, but don't force it
// Avoid: LINQ used where a simple loop is clearer
var result = items.Select((x, i) => new { x, i })
.Aggregate(new StringBuilder(), (sb, xi) => sb.Append(xi.i).Append(xi.x));
// Prefer: a plain loop, because that's what this actually needs
var sb = new StringBuilder();
for (int i = 0; i < items.Count; i++)
{
sb.Append(i).Append(items[i]);
}LINQ is excellent for filtering, transforming, and querying — it gets worse the moment you're fighting it to express something a plain loop would say more plainly. If a LINQ chain needs a comment to explain what it's doing, that's usually a sign a loop would read better.
Dispose of resources properly
Always wrap IDisposable resources (file handles, database connections, HttpClient instances used outside of dependency injection) in using, as covered in the exceptions lesson — a forgotten Dispose() call is a slow, hard-to-diagnose resource leak, not an obvious crash.
A final checklist
- [ ] Nullable reference types are enabled, and warnings are treated as real signal, not noise.
- [ ] Public members are properties, not raw public fields.
- [ ] Exceptions are specific, never swallowed silently, and reserved for genuinely exceptional cases.
- [ ]
IDisposableresources are wrapped inusing. - [ ] Async methods are named with an
Asyncsuffix and awaited all the way up the call chain. - [ ] Naming follows .NET conventions (
PascalCasefor public members,camelCasefor locals). - [ ] LINQ is used where it clarifies intent, not where a loop would be clearer.
None of these are exotic techniques — they're the same language features covered throughout this course, applied with a bit more discipline. That discipline is most of what separates C# that merely runs from C# that's genuinely built to last.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.