DEV_NET_CORE
GET_STARTED
.NETAPI design and implementation

Endpoint routing and route matching

Overview

Endpoint routing is the ASP.NET Core system that matches incoming HTTP requests to executable endpoints such as controller actions, minimal API handlers, Razor Pages, SignalR hubs, gRPC services, and health check endpoints.

In an API, routing answers questions like:

  • Which handler should process GET /api/products/10?
  • Should /api/products/search go to a search endpoint or be interpreted as an {id} parameter?
  • Should /files/images/2026/logo.png be handled by a catch-all route?
  • Should an invalid route parameter return 404 Not Found or should model validation return 400 Bad Request?
  • What happens when two routes could both match the same URL?

This topic matters because routing is one of the first design decisions in an ASP.NET Core application. A good route design makes an API predictable, stable, secure, and easy to consume. A poor route design can create ambiguous endpoints, incorrect authorization behavior, broken link generation, hard-to-debug 404 responses, and inconsistent API contracts.

In interviews, this topic is important because it connects several practical ASP.NET Core skills:

  • API design
  • Controller routing
  • Minimal API routing
  • Middleware ordering
  • Authorization and endpoint metadata
  • Model binding
  • Route constraints
  • REST-style URL design
  • Debugging ambiguous or unexpected route matches

A strong candidate should understand not only how to write routes, but also how ASP.NET Core chooses the best route, when to use constraints, when optional parameters are risky, and why catch-all routes should be designed carefully.

Core Concepts

Endpoint Routing

Endpoint routing is the modern ASP.NET Core routing model. It separates route matching from endpoint execution.

A route endpoint usually contains:

  • A route pattern, such as /api/products/{id:int}
  • A request delegate or action method to execute
  • Metadata, such as HTTP method, authorization requirements, CORS policy, filters, endpoint name, or OpenAPI information

Example using minimal APIs:

Code
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/api/products/{id:int}", (int id) =>
{
    return Results.Ok(new { Id = id, Name = "Keyboard" });
})
.WithName("GetProductById")
.RequireAuthorization();

app.Run();

In this example:

  • /api/products/{id:int} is the route pattern.
  • {id:int} captures a route value named id.
  • :int is a route constraint.
  • MapGet adds HTTP method metadata for GET.
  • RequireAuthorization adds authorization metadata.
  • .WithName(...) gives the endpoint a name useful for link generation.

Endpoint routing is used by both minimal APIs and MVC controllers. The definition style is different, but the routing system still selects an endpoint based on route patterns, HTTP method metadata, constraints, and precedence.

Route Matching Pipeline

ASP.NET Core route matching can be understood as a filtering process.

For an incoming request, ASP.NET Core generally:

  1. Compares the request path against available route templates.
  2. Removes candidates that fail route constraints.
  3. Applies endpoint selection policies, such as HTTP method matching.
  4. Chooses the highest-priority endpoint.
  5. Throws an ambiguity error if multiple endpoints have the same priority and no single best match exists.

Example:

Code
app.MapGet("/api/products/list", () => "Product list");
app.MapGet("/api/products/{id:int}", (int id) => $"Product {id}");

Requests:

Code
GET /api/products/list  -> matches /api/products/list
GET /api/products/10    -> matches /api/products/{id:int}
GET /api/products/abc   -> no match for {id:int}, usually 404

The literal route /api/products/list is more specific than the parameter route /api/products/{id:int}.

Endpoint Routing and Middleware Ordering

Routing is closely related to middleware ordering.

A common ASP.NET Core pipeline looks like this:

Code
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

In modern minimal-hosting ASP.NET Core applications, the framework automatically places routing and endpoint execution around mapped endpoints in common scenarios. However, understanding the conceptual order is still important:

Code
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

The important idea is:

  • Middleware before routing cannot see the selected endpoint.
  • Middleware after routing and before endpoint execution can inspect endpoint metadata.
  • Authorization middleware must run after routing has selected an endpoint so it can read endpoint authorization metadata.
  • Endpoint execution is terminal when an endpoint is matched.

