Routing

Routing is the process of mapping current request to a controller stored-procedure that will provide its response.

In Chameleon routes are defined in a table named chameleon.Routes. Routing is the first step in serving a request. This is done by a Chameleon middleware named RoutingMiddleware. It uses a CLR stored-procedure named chm.FindRoute for this purpose.

chm.FindRoute UDF performs two main jobs:

  1. First, it looks for a route in chameleon.Routes table that matches current request (based on its URL and its method).
  2. Second, it determines name of a stored-procedure that will act as a Controller (the C part in in MVC terminology) to provide the response of current request.

If RoutingMiddleware finds a route, it selects list of values for the route parameters (if the route has any parameter).

In this section, different features of RoutingMiddleware is explained.

Defining routes

A route is a pattern that can cover one or more URL. It is defined like a relative path using slash separator.

			  /a/b/c
			

In order for a route to match a given URL, its segments or fragments -the parts between slash- should match their equivalent segment in the URL.

The example above matches a URL whose path is exactly as /a/b/c. No path with less or greater number of segments like /a, /a/b or /a/b/c/d will match this route.

Route Structure

As it was stated earlier, routes are defined in a table named chameleon.Routes. Structure or schema of this table is as follows:

Column Type Descrption
id int PK (identity)
name nvarchar(200) Route name. An optional name that describes what the route does or meant for.
route nvarchar(1000) Route pattern
isActive bit Wether the route should participate in route matching or not
defaults nvarchar(2000) Default values for route parameters
constraints nvarchar(2000) Route parameter constraints
sproc nvarchar(500) Explicit name of controller stored procedure that will provide response of current request.
httpMethods varchar(1000) List of http methods that the route accepts and can match with.
settings nvarchar(4000) Route's settings

Parameters

Each segment of a route is defined as a fixed value or a parameter.

Fixed values must match exactly with their equivalent segment in order to match as we saw in previous section. The comparison is performed case-insensitively. So, /product matches /Product and /PRODUCT as well.

Parameters are merely a placeholder for an equivalent segment in the given URL and span multiple values.

Route parameters are defined using brace characters as in {controller}. They match any value in the equivalent segment in the given URL.

Example:

  /product/{action}

The above route matches any URL that has two segments and starts with /product like /product/list, /product/show, /product/add but not /product since it has only one segment or /product/edit/123 since it has three segments.

System Parameters

There are three system parameters that RoutingMiddleware supports and have a special meaning:

  • {controller}: This parameter defines a logical name by which a group of controller stored procedures are targeted. It denotes a logical controller that does the job, not a physical object.
  • {action}: This parameter defines the action that the final controller should carry out in order to fulfill current request.
  • {area}: This parameter is a logical name that defines an area or group that the controller belongs to. This provides the ability to have controllers with the same name that control different parts of the application.

These system parameters participate actively in determining name of the controller stored-procedure that will be invoked in the next step of request pipeline by MvcMiddleware (or any middleware that is responsible for invoking controller stored procedure).

If RoutingMiddleware finds a route match, it adds two more system parameters named $route_id and $route_sproc to route values dictionary. $route_id is the [id] of matched route and $route_sproc is the name of controller stored procedure that should satisfy the response.

Determining name of the controller stored-procedure will be explained later in Routing and name of controller SPROC section.

Optional Parameters

Optional parameters are defined using a question mark at the end of their name as in {controller?}. When a parameter is optional, their equivalent segment in the given URL could be missing.

Example:

		  /product/{action?}
		

The above route matches /product in addition to /product/list, /product/show, /product/edit that the previous route (i.e. /product/{action} could accept).

Processing Order

RoutingMiddleware processes routes in chameleon.Routes table based on their routeOrder column (in ascending order).

Example:

route routeOrder
/{controller}/{action?} 2
/product/{action?} 1

If given URL is /product/list, the second route is matched, since it has more priority over /{controller}/{action?} due to its routeOrder.

