C# Best Practices and Interview Q&A
Level: Intermediate to Advanced
Goal: Write professional-quality C# code that others can read and maintain.
Good code is:
- Readable: Easy to understand at a glance
- Maintainable: Easy to change without breaking things
- Performant: Runs efficiently
- Testable: Can be tested easily
This article brings together lessons from previous 29 articles.
- C# naming conventions - PascalCase, camelCase, _camelCase
- Code quality rules - readonly, var, null checks, async/await
- 20+ interview questions covering core C# concepts
- SOLID principles in practice
- Common mistakes and how to avoid them
- Code review checklist for quality code
- Performance tips and profiling
- Real-world best practices from .NET industry
Best Practices Quick Reference
| Area | Practice | Why | Example |
|---|---|---|---|
| Naming | PascalCase for classes/methods | Convention, readability | StudentService, GetGrade() |
| Naming | camelCase for locals/params | C# standard | studentName, totalMarks |
| Naming | _camelCase for private fields | Distinguish from locals | private int _count; |
| Naming | I-prefix for interfaces | Identify contracts | IStudentRepository, IGradable |
| Code | Use readonly when possible | Immutability, thread-safety | private readonly IRepo _repo; |
| Code | var when type obvious | Cleaner syntax | var students = new List<Student>(); |
| Code | ArgumentNullException.ThrowIfNull | Guard clause | .ThrowIfNull(student) |
| Code | using for disposables | Always cleanup | using var conn = new SqlConnection(); |
| Async | Never .Result or .Wait() | Deadlock risk | Always await |
| Collections | Return IEnumerable<T> from methods | Flexibility | IEnumerable<Student> GetAll() |
| Error Handling | Catch specific exceptions | Clarity | catch (FileNotFoundException) not catch (Exception) |
| Performance | Use StringBuilder in loops | Avoid GC pressure | Loop through items, append to SB |
Quick Definitions
- Best practice - Standard approach for writing quality code
- Naming convention - PascalCase, camelCase, CONSTANT_CASE rules
- Code review - Team checks code before merge
- SOLID - Five principles for maintainable code
- DRY - Don't Repeat Yourself (reuse code)
- YAGNI - You Aren't Gonna Need It (no over-engineering)
- Immutable - Cannot change after creation
- Return early - Guard clauses to exit early
- Single responsibility - One class = one reason to change
- Code smell - Pattern indicating deeper problem
When You'll Use This in SMS
SMS code should follow best practices:
// OK GOOD - readable names, clear intent
public async Task<StudentEnrollmentResult> EnrollStudentAsync(
Student student,
string className,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(student);
if (student.Age < 5 || student.Age > 25)
return StudentEnrollmentResult.InvalidAge;
// Process enrollment
return await _enrollmentService.ProcessAsync(student, className, ct);
}
// Wrong BAD - unclear names, mixed concerns, no error handling
public StudentEnrollmentResult E(Student s, string c)
{
var result = ProcessStudent(s, c, LogError, SendEmail, SaveDb);
return result; // What could go wrong?
}
Real impact: Without practices = unmaintainable code, slow onboarding, frequent bugs. With practices = clean, testable, sustainable codebase.
Try This Now
- Review existing SMS code for naming clarity
- Refactor one method: add guard clauses, single responsibility
- Extract 3 magic strings to named constants
- Add XML documentation to 5 public methods
- Check code follows SOLID principles
Best practices explained: naming conventions, SOLID principles, code smells, when to refactor, interview tips. Video coming soon. Subscribe to NexCoding YouTube for updates.
Naming Conventions
// Classes, Methods, Properties - PascalCase
public class StudentService { }
public void CalculateGrade() { }
public string FullName { get; }
// Local variables, parameters - camelCase
string studentName = "";
int totalMarks = 0;
// Private fields - _camelCase
private readonly IStudentRepository _repository;
private int _count;
// Constants - PascalCase or UPPER_SNAKE
const int MaxStudentsPerClass = 40;
const string DefaultSection = "A";
// Interfaces - I prefix
public interface IStudentRepository { }
public interface IGradable { }
// Async methods - Async suffix
public async Task<Student> GetStudentAsync(int id) { }
Code Quality Rules
// 1. Use readonly for fields set only in constructor
private readonly IStudentRepository _repo;
private readonly string _connectionString;
// 2. Prefer var when type is obvious
var students = new List<Student>(); // ?
var result = GetSomething(); // -- - unclear
// 3. Null checks - use modern syntax
ArgumentNullException.ThrowIfNull(student); // .NET 6+
ArgumentException.ThrowIfNullOrEmpty(name); // .NET 7+
// 4. async all the way - never block
// -- Deadlock risk
var data = GetDataAsync().Result;
// ?
var data = await GetDataAsync();
// 5. using for disposables - always
using var conn = new SqlConnection(cs);
using var writer = new StreamWriter(path);
// 6. StringBuilder in loops
var sb = new StringBuilder();
foreach (var s in students) sb.AppendLine(s.Name);
// 7. Return IEnumerable not List from methods
IEnumerable<Student> GetPassedStudents() { } // ? flexible
// Return List only if caller needs to add/remove
// 8. Early return - no deep nesting
// ? Deep nesting
if (student != null)
if (student.IsActive)
if (student.Percentage >= 35)
Console.WriteLine("Pass");
// ? Early return / guard clauses
if (student is null) return;
if (!student.IsActive) return;
if (student.Percentage < 35) return;
Console.WriteLine("Pass");
Common Mistakes
? Using .Result or .Wait() on async methods (deadlock risk):
var data = GetDataAsync().Result; // Blocks thread, can deadlock
? Always use await:
var data = await GetDataAsync(); // Thread free, no deadlock
? Creating new object in loop (GC pressure):
var results = new List<Student>();
foreach (var id in studentIds)
{
var student = new Student(); // Created 1000 times
results.Add(student);
}
? Create once, reuse:
var student = new Student();
foreach (var id in studentIds)
{
student.Id = id;
results.Add(student);
}
? Not checking null before using:
var student = GetStudent(999);
Console.WriteLine(student.Name); // NullReferenceException if not found
? Guard clause:
var student = GetStudent(999);
if (student is null) return;
Console.WriteLine(student.Name); // Safe
Best Practices Summary
- Follow naming conventions - Consistency aids readability
- Use
readonlyfor immutable fields - Prevent accidental changes - Prefer
varfor obvious types - Cleaner code - Always validate inputs - Fail fast with clear messages
- Use
async/awaitconsistently - Never.Resultor.Wait() - Return interfaces, not concrete types - Flexibility for callers
- Write small methods - One responsibility, easy to test
- Use early return - Avoid deep nesting
- Use
StringBuilderin loops - Avoid GC pressure - Test all paths - Unit tests catch edge cases
- Document complex logic - Comments explain "why", not "what"
- Profile before optimizing - Measure, don't guess
Top 20+ C# Interview Questions
Value type: Stored on stack. Copied on assignment. int, double, bool, struct.
Reference type: Stored on heap. Reference copied on assignment. class, string, array.
int x = 10;
int y = x; // y = 10, copy of value
y = 20; // x still 10
var s1 = new Student { Name = "Sahasra" };
var s2 = s1; // s2 = reference to same object
s2.Name = "Priya"; // s1.Name also "Priya"!
Value types safe for math. Reference types for objects. Know the difference.
Boxing = value type ? object (heap allocation). Unboxing = object ? value type.
int value = 42;
object boxed = value; // Boxing - allocation on heap
int unboxed = (int)boxed; // Unboxing - copy from heap
Avoid in loops - causes GC pressure, allocations slow down code.
Abstract class: IS-A relationship. Shared code, single inheritance.
Interface: CAN-DO capability. Multiple implementation, no code (pre-C# 8).
Abstract for related types with shared code. Interfaces for capabilities.
IEnumerable: In-memory. LINQ runs in C#. All data loaded first.
IQueryable: Translates LINQ to SQL (EF Core). Filtered at database.
For DB: use IQueryable, let database filter. For in-memory: IEnumerable OK.
async = marks method as asynchronous. await = suspends until Task completes. Thread freed for other work.
Key: Thread doesn't block during I/O. Can handle 1000s concurrent requests.
Delegate: Type-safe method reference. Caller can invoke directly.
Event: Delegate + protection. Outside code can only +=, -=, not invoke.
Events safer - encapsulation. Publisher controls when notifications sent.
Language Integrated Query - query collections in C# syntax.
Works on: List, Array, IQueryable (DB), XML.
Deferred execution - query runs when enumerated (.ToList(), foreach).
Powerful for data processing, less code than loops.
throw; = preserves stack trace. throw ex; = resets to catch block.
Always use throw; when re-throwing.
Type safety without duplication. List<Student> instead of ArrayList.
Repository<T> works for any entity type. Write once, use everywhere.
Cannot be inherited. Used when no further extension allowed.
string is sealed. records sealed by default.
Useful when: design complete, no subclassing needed, performance critical.
const = compile-time, must initialize at declaration. readonly = runtime, set in constructor.
Use const for never-changing values. Use readonly for values known at runtime.
When class holds unmanaged resources: file handles, DB connections, HTTP clients.
Pattern: Dispose(bool), GC.SuppressFinalize, ~Finalizer.
Always use with using statement. See Article 24 for full Dispose pattern.
String: == compares value.
Class: == compares reference, .Equals() compares value.
Record: == compares value (built-in).
Override .Equals() for custom classes if value equality needed.
Immutable reference type with value equality built-in.
Best for DTOs, API responses, value objects.
Records = concise, immutable, value-equal. Perfect for data.
Lazy initialization. Instance created only when first accessed. Thread-safe by default.
Used for: Singleton pattern (thread-safe), expensive initialization.
Lazy iterator - generates values one at a time.
IEnumerable method with yield return pauses at each return.
See Article 22 for full yield explanation.
C# 8+ feature. string? = nullable, string = non-nullable (warning if null).
Catches NullReferenceException at compile time.
See Article 23 for nullable types deep dive.
Must be: static class, static method, first param = this T.
Cannot: access private members, override existing methods.
Can: extend sealed classes, built-in types (string, int, IEnumerable).
See Article 21 for extension methods.
Objects receive dependencies from outside rather than creating them.
Makes code testable (inject mock in tests, real in production).
ASP.NET Core has built-in DI container (IServiceCollection).
DI = flexibility, testability, loose coupling.
- Use
async/awaitfor I/O - never.Resultor.Wait() StringBuilderfor string concatenation in loopsSpan<T>for zero-allocation string/array slicing- Avoid LINQ in hot paths - use for loops
- Pool objects with
ArrayPool<T>/ObjectPool<T> - Profile before optimizing - measure, don't guess
Profiling > guessing. Measure first.
Use ChatGPT, Claude, or Copilot to go deeper on C# best practices and interview preparation. Try these prompts:
"Quiz me on top C# interview questions - ask 5 questions one by one and wait for my answers""What are the most common C# mistakes junior developers make in job interviews?""What C# topics should I focus on for a .NET backend developer job interview?""Review this C# code and tell me what best practices I am violating"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.
?? C# Complete!
Congratulations - you have covered all 30 C# articles. You are ready to start ASP.NET Core.
Next Section: .NET Introduction ->