This is why endpoint metadata matters. For example:

Code
app.MapGet("/admin/reports", () => "Admin reports")
   .RequireAuthorization("AdminOnly");

The route itself defines authorization metadata. The authorization middleware reads that metadata before the endpoint runs.

Attribute Routing

Attribute routing defines routes directly on controllers and actions.

Example:

Code
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll()
    {
        return Ok();
    }

    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        return Ok(new { Id = id });
    }

    [HttpPost]
    public IActionResult Create(CreateProductRequest request)
    {
        return CreatedAtAction(nameof(GetById), new { id = 10 }, request);
    }
}

Resulting routes:

Code
GET  /api/products
GET  /api/products/10
POST /api/products

In attribute routing:

  • The route on the controller is usually a shared prefix.
  • The route on the action is appended to the controller route.
  • HTTP verb attributes such as [HttpGet], [HttpPost], and [HttpDelete] also define route templates.
  • Controller and action names do not affect route matching unless token replacement is used.

Combining Controller and Action Routes

A controller-level route can be combined with an action-level route.

Code
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        return Ok();
    }
}

The final route is:

Code
GET /api/orders/{id:int}

If an action route starts with / or ~/, it becomes an absolute route and is not combined with the controller route.

Code
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    [HttpGet("/health/orders")]
    public IActionResult Health()
    {
        return Ok("Orders API is healthy");
    }
}

The final route is:

Code
GET /health/orders

not:

Code
GET /api/orders/health/orders

This can be useful, but it can also surprise developers during refactoring.

HTTP Verb Attributes

HTTP verb attributes restrict a route to a specific HTTP method.

Common attributes include:

  • [HttpGet]
  • [HttpPost]
  • [HttpPut]
  • [HttpPatch]
  • [HttpDelete]
  • [HttpHead]

Example:

Code
[ApiController]
[Route("api/customers")]
public class CustomersController : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        return Ok();
    }

    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id)
    {
        return NoContent();
    }
}

Both actions use the same route template, but different HTTP methods:

Code
GET    /api/customers/5
DELETE /api/customers/5

This is a common REST-style API design.

Route Templates

A route template describes the URL pattern an endpoint can match.

Examples:

Code
api/products
api/products/{id}
api/products/{id:int}
api/orders/{orderId:int}/items/{itemId:int}
files/{**path}

A route template can contain:

  • Literal segments
  • Route parameters
  • Optional parameters
  • Default values
  • Constraints
  • Catch-all parameters
  • Complex segments
  • Parameter transformers

Literal Segments

Literal segments must match the URL path text.

Code
app.MapGet("/api/products/search", () => "Search products");

This route matches:

Code
GET /api/products/search

It does not match:

Code
GET /api/products/10
GET /api/products/SearchByName

Literal routes are more specific than parameter routes, so they usually win when both could match.

Route Parameters

A route parameter captures part of the URL into a named value.

Code
app.MapGet("/api/products/{id}", (string id) =>
{
    return Results.Ok($"Product id: {id}");
});

For:

Code
GET /api/products/abc123

id is captured as:

Code
abc123

In controllers:

Code
[HttpGet("{id}")]
public IActionResult GetById(string id)
{
    return Ok(id);
}

Route parameters are commonly used for resource identifiers.

Typed Route Parameters in Handlers

In minimal APIs and controllers, route values can be bound to typed parameters.

Code
app.MapGet("/api/products/{id:int}", (int id) =>
{
    return Results.Ok(id);
});

The route constraint ensures that only integer-looking values match. The handler parameter is then bound as an int.

Without the constraint:

Code
app.MapGet("/api/products/{id}", (int id) =>
{
    return Results.Ok(id);
});

a non-integer path may still match the route pattern, then fail during binding or validation depending on the endpoint style and configuration. For public APIs, route constraints make the intended URL shape clearer.

Route Constraints

Route constraints limit which URL values can match a route parameter.