Disable/Enable

Each route in chameleon.Routes table has an isActive column with bit data type. RoutingMiddleware assumes only those routes whose isActive equals 1. This enables us to dynamically disable/enable routes.

Defaults

Using the defaults column in chameleon.Routes we can specify default values for route parameters in case the parameter is missing in the URL. The defaults value should be in JSON format.

Example:

route defaults
/{controller}/{action} { "controller": "home", "action": "index" }

Although action parameter in the above route is not optional, the route matches a /product URL, since there is a default value for action parameter in the defaults column.

Area

AS stated earlier in System Parameters section, there is a system parameter named area by which we can define the same route in different areas of our application. If we use area parameter directly in the route (somewhere inside route as {area}) or indirectly (in defaults field), the value of the area parameter will be added to the name of the stored-procedure that RoutingMiddleware will determine after matching the route.

Example 1: explicit area

  /{area}/{controller}/{action}/{id?}

The above route will match /admin/product/add, /user/product/favorite but not /product/list.

Example 2: implicit area

route defaults
/api/{controller}/{action} { "area": "api" }

The above route matches /api/product/add but not /admin/product/add.

Parameters Constraints

It is possible to define constraints for route parameters using the constraints field. The value of this field should be a JSON object whose keys are route parameter names and their values are constraints that are defined for each route.

RoutingMiddleware supports two types of route parameter constraints:

  • Regex: we can specify a regular expression for the route parameter.
  • UDF: we can specify name of a User-Defined-Function that will check whether provided value for the route parameter matches its condition. The signature of the UDF should correspond to the following function:
  CREATE or ALTER FUNCTION dbo.RouteParameterConstraint
(
	@context_id int,
	@value      nvarchar(4000)
)
RETURNS bit
AS
...

That is, it should have two parameters:

  • @context_id of int type
  • @value of nvarchar(4000) type

It should also return bit (1 as true or 0 as false).

Upon invocation, RoutingMiddleware will pass current context-id to the first parameter and value of the route parameter for the second parameter.

If the UDF returns 1, it is assumed that route parameter satisfies its constraint, otherwise it does not and hence, the route will not match current URL.

Example 1: Specifying a regular expression

route constraints
/{controller}/{action}/{id} { "id": "^\\d+$" }

The above route matches /product/show/123 but will not match /product/list/all since all does not match ^\d+$ regular expression that is defined for id parameter.

Example 2: Specifying a UDF

Suppose we have the following UDF:

  create or alter function dbo.isValidAction
(
	@context_id int,
	@value      nvarchar(4000)
) returns bit
as
begin
	return case when @value in ('index', 'list', 'show') then 1 else 0 end
end

We have also the following route in chameleon.Routes table:

route constraints
/{controller}/{action}/{id?} { "action": "isValidAction" }

The above route matches /product/show but will not match /product/add since dbo.isValidAction will not return 1 for add.

Http Method Constraint

Using httpMethods field we can specify http method constraint i.e. specify a list of http methods that the route will match only when the request is sent with one of them. Value of this field should be a JSON array.

Example:

route httpMethods
/{controller}/{action} [ "GET", "POST" ]

Here, the above route will only match with a URL like /product only when the request is sent using GET or POST method. If the request is sent using say PUT method the route will not match.

Http Method Mapping

RoutingMiddleware provides another option for using default value for action parameter when it is not specified in the route, no value was found for it in the URL or not default value for it is specified in defaults field. We can ask RoutingMiddleware to use the http method -or a mapping of http method- as the default value for action parameter. This is primary useful in providing REST apis, but it can also be used in other cases as well.

Http method mapping can be done at two levels: global and local.

Global Mapping

There is a global setting named Chameleon.Routing.HttpMethodAsAction in chameleon.Settings table that enacts http method to action mapping. There is also a global setting named Chameleon.Routing.HttpMethodMapping that defines how http method should be mapped to actions. Here is an example:

route
/{controller}/{action?}

The default value for global Chameleon.Routing.HttpMethodMapping setting is as follows:

  { "post": "insert", "put":"update", "delete": "delete", "get": "get" }

Now, if current URL misses a value for action parameter (e.g. current URL is /product) and the request is sent using say a POST method, RoutingMiddleware will uses insert for the value of action parameter.

If no value is found in global Chameleon.Routing.HttpMethodMapping setting for current http method, RoutingMiddleware will use the http verb itself for the value of action parameter. For example, if current URL is /product and it is sent using a PATCH method, the value for action parameter will be patch (in lowercase).

Chameleon.Routing.HttpMethodAsAction is true (1) by default.

Note: The defaults field takes precendence over global http method to action mapping. If a default value for action parameter is specified in defaults (like { "action": "index" }), the final value for action parameter will be index.

Local Mapping

Sometimes we may want to override global http mapping. We can specify desired http mapping value in the httpMethods for any http verb we want. Look at the following example:

route httpMethods
/{controller}/{action?} [ "GET", { "POST": "add" }, { "PUT": "edit" } ]

Here, the route will only match for requests sent in GET, POST and PUT. Now, if the request misses a value for action (the URL is e.g. /product), RoutingMiddleware will use add if it is sent in POST or will use edit if it is sent in PUT.

Pay attention that, local http method mapping has more priority over defaults. Look at the following example:

route defaults httpMethods
/{controller}/{action} { "action": "index" } [ "GET", { "POST": "add" }, { "PUT": "edit" } ]

Here, if the request URL is /product and it is sent using GET method, the value for action will be index since no override value for GET is defined. However, if the request is sent using POST, the add value takes precedence over the default index value. So, the value of action parameter will be add.

As it was stated in System Parameters section, action plays a primary role in determining name of controller stored-procedure. This is explained in Controller Factory section.

Disable http mapping locally

In some situations, we may want to disable http method mapping for a specific route. As it was said, http method mapping is globally active. Thus, if action parameter has no value and no default value for action is specified, RoutingMiddleware will go for global http method mapping. We can prevent this in a specific route by a setting named httpMethodAsAction with a false or 1 value in route's settings. Look at the following example:

route defaults settings
/{controller}/{action?} { "httpMethodAsAction": false }

If current URL is /product, the final value for action will be an empty string. In fact no value for action will be set. This enables us to have a single controller stored-procedure for any http method the request is sent with. Note that, in order for this feature to work, we should not specify a default value for action in the defaults, otherwise, the default value will be used for action.

Catch-All routes

One scenario regarding routing is having the ability to define a catch-all route i.e. a route that is able to catch any URL. This is possible using * character at the beginnng of a route parameter. If a route parameter stars with *, it can match any segments from that segments onward it equals to.

Example:

  route = /{controller}/{action}/{rest*}

The above route matches any URL with 2 and more segments.

Mateched routes example:

  • /product/list: controller = product, action = list, rest = '/'
  • /product/edit/123: controller = product, action = edit, rest = '/123'
  • /product/books/tags/csharp: controller = product, action = books, rest = '/tags/csharp'
  • /blog/john-doe/2022/08: controller = blog, action = john-doe, rest = '/2022/08'

Name of controller SPROC

The second job of RoutingMiddleware is determining name of controller SPROC. In this step, system route parameters play a primary role. RoutingMiddleware uses the following format or formula in order to determine name of controller stored-procedure.

[ {schema} ].[{prefix}{area}{controller}{action}]

Notes:

  • The default value of {schema} is dbo. This can be changed using Chameleon.Routing.SprocDefaultSchema setting.
  • The default value of {prefix} is USP. This can be changed using Chameleon.Routing.SprocPrefix setting.
  • Between the parametric parts a dash or - character is used. This can also be changed using Chameleon.Routing.SprocPartSeparator setting.

