[{"data":1,"prerenderedAt":1529},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-aspnet-core-controllers-actions":3},{"id":4,"title":5,"body":6,"description":1514,"difficulty":1515,"extension":1516,"framework":1517,"frameworkSlug":1518,"meta":1519,"navigation":99,"order":48,"path":1520,"qaPath":1521,"seo":1522,"stem":1523,"subtopic":1524,"topic":1525,"topicSlug":1526,"updated":1527,"__hash__":1528},"blog\u002Fblog\u002Fdotnet-aspnet-core-controllers-actions.md","ASP.NET Core Controllers, Model Binding, and Action Filters",{"type":7,"value":8,"toc":1500},"minimark",[9,14,18,22,204,235,243,253,259,345,350,383,388,408,414,418,514,527,531,631,640,675,679,759,762,800,807,867,882,886,993,1004,1008,1252,1258,1262,1265,1294,1297,1326,1333,1337,1458,1465,1469,1496],[10,11,13],"h2",{"id":12},"what-interviewers-test-about-controllers","What interviewers test about controllers",[15,16,17],"p",{},"Controllers are where most of an ASP.NET Core API's behavior lives — model binding, validation,\nresponse shaping, and action filters all feed through them. Interviewers test the nuances\nbecause they reveal whether a developer understands the framework's conventions or is just\nfollowing copy-pasted patterns.",[10,19,21],{"id":20},"controller-vs-controllerbase-pick-the-right-base","Controller vs ControllerBase — pick the right base",[23,24,29],"pre",{"className":25,"code":26,"language":27,"meta":28,"style":28},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F ControllerBase — for Web APIs (no Razor View support):\n[ApiController]\n[Route(\"api\u002F[controller]\")]\npublic class ProductsController : ControllerBase\n{\n    \u002F\u002F Ok(), NotFound(), BadRequest(), Created()... all available\n    \u002F\u002F View() does NOT exist here\n    [HttpGet]\n    public IActionResult List() => Ok(_products);\n}\n\n\u002F\u002F Controller — for MVC apps with Razor Views:\npublic class HomeController : Controller\n{\n    public IActionResult Index()\n    {\n        ViewData[\"Title\"] = \"Home\";\n        return View(); \u002F\u002F renders Views\u002FHome\u002FIndex.cshtml\n    }\n\n    [HttpPost]\n    public IActionResult Create(Product p)\n    {\n        if (!ModelState.IsValid)\n            return View(p);   \u002F\u002F re-render form with errors\n        _repo.Add(p);\n        return RedirectToAction(nameof(Index));\n    }\n}\n","csharp","",[30,31,32,40,46,52,58,64,70,76,82,88,94,101,107,113,118,124,130,136,142,148,153,159,165,170,176,182,188,194,199],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,39],{},"\u002F\u002F ControllerBase — for Web APIs (no Razor View support):\n",[33,41,43],{"class":35,"line":42},2,[33,44,45],{},"[ApiController]\n",[33,47,49],{"class":35,"line":48},3,[33,50,51],{},"[Route(\"api\u002F[controller]\")]\n",[33,53,55],{"class":35,"line":54},4,[33,56,57],{},"public class ProductsController : ControllerBase\n",[33,59,61],{"class":35,"line":60},5,[33,62,63],{},"{\n",[33,65,67],{"class":35,"line":66},6,[33,68,69],{},"    \u002F\u002F Ok(), NotFound(), BadRequest(), Created()... all available\n",[33,71,73],{"class":35,"line":72},7,[33,74,75],{},"    \u002F\u002F View() does NOT exist here\n",[33,77,79],{"class":35,"line":78},8,[33,80,81],{},"    [HttpGet]\n",[33,83,85],{"class":35,"line":84},9,[33,86,87],{},"    public IActionResult List() => Ok(_products);\n",[33,89,91],{"class":35,"line":90},10,[33,92,93],{},"}\n",[33,95,97],{"class":35,"line":96},11,[33,98,100],{"emptyLinePlaceholder":99},true,"\n",[33,102,104],{"class":35,"line":103},12,[33,105,106],{},"\u002F\u002F Controller — for MVC apps with Razor Views:\n",[33,108,110],{"class":35,"line":109},13,[33,111,112],{},"public class HomeController : Controller\n",[33,114,116],{"class":35,"line":115},14,[33,117,63],{},[33,119,121],{"class":35,"line":120},15,[33,122,123],{},"    public IActionResult Index()\n",[33,125,127],{"class":35,"line":126},16,[33,128,129],{},"    {\n",[33,131,133],{"class":35,"line":132},17,[33,134,135],{},"        ViewData[\"Title\"] = \"Home\";\n",[33,137,139],{"class":35,"line":138},18,[33,140,141],{},"        return View(); \u002F\u002F renders Views\u002FHome\u002FIndex.cshtml\n",[33,143,145],{"class":35,"line":144},19,[33,146,147],{},"    }\n",[33,149,151],{"class":35,"line":150},20,[33,152,100],{"emptyLinePlaceholder":99},[33,154,156],{"class":35,"line":155},21,[33,157,158],{},"    [HttpPost]\n",[33,160,162],{"class":35,"line":161},22,[33,163,164],{},"    public IActionResult Create(Product p)\n",[33,166,168],{"class":35,"line":167},23,[33,169,129],{},[33,171,173],{"class":35,"line":172},24,[33,174,175],{},"        if (!ModelState.IsValid)\n",[33,177,179],{"class":35,"line":178},25,[33,180,181],{},"            return View(p);   \u002F\u002F re-render form with errors\n",[33,183,185],{"class":35,"line":184},26,[33,186,187],{},"        _repo.Add(p);\n",[33,189,191],{"class":35,"line":190},27,[33,192,193],{},"        return RedirectToAction(nameof(Index));\n",[33,195,197],{"class":35,"line":196},28,[33,198,147],{},[33,200,202],{"class":35,"line":201},29,[33,203,93],{},[15,205,206,209,210,213,214,213,217,213,220,213,223,226,227,230,231,234],{},[30,207,208],{},"Controller"," adds: ",[30,211,212],{},"View()",", ",[30,215,216],{},"PartialView()",[30,218,219],{},"ViewData",[30,221,222],{},"ViewBag",[30,224,225],{},"TempData",",\n",[30,228,229],{},"RedirectToAction",". For pure APIs, these are dead weight — use ",[30,232,233],{},"ControllerBase",".",[10,236,238,239,242],{"id":237},"what-apicontroller-actually-does","What ",[33,240,241],{},"ApiController"," actually does",[15,244,245,248,249,252],{},[30,246,247],{},"[ApiController]"," on a controller class (or applied assembly-wide with\n",[30,250,251],{},"[assembly: ApiController]",") enables four behaviors:",[15,254,255],{},[256,257,258],"strong",{},"1. Automatic model state validation — 400 before the action runs:",[23,260,262],{"className":25,"code":261,"language":27,"meta":28,"style":28},"\u002F\u002F Without [ApiController]:\n[HttpPost]\npublic IActionResult Create([FromBody] CreateOrderDto dto)\n{\n    if (!ModelState.IsValid)                           \u002F\u002F must check manually\n        return BadRequest(ModelState);\n    var order = _svc.Create(dto);\n    return Created($\"\u002Forders\u002F{order.Id}\", order);\n}\n\n\u002F\u002F With [ApiController]:\n[HttpPost]\npublic IActionResult Create([FromBody] CreateOrderDto dto)\n{\n    \u002F\u002F If dto fails validation, 400 is returned automatically — this line is never reached\n    var order = _svc.Create(dto);\n    return Created($\"\u002Forders\u002F{order.Id}\", order);\n}\n",[30,263,264,269,274,279,283,288,293,298,303,307,311,316,320,324,328,333,337,341],{"__ignoreMap":28},[33,265,266],{"class":35,"line":36},[33,267,268],{},"\u002F\u002F Without [ApiController]:\n",[33,270,271],{"class":35,"line":42},[33,272,273],{},"[HttpPost]\n",[33,275,276],{"class":35,"line":48},[33,277,278],{},"public IActionResult Create([FromBody] CreateOrderDto dto)\n",[33,280,281],{"class":35,"line":54},[33,282,63],{},[33,284,285],{"class":35,"line":60},[33,286,287],{},"    if (!ModelState.IsValid)                           \u002F\u002F must check manually\n",[33,289,290],{"class":35,"line":66},[33,291,292],{},"        return BadRequest(ModelState);\n",[33,294,295],{"class":35,"line":72},[33,296,297],{},"    var order = _svc.Create(dto);\n",[33,299,300],{"class":35,"line":78},[33,301,302],{},"    return Created($\"\u002Forders\u002F{order.Id}\", order);\n",[33,304,305],{"class":35,"line":84},[33,306,93],{},[33,308,309],{"class":35,"line":90},[33,310,100],{"emptyLinePlaceholder":99},[33,312,313],{"class":35,"line":96},[33,314,315],{},"\u002F\u002F With [ApiController]:\n",[33,317,318],{"class":35,"line":103},[33,319,273],{},[33,321,322],{"class":35,"line":109},[33,323,278],{},[33,325,326],{"class":35,"line":115},[33,327,63],{},[33,329,330],{"class":35,"line":120},[33,331,332],{},"    \u002F\u002F If dto fails validation, 400 is returned automatically — this line is never reached\n",[33,334,335],{"class":35,"line":126},[33,336,297],{},[33,338,339],{"class":35,"line":132},[33,340,302],{},[33,342,343],{"class":35,"line":138},[33,344,93],{},[15,346,347],{},[256,348,349],{},"2. Binding source inference:",[23,351,353],{"className":25,"code":352,"language":27,"meta":28,"style":28},"[HttpGet(\"{id}\")]\npublic IActionResult Get(int id,           \u002F\u002F [FromRoute] inferred\n                         string? sort)     \u002F\u002F [FromQuery] inferred — GET has no body\n\n[HttpPost]\npublic IActionResult Create(CreateDto dto) \u002F\u002F [FromBody] inferred — complex type, POST\n",[30,354,355,360,365,370,374,378],{"__ignoreMap":28},[33,356,357],{"class":35,"line":36},[33,358,359],{},"[HttpGet(\"{id}\")]\n",[33,361,362],{"class":35,"line":42},[33,363,364],{},"public IActionResult Get(int id,           \u002F\u002F [FromRoute] inferred\n",[33,366,367],{"class":35,"line":48},[33,368,369],{},"                         string? sort)     \u002F\u002F [FromQuery] inferred — GET has no body\n",[33,371,372],{"class":35,"line":54},[33,373,100],{"emptyLinePlaceholder":99},[33,375,376],{"class":35,"line":60},[33,377,273],{},[33,379,380],{"class":35,"line":66},[33,381,382],{},"public IActionResult Create(CreateDto dto) \u002F\u002F [FromBody] inferred — complex type, POST\n",[15,384,385],{},[256,386,387],{},"3. Problem Details response format (RFC 7807):",[23,389,391],{"className":25,"code":390,"language":27,"meta":28,"style":28},"\u002F\u002F Automatic 400 response:\n\u002F\u002F { \"type\": \"...\", \"title\": \"One or more validation errors occurred.\",\n\u002F\u002F \"status\": 400, \"errors\": { \"Email\": [\"The Email field is required.\"] } }\n",[30,392,393,398,403],{"__ignoreMap":28},[33,394,395],{"class":35,"line":36},[33,396,397],{},"\u002F\u002F Automatic 400 response:\n",[33,399,400],{"class":35,"line":42},[33,401,402],{},"\u002F\u002F { \"type\": \"...\", \"title\": \"One or more validation errors occurred.\",\n",[33,404,405],{"class":35,"line":48},[33,406,407],{},"\u002F\u002F \"status\": 400, \"errors\": { \"Email\": [\"The Email field is required.\"] } }\n",[15,409,410,413],{},[256,411,412],{},"4. Attribute routing required"," — conventional routes are disabled for the controller.",[10,415,417],{"id":416},"iactionresult-vs-actionresultt-vs-t","IActionResult vs ActionResult\u003CT> vs T",[23,419,421],{"className":25,"code":420,"language":27,"meta":28,"style":28},"\u002F\u002F IActionResult — flexible, but OpenAPI\u002FSwagger can't infer the response body type:\n[HttpGet(\"{id}\")]\npublic IActionResult Get(int id)\n{\n    var p = _repo.Find(id);\n    return p is null ? NotFound() : Ok(p); \u002F\u002F Ok() wraps Product but returns IActionResult\n}\n\n\u002F\u002F ActionResult\u003CT> — preferred: Swagger generates accurate Product schema,\n\u002F\u002F and implicit conversion keeps code clean:\n[HttpGet(\"{id}\")]\npublic ActionResult\u003CProduct> Get(int id)\n{\n    var p = _repo.Find(id);\n    return p is null ? NotFound() : p; \u002F\u002F Product implicitly converts to ActionResult\u003CProduct>\n}\n\n\u002F\u002F T directly — simplest; always 200 OK, no way to return error codes:\n[HttpGet(\"all\")]\npublic IEnumerable\u003CProduct> List() => _repo.GetAll();\n",[30,422,423,428,432,437,441,446,451,455,459,464,469,473,478,482,486,491,495,499,504,509],{"__ignoreMap":28},[33,424,425],{"class":35,"line":36},[33,426,427],{},"\u002F\u002F IActionResult — flexible, but OpenAPI\u002FSwagger can't infer the response body type:\n",[33,429,430],{"class":35,"line":42},[33,431,359],{},[33,433,434],{"class":35,"line":48},[33,435,436],{},"public IActionResult Get(int id)\n",[33,438,439],{"class":35,"line":54},[33,440,63],{},[33,442,443],{"class":35,"line":60},[33,444,445],{},"    var p = _repo.Find(id);\n",[33,447,448],{"class":35,"line":66},[33,449,450],{},"    return p is null ? NotFound() : Ok(p); \u002F\u002F Ok() wraps Product but returns IActionResult\n",[33,452,453],{"class":35,"line":72},[33,454,93],{},[33,456,457],{"class":35,"line":78},[33,458,100],{"emptyLinePlaceholder":99},[33,460,461],{"class":35,"line":84},[33,462,463],{},"\u002F\u002F ActionResult\u003CT> — preferred: Swagger generates accurate Product schema,\n",[33,465,466],{"class":35,"line":90},[33,467,468],{},"\u002F\u002F and implicit conversion keeps code clean:\n",[33,470,471],{"class":35,"line":96},[33,472,359],{},[33,474,475],{"class":35,"line":103},[33,476,477],{},"public ActionResult\u003CProduct> Get(int id)\n",[33,479,480],{"class":35,"line":109},[33,481,63],{},[33,483,484],{"class":35,"line":115},[33,485,445],{},[33,487,488],{"class":35,"line":120},[33,489,490],{},"    return p is null ? NotFound() : p; \u002F\u002F Product implicitly converts to ActionResult\u003CProduct>\n",[33,492,493],{"class":35,"line":126},[33,494,93],{},[33,496,497],{"class":35,"line":132},[33,498,100],{"emptyLinePlaceholder":99},[33,500,501],{"class":35,"line":138},[33,502,503],{},"\u002F\u002F T directly — simplest; always 200 OK, no way to return error codes:\n",[33,505,506],{"class":35,"line":144},[33,507,508],{},"[HttpGet(\"all\")]\n",[33,510,511],{"class":35,"line":150},[33,512,513],{},"public IEnumerable\u003CProduct> List() => _repo.GetAll();\n",[15,515,516,519,520,213,523,526],{},[30,517,518],{},"ActionResult\u003CT>"," wins for most API actions — it keeps the return type clear for tooling\nwhile still allowing ",[30,521,522],{},"NotFound()",[30,524,525],{},"BadRequest()",", etc.",[10,528,530],{"id":529},"model-binding-where-values-come-from","Model binding — where values come from",[23,532,534],{"className":25,"code":533,"language":27,"meta":28,"style":28},"[HttpGet(\"search\")]\npublic IActionResult Search(\n    string term,                              \u002F\u002F [FromQuery] ?term=shoes\n    int page = 1,                             \u002F\u002F [FromQuery] with default\n    [FromHeader(Name = \"X-Locale\")] string locale = \"en\") \u002F\u002F header\n    => Ok(_svc.Search(term, page, locale));\n\n[HttpPost]\npublic IActionResult Create([FromBody] CreateProductDto dto) \u002F\u002F JSON body\n    => Created($\"\u002Fproducts\u002F{dto.Id}\", dto);\n\n[HttpPost(\"upload\")]\npublic IActionResult Upload(\n    [FromForm] string category,\n    IFormFile file)                           \u002F\u002F multipart form upload\n{\n    using var stream = file.OpenReadStream();\n    _importer.Import(stream, category);\n    return Ok();\n}\n",[30,535,536,541,546,551,556,561,566,570,574,579,584,588,593,598,603,608,612,617,622,627],{"__ignoreMap":28},[33,537,538],{"class":35,"line":36},[33,539,540],{},"[HttpGet(\"search\")]\n",[33,542,543],{"class":35,"line":42},[33,544,545],{},"public IActionResult Search(\n",[33,547,548],{"class":35,"line":48},[33,549,550],{},"    string term,                              \u002F\u002F [FromQuery] ?term=shoes\n",[33,552,553],{"class":35,"line":54},[33,554,555],{},"    int page = 1,                             \u002F\u002F [FromQuery] with default\n",[33,557,558],{"class":35,"line":60},[33,559,560],{},"    [FromHeader(Name = \"X-Locale\")] string locale = \"en\") \u002F\u002F header\n",[33,562,563],{"class":35,"line":66},[33,564,565],{},"    => Ok(_svc.Search(term, page, locale));\n",[33,567,568],{"class":35,"line":72},[33,569,100],{"emptyLinePlaceholder":99},[33,571,572],{"class":35,"line":78},[33,573,273],{},[33,575,576],{"class":35,"line":84},[33,577,578],{},"public IActionResult Create([FromBody] CreateProductDto dto) \u002F\u002F JSON body\n",[33,580,581],{"class":35,"line":90},[33,582,583],{},"    => Created($\"\u002Fproducts\u002F{dto.Id}\", dto);\n",[33,585,586],{"class":35,"line":96},[33,587,100],{"emptyLinePlaceholder":99},[33,589,590],{"class":35,"line":103},[33,591,592],{},"[HttpPost(\"upload\")]\n",[33,594,595],{"class":35,"line":109},[33,596,597],{},"public IActionResult Upload(\n",[33,599,600],{"class":35,"line":115},[33,601,602],{},"    [FromForm] string category,\n",[33,604,605],{"class":35,"line":120},[33,606,607],{},"    IFormFile file)                           \u002F\u002F multipart form upload\n",[33,609,610],{"class":35,"line":126},[33,611,63],{},[33,613,614],{"class":35,"line":132},[33,615,616],{},"    using var stream = file.OpenReadStream();\n",[33,618,619],{"class":35,"line":138},[33,620,621],{},"    _importer.Import(stream, category);\n",[33,623,624],{"class":35,"line":144},[33,625,626],{},"    return Ok();\n",[33,628,629],{"class":35,"line":150},[33,630,93],{},[15,632,633,635,636,639],{},[30,634,247],{}," inference rules (when no explicit ",[30,637,638],{},"[From*]"," is provided):",[641,642,643,650,663,669],"ul",{},[644,645,646,647],"li",{},"Route parameter name matches → ",[30,648,649],{},"[FromRoute]",[644,651,652,655,656,659,660],{},[30,653,654],{},"IFormFile"," \u002F ",[30,657,658],{},"IFormFileCollection"," → ",[30,661,662],{},"[FromForm]",[644,664,665,666],{},"Complex type on POST\u002FPUT\u002FPATCH → ",[30,667,668],{},"[FromBody]",[644,670,671,672],{},"Simple type on GET or unmatched → ",[30,673,674],{},"[FromQuery]",[10,676,678],{"id":677},"model-validation-with-data-annotations","Model validation with Data Annotations",[23,680,682],{"className":25,"code":681,"language":27,"meta":28,"style":28},"public class RegisterDto\n{\n    [Required(ErrorMessage = \"Email is required\")]\n    [EmailAddress]\n    public string Email { get; set; } = default!;\n\n    [Required]\n    [StringLength(100, MinimumLength = 8)]\n    public string Password { get; set; } = default!;\n\n    [Range(18, 120)]\n    public int Age { get; set; }\n\n    [Url]\n    public string? Website { get; set; }\n}\n",[30,683,684,689,693,698,703,708,712,717,722,727,731,736,741,745,750,755],{"__ignoreMap":28},[33,685,686],{"class":35,"line":36},[33,687,688],{},"public class RegisterDto\n",[33,690,691],{"class":35,"line":42},[33,692,63],{},[33,694,695],{"class":35,"line":48},[33,696,697],{},"    [Required(ErrorMessage = \"Email is required\")]\n",[33,699,700],{"class":35,"line":54},[33,701,702],{},"    [EmailAddress]\n",[33,704,705],{"class":35,"line":60},[33,706,707],{},"    public string Email { get; set; } = default!;\n",[33,709,710],{"class":35,"line":66},[33,711,100],{"emptyLinePlaceholder":99},[33,713,714],{"class":35,"line":72},[33,715,716],{},"    [Required]\n",[33,718,719],{"class":35,"line":78},[33,720,721],{},"    [StringLength(100, MinimumLength = 8)]\n",[33,723,724],{"class":35,"line":84},[33,725,726],{},"    public string Password { get; set; } = default!;\n",[33,728,729],{"class":35,"line":90},[33,730,100],{"emptyLinePlaceholder":99},[33,732,733],{"class":35,"line":96},[33,734,735],{},"    [Range(18, 120)]\n",[33,737,738],{"class":35,"line":103},[33,739,740],{},"    public int Age { get; set; }\n",[33,742,743],{"class":35,"line":109},[33,744,100],{"emptyLinePlaceholder":99},[33,746,747],{"class":35,"line":115},[33,748,749],{},"    [Url]\n",[33,751,752],{"class":35,"line":120},[33,753,754],{},"    public string? Website { get; set; }\n",[33,756,757],{"class":35,"line":126},[33,758,93],{},[15,760,761],{},"Custom validation attribute:",[23,763,765],{"className":25,"code":764,"language":27,"meta":28,"style":28},"public class FutureDateAttribute : ValidationAttribute\n{\n    protected override ValidationResult? IsValid(object? value, ValidationContext ctx)\n        => value is DateTime dt && dt > DateTime.Today\n            ? ValidationResult.Success\n            : new ValidationResult(\"Date must be in the future\");\n}\n",[30,766,767,772,776,781,786,791,796],{"__ignoreMap":28},[33,768,769],{"class":35,"line":36},[33,770,771],{},"public class FutureDateAttribute : ValidationAttribute\n",[33,773,774],{"class":35,"line":42},[33,775,63],{},[33,777,778],{"class":35,"line":48},[33,779,780],{},"    protected override ValidationResult? IsValid(object? value, ValidationContext ctx)\n",[33,782,783],{"class":35,"line":54},[33,784,785],{},"        => value is DateTime dt && dt > DateTime.Today\n",[33,787,788],{"class":35,"line":60},[33,789,790],{},"            ? ValidationResult.Success\n",[33,792,793],{"class":35,"line":66},[33,794,795],{},"            : new ValidationResult(\"Date must be in the future\");\n",[33,797,798],{"class":35,"line":72},[33,799,93],{},[15,801,802,803,806],{},"Cross-property validation with ",[30,804,805],{},"IValidatableObject",":",[23,808,810],{"className":25,"code":809,"language":27,"meta":28,"style":28},"public class DateRangeDto : IValidatableObject\n{\n    public DateTime Start { get; set; }\n    public DateTime End { get; set; }\n\n    public IEnumerable\u003CValidationResult> Validate(ValidationContext ctx)\n    {\n        if (End \u003C= Start)\n            yield return new ValidationResult(\"End must be after Start\",\n                new[] { nameof(End) });\n    }\n}\n",[30,811,812,817,821,826,831,835,840,844,849,854,859,863],{"__ignoreMap":28},[33,813,814],{"class":35,"line":36},[33,815,816],{},"public class DateRangeDto : IValidatableObject\n",[33,818,819],{"class":35,"line":42},[33,820,63],{},[33,822,823],{"class":35,"line":48},[33,824,825],{},"    public DateTime Start { get; set; }\n",[33,827,828],{"class":35,"line":54},[33,829,830],{},"    public DateTime End { get; set; }\n",[33,832,833],{"class":35,"line":60},[33,834,100],{"emptyLinePlaceholder":99},[33,836,837],{"class":35,"line":66},[33,838,839],{},"    public IEnumerable\u003CValidationResult> Validate(ValidationContext ctx)\n",[33,841,842],{"class":35,"line":72},[33,843,129],{},[33,845,846],{"class":35,"line":78},[33,847,848],{},"        if (End \u003C= Start)\n",[33,850,851],{"class":35,"line":84},[33,852,853],{},"            yield return new ValidationResult(\"End must be after Start\",\n",[33,855,856],{"class":35,"line":90},[33,857,858],{},"                new[] { nameof(End) });\n",[33,860,861],{"class":35,"line":96},[33,862,147],{},[33,864,865],{"class":35,"line":103},[33,866,93],{},[15,868,869,872,873,877,878,881],{},[256,870,871],{},"Important architecture note:"," Data Annotations validate ",[874,875,876],"em",{},"input format",", not business\nrules. An email address field with ",[30,879,880],{},"[EmailAddress]"," validates the format — it doesn't check\nif that email is already registered. Business validation (duplicates, business rules) belongs\nin your service layer, not the DTO.",[10,883,885],{"id":884},"action-result-helper-methods","Action result helper methods",[23,887,889],{"className":25,"code":888,"language":27,"meta":28,"style":28},"\u002F\u002F 2xx:\nreturn Ok(product);                         \u002F\u002F 200 with JSON body\nreturn Created($\"\u002Fproducts\u002F{id}\", product); \u002F\u002F 201 Created + Location header\nreturn CreatedAtAction(nameof(Get), new { id }, product); \u002F\u002F 201 with route-based Location\nreturn Accepted();                          \u002F\u002F 202 Accepted (async operation)\nreturn NoContent();                         \u002F\u002F 204 No Content\n\n\u002F\u002F 4xx:\nreturn BadRequest(ModelState);              \u002F\u002F 400 with validation errors\nreturn Unauthorized();                      \u002F\u002F 401\nreturn Forbid();                            \u002F\u002F 403\nreturn NotFound();                          \u002F\u002F 404\nreturn Conflict(\"Duplicate key\");           \u002F\u002F 409\nreturn UnprocessableEntity(errors);         \u002F\u002F 422\n\n\u002F\u002F RFC 7807 Problem Details:\nreturn Problem(title: \"Not Found\", detail: $\"Order {id} not found\", statusCode: 404);\nreturn ValidationProblem(ModelState);       \u002F\u002F structured 400 for model errors\n\n\u002F\u002F File download:\nreturn File(bytes, \"application\u002Fpdf\", \"document.pdf\");\n",[30,890,891,896,901,906,911,916,921,925,930,935,940,945,950,955,960,964,969,974,979,983,988],{"__ignoreMap":28},[33,892,893],{"class":35,"line":36},[33,894,895],{},"\u002F\u002F 2xx:\n",[33,897,898],{"class":35,"line":42},[33,899,900],{},"return Ok(product);                         \u002F\u002F 200 with JSON body\n",[33,902,903],{"class":35,"line":48},[33,904,905],{},"return Created($\"\u002Fproducts\u002F{id}\", product); \u002F\u002F 201 Created + Location header\n",[33,907,908],{"class":35,"line":54},[33,909,910],{},"return CreatedAtAction(nameof(Get), new { id }, product); \u002F\u002F 201 with route-based Location\n",[33,912,913],{"class":35,"line":60},[33,914,915],{},"return Accepted();                          \u002F\u002F 202 Accepted (async operation)\n",[33,917,918],{"class":35,"line":66},[33,919,920],{},"return NoContent();                         \u002F\u002F 204 No Content\n",[33,922,923],{"class":35,"line":72},[33,924,100],{"emptyLinePlaceholder":99},[33,926,927],{"class":35,"line":78},[33,928,929],{},"\u002F\u002F 4xx:\n",[33,931,932],{"class":35,"line":84},[33,933,934],{},"return BadRequest(ModelState);              \u002F\u002F 400 with validation errors\n",[33,936,937],{"class":35,"line":90},[33,938,939],{},"return Unauthorized();                      \u002F\u002F 401\n",[33,941,942],{"class":35,"line":96},[33,943,944],{},"return Forbid();                            \u002F\u002F 403\n",[33,946,947],{"class":35,"line":103},[33,948,949],{},"return NotFound();                          \u002F\u002F 404\n",[33,951,952],{"class":35,"line":109},[33,953,954],{},"return Conflict(\"Duplicate key\");           \u002F\u002F 409\n",[33,956,957],{"class":35,"line":115},[33,958,959],{},"return UnprocessableEntity(errors);         \u002F\u002F 422\n",[33,961,962],{"class":35,"line":120},[33,963,100],{"emptyLinePlaceholder":99},[33,965,966],{"class":35,"line":126},[33,967,968],{},"\u002F\u002F RFC 7807 Problem Details:\n",[33,970,971],{"class":35,"line":132},[33,972,973],{},"return Problem(title: \"Not Found\", detail: $\"Order {id} not found\", statusCode: 404);\n",[33,975,976],{"class":35,"line":138},[33,977,978],{},"return ValidationProblem(ModelState);       \u002F\u002F structured 400 for model errors\n",[33,980,981],{"class":35,"line":144},[33,982,100],{"emptyLinePlaceholder":99},[33,984,985],{"class":35,"line":150},[33,986,987],{},"\u002F\u002F File download:\n",[33,989,990],{"class":35,"line":155},[33,991,992],{},"return File(bytes, \"application\u002Fpdf\", \"document.pdf\");\n",[15,994,995,996,999,1000,1003],{},"Always use these helper methods rather than setting ",[30,997,998],{},"Response.StatusCode"," directly — they\nproduce properly typed ",[30,1001,1002],{},"IActionResult"," objects that participate correctly in content\nnegotiation and result filters.",[10,1005,1007],{"id":1006},"action-filters-adding-cross-cutting-behavior","Action filters — adding cross-cutting behavior",[23,1009,1011],{"className":25,"code":1010,"language":27,"meta":28,"style":28},"\u002F\u002F Synchronous filter:\npublic class AuditFilter : IActionFilter\n{\n    private readonly IAuditService _audit;\n    public AuditFilter(IAuditService audit) => _audit = audit;\n\n    public void OnActionExecuting(ActionExecutingContext context)\n        => _audit.LogStart(context.ActionDescriptor.DisplayName,\n                           context.ActionArguments);\n\n    public void OnActionExecuted(ActionExecutedContext context)\n        => _audit.LogEnd(context.ActionDescriptor.DisplayName,\n                         (context.Result as ObjectResult)?.StatusCode);\n}\n\n\u002F\u002F Async filter that can short-circuit:\npublic class EnsureProductExistsFilter : IAsyncActionFilter\n{\n    private readonly IProductRepo _repo;\n    public EnsureProductExistsFilter(IProductRepo repo) => _repo = repo;\n\n    public async Task OnActionExecutionAsync(\n        ActionExecutingContext context, ActionExecutionDelegate next)\n    {\n        if (context.ActionArguments.TryGetValue(\"id\", out var idObj)\n            && idObj is int id\n            && !await _repo.ExistsAsync(id))\n        {\n            context.Result = new NotFoundResult(); \u002F\u002F short-circuit\n            return;\n        }\n        await next(); \u002F\u002F proceed to action\n    }\n}\n\n\u002F\u002F Apply globally — runs on every action:\nbuilder.Services.AddControllers(o =>\n    o.Filters.Add\u003CAuditFilter>());\n\n\u002F\u002F Apply per-controller using DI:\n[ServiceFilter(typeof(EnsureProductExistsFilter))]\npublic class ProductsController : ControllerBase { ... }\n\n\u002F\u002F Apply per-action as attribute (must implement IFilterFactory or derive from ActionFilterAttribute):\n[HttpGet(\"{id:int}\")]\n[ServiceFilter(typeof(EnsureProductExistsFilter))]\npublic IActionResult Get(int id) => Ok(_repo.Find(id));\n",[30,1012,1013,1018,1023,1027,1032,1037,1041,1046,1051,1056,1060,1065,1070,1075,1079,1083,1088,1093,1097,1102,1107,1111,1116,1121,1125,1130,1135,1140,1145,1150,1156,1162,1168,1173,1178,1183,1189,1195,1201,1206,1212,1218,1224,1229,1235,1241,1246],{"__ignoreMap":28},[33,1014,1015],{"class":35,"line":36},[33,1016,1017],{},"\u002F\u002F Synchronous filter:\n",[33,1019,1020],{"class":35,"line":42},[33,1021,1022],{},"public class AuditFilter : IActionFilter\n",[33,1024,1025],{"class":35,"line":48},[33,1026,63],{},[33,1028,1029],{"class":35,"line":54},[33,1030,1031],{},"    private readonly IAuditService _audit;\n",[33,1033,1034],{"class":35,"line":60},[33,1035,1036],{},"    public AuditFilter(IAuditService audit) => _audit = audit;\n",[33,1038,1039],{"class":35,"line":66},[33,1040,100],{"emptyLinePlaceholder":99},[33,1042,1043],{"class":35,"line":72},[33,1044,1045],{},"    public void OnActionExecuting(ActionExecutingContext context)\n",[33,1047,1048],{"class":35,"line":78},[33,1049,1050],{},"        => _audit.LogStart(context.ActionDescriptor.DisplayName,\n",[33,1052,1053],{"class":35,"line":84},[33,1054,1055],{},"                           context.ActionArguments);\n",[33,1057,1058],{"class":35,"line":90},[33,1059,100],{"emptyLinePlaceholder":99},[33,1061,1062],{"class":35,"line":96},[33,1063,1064],{},"    public void OnActionExecuted(ActionExecutedContext context)\n",[33,1066,1067],{"class":35,"line":103},[33,1068,1069],{},"        => _audit.LogEnd(context.ActionDescriptor.DisplayName,\n",[33,1071,1072],{"class":35,"line":109},[33,1073,1074],{},"                         (context.Result as ObjectResult)?.StatusCode);\n",[33,1076,1077],{"class":35,"line":115},[33,1078,93],{},[33,1080,1081],{"class":35,"line":120},[33,1082,100],{"emptyLinePlaceholder":99},[33,1084,1085],{"class":35,"line":126},[33,1086,1087],{},"\u002F\u002F Async filter that can short-circuit:\n",[33,1089,1090],{"class":35,"line":132},[33,1091,1092],{},"public class EnsureProductExistsFilter : IAsyncActionFilter\n",[33,1094,1095],{"class":35,"line":138},[33,1096,63],{},[33,1098,1099],{"class":35,"line":144},[33,1100,1101],{},"    private readonly IProductRepo _repo;\n",[33,1103,1104],{"class":35,"line":150},[33,1105,1106],{},"    public EnsureProductExistsFilter(IProductRepo repo) => _repo = repo;\n",[33,1108,1109],{"class":35,"line":155},[33,1110,100],{"emptyLinePlaceholder":99},[33,1112,1113],{"class":35,"line":161},[33,1114,1115],{},"    public async Task OnActionExecutionAsync(\n",[33,1117,1118],{"class":35,"line":167},[33,1119,1120],{},"        ActionExecutingContext context, ActionExecutionDelegate next)\n",[33,1122,1123],{"class":35,"line":172},[33,1124,129],{},[33,1126,1127],{"class":35,"line":178},[33,1128,1129],{},"        if (context.ActionArguments.TryGetValue(\"id\", out var idObj)\n",[33,1131,1132],{"class":35,"line":184},[33,1133,1134],{},"            && idObj is int id\n",[33,1136,1137],{"class":35,"line":190},[33,1138,1139],{},"            && !await _repo.ExistsAsync(id))\n",[33,1141,1142],{"class":35,"line":196},[33,1143,1144],{},"        {\n",[33,1146,1147],{"class":35,"line":201},[33,1148,1149],{},"            context.Result = new NotFoundResult(); \u002F\u002F short-circuit\n",[33,1151,1153],{"class":35,"line":1152},30,[33,1154,1155],{},"            return;\n",[33,1157,1159],{"class":35,"line":1158},31,[33,1160,1161],{},"        }\n",[33,1163,1165],{"class":35,"line":1164},32,[33,1166,1167],{},"        await next(); \u002F\u002F proceed to action\n",[33,1169,1171],{"class":35,"line":1170},33,[33,1172,147],{},[33,1174,1176],{"class":35,"line":1175},34,[33,1177,93],{},[33,1179,1181],{"class":35,"line":1180},35,[33,1182,100],{"emptyLinePlaceholder":99},[33,1184,1186],{"class":35,"line":1185},36,[33,1187,1188],{},"\u002F\u002F Apply globally — runs on every action:\n",[33,1190,1192],{"class":35,"line":1191},37,[33,1193,1194],{},"builder.Services.AddControllers(o =>\n",[33,1196,1198],{"class":35,"line":1197},38,[33,1199,1200],{},"    o.Filters.Add\u003CAuditFilter>());\n",[33,1202,1204],{"class":35,"line":1203},39,[33,1205,100],{"emptyLinePlaceholder":99},[33,1207,1209],{"class":35,"line":1208},40,[33,1210,1211],{},"\u002F\u002F Apply per-controller using DI:\n",[33,1213,1215],{"class":35,"line":1214},41,[33,1216,1217],{},"[ServiceFilter(typeof(EnsureProductExistsFilter))]\n",[33,1219,1221],{"class":35,"line":1220},42,[33,1222,1223],{},"public class ProductsController : ControllerBase { ... }\n",[33,1225,1227],{"class":35,"line":1226},43,[33,1228,100],{"emptyLinePlaceholder":99},[33,1230,1232],{"class":35,"line":1231},44,[33,1233,1234],{},"\u002F\u002F Apply per-action as attribute (must implement IFilterFactory or derive from ActionFilterAttribute):\n",[33,1236,1238],{"class":35,"line":1237},45,[33,1239,1240],{},"[HttpGet(\"{id:int}\")]\n",[33,1242,1244],{"class":35,"line":1243},46,[33,1245,1217],{},[33,1247,1249],{"class":35,"line":1248},47,[33,1250,1251],{},"public IActionResult Get(int id) => Ok(_repo.Find(id));\n",[15,1253,1254,1255],{},"Filter execution order within an action call:\n",[30,1256,1257],{},"Authorization → Resource → Action → Result → Exception",[10,1259,1261],{"id":1260},"content-negotiation","Content negotiation",[15,1263,1264],{},"By default ASP.NET Core serializes everything as JSON. Adding XML support:",[23,1266,1268],{"className":25,"code":1267,"language":27,"meta":28,"style":28},"builder.Services.AddControllers()\n    .AddXmlSerializerFormatters();\n\n\u002F\u002F Client sends: Accept: application\u002Fxml\n\u002F\u002F → response is serialized as XML\n",[30,1269,1270,1275,1280,1284,1289],{"__ignoreMap":28},[33,1271,1272],{"class":35,"line":36},[33,1273,1274],{},"builder.Services.AddControllers()\n",[33,1276,1277],{"class":35,"line":42},[33,1278,1279],{},"    .AddXmlSerializerFormatters();\n",[33,1281,1282],{"class":35,"line":48},[33,1283,100],{"emptyLinePlaceholder":99},[33,1285,1286],{"class":35,"line":54},[33,1287,1288],{},"\u002F\u002F Client sends: Accept: application\u002Fxml\n",[33,1290,1291],{"class":35,"line":60},[33,1292,1293],{},"\u002F\u002F → response is serialized as XML\n",[15,1295,1296],{},"Constrain and document accepted\u002Fproduced formats:",[23,1298,1300],{"className":25,"code":1299,"language":27,"meta":28,"style":28},"[HttpGet(\"{id}\")]\n[Produces(\"application\u002Fjson\")]\n[ProducesResponseType\u003CProduct>(200)]\n[ProducesResponseType(404)]\npublic ActionResult\u003CProduct> Get(int id) { ... }\n",[30,1301,1302,1306,1311,1316,1321],{"__ignoreMap":28},[33,1303,1304],{"class":35,"line":36},[33,1305,359],{},[33,1307,1308],{"class":35,"line":42},[33,1309,1310],{},"[Produces(\"application\u002Fjson\")]\n",[33,1312,1313],{"class":35,"line":48},[33,1314,1315],{},"[ProducesResponseType\u003CProduct>(200)]\n",[33,1317,1318],{"class":35,"line":54},[33,1319,1320],{},"[ProducesResponseType(404)]\n",[33,1322,1323],{"class":35,"line":60},[33,1324,1325],{},"public ActionResult\u003CProduct> Get(int id) { ... }\n",[15,1327,1328,1329,1332],{},"Without ",[30,1330,1331],{},"[ProducesResponseType]",", Swagger shows \"200 Undocumented\" and cannot generate\naccurate client SDKs.",[10,1334,1336],{"id":1335},"async-actions-the-right-pattern","Async actions — the right pattern",[23,1338,1340],{"className":25,"code":1339,"language":27,"meta":28,"style":28},"\u002F\u002F Async I\u002FO — thread released while awaiting DB:\n[HttpGet]\npublic async Task\u003CActionResult\u003CIEnumerable\u003CProduct>>> List(CancellationToken ct)\n{\n    var products = await _db.Products\n        .AsNoTracking()\n        .ToListAsync(ct);   \u002F\u002F non-blocking; ct cancels if client disconnects\n    return Ok(products);\n}\n\n\u002F\u002F Propagate CancellationToken to every I\u002FO call:\n[HttpGet(\"{id:int}\")]\npublic async Task\u003CActionResult\u003CProduct>> Get(int id, CancellationToken ct)\n{\n    var p = await _repo.FindAsync(id, ct);\n    return p is null ? NotFound() : Ok(p);\n}\n\n\u002F\u002F Never block async code:\n[HttpGet(\"bad\")]\npublic IActionResult GetBad()\n{\n    var p = _repo.FindAsync(1).Result; \u002F\u002F blocks thread — kills scalability!\n    return Ok(p);\n}\n",[30,1341,1342,1347,1352,1357,1361,1366,1371,1376,1381,1385,1389,1394,1398,1403,1407,1412,1417,1421,1425,1430,1435,1440,1444,1449,1454],{"__ignoreMap":28},[33,1343,1344],{"class":35,"line":36},[33,1345,1346],{},"\u002F\u002F Async I\u002FO — thread released while awaiting DB:\n",[33,1348,1349],{"class":35,"line":42},[33,1350,1351],{},"[HttpGet]\n",[33,1353,1354],{"class":35,"line":48},[33,1355,1356],{},"public async Task\u003CActionResult\u003CIEnumerable\u003CProduct>>> List(CancellationToken ct)\n",[33,1358,1359],{"class":35,"line":54},[33,1360,63],{},[33,1362,1363],{"class":35,"line":60},[33,1364,1365],{},"    var products = await _db.Products\n",[33,1367,1368],{"class":35,"line":66},[33,1369,1370],{},"        .AsNoTracking()\n",[33,1372,1373],{"class":35,"line":72},[33,1374,1375],{},"        .ToListAsync(ct);   \u002F\u002F non-blocking; ct cancels if client disconnects\n",[33,1377,1378],{"class":35,"line":78},[33,1379,1380],{},"    return Ok(products);\n",[33,1382,1383],{"class":35,"line":84},[33,1384,93],{},[33,1386,1387],{"class":35,"line":90},[33,1388,100],{"emptyLinePlaceholder":99},[33,1390,1391],{"class":35,"line":96},[33,1392,1393],{},"\u002F\u002F Propagate CancellationToken to every I\u002FO call:\n",[33,1395,1396],{"class":35,"line":103},[33,1397,1240],{},[33,1399,1400],{"class":35,"line":109},[33,1401,1402],{},"public async Task\u003CActionResult\u003CProduct>> Get(int id, CancellationToken ct)\n",[33,1404,1405],{"class":35,"line":115},[33,1406,63],{},[33,1408,1409],{"class":35,"line":120},[33,1410,1411],{},"    var p = await _repo.FindAsync(id, ct);\n",[33,1413,1414],{"class":35,"line":126},[33,1415,1416],{},"    return p is null ? NotFound() : Ok(p);\n",[33,1418,1419],{"class":35,"line":132},[33,1420,93],{},[33,1422,1423],{"class":35,"line":138},[33,1424,100],{"emptyLinePlaceholder":99},[33,1426,1427],{"class":35,"line":144},[33,1428,1429],{},"\u002F\u002F Never block async code:\n",[33,1431,1432],{"class":35,"line":150},[33,1433,1434],{},"[HttpGet(\"bad\")]\n",[33,1436,1437],{"class":35,"line":155},[33,1438,1439],{},"public IActionResult GetBad()\n",[33,1441,1442],{"class":35,"line":161},[33,1443,63],{},[33,1445,1446],{"class":35,"line":167},[33,1447,1448],{},"    var p = _repo.FindAsync(1).Result; \u002F\u002F blocks thread — kills scalability!\n",[33,1450,1451],{"class":35,"line":172},[33,1452,1453],{},"    return Ok(p);\n",[33,1455,1456],{"class":35,"line":178},[33,1457,93],{},[15,1459,1460,1461,1464],{},"ASP.NET Core binds ",[30,1462,1463],{},"CancellationToken"," parameters automatically from the HTTP request's\nabort token — when the client disconnects, the token fires, and your DB queries cancel.\nThis prevents wasted work on abandoned requests.",[10,1466,1468],{"id":1467},"recap","Recap",[15,1470,1471,1472,1474,1475,1477,1478,1480,1481,1483,1484,1486,1487,1489,1490,1492,1493,1495],{},"Inherit ",[30,1473,233],{}," for APIs, ",[30,1476,208],{}," for MVC views. Add ",[30,1479,247],{}," to get\nautomatic model state validation, binding inference, and Problem Details responses. Prefer\n",[30,1482,518],{}," over ",[30,1485,1002],{}," for accurate OpenAPI schemas. Use ",[30,1488,1331],{},"\non every action. Model binding pulls values from route, query, body, form, and headers — with\n",[30,1491,247],{},", source inference handles most cases automatically. Use Data Annotations for\nformat validation; put business rules in the service layer. Make actions async and accept\n",[30,1494,1463],{}," for any I\u002FO operation.",[1497,1498,1499],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":28,"searchDepth":42,"depth":42,"links":1501},[1502,1503,1504,1506,1507,1508,1509,1510,1511,1512,1513],{"id":12,"depth":42,"text":13},{"id":20,"depth":42,"text":21},{"id":237,"depth":42,"text":1505},"What ApiController actually does",{"id":416,"depth":42,"text":417},{"id":529,"depth":42,"text":530},{"id":677,"depth":42,"text":678},{"id":884,"depth":42,"text":885},{"id":1006,"depth":42,"text":1007},{"id":1260,"depth":42,"text":1261},{"id":1335,"depth":42,"text":1336},{"id":1467,"depth":42,"text":1468},"How ASP.NET Core controllers process requests — model binding sources, the ApiController attribute, action filters, content negotiation, and the IActionResult hierarchy.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-aspnet-core-controllers-actions","\u002Fdotnet\u002Faspnet-core\u002Fcontrollers-actions",{"title":5,"description":1514},"blog\u002Fdotnet-aspnet-core-controllers-actions","Controllers & Actions","ASP.NET Core","aspnet-core","2026-06-23","LYTt2tT4mKP-_D-G6zj8t6aLaOd8gwqQo4mwFNVK9AY",1782244086258]