
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