Controllers and Model Binding
The controller is where HTTP meets your code, and model binding is the machinery that turns a raw request into typed C# parameters. Understanding exactly where bound values come from β and what happens when binding fails β eliminates a whole category of "it's always null" afternoons.
Where bound values come from
Given public IActionResult Save(OrderViewModel model, int page = 1), the binder fills parameters
by name from, in effect: route values (/orders/edit/5 β id = 5), the query string
(?page=2), and form fields (POST bodies where input name attributes match property names).
Complex types are bound property-by-property; collections bind from indexed names
(Items[0].Qty). When you need to be explicit, the source attributes β [FromBody] (JSON),
[FromRoute], [FromQuery], [FromForm] β pin a parameter to one source; note that JSON
payloads on MVC controllers bind only with [FromBody], never implicitly. When a value can't
be found, you get the type's default
(null, 0) β not an error. That's why the mismatched-name bug is silent: the form posts
CustNo, the property is CustomerNo, and the property just stays null. First debugging move for
"my parameter is empty": open the browser dev tools, look at the actual posted names, and compare
them to your property names (matching is case-insensitive, but the names must match).
Validation: annotations + ModelState
Decorate the ViewModel with data annotations β [Required], [StringLength(100)],
[Range(1, 999)], [EmailAddress] β and the binder validates during binding, recording every
failure in ModelState. The canonical POST action shape is muscle memory:
[HttpPost, ValidateAntiForgeryToken]
public IActionResult Create(OrderViewModel model)
{
if (!ModelState.IsValid) return View(model);
_orderService.Create(model);
return RedirectToAction(nameof(Index));
}
Binding failures land in ModelState too β "abc" posted to an int property is a model error even
with no annotations. Redisplaying the same view with the invalid model is what makes the
validation summary and field messages appear (Module 3 closes this loop client-side).
Anti-patterns that define the job
Fat controllers. An action that opens a DbContext, runs business rules, sends an email, and builds three ViewModels is untestable and unreviewable. The controller's whole job is: bind, check ModelState, call one service method, choose a result. If an action is 80 lines, the missing service layer is the actual finding.
Binding to entities (overposting). public IActionResult Edit(Employee entity) binds every
property a malicious client posts β including Salary or IsAdmin, whether or not your form
rendered a field for them. This is overposting (mass assignment), and it's a real
vulnerability class in government apps. The fix is structural, not [Bind(...)] property lists:
bind to a ViewModel that contains only the editable fields, then map to the entity server-side.
Doing HTTP in services. Keep Request, Session, and HttpContext out of your service
layer β pass values in as parameters, or the layer below MVC becomes untestable too.