URL Generation
🎯 What You'll Learn
- Why generate URLs programmatically
- Url.Action() and Url.RouteUrl()
- CreatedAtAction() for APIs
- LinkGenerator service
- Named routes
- Best practices
Why Generate URLs?
URL generation creates URLs programmatically instead of hardcoding them.
Benefits
- Maintainability: Change routes in one place
- Type safety: Compile-time checking
- Consistency: URLs always match routes
- Refactoring: Rename actions without breaking links
// ❌ Hardcoded (brittle)
return Created("/api/products/123", product);
// ✅ Generated (maintainable)
return CreatedAtAction(nameof(GetProduct), new { id = 123 }, product);
Url.Action() - MVC
Url.Action() generates URLs for MVC controller actions.
// Current controller
var url = Url.Action("Details");
// → /Products/Details
// Different controller
var url = Url.Action("Index", "Home");
// → /Home/Index
// With route values
var url = Url.Action("Details", "Products", new { id = 123 });
// → /Products/Details/123
// Absolute URL
var url = Url.Action("Details", "Products", new { id = 123 }, Request.Scheme);
// → https://example.com/Products/Details/123
In Razor Views
<a href="@Url.Action("Details", "Products", new { id = 123 })">
View Product
</a>
<!-- Or use asp-* tag helpers -->
<a asp-controller="Products" asp-action="Details" asp-route-id="123">
View Product
</a>
CreatedAtAction() - APIs
CreatedAtAction() returns 201 Created with a Location header.
[HttpPost]
public IActionResult CreateProduct([FromBody] ProductDto dto)
{
var product = new Product { Id = 123, Name = dto.Name };
// Generate URL to GetProduct action
return CreatedAtAction(
nameof(GetProduct), // Action name
new { id = product.Id }, // Route values
product); // Response body
}
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
return Ok(new Product { Id = id });
}
// Response:
// HTTP/1.1 201 Created
// Location: /api/products/123
// { "id": 123, "name": "Laptop" }
CreatedAtRoute()
[HttpGet("{id}", Name = "GetProduct")]
public IActionResult GetProduct(int id) { }
[HttpPost]
public IActionResult CreateProduct([FromBody] ProductDto dto)
{
var product = new Product { Id = 123 };
return CreatedAtRoute(
"GetProduct", // Route name
new { id = product.Id },
product);
}
LinkGenerator Service
LinkGenerator generates URLs outside of controllers (services, middleware, etc.).
public class EmailService
{
private readonly LinkGenerator _linkGenerator;
public EmailService(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public string GenerateConfirmationEmail(int orderId)
{
// Generate URL by action
var url = _linkGenerator.GetUriByAction(
"Details",
"Orders",
new { id = orderId },
"https",
new HostString("inventrackapp.com"));
return $"View your order: {url}";
}
}
LinkGenerator Methods
// By action
var url = _linkGenerator.GetPathByAction("Details", "Products", new { id = 123 });
// → /Products/Details/123
// By route name
var url = _linkGenerator.GetPathByName("GetProduct", new { id = 123 });
// → /api/products/123
// Absolute URI
var url = _linkGenerator.GetUriByAction(
"Details", "Products", new { id = 123 },
"https", new HostString("example.com"));
// → https://example.com/Products/Details/123
Named Routes
Named routes provide a stable identifier for URL generation.
[HttpGet("{id}", Name = "GetProduct")]
public IActionResult GetProduct(int id) { }
// Generate URL by name
var url = Url.RouteUrl("GetProduct", new { id = 123 });
app.MapControllerRoute(
name: "product-details",
pattern: "products/{id}",
defaults: new { controller = "Products", action = "Details" });
// Generate URL by name
var url = Url.RouteUrl("product-details", new { id = 123 });
Redirect to Action
// Redirect to action in same controller
return RedirectToAction("Index");
// Redirect to action in different controller
return RedirectToAction("Details", "Products", new { id = 123 });
// Redirect to named route
return RedirectToRoute("GetProduct", new { id = 123 });
Complete InvenTrack Example
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
// Named route for URL generation
[HttpGet("{id}", Name = "GetProduct")]
public async Task<IActionResult> GetProduct(int id)
{
var product = await _productService.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
// CreatedAtAction with URL generation
[HttpPost]
public async Task<IActionResult> CreateProduct([FromBody] CreateProductDto dto)
{
var product = await _productService.CreateAsync(dto);
// Generate URL to GetProduct action
return CreatedAtAction(
nameof(GetProduct),
new { id = product.Id },
product);
// Response:
// HTTP/1.1 201 Created
// Location: /api/products/123
}
// CreatedAtRoute with named route
[HttpPost("batch")]
public async Task<IActionResult> CreateBatch([FromBody] List<CreateProductDto> dtos)
{
var products = await _productService.CreateBatchAsync(dtos);
// Generate URLs for each product
var result = products.Select(p => new
{
product = p,
url = Url.RouteUrl("GetProduct", new { id = p.Id })
});
return Ok(result);
}
}
Email Service with LinkGenerator
public class EmailService
{
private readonly LinkGenerator _linkGenerator;
public EmailService(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public string GenerateOrderConfirmation(int orderId)
{
// Generate absolute URL for email
var orderUrl = _linkGenerator.GetUriByName(
"GetOrder",
new { id = orderId },
"https",
new HostString("inventrackapp.com"));
return $@"
Thank you for your order!
View your order: {orderUrl}
";
}
}
Best Practices
- Use nameof():
nameof(GetProduct)for type safety - Named routes: Use for stable API contracts
- CreatedAtAction(): Always use for POST endpoints
- LinkGenerator: Use in services/middleware
- Avoid hardcoding: Never hardcode URLs
- Absolute URLs: Use for emails, external links
- Relative URLs: Use for internal navigation
Key Takeaways
- URL generation: Create URLs programmatically
- Url.Action(): Generate URLs in MVC controllers
- CreatedAtAction(): Return 201 with Location header
- CreatedAtRoute(): Use named routes
- LinkGenerator: Generate URLs in services
- Named routes: Stable identifiers for URL generation
- RedirectToAction(): Redirect to another action
- Type safety: Use
nameof()
You now understand URL generation! In the final section, we'll explore Areas—how to organize large applications into logical sections with their own controllers, views, and routes.