Examples
Examples
Complete working examples demonstrating Route Standardization across MVC Controllers, Minimal APIs, and Razor Pages.
MVC Controller API
A complete REST API controller with various HTTP methods and parameter types.
[ApiController]
[Route("api/[controller]")]
public class UserProfileController : ControllerBase
{
// GET /api/user-profile/get-by-id/{user-id}
[HttpGet("GetById/{UserId}")]
public ActionResult GetById(int UserId) => Ok(new { UserId });
// GET /api/user-profile/search?first-name=John&last-name=Doe
[HttpGet("[action]")]
public ActionResult Search([FromQuery] UserSearchRequest request) => Ok(request);
// POST /api/user-profile/create-account
[HttpPost("CreateAccount")]
public ActionResult CreateAccount([FromBody] CreateUserRequest request) => Ok();
// PUT /api/user-profile/update-settings/{user-id}
[HttpPut("UpdateSettings/{UserId}")]
public ActionResult UpdateSettings(int UserId, [FromBody] UserSettings settings) => Ok();
// DELETE /api/user-profile/delete-account/{user-id}
[HttpDelete("DeleteAccount/{UserId}")]
public ActionResult DeleteAccount(int UserId) => Ok();
}
public class UserSearchRequest
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class CreateUserRequest
{
public string UserName { get; set; }
public string EmailAddress { get; set; }
}
public class UserSettings
{
public bool EnableNotifications { get; set; }
public string PreferredLanguage { get; set; }
}
Generated Routes
| HTTP Method | Original | Transformed |
|---|---|---|
| GET | /api/UserProfile/GetById/{UserId} |
/api/user-profile/get-by-id/{user-id} |
| GET | /api/UserProfile/Search |
/api/user-profile/search?first-name=&last-name= |
| POST | /api/UserProfile/CreateAccount |
/api/user-profile/create-account |
| PUT | /api/UserProfile/UpdateSettings/{UserId} |
/api/user-profile/update-settings/{user-id} |
| DELETE | /api/UserProfile/DeleteAccount/{UserId} |
/api/user-profile/delete-account/{user-id} |
Minimal API
A complete Minimal API setup with route groups and exclusions.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Configure with route prefix
var api = app.UseAspNetConventions("/api");
// GET /api/weather-forecast/{city-name}
api.MapGet("/WeatherForecast/{CityName}", (string CityName) =>
Results.Ok(new { City = CityName, Temperature = 72 }));
// GET /api/order-history/{customer-id}
api.MapGet("/OrderHistory/{CustomerId}", (int CustomerId) =>
Results.Ok(new { CustomerId, Orders = new[] { "Order1", "Order2" } }));
// POST /api/create-order
api.MapPost("/CreateOrder", (CreateOrderRequest request) =>
Results.Created("/orders/123", request));
// GET /api/product-catalog/by-category/{category-id}
api.MapGet("/ProductCatalog/ByCategory/{CategoryId}", (int CategoryId) =>
Results.Ok(new { CategoryId }));
// Excluded from transformation (tagged as internal)
api.MapGet("/health", () => Results.Ok("Healthy"))
.WithTags("internal");
api.MapGet("/metrics", () => Results.Ok(new { uptime = "24h" }))
.WithTags("internal");
app.Run();
public record CreateOrderRequest(string ProductName, int Quantity);
Configuration
var api = app.UseAspNetConventions("/api", options =>
{
options.Route.CaseStyle = CasingStyle.KebabCase;
options.Route.MinimalApi.ExcludeTags.Add("internal");
});
Generated Routes
| HTTP Method | Original | Transformed |
|---|---|---|
| GET | /api/WeatherForecast/{CityName} |
/api/weather-forecast/{city-name} |
| GET | /api/OrderHistory/{CustomerId} |
/api/order-history/{customer-id} |
| POST | /api/CreateOrder |
/api/create-order |
| GET | /api/ProductCatalog/ByCategory/{CategoryId} |
/api/product-catalog/by-category/{category-id} |
| GET | /health |
/health (excluded) |
| GET | /metrics |
/metrics (excluded) |
Razor Pages
A Razor Pages example with route parameters and form binding.
Page Model
// Pages/UserProfile/EditAddress.cshtml.cs
public class EditAddressModel : PageModel
{
[BindProperty(SupportsGet = true)]
public int UserId { get; set; }
[BindProperty(SupportsGet = true)]
public int AddressId { get; set; }
[BindProperty]
public AddressForm Address { get; set; } = new();
public void OnGet()
{
// Load address data
}
public ActionResult OnPost()
{
if (!ModelState.IsValid)
return Page();
// Save address
return RedirectToPage("/UserProfile/Index", new { UserId });
}
}
public class AddressForm
{
public string StreetAddress { get; set; }
public string CityName { get; set; }
public string PostalCode { get; set; }
public string CountryCode { get; set; }
}
Razor View
<!-- Pages/UserProfile/EditAddress.cshtml -->
@page "{UserId:int}/{AddressId:int}"
@model EditAddressModel
<h1>Edit Address</h1>
<form method="post">
<div>
<label for="street-address">Street Address</label>
<input type="text" name="street-address" value="@Model.Address.StreetAddress" />
</div>
<div>
<label for="city-name">City</label>
<input type="text" name="city-name" value="@Model.Address.CityName" />
</div>
<div>
<label for="postal-code">Postal Code</label>
<input type="text" name="postal-code" value="@Model.Address.PostalCode" />
</div>
<div>
<label for="country-code">Country</label>
<input type="text" name="country-code" value="@Model.Address.CountryCode" />
</div>
<button type="submit">Save</button>
</form>
Generated Routes
| Action | Original | Transformed |
|---|---|---|
| GET | /UserProfile/EditAddress/{UserId}/{AddressId} |
/user-profile/edit-address/{user-id}/{address-id} |
| POST | /UserProfile/EditAddress/{UserId}/{AddressId} |
/user-profile/edit-address/{user-id}/{address-id} |
Form Fields
| Original Property | Transformed Field Name |
|---|---|
StreetAddress |
street-address |
CityName |
city-name |
PostalCode |
postal-code |
CountryCode |
country-code |
Mixed Endpoint Configuration
Configure MVC Controllers and Minimal APIs with different settings in the same application.
var builder = WebApplication.CreateBuilder(args);
// MVC Controllers with kebab-case and prefix removal
builder.Services.AddControllers()
.AddAspNetConventions(options =>
{
options.Route.CaseStyle = CasingStyle.KebabCase;
options.Route.Controllers.RemoveActionPrefixes.Add("Get");
options.Route.Controllers.RemoveActionPrefixes.Add("Post");
options.Route.Controllers.RemoveActionSuffixes.Add("Async");
options.Route.Controllers.ExcludeControllers.Add("Health");
});
var app = builder.Build();
// Minimal APIs with snake_case for a different API version
var apiV2 = app.UseAspNetConventions("/api/v2", options =>
{
options.Route.CaseStyle = CasingStyle.SnakeCase;
});
app.MapControllers();
// MVC Controller endpoints (kebab-case, prefix removed)
// GET /api/user-profile/by-id/{user-id} (was GetById)
// POST /api/user-profile/account (was PostAccount)
// Minimal API endpoints (snake_case)
apiV2.MapGet("/WeatherForecast/{CityName}", handler);
// GET /api/v2/weather_forecast/{city_name}
apiV2.MapPost("/CreateOrder/{StoreId}", handler);
// POST /api/v2/create_order/{store_id}
app.Run();
Result Comparison
| Endpoint Type | Original | Transformed |
|---|---|---|
| MVC (kebab) | GetUserById/{UserId} |
user-by-id/{user-id} |
| MVC (kebab) | PostAccount |
account |
| Minimal (snake) | WeatherForecast/{CityName} |
weather_forecast/{city_name} |
| Minimal (snake) | CreateOrder/{StoreId} |
create_order/{store_id} |
Complete Application Setup
A full Program.cs example with all features configured.
using AspNetConventions;
using AspNetConventions.Core.Enums;
var builder = WebApplication.CreateBuilder(args);
// Configure MVC with AspNetConventions
builder.Services.AddControllers()
.AddAspNetConventions(options =>
{
// Global settings
options.Route.IsEnabled = true;
options.Route.CaseStyle = CasingStyle.KebabCase;
// MVC Controllers
options.Route.Controllers.TransformParameterNames = true;
options.Route.Controllers.TransformRouteTokens = true;
options.Route.Controllers.RemoveActionPrefixes.Add("Get");
options.Route.Controllers.ExcludeControllers.Add("Health");
// Hooks for debugging
options.Route.Hooks.AfterRouteTransform = (newRoute, originalRoute, model) =>
{
if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"[{model.Identity.Kind}] {originalRoute} → {newRoute}");
}
};
});
// Configure Razor Pages
builder.Services.AddRazorPages()
.AddAspNetConventions(options =>
{
options.Route.RazorPages.TransformParameterNames = true;
options.Route.RazorPages.TransformPropertyNames = true;
options.Route.RazorPages.ExcludeFolders.Add("Admin");
});
var app = builder.Build();
// Configure Minimal APIs
var api = app.UseAspNetConventions("/api/v1", options =>
{
options.Route.MinimalApi.ExcludeRoutePatterns.Add("/health");
options.Route.MinimalApi.ExcludeTags.Add("internal");
});
// Health check (excluded)
api.MapGet("/health", () => Results.Ok(new { status = "healthy" }))
.WithTags("internal");
// API endpoints
api.MapGet("/Products/{ProductId}", (int ProductId) =>
Results.Ok(new { ProductId }));
api.MapPost("/Orders/Create", (CreateOrderDto order) =>
Results.Created($"/orders/{order.OrderId}", order));
app.MapControllers();
app.MapRazorPages();
app.Run();
public record CreateOrderDto(Guid OrderId, string ProductName, int Quantity);
Output in Development
[MvcAction] api/UserProfile/GetById/{UserId} → api/user-profile/by-id/{user-id}
[MvcAction] api/UserProfile/CreateAccount → api/user-profile/create-account
[MinimalApi] /api/v1/Products/{ProductId} → /api/v1/products/{product-id}
[MinimalApi] /api/v1/Orders/Create → /api/v1/orders/create
[RazorPage] UserProfile/Edit/{UserId} → user-profile/edit/{user-id}