Example:

Code
[HttpGet("{id:int}")]
public IActionResult GetById(int id)
{
    return Ok();
}

[HttpGet("{slug:alpha}")]
public IActionResult GetBySlug(string slug)
{
    return Ok();
}

Possible behavior:

Code
GET /api/products/123       -> GetById
GET /api/products/keyboard  -> GetBySlug
GET /api/products/abc123    -> no match

Common route constraints include:

ConstraintExampleMeaning
int{id:int}Must be an integer
long{id:long}Must be a long integer
guid{id:guid}Must be a GUID
alpha{name:alpha}Must contain alphabetic characters
bool{active:bool}Must be Boolean-like
datetime{date:datetime}Must be date/time-like
decimal{amount:decimal}Must be decimal-like
min{id:min(1)}Must be at least a value
max{id:max(100)}Must be at most a value
range{id:range(1,100)}Must be in a range
length{code:length(3)}Must have exact length
minlength{name:minlength(3)}Must have minimum length
maxlength{name:maxlength(50)}Must have maximum length
regex{code:regex(^[A-Z]{3}$)}Must match a regular expression

Multiple constraints can be combined:

Code
[HttpGet("{id:int:min(1)}")]
public IActionResult GetById(int id)
{
    return Ok();
}

This route requires id to be an integer and at least 1.

Constraints Are Not Input Validation

A common interview mistake is saying route constraints are validation.

They are not a replacement for request validation.

Route constraints are mainly for route disambiguation and URL shape matching. If a constraint fails, the route does not match, and the client typically receives 404 Not Found.

Validation should return 400 Bad Request with a useful error message.

Example:

Code
[HttpGet("{id:int:min(1)}")]
public IActionResult GetById(int id)
{
    return Ok();
}

Request:

Code
GET /api/products/abc

This does not match the route because abc is not an integer. A 404 response is expected.

Request:

Code
GET /api/products/0

This does not match if min(1) is used. A 404 response is expected.

If the API needs to tell the client that 0 is invalid input, handle it through validation instead:

Code
[HttpGet("{id:int}")]
public IActionResult GetById(int id)
{
    if (id <= 0)
    {
        return BadRequest("Product id must be greater than zero.");
    }

    return Ok();
}

Optional Parameters

Optional route parameters are marked with ?.

Code
[HttpGet("products/{id?}")]
public IActionResult GetProduct(int? id)
{
    if (id is null)
    {
        return Ok("All products");
    }

    return Ok($"Product {id}");
}

Matches:

Code
GET /products
GET /products/10

Optional parameters are useful in some MVC-style pages, but they should be used carefully in APIs.

For APIs, it is often clearer to define separate endpoints:

Code
[HttpGet("products")]
public IActionResult GetProducts()
{
    return Ok();
}

[HttpGet("products/{id:int}")]
public IActionResult GetProductById(int id)
{
    return Ok();
}

This gives each endpoint a clear purpose and avoids overloaded route behavior.

Optional Parameters vs Default Values

Optional parameters and default values are similar but not the same.

Optional parameter:

Code
{category?}

A value is produced only when the segment exists.

Default value:

Code
{category=all}

A value is produced even when the segment does not exist.

Example:

Code
app.MapGet("/products/{category=all}", (string category) =>
{
    return Results.Ok($"Category: {category}");
});

Request:

Code
GET /products

The route value is:

Code
category = all

For APIs, default route values are more common in conventional MVC routes than in explicit REST-style endpoints.

Catch-All Routes

A catch-all route captures the rest of the URL path.

Code
app.MapGet("/files/{**path}", (string? path) =>
{
    return Results.Ok($"Requested file path: {path}");
});

Matches:

Code
GET /files/readme.txt
GET /files/images/products/keyboard.png
GET /files/

Catch-all parameters are useful for:

  • File path routing
  • CMS pages
  • Slug-based pages
  • Documentation routes
  • Fallback routes for single-page applications
  • Proxy-like endpoints

