Introduction
In this article, we will learn about 10 Secrets Senior Developers Use to Make Controllers 10× Better (.NET).
Let's get started.
1. Controllers Should Be Painfully Thin
Junior mindset: “The controller is where I put my logic.”
Senior mindset: “If my controller does more than mapping → calling → returning, it’s a smell.”
✅ Good controller responsibilities
HTTP concerns (route, status code, headers)
Model binding & validation
Calling application/domain services
❌ Bad responsibilities
Business rules
Data access
Calculations
Condition-heavy logic
Before (junior-style):
[HttpPost]
public IActionResult CreateOrder(CreateOrderDto dto)
{
if (dto.Quantity <= 0) return BadRequest();
var price = dto.Quantity * 100;
var order = new Order { Price = price };
_db.Orders.Add(order);
_db.SaveChanges();
return Ok(order);
}
After (senior-style):
[HttpPost]
public async Task<IActionResult> CreateOrder(
CreateOrderRequest request)
{
var result = await _orderService.CreateAsync(request);
return Ok(result);
}
2. Controllers Should NOT Know About EF Core
If your controller references:
DbContext
DbSet
.Include()
.SaveChanges()
🚨 You already lost separation of concerns
Senior pattern
Controller → Application Service → Domain → Infrastructure (EF)
Controllers talk to services, not databases.
3. Always Return ActionResult (Not Just T)
Junior mistake
[HttpGet]
public Order Get(int id)
This locks you into one response shape.
Senior approach
[HttpGet("{id}")]
public async Task<ActionResult<OrderDto>> Get(int id)
{
var order = await _service.GetAsync(id);
if (order is null) return NotFound();
return Ok(order);
}
✅ Enables
4. Trust Model Validation — Don’t Re‑Validate Manually
Juniors re-check everything
if (string.IsNullOrEmpty(dto.Name)) ...
Seniors let the framework work
public class CreateUserRequest
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
[ApiController]
public class UsersController : ControllerBase
✅ [ApiController] gives you
5. Never Accept Domain Models as Request Bodies
Junior shortcut
public IActionResult Create(User user)
💥 This causes:
Senior rule: One DTO per use case
public record CreateUserRequest(
string Email,
string Password
);
Controllers deal only in DTOs, not domain entities.
6. Controllers Should Be Dumb About Business Errors
Junior approach
if (!user.IsActive)
return BadRequest("Inactive user");
Senior approach
try
{
await _service.DoSomethingAsync(id);
return NoContent();
}
catch (UserInactiveException)
{
return Conflict("User is inactive");
}
✅ Even better
7. Use Explicit Response Types for APIs
Seniors care about consumer clarity.
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet("{id}")]
public async Task<ActionResult<OrderDto>> Get(int id)
✅ Benefits
8. Controllers Are Not for Orchestration
If you see this…
var a = await serviceA.Run();
var b = await serviceB.Run(a);
await serviceC.Run(b);
🚨 That is application orchestration, not controller logic.
Senior fix
await _checkoutWorkflow.ExecuteAsync(request);
✅ Controllers delegate workflows to services
9. Route Design Is Part of Controller Quality
Junior routes
GET /GetUserById
POST /CreateUser
Senior RESTful routes
GET /users/{id}
POST /users
PUT /users/{id}
DELETE /users/{id}
✅ Seniors think in resources, not methods
10. One Controller = One Aggregate / Use Area
Junior controllers grow endlessly
UsersController
├─ Login
├─ Register
├─ ChangePassword
├─ UploadAvatar
├─ DeleteAccount
Senior split by responsibility
AuthController
ProfileController
UsersController
✅ Smaller controllers
Easier to test
Easier to reason about
Lower merge conflicts
Conclusion
Here, we tried to cover 10 Secrets Senior Developers Use to Make Controllers 10× Better (.NET).