Chapter 25: Best Practices and Tools
Everything we’ve learned so far (OOP, LINQ, async, modern C# features…) is the foundation. Now we’re going to learn how to write professional, clean, maintainable, testable, and high-performance C# code – the kind that real companies pay big salaries for!
We’ll cover:
- Unit Testing (xUnit – the modern favorite)
- Dependency Injection (the heart of modern .NET apps)
- Git & Version Control (essential for every developer)
- Design Patterns & SOLID Principles (writing beautiful, scalable code)
- Debugging & Performance (finding bugs fast + making code fly)
I’m going to explain everything very slowly, step by step, with tons of real-life examples, before & after comparisons, and practical mini-projects — just like we’re sitting together in Hyderabad looking at the same screen. Let’s dive in! 🚀
1. Unit Testing – Writing Code You Can Trust (xUnit)
Why unit testing?
- Catch bugs early (before they reach production)
- Refactor fearlessly (know nothing breaks)
- Document your code (tests show how it should be used)
- Make code maintainable (tests are living documentation)
xUnit is the modern, clean, community-favorite testing framework in .NET (preferred over NUnit & MSTest in 2026).
Setup:
- Create a new test project: dotnet new xunit -o MyApp.Tests
- Add reference to your main project: dotnet add MyApp.Tests/MyApp.Tests.csproj reference ../MyApp/MyApp.csproj
Simple Example – Testing a Calculator
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Calculator.cs (in main project) public class Calculator { public int Add(int a, int b) => a + b; public int Divide(int a, int b) { if (b == 0) throw new DivideByZeroException("Cannot divide by zero!"); return a / b; } } |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// CalculatorTests.cs (in test project) using Xunit; public class CalculatorTests { [Fact] // Simple test public void Add_TwoPositiveNumbers_ReturnsSum() { // Arrange var calc = new Calculator(); // Act int result = calc.Add(5, 3); // Assert Assert.Equal(8, result); } [Theory] // Data-driven test [InlineData(10, 2, 5)] [InlineData(20, 4, 5)] [InlineData(100, 25, 4)] public void Divide_ValidInput_ReturnsCorrectResult(int a, int b, int expected) { var calc = new Calculator(); int result = calc.Divide(a, b); Assert.Equal(expected, result); } [Fact] public void Divide_ByZero_ThrowsDivideByZeroException() { var calc = new Calculator(); Assert.Throws<DivideByZeroException>(() => calc.Divide(10, 0)); } } |
Run tests:
|
0 1 2 3 4 5 6 |
dotnet test |
Best Practice Tips (2026):
- One assert per test (clear what fails)
- Use AAA pattern (Arrange, Act, Assert)
- Name tests clearly: Method_WhenCondition_ExpectedResult
- Use [Fact] for single cases, [Theory] + [InlineData] for many cases
- Aim for 80–90% code coverage (but focus on important logic)
2. Dependency Injection (DI) – The Heart of Modern .NET
What is DI? Instead of creating dependencies inside a class (tight coupling), you inject them from outside (loose coupling).
Why DI?
- Easy to swap implementations (real vs mock)
- Easy to test (inject fake services)
- Promotes SOLID (especially Dependency Inversion)
Built-in DI in ASP.NET Core (also usable in console apps)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// Services.cs public interface INotifier { void Send(string message); } public class EmailNotifier : INotifier { public void Send(string message) => Console.WriteLine($"Email: {message}"); } public class SmsNotifier : INotifier { public void Send(string message) => Console.WriteLine($"SMS: {message}"); } // Program.cs (top-level statements) using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection(); // Register services services.AddTransient<INotifier, EmailNotifier>(); // or SmsNotifier var serviceProvider = services.BuildServiceProvider(); // Use it var notifier = serviceProvider.GetRequiredService<INotifier>(); notifier.Send("Hello from DI! 🚀"); |
Real-world example – In ASP.NET Core Web API
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// Startup.cs or Program.cs (.NET 6+) builder.Services.AddScoped<INotifier, EmailNotifier>(); // Controller [ApiController] [Route("[controller]")] public class NotificationController : ControllerBase { private readonly INotifier _notifier; public NotificationController(INotifier notifier) { _notifier = notifier; } [HttpPost] public IActionResult Send([FromBody] string message) { _notifier.Send(message); return Ok("Sent!"); } } |
Lifetimes (very important):
- Transient → new instance every time requested
- Scoped → new instance per HTTP request / scope
- Singleton → same instance for entire application lifetime
3. Git & Version Control – Essential for Every Developer
Git = the industry standard for tracking code changes.
Quick Setup (2026):
- Install Git → https://git-scm.com/
- Create account on GitHub, GitLab, or Azure DevOps
- Configure Git:
|
0 1 2 3 4 5 6 7 |
git config --global user.name "Webliance" git config --global user.email "your.email@example.com" |
Basic Workflow (Console):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# 1. Create project folder mkdir MyAwesomeApp cd MyAwesomeApp # 2. Initialize Git git init # 3. Add files git add . # 4. First commit git commit -m "Initial commit - Hello World app" # 5. Create remote repo on GitHub → copy URL git remote add origin https://github.com/Webliance/MyAwesomeApp.git # 6. Push git push -u origin main # Later: work → stage → commit → push git add . git commit -m "Added user authentication" git push |
Best Practice Tips:
- Commit small & often (atomic commits)
- Write clear commit messages
- Use branches (git branch feature/login, git checkout -b feature/login)
- Use pull requests for code reviews
- Use .gitignore (ignore bin/, obj/, packages/, *.user…)
4. Design Patterns & SOLID Principles
SOLID = 5 principles for writing clean, maintainable code:
- Single Responsibility Principle → One class = one responsibility
- Open-Closed Principle → Open for extension, closed for modification
- Liskov Substitution Principle → Derived classes must be substitutable for base
- Interface Segregation Principle → Many small interfaces > one big
- Dependency Inversion Principle → Depend on abstractions, not concretions
Common Design Patterns (most used in C#):
| Pattern | When to use | Simple Example |
|---|---|---|
| Singleton | One instance only (logger, config) | public static readonly Lazy<Logger> Instance = new(() => new Logger()); |
| Factory | Create objects without knowing exact type | IVehicle vehicle = VehicleFactory.Create(“car”); |
| Repository | Abstract data access (DB, file…) | IRepository<User> userRepo = new UserRepository(); |
| Dependency Injection | Already covered – inject services | Constructor injection |
| Observer | Event system (like events & delegates) | Publisher → Subscribers |
5. Debugging & Performance – Become a Pro
Debugging in Visual Studio 2026:
- F5 → Start debugging
- F9 → Set breakpoint
- F10 → Step over
- F11 → Step into
- Shift+F11 → Step out
- Watch window → Watch variables
- Immediate window → Run code while debugging
- Diagnostic Tools → See CPU, memory usage live
Performance Tips (2026):
- Use async/await for I/O → don’t block threads
- Use Span<T>, Memory<T> for high-performance string/byte work
- Use ValueTask instead of Task when result is often sync
- Use List<T>.Capacity to pre-allocate
- Profile with dotTrace or Visual Studio Profiler
Mini-Project: Testable & Injectable Logger
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public interface ILogger { void Log(string message); } public class ConsoleLogger : ILogger { public void Log(string message) => Console.WriteLine($"[LOG] {message}"); } public class Service { private readonly ILogger _logger; public Service(ILogger logger) { _logger = logger; } public void DoWork() { _logger.Log("Starting work..."); // ... _logger.Log("Work finished!"); } } // In Program.cs var services = new ServiceCollection(); services.AddSingleton<ILogger, ConsoleLogger>(); services.AddTransient<Service>(); var service = services.BuildServiceProvider().GetRequiredService<Service>(); service.DoWork(); |
Summary – What We Learned Today
- Unit testing → xUnit + AAA pattern
- Dependency Injection → loose coupling, testability
- Git → version control, collaboration
- SOLID + patterns → clean, scalable code
- Debugging & performance → find bugs fast, make code fast
Your Final Homework (Real-World Ready!)
- Take any console project you built earlier
- Add xUnit tests for at least 5 important methods
- Refactor it to use Dependency Injection
- Put the project on GitHub with a good README
- Bonus: Add a simple ASP.NET Core API version of it
You’ve now completed the full journey from beginner to professional-level C# developer! 🎉 You can proudly say: “I know modern C# – from basics to building real applications.”
Any final questions? Want help polishing a project for your portfolio? Just tell me — I’m right here for you forever! 💙
You’re amazing! Keep coding, keep learning, keep shining! 🌟