Two forms exist:

Code
{*path}
{**path}

The difference matters most during URL generation:

  • {*path} escapes path separators when generating links.
  • {**path} preserves path separators when generating links.

For route matching, both can capture multiple path segments. For URL generation, {**path} is usually the better choice when the captured value is intended to remain a path.

Catch-All Routes Should Usually Be Last Conceptually

Catch-all routes are greedy because they can match many URLs.

Example:

Code
app.MapGet("/api/{**path}", (string path) => $"Fallback API path: {path}");
app.MapGet("/api/products/{id:int}", (int id) => $"Product {id}");

In endpoint routing, route precedence usually prevents a catch-all from beating a more specific route. However, catch-all routes should still be designed carefully because they can make APIs harder to reason about and can hide mistakes.

A better design is often:

Code
app.MapGet("/api/products/{id:int}", (int id) => $"Product {id}");
app.MapGet("/api/{**path}", (string path) => Results.NotFound());

For conventional routing, greedy routes should be placed later because conventional routing is order-dependent.

Route Precedence

Route precedence is the system ASP.NET Core uses to choose the most specific route when multiple endpoints could match.

General rules:

  • More segments are usually more specific.
  • Literal segments are more specific than parameter segments.
  • Constrained parameters are more specific than unconstrained parameters.
  • Complex segments are treated as more specific than simple unconstrained parameters.
  • Catch-all parameters are the least specific.

Example:

Code
app.MapGet("/products/list", () => "List");
app.MapGet("/products/{id}", (string id) => $"Product {id}");
app.MapGet("/products/{id:int}", (int id) => $"Product {id}");
app.MapGet("/products/{**path}", (string path) => $"Fallback {path}");

Possible matches:

Code
GET /products/list      -> /products/list
GET /products/123       -> /products/{id:int}
GET /products/abc       -> /products/{id}
GET /products/a/b/c     -> /products/{**path}

The more specific route wins.

Ambiguous Routes

Ambiguous routes happen when ASP.NET Core cannot choose a single best endpoint.

Example:

Code
app.MapGet("/products/{value}", (string value) => $"By value: {value}");
app.MapGet("/products/{name}", (string name) => $"By name: {name}");

Both routes have the same shape and priority. A request like:

Code
GET /products/keyboard

could match both. ASP.NET Core cannot know which one is intended.

Better:

Code
app.MapGet("/products/by-code/{code}", (string code) => $"By code: {code}");
app.MapGet("/products/by-name/{name}", (string name) => $"By name: {name}");

or use constraints when the shapes are truly different:

Code
app.MapGet("/products/{id:int}", (int id) => $"By id: {id}");
app.MapGet("/products/{slug:alpha}", (string slug) => $"By slug: {slug}");

Route Order

Endpoint routing mostly relies on route precedence instead of registration order. In common cases, you should not need to manually set route order.

However, order can still matter in some scenarios:

  • Conventional MVC routing
  • Explicit Order values on route endpoints
  • Greedy catch-all conventional routes
  • Fallback routes
  • Routes with identical precedence where ambiguity must be avoided

For conventional controller routes:

Code
app.MapControllerRoute(
    name: "areas",
    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

The more specific area route is placed before the default route.

For attribute routing and minimal APIs, prefer unique, clear route templates instead of relying on manual order.

Conventional Routing vs Attribute Routing

Conventional routing defines a pattern centrally.

Code
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

This can match:

Code
/Home/Index/10
/Products/Details/5

Attribute routing defines routes on actions.

Code
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        return Ok();
    }
}

Comparison:

FeatureConventional RoutingAttribute Routing
Route definitionCentralized in startup/program configurationClose to controller/action
Common useMVC web appsWeb APIs
URL styleOften controller/action basedOften resource based
PrecisionLess explicit per actionMore explicit per action
Order sensitivityMore order-dependentMostly precedence-based
Refactoring riskController/action names can affect routesRoutes remain explicit unless tokens are used

