Why does controller extend controllerBase?
- Enables Web API Features , ControllerBase provides core functionalities required by Web APIs, including: Model binding, Request/response handling, Routing, Filters.
- Access to Built-in Helper Methods Methods like Ok(), BadRequest(), NotFound(), and Created() are defined in ControllerBase. These are essential for returning HTTP status codes and formatted responses.
- Integration with [ApiController] Attribute The [ApiController] attribute depends on ControllerBase. It enables automatic: Model validation, Error responses, Binding source inference (e.g., [FromBody], [FromQuery])
- Required by ASP.NET Core Runtime , If you don’t inherit from ControllerBase or Controller, the ASP.NET Core runtime won’t treat your class as a controller. Your route attributes and methods won’t be discovered or executed.
What [ApiController] Actually Does
- Automatic Model Validation , ASP.NET Core automatically returns 400 Bad Request if the request body is invalid.
- Binding Source Inference, You don't need to explicitly decorate every parameter with [FromQuery], [FromBody], etc.—the runtime infers them based on HTTP method and parameter types.
- Problem Details for Error Responses, Error responses follow the standardized RFC 7807 format (application/problem+json), which helps frontends parse error information consistently.
- Required Parameters Enforcement, If you forget to pass a required query string or route parameter, it returns 400 Bad Request automatically.
Attribute Routing
Attribute routing allows you to define URL patterns directly on controllers and actions:
[Route("api/[controller]")]
public class TaskController : ControllerBase
{
[HttpGet("all")]
public IActionResult GetAllTasks() => Ok(...);
}
[controller] is replaced by the class name without the Controller suffix.
[action] is replaced by the method name.
- Pitfall: Using [action] can be fragile — renaming methods will change the endpoint. Better to use static route strings. Using [Controller] can be strange sometime, for example, if a controller named UserV2Controller, and [Route("api/[controller]")] may be parsed api/UserV, I do not know why, but I observed this.
Attribute Routing Styles
Recommended:
[Route("api/task")]
[ApiController]
public class TaskController : ControllerBase
{
[HttpGet("all")]
public IActionResult GetAll() { ... }
}
Not recommended:
[Route("api/[controller]/[action]")]
public class TaskController : ControllerBase
{
[HttpGet]
public IActionResult GetAll() { ... }
}
Route Parameters
[HttpGet("user/{id}")]
public IActionResult GetUserById(int id)
Example request: GET /api/user/123
The value 123 is passed via the URL path.
Route parameters are required by default, unless marked as optional (e.g., {id?}).
Useful for identifying a specific resource.
Query Parameters
[HttpGet("user")]
public IActionResult GetUserById([FromQuery] int id)
Example request: GET /api/user?id=123
The parameter is passed after the ? as part of the query string.
Commonly used for filtering, pagination, or optional values.
Optional Route Parameters / Default Values
[HttpGet("user/{id?}")]
public IActionResult GetUserById(int? id = null)
You can make route parameters optional and assign default values.
Combining Route and Query Parameters
[HttpGet("user/{id}")]
public IActionResult GetUser(int id, [FromQuery] string status)
Example: GET /api/user/123?status=active
Parameters from Request Body (POST/PUT)
[HttpPost("user")]
public IActionResult Create([FromBody] UserDto dto)
Used to bind JSON data from the body of a POST or PUT request.
Route Constraints
1.Avoid Mapping the Same URL to Multiple Actions
Route constraints like {id:int} help the routing system differentiate between similar-looking URLs before the request reaches the controller.
This prevents ambiguity and runtime errors caused by multiple actions matching the same route.
[HttpGet("user/{id:int}")]
public IActionResult GetById(int id)
[HttpGet("user/{name}")]
public IActionResult GetByName(string name)
/user/123 → clearly matches GetById
/user/Alice → clearly matches GetByName
Without {id:int}, both routes would match /user/123, causing a conflict and potentially throwing an exception.
2.Different Execution Timing — Happens Before the Controller Is Invoked
If the URL doesn't meet the constraint (e.g., not an int), the system will not invoke the action at all.
This improves both security and performance — the request is rejected before model binding or execution begins.
3.Execution Flow Overview
[Incoming HTTP Request]
↓
[Route Matching Phase] ← Route constraint (e.g., {id:int}) is applied here
↓
[Matched Action Found]
↓
[Model Binding Phase] ← Parameters like (int id) are bound here
↓
[Controller Method Execution]
4.Conclusion
"Route constraints act as a gatekeeper before the controller is even considered, while method parameter types validate the value after entry."
Using constraints effectively is especially important in large-scale APIs where route conflicts can arise easily.
If a route parameter includes a type constraint (e.g., {id:int}), requests that don't satisfy the constraint will fail during the routing phase and return a 404 Not Found. If no constraint is specified and the method relies solely on the parameter type declaration (e.g., int id), the request will proceed to the model binding phase, where an invalid value will result in a 400 Bad Request with a JSON error response.
How to Configure API Versioning in ASP.NET Core(and show in Swagger)
- Add Required NuGet Packages
dotnet add package Swashbuckle.AspNetCore
dotnet add package Microsoft.AspNetCore.Mvc.Versioning
dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
- Configure Services in Program.cs Note: Version format 'v'VVV means it will generate "v1", "v1.0", "v2" depending on your version numbers. It's true. But when you use Swagger, the swagger only get like v1.0.1 or v1.1 when you use 'v'VV.V or 'v'V.VV, unless you use 'v'VVV, the Swagger will be v1.(I do not know why until now)
OpenApi + Scalar may not support multiple versions, so I use Swagger in multiple versions. (I will try again in the future)
If you visit this api from browser, there is no such issues.
builder.Services.AddApiVersioning(options =>
{
// Assume a default version if the client hasn't specified one
options.AssumeDefaultVersionWhenUnspecified = true;
// Set the default API version to 1.0
options.DefaultApiVersion = new ApiVersion(1, 0);
// Report supported API versions in the response headers
options.ReportApiVersions = true;
// Use URL segments to determine the API version (e.g., /api/v1/controller)
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
builder.Services.AddVersionedApiExplorer(options =>
{
// Format for grouping API versions (e.g., "v1", "v1.1")
options.GroupNameFormat = "'v'VVV";
// Replace {version:apiVersion} tokens in route templates
options.SubstituteApiVersionInUrl = true;
});
// If you're using Swagger or Scalar, this setup ensures correct version grouping and endpoint discovery.
- Config Swagger Multiple version options
//Create ConfigureSwaggerOptions.cs:
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;
using Microsoft.OpenApi.Models;
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _provider;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
{
_provider = provider;
}
public void Configure(SwaggerGenOptions options)
{
foreach (var description in _provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, new OpenApiInfo
{
Title = $"My API {description.ApiVersion}",
Version = description.ApiVersion.ToString()
});
}
}
}
- register it in Program.cs
builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();
// after builder.build()
if (app.Environment.IsDevelopment())
{
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
$"API {description.GroupName}");
}
});
}
- Define Versioned Controller Routes
[ApiController]
[ApiVersion("1.0")]
// give different group name
[ApiExplorerSettings(GroupName = "v1")]
[Route("api/v{version:apiVersion}/RoutingAttribute")]
public class RoutingAttributeController : ControllerBase
{
[HttpGet("TaxRate")]
public IActionResult GetTaxRate()
{
return Ok("Tax rate is 15%");
}
}
// v{version:apiVersion} will be automatically replaced with the correct version.
// You can add multiple [ApiVersion("x.x")] attributes to support multiple versions in the same controller.
RESTful Routing
What is REST?
REST (Representational State Transfer) is an architectural style for designing networked applications. RESTful APIs use HTTP methods to operate on resources identified by URLs, promoting a clear, scalable, and standardized interface.
Best Practices
- Pluralize resource names: users, posts, comments
- Use consistent naming: Stick to lowercase, hyphenated URLs if needed
- Return appropriate HTTP status codes: 200 OK for success 201 Created after POST 204 No Content for successful DELETE 400 Bad Request for validation errors 404 Not Found when the resource doesn’t exist 500 Internal Server Error for unexpected failures
- Use [ProducesResponseType] in ASP.NET Core to document responses in Swagger
- Support filtering, pagination, sorting via query strings GET /users?page=2&size=20&sort=createdAt
How to Talk About It in an Interview
“In my API design, I follow RESTful conventions by using clear, noun-based resource paths and leveraging HTTP methods to express actions. I prefer pluralized resource names, nested routes to express relationships, and query parameters for filtering and pagination. I also ensure that Swagger is integrated with proper response annotations like [ProducesResponseType] so documentation is consistent and developer-friendly.”
Top comments (0)