Skip to main content
.NET Mastery: 40+ Essential Practices for Clean, Secure, and Scalable Code

1. Use Dependency Injection (DI) Properly

Bad:

var service = new MyService();

Good:

public class MyController
{
private readonly IMyService _service;
public MyController(IMyService service)
{
_service = service;
}
}

Why: Keeps code loosely coupled and testable.

2. Write Unit Tests — Even for Business Logic

Bad: No tests.
Good:

[Fact]
public void Should_Return_True_When_Valid()
{
var validator = new OrderValidator();
var result = validator.IsValid("ORD123");
Assert.True(result);
}

Why: Catches bugs early and prevents regressions.

3. Avoid Business Logic in Controllers

Bad:

public IActionResult Save(Order order)
{
if(order.Amount > 1000)
order.IsDiscounted = true;
// Save to DB
}

Good:

public IActionResult Save(Order order)
{
_orderService.ApplyDiscount(order);
_orderService.Save(order);
}

Why: Keeps controllers thin and logic testable.

4. Never Hardcode Configuration or Secrets

Wrong:

var apiKey = "SECRET123";

Right:

// appsettings.json
"MyService": {
"ApiKey": "xyz"
}
var key = _config["MyService:ApiKey"];

Why: Improves security and flexibility.

5. Use Global Exception Handling

Bad: No centralized error handling.
Good:

app.UseExceptionHandler("/error");

Or custom middleware:

public class ErrorHandlingMiddleware
{
public async Task Invoke(HttpContext context)
{
try { await _next(context); }
catch(Exception ex)
{
_logger.LogError(ex, "Unhandled Exception");
context.Response.StatusCode = 500;
}
}
}

Why: Avoids duplicated try-catch and consistent error logging.

6. Follow SOLID Principles

Bad: Bloated, tightly coupled classes.
Good: Classes with single responsibility, using interfaces, etc.
Why: Makes code maintainable and extensible.

7. Use Async/Await Properly

Bad:

var data = service.GetData().Result;

Good:

var data = await service.GetDataAsync();

Why: Prevents thread blocking and deadlocks.

8. Dispose What You Use

Bad:

var client = new HttpClient();

Good:

using (var client = new HttpClient()) { ... }

Why: Prevents resource leaks.

9. Use IHttpClientFactory

Bad: Instantiating HttpClient repeatedly.
Good:

services.AddHttpClient<IMyService, MyService>();

Why: Avoids socket exhaustion and improves testability.

10. Don’t Swallow Exceptions

Bad:

try { ... } catch { }

Good:

catch(Exception ex)
{
_logger.LogError(ex, "Operation failed");
}

Why: Hides bugs and makes debugging hard.

11. Use DTOs Instead of EF Models

Bad: Passing EF models directly to API.
Good:

public class OrderDto
{
public int Id { get; set; }
public decimal Amount { get; set; }
}

Why: Prevents over-posting and tight DB coupling.

12. Keep Method Size Small

Bad: Large, multi-purpose methods.
Good: Split into small, focused methods.
Why: Easier to understand, test, and maintain.

13. Use FluentValidation

Bad: Validation in models/controllers.
Good:

public class OrderValidator : AbstractValidator<OrderDto>
{
public OrderValidator()
{
RuleFor(x => x.Amount).GreaterThan(0);
}
}

Why: Keeps validation logic clean and separate.

14. Always Validate Inputs

Bad: Trusting user input.
Good:

[ApiController]
public class OrdersController : ControllerBase
{
[HttpPost]
public IActionResult Post(OrderDto order)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
...
}
}

Why: Prevents injection and runtime errors.

15. Use Meaningful Names

Bad:

DoTask();

Good:

GenerateReportForUser();

Why: Improves code readability.

16. Secure APIs with [Authorize]

Bad: No authorization attributes.
Good:

[Authorize(Roles = "Admin")]

Why: Prevents unauthorized access.

17. Clean Project Structure

Bad: All files in one folder.
Good:

Controllers/
Services/
Repositories/
Models/
DTOs/

Why: Improves navigation and scalability.

18. Implement Logging

Bad: No logs or only Console.WriteLine.
Good:

_logger.LogInformation("Order processed: {@order}", order);

Why: Aids debugging and monitoring.

19. Prefer Configuration Binding

Bad:

var value = config["MySettings:SomeValue"];

Good:

services.Configure<MySettings>(config.GetSection("MySettings"));

Why: Avoids magic strings and supports strong typing.

20. Use Middleware for Cross-Cutting Concerns

Bad: Duplicated logic in controllers.
Good: Logging, CORS, Auth, Exception handling in middleware.
Why: Cleaner controller code.

21. Cache Smartly

Bad: No caching for frequent requests.
Good:

IMemoryCache.TryGetValue(key, out result);

Why: Improves performance.

22. Paginate API Responses

Bad: Returning large datasets.
Good:

.Skip(page * pageSize).Take(pageSize);

Why: Prevents OOM and improves performance.

23. Profile with MiniProfiler or BenchmarkDotNet

Bad: No performance profiling.
Good: Use profiling tools to spot slow code.
Why: Identifies bottlenecks.

24. Handle Nulls with Null-Conditional Operator

Bad:

var name = user.Profile.Name;

Good:

var name = user?.Profile?.Name;

Why: Prevents NullReferenceException.

25. Avoid Static State

Bad:

public static int Count;

Good: Use DI with Singleton or Scoped services.
Why: Static state isn’t thread-safe and breaks testability.

26. Version Your APIs

Bad: No versioning.
Good:

[Route("api/v1/[controller]")]

Why: Prevents breaking existing clients.

27. Use Mapperly

Bad: Manual mapping everywhere.
Good:

var userDto = userMapper.ToUserDto(user);

Why: Reduces boilerplate mapping code.

28. Don’t Overuse Try-Catch

Bad: Wrapping everything in try-catch.
Good: Only wrap risky calls.
Why: Hides real problems.

29. Write Integration Tests

Bad: Only unit tests.
Good:

var client = factory.CreateClient();

Why: Ensures end-to-end safety.

30. Avoid Logging Sensitive Info

Bad:

_logger.Log("Token: " + token);

Good:

_logger.Log("Token: ****");

Why: Security risk.

31. Use Swagger for API Documentation

Bad: No API docs.
Good:

builder.Services.AddSwaggerGen();

Why: Makes APIs easy to test and understand.

32. Use CancellationToken

Bad: Ignoring cancellation.
Good:

public async Task GetOrders(CancellationToken cancellationToken)

Why: Allows graceful shutdowns.

33. Prefer Task.Run Only for CPU-Bound Work

Bad:

await Task.Run(() => File.ReadAllTextAsync(path));

Good:

await File.ReadAllTextAsync(path);

Why: Task.Run is for CPU-bound, not I/O-bound operations.

34. Use String Interpolation Over Concatenation

Bad:

"Order " + order.Id + " processed"

Good:

$"Order {order.Id} processed"

Why: Improves readability and performance.

35. Avoid Magic Numbers and Strings

Bad:

if (status == 3)

Good:

const int ApprovedStatus = 3;
if (status == ApprovedStatus)

Why: Makes code more maintainable.

36. Prefer Records for Immutable Data

Bad: Mutable DTOs for value objects.
Good:

public record UserDto(int Id, string Name);

Why: Records simplify immutable data structures.

37. Use Extension Methods Judiciously

Bad: Extension methods everywhere.
Good: Use for cross-cutting concerns, keep namespaces clean.
Why: Avoids namespace pollution.

38. Validate Third-Party Dependencies

Bad: Outdated or vulnerable NuGet packages.
Good: Regularly update and audit dependencies.
Why: Keeps your app secure.

39. Prefer Async Streams for Large Data Sets

Bad:

foreach(var item in await repo.GetAllAsync()) { ... }

Good:

await foreach(var item in repo.GetAllAsync())
{
...
}

Why: Efficient data processing.

40. Document Public APIs

Bad: No documentation.
Good:

/// <summary>
/// Gets all orders.
/// </summary>
public IEnumerable<OrderDto> GetOrders() { ... }

Why: Improves API usability.

41. Use Feature Folders for Large Projects

Bad: All Controllers in one folder.
Good:

Features/
Orders/
OrdersController.cs
OrderService.cs
OrderDto.cs

Why: Scales better for large teams.

42. Prefer Scoped Over Singleton Where Possible

Bad: Singleton for everything.
Good: Use Scoped for per-request services.
Why: Avoids unwanted shared state.

43. Monitor Health with HealthChecks

Bad: No health endpoints.
Good:

app.UseHealthChecks("/health");

Why: Enables monitoring and alerting.

44. Handle Time Zones Carefully

Bad: Storing local time in DB.
Good: Store UTC, convert for display.
Why: Prevents time zone bugs.

45. Use ValueTask for High-Performance Scenarios

Bad: Always use Task.
Good: Use ValueTask when async may complete synchronously.
Why: Improves performance.

46. Use Guard Clauses

Bad:

if (order == null) { /* ... */ }

Good:

if (order == null) throw new ArgumentNullException(nameof(order));

Why: Fail fast for invalid inputs.

47. Prefer Composition Over Inheritance

Bad: Deep inheritance hierarchies.
Good: Compose small, focused classes.
Why: Keeps code flexible.

48. Avoid Overusing Regions

Bad:

#region Everything
...
#endregion

Good: Use clear, small classes instead.
Why: Regions hide code smells.

49. Use Dependency Injection for Configuration

Bad:

var value = config["MySettings:SomeValue"];

Good:

public class MyService
{
public MyService(IOptions<MySettings> settings) { ... }
}

Why: Supports strong typing and testability.

50. Regularly Refactor Legacy Code

Bad: Letting technical debt grow.
Good: Schedule regular refactoring sessions.
Why: Keeps codebase healthy.

Final Thoughts:
Adopting these best practices will help you build .NET applications that are robust, maintainable, and secure — whether you’re solo or on a team!

👋Ultimate Collection of .NET Web Apps for Developers and Businesses
🚀 My YouTube Channel
💻 Github

Here are three ways you can help me out:
Please drop me a follow →👍 R M Shahidul Islam Shahed
Receive an e-mail every time I post on Medium → 💌 Click Here
Grab a copy of my E-Book on OOP with C# → 📚 Click Here

Leave a Reply