For APIs, attribute routing is generally preferred because it makes the public API contract explicit.

Token Replacement

Attribute routes can use tokens:

Code
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("[action]/{id:int}")]
    public IActionResult Details(int id)
    {
        return Ok();
    }
}

This produces:

Code
GET /api/products/details/10

Common tokens:

  • [controller]
  • [action]
  • [area]

Token replacement can reduce repetition, but it can also make public URLs dependent on class and method names. For public APIs, be careful because renaming a controller or action can unintentionally break routes.

A more stable route is often:

Code
[Route("api/products")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        return Ok();
    }
}

Reserved Route Parameter Names

Some route names have special meaning in MVC and Razor Pages routing, such as:

  • action
  • area
  • controller
  • handler
  • page

Avoid using these names as normal business parameters in controller or Razor Pages routes.

Risky:

Code
[HttpGet("documents/{page}")]
public IActionResult GetDocumentPage(string page)
{
    return Ok();
}

Better:

Code
[HttpGet("documents/{pageNumber:int}")]
public IActionResult GetDocumentPage(int pageNumber)
{
    return Ok();
}

Using reserved names incorrectly can cause confusing link generation or route matching behavior.

Route Values and Model Binding

Route parameters become route values and can be bound to action parameters.

Code
[HttpGet("orders/{orderId:int}/items/{itemId:int}")]
public IActionResult GetOrderItem(int orderId, int itemId)
{
    return Ok(new { orderId, itemId });
}

Request:

Code
GET /orders/100/items/5

Bound values:

Code
orderId = 100
itemId = 5

Route value names should match method parameter names unless explicit binding is used.

Code
[HttpGet("orders/{id:int}")]
public IActionResult GetOrder([FromRoute(Name = "id")] int orderId)
{
    return Ok(orderId);
}

This is valid, but matching names are clearer:

Code
[HttpGet("orders/{orderId:int}")]
public IActionResult GetOrder(int orderId)
{
    return Ok(orderId);
}

Route Design for REST APIs

For REST-style APIs, route templates should usually model resources, not actions.

Prefer:

Code
GET    /api/products
GET    /api/products/{id}
POST   /api/products
PUT    /api/products/{id}
PATCH  /api/products/{id}
DELETE /api/products/{id}

Avoid action-heavy routes when standard HTTP methods already communicate intent:

Code
GET  /api/products/getAllProducts
POST /api/products/createProduct
POST /api/products/deleteProduct/10

Action-like routes can still be appropriate for operations that do not map cleanly to CRUD:

Code
POST /api/orders/{id}/cancel
POST /api/invoices/{id}/send
POST /api/reports/monthly:generate

The important habit is consistency.

Nested Resource Routes

Nested routes express relationships between resources.

Code
[ApiController]
[Route("api/orders/{orderId:int}/items")]
public class OrderItemsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetItems(int orderId)
    {
        return Ok();
    }

    [HttpGet("{itemId:int}")]
    public IActionResult GetItem(int orderId, int itemId)
    {
        return Ok();
    }
}

Routes:

Code
GET /api/orders/10/items
GET /api/orders/10/items/5

Nested routes are useful when the child resource is naturally scoped by the parent. Avoid overly deep nesting such as:

Code
/api/customers/1/orders/2/items/3/discounts/4

Very deep routes can become hard to maintain. Consider query parameters or separate top-level resources when relationships become complex.

Query String vs Route Parameters

Route parameters are best for identifying resources.

Code
GET /api/products/10

Query strings are best for filtering, sorting, searching, and paging.

Code
GET /api/products?category=books&page=2&pageSize=20

Good route design:

Code
[HttpGet]
public IActionResult SearchProducts(
    [FromQuery] string? category,
    [FromQuery] int page = 1,
    [FromQuery] int pageSize = 20)
{
    return Ok();
}

[HttpGet("{id:int}")]
public IActionResult GetProductById(int id)
{
    return Ok();
}

Avoid putting many optional filters into the path:

Code
/api/products/books/active/price/10/100/page/2