Example:

route defaults routeOrder settings
api/{controller}/{id?} { "area": "api" } 1 { "httpMethodsAsAction": true }
admin/{controller}/{action?}/{id?} { "area": "admin", "controller": "home", "action": "index" } 2
{controller?}/{action?}/{id?} { "controller": "home", "action": "index" } 3

Sample URLs and generated controller SPROC name:

URL SPROC
/ [dbo].[USP_Home_Index]
/product [dbo].[USP_Product_Index]
/product/list [dbo].[USP_Product_List]
/product/show/123 [dbo].[USP_Product_Show]
/api/product with GET [dbo].[USP_api_Product_Get]
/api/product with POST [dbo].[USP_api_Product_Post]
/api/product with PUT [dbo].[USP_api_Product_Put]
/api/product with DELETE [dbo].[USP_api_Product_Delete]
/admin/product [dbo].[USP_admin_Product_Index]
/admin/product/list [dbo].[USP_admin_Product_List]
/admin/product/edit/123 [dbo].[USP_admin_Product_Edit]

Manual SPROC name

We can override RoutingMiddleware sproc name determination in two ways:

  1. Static Name: Specifying name of controller SPROC explicitly using Sproc field in a route. This way the specified sproc will handle all URLs that match the route.
  2. Dynamic Name: Specifying name of a get-sproc UDF that will return name of controller SPROC. This way name of controller sproc will be dynamic. The name of get-sproc UDF can be specified in two ways:
    1. Global Chameleon.Routing.GetSproc setting.
    2. Local getSproc setting in route's settings field.

In the second method, name of the get-sproc UDF should only contain alphanumeric characters, dot , bracket or underline characters. The get-sproc UDF should have a single int parameter and return nvarchar. When the UDF is called, current context-id is passed to the UDF.

Example:

  CREATE OR ALTER FUNCTION dbo.GetRouteSproc
(
	@context_id int
)
RETURNS NVARCHAR(500)
AS
BEGIN
	declare @result nvarchar(500)
	declare @area nvarchar(500)
	declare @controller nvarchar(500)
	declare @action nvarchar(500)
	declare @method nvarchar(500)

	select @method = [method] from chameleon._requestUrl where [context_id] = @context_id
	set @method = '_' + isnull(@method, 'get')
	set @area = isnull('_' + chameleon.[Route.GetValue](@context_id, 'area', null), '')
	set @controller = isnull('_' + chameleon.[Route.GetValue](@context_id, 'controller', null), '')
	set @action = isnull('_' + chameleon.[Route.GetValue](@context_id, 'action', null), '')

	set @result = 'dbo.usp' + lower(@area) + lower(@controller) + lower(@action) + @method

	return @result
END

This get-sproc UDF makes area, controller, action parameters lowercase and adds http method to the end of sproc name. For a URL like /product/list that is requested with GET and POST method, the name of controller sproc will be dbo.usp_product_list_get and dbo.usp_product_list_post respectively.

Related Settings

[Chameleon.Routing.SprocPrefix]: string (default = USP)

Specifies default prefix of controller stored procedure. It will be added to the begining of the name of controller sproc.

[Chameleon.Routing.SprocDefaultSchema]: string (default = dbo)

Specifies default schema of controller stored procedure.

[Chameleon.Routing.SprocPartSeparator]: string (default =_)

Specifies default separator by which different parts of controller stored procedure will be concatenated with. Default is underline character.

[Chameleon.Routing.HttpMethodAsAction]: boolean (default = true)

This is a global setting that specifies whether HTTP methods should be used as action in case of action parameter is missing.

[Chameleon.Routing.HttpMethodMapping]: string

Specifies how http methods should be mapped to actions when action parameter is missing. Default value for this setting is as follows:

  { "GET":"get", "POST": "insert", "PUT": "update", "PATCH": "modify", "DELETE": "delete" }