That style is harder to evolve and understand.

Minimal API Route Groups

Minimal APIs can group routes with a shared prefix and shared metadata.

Code
var products = app.MapGroup("/api/products")
                  .RequireAuthorization()
                  .WithTags("Products");

products.MapGet("/", () => "All products");

products.MapGet("/{id:int}", (int id) => $"Product {id}");

products.MapPost("/", (CreateProductRequest request) =>
{
    return Results.Created($"/api/products/10", request);
});

Route groups help avoid repetition and keep related endpoints together.

They are useful for:

  • Shared authorization
  • API version prefixes
  • Tags for OpenAPI
  • Shared filters
  • Common route prefixes

Fallback Routes

Fallback routes are used when no other route matches.

They are common for single-page applications:

Code
app.MapFallbackToFile("index.html");

A custom fallback could look like:

Code
app.MapFallback(() => Results.NotFound(new
{
    Message = "The requested API endpoint was not found."
}));

Fallback routes should be used carefully in APIs. A fallback that returns 200 OK for unknown API paths can hide client errors and make debugging harder.

Routing is not only for matching incoming requests. It is also used to generate URLs.

In controllers:

Code
[HttpPost]
public IActionResult Create(CreateProductRequest request)
{
    var id = 10;

    return CreatedAtAction(
        nameof(GetById),
        new { id },
        new { Id = id, request.Name });
}

[HttpGet("{id:int}", Name = "GetProductById")]
public IActionResult GetById(int id)
{
    return Ok();
}

Named routes can make link generation more stable:

Code
return CreatedAtRoute(
    "GetProductById",
    new { id = 10 },
    new { Id = 10 });

In minimal APIs:

Code
app.MapGet("/api/products/{id:int}", (int id) => Results.Ok())
   .WithName("GetProductById");

app.MapPost("/api/products", (LinkGenerator links, HttpContext context) =>
{
    var uri = links.GetUriByName(context, "GetProductById", new { id = 10 });
    return Results.Created(uri!, new { Id = 10 });
});

Link generation helps avoid hardcoding URLs throughout the application.

Common Mistakes

Common mistakes include:

  • Relying on route constraints as business validation.
  • Creating ambiguous routes with the same shape.
  • Using optional parameters too broadly in APIs.
  • Creating greedy catch-all routes without considering precedence.
  • Mixing conventional and attribute routing without a clear reason.
  • Using [action] token replacement in public APIs and accidentally changing URLs during refactoring.
  • Using reserved route names as business parameter names.
  • Returning 200 OK from fallback routes for unknown API paths.
  • Making routes action-based when resource-based routes would be clearer.
  • Adding route parameters for filters that belong in the query string.
  • Forgetting HTTP method attributes on controller actions.
  • Expecting route registration order to solve route ambiguity in endpoint routing.

Best Practices

Practical best practices:

  • Prefer attribute routing for Web APIs.
  • Use resource-oriented route names.
  • Use HTTP methods to represent operations where appropriate.
  • Use route constraints to disambiguate similar routes.
  • Use validation for invalid business input.
  • Keep route templates explicit and stable.
  • Prefer separate endpoints over overloaded optional parameters.
  • Put filters, sorting, searching, and paging in query strings.
  • Avoid deep nested routes unless the hierarchy is essential.
  • Avoid catch-all routes unless there is a clear use case.
  • Name important endpoints for link generation.
  • Keep route parameter names consistent with method parameter names.
  • Test important route behaviors with integration tests.

Example integration test idea:

Code
[Fact]
public async Task GetProduct_WithNonIntegerId_ReturnsNotFound()
{
    await using var factory = new WebApplicationFactory<Program>();
    using var client = factory.CreateClient();

    var response = await client.GetAsync("/api/products/abc");

    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

Integration tests are especially useful for verifying route constraints, authorization behavior, and ambiguous route fixes.

Interview Practice

PreviousControllers vs Minimal APIsNext UpOpenAPI generation and API discoverability