Quantcast
Channel: ASP.NET MVC / Web API / Web Pages
Viewing all 7925 articles
Browse latest View live

Closed Issue: HttpActionDescriptorTracer is doubly wrapped [1080]

$
0
0
The ActionSelectorTracer unconditionally wraps an HttpActionDescriptorTracer around whatever object is returned from innerSelector.SelectAction. But if that object is already wrapped by a tracer, it gets wrapped a 2nd time.

ActionSelectorTracer should check that its HttpActionDescriptor has already been wrapped by a tracer.
Comments: Verified.

Commented Issue: [WebPages]: Update MimeMapping.cs to be match IIS8 applicationHost.config [1072]

$
0
0
The file was generated from the mimeMap in %System32%\inetsrv\config\applicationHost.config and it looks like IIS 8 has some slightly different mime mappings. It should probably be updated to match this file although the utility of it is highly questionable.
Comments: Adding more context for the bug: The WebPages’ “MimeMapping.cs” file is originally engineered based on the “ApplicationHost.config” file from IIS 7.5. The newly released IIS8.0 has some minor change in the ApplicationHost.config file which cause some mismatch entry from the MimeMapping.cs in WebPages. The usage that potentially would impact is the System.Web.Helper e.g. the System.Image class; the other possible usage is the WebPage Administration. The entry pointed out by the report is actually boils down to one line of code; the difference is listed below: From MimeMapping.cs: { ".js", "application/x-javascript" }, From the new ApplicationHost.config: <mimeMap fileExtension=".js" mimeType="application/javascript" />

Created Unassigned: Json.Encode doesn't encode array properties returned from Json.Decode [1085]

$
0
0
With this code:
```
var personJson = "{name:\"George\",aliases:[\"Peter\",\"David\"]}";
var person = Json.Decode(personJson);
person.name = "Michael";
personJson = Json.Encode(person);
```

Json.Encode is currently outputting:
```
"{\"name\":\"Michael\",\"aliases\":{}}"
```

When it should be:
```
"{\"name\":\"Michael\",\"aliases\":[\"Peter\",\"David\"]}"
```

The fix is in System.Web.Helpers/DynamicJavaScriptConverter.cs

```
// Get the value for each member in the dynamic object
foreach (string memberName in memberNames)
{
- values[memberName] = DynamicHelper.GetMemberValue(obj, memberName);
+ var value = DynamicHelper.GetMemberValue(obj, memberName);
+ if (value is DynamicJsonArray)
+ value = (object[])(DynamicJsonArray)value;
+ values[memberName] = value;
}

return values;
```

Commented Unassigned: Json.Encode doesn't encode array properties returned from Json.Decode [1085]

$
0
0
With this code:
```
var personJson = "{name:\"George\",aliases:[\"Peter\",\"David\"]}";
var person = Json.Decode(personJson);
person.name = "Michael";
personJson = Json.Encode(person);
```

Json.Encode is currently outputting:
```
"{\"name\":\"Michael\",\"aliases\":{}}"
```

When it should be:
```
"{\"name\":\"Michael\",\"aliases\":[\"Peter\",\"David\"]}"
```

The fix is in System.Web.Helpers/DynamicJavaScriptConverter.cs

```
// Get the value for each member in the dynamic object
foreach (string memberName in memberNames)
{
- values[memberName] = DynamicHelper.GetMemberValue(obj, memberName);
+ var value = DynamicHelper.GetMemberValue(obj, memberName);
+ if (value is DynamicJsonArray)
+ value = (object[])(DynamicJsonArray)value;
+ values[memberName] = value;
}

return values;
```
Comments: Just so you know a bug was filed by someone else on Microsoft Connect: http://connect.microsoft.com/VisualStudio/feedback/details/779119

Closed Issue: Html.EditorFor helper generates wrong date/time format [630]

$
0
0
In MVC applications, if someone uses the Html.EditorFor helper to generate UI for a model that includes a DateTime field, the resulting HTML markup will render a date in a culture format that doesn't resemble the one prescribed by the HTML 5 standard, which references RFC 3339.

To fix this, we need to change our generated markup to use one of the formats prescribed in the RFC.

Comments: Verified with the 5/30 build

Reviewed: v4.0 RTM (六月 08, 2013)

$
0
0
Rated 4 Stars (out of 5) - I like it!

Reviewed: v4.0 RTM (六月 08, 2013)

$
0
0
Rated 5 Stars (out of 5) - .I hope i can understand

Commented Unassigned: Global error handler for Web API [1001]

$
0
0
If my Web API application is going to return an error to a client, I want to know about it.

I want to set up something like ELMAH to notify me when the problem occurs, and I want full details of the error, including exception stack trace, logged on the server.

In MVC, the __Application_Error__ event is how you achive this, but in Web Api this event often isn't triggered because exceptions are converted into **HttpResponseMessage**s higher in the call stack, and because some error conditions create a **HttpResponseMessage** directly without using an exception. One source of this is __HttpControllerDispatcher.SendAsync__, which catches all exceptions and turns them into error responses, discarding the exception information.

Here are some examples of exceptions that I am interested in handling:

* Exceptions thrown from action methods and action filters
* Exceptions thrown when creating a controller instance
* Exceptions due to routing errors, bad requests, authentication problems, and invalid OData queries
* Exceptions thrown when an IQueryable returned by an action method fails to execute
* Exceptions thrown by custom message handlers and formatters

When I started using Web API, I was surprised when my __Application_Error__ handler did not pick up an action method exception. I added a global __ExceptionFilterAttribute__ to take care of that, but then found several other categories of errors that could be returned to the client and still leave silence in the server logs.

In a production environment, relying on clients to report errors and having no error information available on the server will make diagnosing problems reactive and difficult.

Please provide a global error handling event for Web API, so that error details can be logged and notifications sent. The event should provide access to unhandled exceptions and non-success **HttpResponseException**s and **HttpResponseMessage**s. In the case of **HttpError** objects created from an exception, the exception details should be retained for the error handling event.
Comments: For those looking for workarounds until this gets implemented, I just found "[Exception Handling for Web API Controller Constructors](http://blog.greatrexpectations.com/2013/05/15/exception-handling-for-web-api-controller-constructors/)" to deal with the second bullet point in this issue.

Edited Issue: Parse function in jquery.validate.unobtrusive.js performance improvement [308]

$
0
0
The jquery statement

$(selector).find(":input[data-val=true]").each(....) - line 173 jquery.validate.unobtrusive.js within the Parse(selector) method.

Provides better performance if it is re-written as a chained statement, oppose to as a single selector:
$(selector).find("input").filter["data-val=true]").each(..)

This is because it allows JQuery to use less of the Sizzle engine (which is known to be poor). On a large DOM, it has shown performance improvements of an order of magnitude firing the DOMContentLoaded event.

We have reduced the duration of our Document ready method from 700ms to 300ms.

Thanks,

Commented Unassigned: WebApi: Filters can't have a set execution order [1065]

$
0
0
MVC filter attributes support the Order property for setting the execution order when multiple instances are applied to a controller or method, those in Web API don't.
Comments: As a work around you can look into overriding GetFilterPipeLine in HttpActionDescriptor. ``` public override Collection<FilterInfo> GetFilterPipeline() { Collection<FilterInfo> originalFilters = _innerDescriptor.GetFilterPipeline(); Collection<FilterInfo> newFilters = new Collection<FilterInfo>(); // AS AN EXAMPLE, HERE IS HOW TO REPLACE A FILTER WITH YOUR OWN // SIMILARLY YOU CAN CHANGE THE ORDER // for any actions that support query composition, we need to replace it with our // query filter. foreach (FilterInfo filterInfo in originalFilters) { FilterInfo newInfo = filterInfo; QueryableAttribute queryableFilter = filterInfo.Instance as QueryableAttribute; if (queryableFilter != null) { newInfo = new FilterInfo(new QueryFilterAttribute() { ResultLimit = queryableFilter.ResultLimit }, filterInfo.Scope); } newFilters.Add(newInfo); } return newFilters; } ```

Closed Issue: Bundler / minifier not handling end of file comments correctly [884]

$
0
0
I have noticed that the bundler doesn't handle single line comments at the end of a javascript file correctly (or as would be expected), when the script is bundled and minified.

A single line comment at the end of one of the script files, without a line break at the end, affects the content/interpretation of the script of the next file.

Suppose script 1 has:

```
var hello = "Hello";

function SayHelloWorld() {
alert(hello + "World");
}

//=======test comment==========(no line break here)
```

and script two has:

```
function Test()
{
this.a = 1;
}

//===== another comment with no line break at the end=======
```

The resulting script after bundling is:

```
function SayHelloWorld(){alert(hello+"World")}var hello="Hello";this.a=1
```
Notice that constructor function Test() has disappeared and only the contents of that function are placed on the resulting script;

Removing the comment at the end of the file, or adding a line break after it outputs the correct and expected script

```
function SayHelloWorld(){alert(hello+"World")}function Test(){this.a=1}var hello="Hello"
```


about the attatchment: I includes a Bundle.cs (with the bundle definition), the javascript files in the script/testscripts directory, and a modified _layout to include the bundle. This can be copied onto a simple MVC4 internet Application project.

Comments: Hello, and thank you for the bug report. I talked to the developer of that project and he says this bug is fixed in v1.1 Beta 1, which is available on NuGet: http://nuget.org/packages/Microsoft.AspNet.Web.Optimization/1.1.0-Beta1

Commented Issue: [WebApiOnOwin]Uploading a 2GB file with custom input buffer selector set to true fails [1036]

$
0
0
Repro sample : FileUploadSample (file://KK-MC2/FileUploadSample)

I have a simple custom buffer selector where UseBufferedInputStream returns true.

When I try to upload a 2GB fails, I am seeing a failure with below stack trace.

{"The stream does not support concurrent IO read or write operations."}

at System.Net.ConnectStream.InternalWrite(Boolean async, Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
at System.Net.ConnectStream.BeginWrite(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
at System.Net.Http.StreamToStreamCopy.TryStartWriteSync(Int32 bytesRead)
at System.Net.Http.StreamToStreamCopy.BufferReadCallback(IAsyncResult ar)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at FileUploadSample.Program.<RunClient>d__0.MoveNext() in e:\TestProjects\FileUploadSample\Program.cs:line 72
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<ThrowAsync>b__1(Object state)
at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()






Comments: A couple notes here: 1) This isn't a valid user scenario. No one should be buffering a 2GB request body. 2) The exception that appears above is a stack trace for the client, not the server. I'm unable to repro the client issue, but it would be a problem with HttpClient and not Web API or OWIN. 3) I'm noticing a weird behavior though where even when an OutOfMemoryException gets thrown, the OWIN HttpListener server fully drains the request body before sending back a 500. This seems like it could expose users to a potential denial of service attack. I'll follow up with the Katana team.

Closed Issue: [WebApiOnOwin]Uploading a 2GB file with custom input buffer selector set to true fails [1036]

$
0
0
Repro sample : FileUploadSample (file://KK-MC2/FileUploadSample)

I have a simple custom buffer selector where UseBufferedInputStream returns true.

When I try to upload a 2GB fails, I am seeing a failure with below stack trace.

{"The stream does not support concurrent IO read or write operations."}

at System.Net.ConnectStream.InternalWrite(Boolean async, Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
at System.Net.ConnectStream.BeginWrite(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
at System.Net.Http.StreamToStreamCopy.TryStartWriteSync(Int32 bytesRead)
at System.Net.Http.StreamToStreamCopy.BufferReadCallback(IAsyncResult ar)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at FileUploadSample.Program.<RunClient>d__0.MoveNext() in e:\TestProjects\FileUploadSample\Program.cs:line 72
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<ThrowAsync>b__1(Object state)
at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()






Edited Issue: [WebApiOnOwin]Uploading a 2GB file with custom input buffer selector set to true fails [1036]

$
0
0
Repro sample : FileUploadSample (file://KK-MC2/FileUploadSample)

I have a simple custom buffer selector where UseBufferedInputStream returns true.

When I try to upload a 2GB fails, I am seeing a failure with below stack trace.

{"The stream does not support concurrent IO read or write operations."}

at System.Net.ConnectStream.InternalWrite(Boolean async, Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
at System.Net.ConnectStream.BeginWrite(Byte[] buffer, Int32 offset, Int32 size, AsyncCallback callback, Object state)
at System.Net.Http.StreamToStreamCopy.TryStartWriteSync(Int32 bytesRead)
at System.Net.Http.StreamToStreamCopy.BufferReadCallback(IAsyncResult ar)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at FileUploadSample.Program.<RunClient>d__0.MoveNext() in e:\TestProjects\FileUploadSample\Program.cs:line 72
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.<ThrowAsync>b__1(Object state)
at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()






Created Unassigned: Improve experience of using UriPathExtensionMapping with AttributeRouting [1086]

$
0
0
__Scenario__:
Its popular with users to use Uri path extensions to get content in a particular format even though one could use Accept headers. My understanding is that this is for users from browsers who cannot modify headers of the request that a browser sends.

I have the following ValuesController which uses AttributeRouting.

```
[RoutePrefix("api/values")]
public class ValuesController : ApiController
{
[HttpGet("")]
public IEnumerable<string> GetAll()
{
return new string[] { "value1", "value2" };
}

[HttpGet("{id}")]
public string GetSingle(int id)
{
return "value";
}

[HttpPost("")]
public void Post([FromBody]string value)
{
}

[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}

[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
```

Now I would like to use UriPathExtensionMapping for the GetAll and GetSingle actions. I would like users to do "/api/values.xml" and "/api/values/10.xml" etc.

__Issue__:
I cannot do something like below for GetAll action, because we append the RoutePrefix with the action's RouteTemplate (ex: api/values/.{ext}). As you can notice this is not the desired behavior.

```
[HttpGet("")]
[HttpGet(".{ext}")]
public IEnumerable<string> GetAll()
```

__Workaround__:

__NOTE__: Here I would be placing the controller name on each of the individual action's route template, which ideally I would like to avoid.

```
[RoutePrefix("api")]
public class ValuesController : ApiController
{
[HttpGet("values")]
[HttpGet("values.{ext}")]
public IEnumerable<string> GetAll()
{
return new string[] { "value1", "value2" };
}

[HttpGet("values/{id}")]
[HttpGet("values/{id}.{ext}")]
public string GetSingle(int id)
{
return "value";
}

[HttpPost("values")]
public void Post([FromBody]string value)
{
}

[HttpPut("values/{id}")]
public void Put(int id, [FromBody]string value)
{
}

[HttpDelete("values/{id}")]
public void Delete(int id)
{
}
}
```

__Proposal__:
Could we introduce ability for individual actions' route attribute to ignore the RoutePrefix.

Example(__Notice__ the GetAll action's attribute):
```
[RoutePrefix("api/values")]
public class ValuesController : ApiController
{
[HttpGet("")]
[HttpGet("~/api/values.{ext}")] //NOTE
public IEnumerable<string> GetAll()
{
return new string[] { "value1", "value2" };
}

[HttpGet("{id}")]
[HttpGet("{id}.{ext}")]
public string GetSingle(int id)
{
return "value";
}

[HttpPost("")]
public void Post([FromBody]string value)
{
}

[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}

[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
```

Created Unassigned: WebApi model binding fails when Property inherits from Dictionary [1087]

$
0
0
When I submit the webpage data to the ApiController the binding will not bind the MyDictionary<string, string> ExtraData property. If I change the property to Dictionary<string, string> ExtraData property then Api controller will bind correctly. However this is not an option because the MyDictionary class has additional functionality that I need. I have four buttons on the webpage for submitting data to the apicontroller. The red button fails to load the MyDictionary ExtraData property. The 3 green buttons load MyDictionary and Dictionary property ok.

Edited Unassigned: Improve experience of using UriPathExtensionMapping with AttributeRouting [1086]

$
0
0
__Scenario__:
Its popular with users to use Uri path extensions to get content in a particular format even though one could use Accept headers. My understanding is that this is for users from browsers who cannot modify headers of the request that a browser sends.

I have the following ValuesController which uses AttributeRouting.

```
[RoutePrefix("api/values")]
public class ValuesController : ApiController
{
[HttpGet("")]
public IEnumerable<string> GetAll()
{
return new string[] { "value1", "value2" };
}

[HttpGet("{id}")]
public string GetSingle(int id)
{
return "value";
}

[HttpPost("")]
public void Post([FromBody]string value)
{
}

[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}

[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
```

Now I would like to use UriPathExtensionMapping for the GetAll and GetSingle actions. I would like users to do "/api/values.xml" and "/api/values/10.xml" etc.

__Issue__:
I cannot do something like below for GetAll action, because we append the RoutePrefix with the action's RouteTemplate (ex: api/values/.{ext}). As you can notice this is not the desired behavior.

```
[HttpGet("")]
[HttpGet(".{ext}")]
public IEnumerable<string> GetAll()
```

__Workaround__:

__NOTE__: Here I would be placing the controller name on each of the individual action's route template, which ideally I would like to avoid.

```
[RoutePrefix("api")]
public class ValuesController : ApiController
{
[HttpGet("values")]
[HttpGet("values.{ext}")]
public IEnumerable<string> GetAll()
{
return new string[] { "value1", "value2" };
}

[HttpGet("values/{id}")]
[HttpGet("values/{id}.{ext}")]
public string GetSingle(int id)
{
return "value";
}

[HttpPost("values")]
public void Post([FromBody]string value)
{
}

[HttpPut("values/{id}")]
public void Put(int id, [FromBody]string value)
{
}

[HttpDelete("values/{id}")]
public void Delete(int id)
{
}
}
```

__Impact__:
1. User experience would be bad as users could be having many controllers and if they like to support Uri path extension on all of them, then they have to do the above workaround.
2. Alternatively, I can think of having non-attribute routes in WebApiConfig.cs which could probably fix this, what do you think?
Example:
```
config.Routes.MapHttpRoute(
name: "DefaultApiWithExtension2",
routeTemplate: "api/{controller}/{id}.{ext}"
);

config.Routes.MapHttpRoute(
name: "DefaultApiWithExtension1",
routeTemplate: "api/{controller}.{ext}");

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
```

__Proposal__:
Could we introduce ability for individual actions' route attribute to ignore the RoutePrefix.

Example(__Notice__ the GetAll action's attribute):
```
[RoutePrefix("api/values")]
public class ValuesController : ApiController
{
[HttpGet("")]
[HttpGet("~/api/values.{ext}")] //NOTE
public IEnumerable<string> GetAll()
{
return new string[] { "value1", "value2" };
}

[HttpGet("{id}")]
[HttpGet("{id}.{ext}")]
public string GetSingle(int id)
{
return "value";
}

[HttpPost("")]
public void Post([FromBody]string value)
{
}

[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}

[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
```

Updated Wiki: Attribute routing in MVC

$
0
0
WORK IN PROGRESS

Attribute routing in ASP.NET MVC

One of the limitations of ASP.NET MVC's routing system is that it requires you to configure routes on a global route table. This has several consequences:
  • It forces you to encode action-specific information like parameter names in a global route table.
  • Routes registered globally can conflict with other routes and end up matching actions they aren't supposed to match.
  • The information about what URI to use to call into a controller is kept in a completely different file from the controller itself. A developer has to look at both the controller and the global route table in configuration to understand how to call into the controller.
An attribute-based approach solves all these problems by allowing you to configure how an action gets called right on the action itself. For most cases, this should improve usability and make Web applications simpler to build and maintain.

Solution

Define routes by applying attributes over the target action methods. This will tie the route rule to a specific MethodInfo, and colocate the routing configuration with the action that is to be executed.

Usage

Defining simple routes

publicclass HomeController : Controller
{
    [HttpGet("")]
    public ActionResult Index() { ... }

    [HttpGet("about")]
    public ActionResult About() { ... }

    [HttpGet("contact-us")]
    public ActionResult ContactUs() { ... }
}

Using route parameters

Route parameters can be specified within the route template.
publicclass GreetingsController : Controller
{
    [HttpGet("say/hello/to/{name}")]
    public ActionResult SayHelloTo(string Name) { ... }
}

Optional parameters and default values

You can specify that a parameter is optional by adding a question mark to the parameter, that is:
[HttpGet("countries/{name?}")]
public ActionResult GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter for action selection to succeed, but we can investigate lifting that restriction. (Please let us know if this is important.)

Default values can be specified in a similar way:
[HttpGet("countries/{name=USA}")]
public ActionResult GetCountry(string name) { }
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.

Defining constraints over route parameters

Route constraints can be applied to particular parameters in the route template itself. Here's an example:
[HttpGet("people/{id:int}")]
public ActionResult Get(int id) { }
This action will only match if id in the route can be converted to an integer. This allows other routes to get selected in more general cases.

The following default inline constraints are defined:


Constraint Key Description Example
bool Matches a Boolean value {x:bool}
datetime Matches a DateTime value {x:datetime}
decimal Matches a Decimal value {x:decimal}
double Matches a 64-bit floating point value {x:double}
float Matches a 32-bit floating point value {x:float}
guid Matches a GUID value {x:guid}
int Matches a 32-bit integer value {x:int}
long Matches a 64-bit integer value {x:long}
minlength Matches a string with the specified minimum length {x:minlength(4)}
maxlength Matches a string with the specified maximum length {x:maxlength(8)}
length Matches a string with the specified length or within the specified range of lengths {x:length(6)}, {x:length(4,8)}
min Matches an integer with the specified minimum value {x:min(100)}
max Matches an integer with the specified maximum value {x:max(200)}
range Matches an integer within the specified range of values {x:range(100,200)}
alpha Matches English uppercase or lowercase alphabet characters {x:alpha}
regex Matches the specified regular expression {x:regex(^\d{3}-\d{3}-\d{4}$)}

Inline constraints can have arguments specified in parentheses - this is used by several of the built-in constraints. Inline constraints can also be chained with a colon used as a separator like this:
[HttpGet("people/{id:int:min(0)}")]
public Person Get(int id) { }
Inline constraints must appear before the optional parameter '?' and default values

The constraint resolution is extensible. See below for details.

Overloading

publicclass UsersController : Controller
{
    [HttpGet("show-user({id:int})")]
    public ActionResult Show(int id) { ... }

    [HttpGet("show-user({username})")]
    public ActionResult Show(int id) { ... }
}

Specifying other (or no) verbs to a route

publicclass UsersController : Controller
{
    [HttpPost("new-user")]
    public ActionResult NewUser(string name, int age) { ... }

    [HttpRoute("any-method-would-work")]
    public ActionResult WorksWithAnyMethod() { ... }
}

Specifying multiple ways to access an action

publicclass ProductsController : Controller
{
    [HttpGet("/p-{productId}")]
    [HttpGet("/p/{title}/{productId}")]
    public ActionResult NewUser(string productId, string title) { ... }
}


Scenario 2: Versioning controllers - different controllers for different versions

[RoutePrefix("api/v1/customers")]
publicclass CustomersV1Controller : Controller { ... }
[RoutePrefix("api/v2/customers")]
publicclass CustomersV2Controller : Controller { ... }

Scenario 3: Nested controllers - hierarchical routing where one controller can be accessed as a sub-resource of another controller

publicclass MoviesController : ApiController
{
    [HttpGet("actors/{actorId}/movies")]
    public Movie Get(int actorId) { }
    [HttpGet("directors/{directorId}/movies")]
    public Movie Get(int directorId) { }
}

Scenario 4: Defining multiple ways to access a resource

publicclass PeopleController : ApiController
{
    [HttpGet("people/{id:int}")]
    public Person Get(int id) { }
    [HttpGet("people/{name}")]
    public Person Get(string name) { }
}
In the controller above, the request api/People/3 would match the first action because routes with constrained parameters are evaluated before unconstrained parameters.

Scenario 5: Defining actions with different parameter names

publicclass MyController : ApiController
{
    [HttpPost("my/action1/{param1}/{param2")]
    publicvoid Action1(string param1, string param2) { }
    [HttpPost("my/action2/{x}/{y}")]
    publicvoid Action2(string x, string y) { }
}

Scenario 6: Defining multiple ways to access a particular action

[RoutePrefix("orders")]
[RoutePrefix("customers/{customerId}/orders")]
publicclass OrdersController : ApiController
{
    [HttpGet("{orderId}")]
    [HttpGet("get/{orderId}")]
    publicvoid Get(string customerId = null, string orderId) { }
}

Design

The experience for getting started with attribute-based routing will look something like this:
  1. Annotate the action with one of our HTTP verb attributes (HttpGet/HttpPost/HttpPut/HttpDelete/HttpPatch/HttpHead/HttpOptions/AcceptVerbs), passing in the route template in the constructor.
  2. Call the HttpConfiguration.MapHttpAttributeRoutes() extension method when configuring routes.
This call will use the controller selector and the action selector from the configuration to explore all the controllers and actions available and retrieve route-defining attributes. It will use these attributes to create routes and add these to the server's route collection.

This design allows attribute-based routing to compose well with the existing routing system since you can call MapHttpAttributeRoutes and still define regular routes using MapHttpRoute. Here's an example:
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute("Default", "api/{controller}");
In most cases, MapHttpAttributeRoutes will be called first so that attribute routes are registered before the global routes (and therefore get a chance to supersede global routes).

Optional parameters and default values

You can specify that a parameter is optional by adding a question mark to the parameter, that is:
[HttpGet("countries/{name?}")]
public Country GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter for action selection to succeed, but we can investigate lifting that restriction. (Please let us know if this is important.)

Default values can be specified in a similar way:
[HttpGet("countries/{name=USA}")]
public Country GetCountry(string name) { }
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.

Inline Constraints

Route constraints can be applied to particular parameters in the route template itself. Here's an example:
[HttpGet("people/{id:int}")]
public Person Get(int id) { }
This action will only match if id in the route can be converted to an integer. This allows other routes to get selected in more general cases.

The following default inline constraints are defined:

Constraint Key Description Example
bool Matches a Boolean value {x:bool}
datetime Matches a DateTime value {x:datetime}
decimal Matches a Decimal value {x:decimal}
double Matches a 64-bit floating point value {x:double}
float Matches a 32-bit floating point value {x:float}
guid Matches a GUID value {x:guid}
int Matches a 32-bit integer value {x:int}
long Matches a 64-bit integer value {x:long}
minlength Matches a string with the specified minimum length {x:minlength(4)}
maxlength Matches a string with the specified maximum length {x:maxlength(8)}
length Matches a string with the specified length or within the specified range of lengths {x:length(6)}, {x:length(4,8)}
min Matches an integer with the specified minimum value {x:min(100)}
max Matches an integer with the specified maximum value {x:max(200)}
range Matches an integer within the specified range of values {x:range(100,200)}
alpha Matches English uppercase or lowercase alphabet characters {x:alpha}
regex Matches the specified regular expression {x:regex(^\d{3}-\d{3}-\d{4}$)}

Inline constraints can have arguments specified in parentheses - this is used by several of the built-in constraints. Inline constraints can also be chained with a colon used as a separator like this:
[HttpGet("people/{id:int:min(0)}")]
public Person Get(int id) { }
Inline constraints must appear before the optional parameter '?' and default values, and the constraint resolution is extensible. See below for details.

Route prefixes

Frequently you’ll want to specify a common prefix for an entire controller. For example:

publicclass CustomersController : ApiController
{
    [HttpGet("customers")]
    public IEnumerable<Customer> Get() { }
    [HttpGet("customers/{id}")]
    public Customer Get(int id) { }
    [HttpPost("customers")]
    publicvoid Post(Customer customer) { }
}

The [RoutePrefix] attribute lets you define a common prefix for an entire controller. So the previous controller definition is simplified as:

[RoutePrefix("customers")]
publicclass CustomersController : ApiController
{
    public IEnumerable<Customer> Get() { }
    [HttpGet("{id}")]
    public Customer Get(int id) { }
    publicvoid Post(Customer customer) { }
}

When MapHttpAttributeRoutes gets called, it goes through all the prefixes and adds a route for every action. If the action has no route provider attribute, the route prefix itself gets added. If the action does have a route provider attribute, the two templates get concatenated. Multiple route prefixes will each register their own routes for each action. So if you have two route prefixes and three actions on a controller, you would have six routes generated.

Optional parameters, default values, and inline constraints can all be applied to route prefixes as well.

Naming

Web API's routing system requires every route to have a distinct name. Route names are useful for generating links by allowing you to identify the route you want to use. You can choose to define the route name right on the attribute itself:
[HttpGet("customers/{id}", RouteName = "GetCustomerById")]

In the absence of a specified route name, Web API will generate a default route name. If there is only one attribute route for the action name on a particular controller, the route name will take the form "ControllerName.ActionName". If there are multiple attributes with the same action name on that controller, a suffix gets added to differentiate between the routes: "Customer.Get1", "Customer.Get2".

Ordering

There is an Order property on the [RoutePrefix] attribute and a RouteOrder on the HTTP verb attributes that allows you to customize the order in which the routes are evaluated. The default order is 0, and routes with a smaller order get evaluated first. Negative numbers can be used to evaluate before the default and positive numbers can be used to evaluate after the default. In addition, a default ordering is used to order routes that don't have an order specified.

Here's how the ordering works:
  1. Compare routes by prefix order. If a prefix order is smaller, it goes earlier into the route collection. If the prefix order is the same, keep going.
  2. Compare routes by the RouteOrder on the HTTP verb attribute. If an order is smaller, it goes earlier into the route collection. If the order is the same, keep going.
  3. Go through the parsed route segment by segment. Apply the following ordering for determining which route goes first:
    1. Literal segments
    2. Constrained parameter segments
    3. Unconstrained parameter segments
    4. Constrained wildcard parameter segments
    5. Unconstrained wildcard parameter segments
  4. If no ordering can be determined up to this point, use an OrdinalIgnoreCase string comparison of the two route templates to ensure that the ordering is stable and won't change if the order of the actions and attributes changes.

Extensibility

There are two main extensibility points that are built in - the HttpRouteBuilder and the IInlineConstraintResolver interface. HttpRouteBuilder is the class that takes a tokenized route template and creates an IHttpRoute for it. It exposes the following virtuals:
publicclass HttpRouteBuilder
{
    publicvirtual IHttpRoute BuildHttpRoute(string routeTemplate, IEnumerable<HttpMethod> httpMethods, string controllerName, string actionName);
    publicvirtual IHttpRoute BuildHttpRoute(HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, string routeTemplate);
}
You can pass in a custom HttpRouteBuilder to the MapHttpAttributeRoutes method:
config.MapHttpAttributeRoutes(new MyRouteBuilder());
Extending the route builder allows you to add or remove constraints, add a per-route message handler, and return a custom instance of IHttpRoute among other things.

The other extensibility point is IInlineConstraintResolver. This interface is responsible for taking an inline constraint and manufacturing an IHttpRouteConstraint for that parameter:
publicinterface IInlineConstraintResolver
{
    IHttpRouteConstraint ResolveConstraint(string inlineConstraint);
}
The inline constraint resolver is an argument to HttpRouteBuilder's constructor, so you could call the following:
config.MapHttpAttributeRoutes(new HttpRouteBuilder(new MyConstraintResolver()));

The default constraint resolver uses a dictionary based approach of mapping constraint keys to a particular constraint type. It contains logic to create an instance of the type based on the constraint arguments. It exposes the dictionary publicly so that you can add custom constraints without having to implement IInlineConstraintResolver. Here's an example:
DefaultInlineConstraintResolver constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("phonenumber", typeof(PhoneNumberRouteConstraint));
config.MapHttpAttributeRoutes(new HttpRouteBuilder(constraintResolver));

Updated Wiki: Attribute routing in MVC

$
0
0
WORK IN PROGRESS

Attribute routing in ASP.NET MVC

One of the limitations of ASP.NET MVC's routing system is that it requires you to configure routes on a global route table. This has several consequences:
  • It forces you to encode action-specific information like parameter names in a global route table.
  • Routes registered globally can conflict with other routes and end up matching actions they aren't supposed to match.
  • The information about what URI to use to call into a controller is kept in a completely different file from the controller itself. A developer has to look at both the controller and the global route table in configuration to understand how to call into the controller.
An attribute-based approach solves all these problems by allowing you to configure how an action would be accessed right on the action itself. For most cases, this should improve usability and make Web applications simpler to build and maintain.

Usage

Defining simple routes

publicclass HomeController : Controller
{
    [HttpGet("")]
    public ActionResult Index() { ... }

    [HttpGet("about")]
    public ActionResult About() { ... }

    [HttpGet("contact-us")]
    public ActionResult ContactUs() { ... }
}

Using route parameters

Route parameters can be specified within the route template.
publicclass GreetingsController : Controller
{
    [HttpGet("say/hello/to/{name}")]
    public ActionResult SayHelloTo(string Name) { ... }
}

Optional parameters and default values

You can specify that a parameter is optional by adding a question mark to the parameter, that is:
[HttpGet("countries/{name?}")]
public ActionResult GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter for action selection to succeed, but we can investigate lifting that restriction. (Please let us know if this is important.)

Default values can be specified in a similar way:
[HttpGet("countries/{name=USA}")]
public ActionResult GetCountry(string name) { }
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.

Defining constraints over route parameters

Route constraints can be applied to particular parameters in the route template itself. Here's an example:
[HttpGet("people/{id:int}")]
public ActionResult Get(int id) { }
This action will only match if id in the route can be converted to an integer. This allows other routes to get selected in more general cases.

The following default inline constraints are defined:


Constraint Key Description Example
bool Matches a Boolean value {x:bool}
datetime Matches a DateTime value {x:datetime}
decimal Matches a Decimal value {x:decimal}
double Matches a 64-bit floating point value {x:double}
float Matches a 32-bit floating point value {x:float}
guid Matches a GUID value {x:guid}
int Matches a 32-bit integer value {x:int}
long Matches a 64-bit integer value {x:long}
minlength Matches a string with the specified minimum length {x:minlength(4)}
maxlength Matches a string with the specified maximum length {x:maxlength(8)}
length Matches a string with the specified length or within the specified range of lengths {x:length(6)}, {x:length(4,8)}
min Matches an integer with the specified minimum value {x:min(100)}
max Matches an integer with the specified maximum value {x:max(200)}
range Matches an integer within the specified range of values {x:range(100,200)}
alpha Matches English uppercase or lowercase alphabet characters {x:alpha}
regex Matches the specified regular expression {x:regex(^\d{3}-\d{3}-\d{4}$)}

Inline constraints can have arguments specified in parentheses - this is used by several of the built-in constraints. Inline constraints can also be chained with a colon used as a separator like this:
[HttpGet("people/{id:int:min(0)}")]
public Person Get(int id) { }
Inline constraints must appear before the optional parameter '?' and default values

The constraint resolution is extensible. See below for details.

Overloading

publicclass UsersController : Controller
{
    [HttpGet("show-user({id:int})")]
    public ActionResult Show(int id) { ... }

    [HttpGet("show-user({username})")]
    public ActionResult Show(int id) { ... }
}

Specifying other (or no) verbs to a route

publicclass UsersController : Controller
{
    [HttpPost("new-user")]
    public ActionResult NewUser(string name, int age) { ... }

    [HttpRoute("any-method-would-work")]
    public ActionResult WorksWithAnyMethod() { ... }
}

Specifying multiple ways to access an action

publicclass ProductsController : Controller
{
    [HttpGet("/p-{productId}")]
    [HttpGet("/p/{title}/{productId}")]
    public ActionResult NewUser(string productId, string title) { ... }
}


Scenario 2: Versioning controllers - different controllers for different versions

[RoutePrefix("api/v1/customers")]
publicclass CustomersV1Controller : Controller { ... }
[RoutePrefix("api/v2/customers")]
publicclass CustomersV2Controller : Controller { ... }

Scenario 3: Nested controllers - hierarchical routing where one controller can be accessed as a sub-resource of another controller

publicclass MoviesController : ApiController
{
    [HttpGet("actors/{actorId}/movies")]
    public Movie Get(int actorId) { }
    [HttpGet("directors/{directorId}/movies")]
    public Movie Get(int directorId) { }
}

Scenario 4: Defining multiple ways to access a resource

publicclass PeopleController : ApiController
{
    [HttpGet("people/{id:int}")]
    public Person Get(int id) { }
    [HttpGet("people/{name}")]
    public Person Get(string name) { }
}
In the controller above, the request api/People/3 would match the first action because routes with constrained parameters are evaluated before unconstrained parameters.

Scenario 5: Defining actions with different parameter names

publicclass MyController : ApiController
{
    [HttpPost("my/action1/{param1}/{param2")]
    publicvoid Action1(string param1, string param2) { }
    [HttpPost("my/action2/{x}/{y}")]
    publicvoid Action2(string x, string y) { }
}

Scenario 6: Defining multiple ways to access a particular action

[RoutePrefix("orders")]
[RoutePrefix("customers/{customerId}/orders")]
publicclass OrdersController : ApiController
{
    [HttpGet("{orderId}")]
    [HttpGet("get/{orderId}")]
    publicvoid Get(string customerId = null, string orderId) { }
}

Design

The experience for getting started with attribute-based routing will look something like this:
  1. Annotate the action with one of our HTTP verb attributes (HttpGet/HttpPost/HttpPut/HttpDelete/HttpPatch/HttpHead/HttpOptions/AcceptVerbs), passing in the route template in the constructor.
  2. Call the HttpConfiguration.MapHttpAttributeRoutes() extension method when configuring routes.
This call will use the controller selector and the action selector from the configuration to explore all the controllers and actions available and retrieve route-defining attributes. It will use these attributes to create routes and add these to the server's route collection.

This design allows attribute-based routing to compose well with the existing routing system since you can call MapHttpAttributeRoutes and still define regular routes using MapHttpRoute. Here's an example:
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute("Default", "api/{controller}");
In most cases, MapHttpAttributeRoutes will be called first so that attribute routes are registered before the global routes (and therefore get a chance to supersede global routes).

Optional parameters and default values

You can specify that a parameter is optional by adding a question mark to the parameter, that is:
[HttpGet("countries/{name?}")]
public Country GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter for action selection to succeed, but we can investigate lifting that restriction. (Please let us know if this is important.)

Default values can be specified in a similar way:
[HttpGet("countries/{name=USA}")]
public Country GetCountry(string name) { }
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.

Inline Constraints

Route constraints can be applied to particular parameters in the route template itself. Here's an example:
[HttpGet("people/{id:int}")]
public Person Get(int id) { }
This action will only match if id in the route can be converted to an integer. This allows other routes to get selected in more general cases.

The following default inline constraints are defined:

Constraint Key Description Example
bool Matches a Boolean value {x:bool}
datetime Matches a DateTime value {x:datetime}
decimal Matches a Decimal value {x:decimal}
double Matches a 64-bit floating point value {x:double}
float Matches a 32-bit floating point value {x:float}
guid Matches a GUID value {x:guid}
int Matches a 32-bit integer value {x:int}
long Matches a 64-bit integer value {x:long}
minlength Matches a string with the specified minimum length {x:minlength(4)}
maxlength Matches a string with the specified maximum length {x:maxlength(8)}
length Matches a string with the specified length or within the specified range of lengths {x:length(6)}, {x:length(4,8)}
min Matches an integer with the specified minimum value {x:min(100)}
max Matches an integer with the specified maximum value {x:max(200)}
range Matches an integer within the specified range of values {x:range(100,200)}
alpha Matches English uppercase or lowercase alphabet characters {x:alpha}
regex Matches the specified regular expression {x:regex(^\d{3}-\d{3}-\d{4}$)}

Inline constraints can have arguments specified in parentheses - this is used by several of the built-in constraints. Inline constraints can also be chained with a colon used as a separator like this:
[HttpGet("people/{id:int:min(0)}")]
public Person Get(int id) { }
Inline constraints must appear before the optional parameter '?' and default values, and the constraint resolution is extensible. See below for details.

Route prefixes

Frequently you’ll want to specify a common prefix for an entire controller. For example:

publicclass CustomersController : ApiController
{
    [HttpGet("customers")]
    public IEnumerable<Customer> Get() { }
    [HttpGet("customers/{id}")]
    public Customer Get(int id) { }
    [HttpPost("customers")]
    publicvoid Post(Customer customer) { }
}

The [RoutePrefix] attribute lets you define a common prefix for an entire controller. So the previous controller definition is simplified as:

[RoutePrefix("customers")]
publicclass CustomersController : ApiController
{
    public IEnumerable<Customer> Get() { }
    [HttpGet("{id}")]
    public Customer Get(int id) { }
    publicvoid Post(Customer customer) { }
}

When MapHttpAttributeRoutes gets called, it goes through all the prefixes and adds a route for every action. If the action has no route provider attribute, the route prefix itself gets added. If the action does have a route provider attribute, the two templates get concatenated. Multiple route prefixes will each register their own routes for each action. So if you have two route prefixes and three actions on a controller, you would have six routes generated.

Optional parameters, default values, and inline constraints can all be applied to route prefixes as well.

Naming

Web API's routing system requires every route to have a distinct name. Route names are useful for generating links by allowing you to identify the route you want to use. You can choose to define the route name right on the attribute itself:
[HttpGet("customers/{id}", RouteName = "GetCustomerById")]

In the absence of a specified route name, Web API will generate a default route name. If there is only one attribute route for the action name on a particular controller, the route name will take the form "ControllerName.ActionName". If there are multiple attributes with the same action name on that controller, a suffix gets added to differentiate between the routes: "Customer.Get1", "Customer.Get2".

Ordering

There is an Order property on the [RoutePrefix] attribute and a RouteOrder on the HTTP verb attributes that allows you to customize the order in which the routes are evaluated. The default order is 0, and routes with a smaller order get evaluated first. Negative numbers can be used to evaluate before the default and positive numbers can be used to evaluate after the default. In addition, a default ordering is used to order routes that don't have an order specified.

Here's how the ordering works:
  1. Compare routes by prefix order. If a prefix order is smaller, it goes earlier into the route collection. If the prefix order is the same, keep going.
  2. Compare routes by the RouteOrder on the HTTP verb attribute. If an order is smaller, it goes earlier into the route collection. If the order is the same, keep going.
  3. Go through the parsed route segment by segment. Apply the following ordering for determining which route goes first:
    1. Literal segments
    2. Constrained parameter segments
    3. Unconstrained parameter segments
    4. Constrained wildcard parameter segments
    5. Unconstrained wildcard parameter segments
  4. If no ordering can be determined up to this point, use an OrdinalIgnoreCase string comparison of the two route templates to ensure that the ordering is stable and won't change if the order of the actions and attributes changes.

Extensibility

There are two main extensibility points that are built in - the HttpRouteBuilder and the IInlineConstraintResolver interface. HttpRouteBuilder is the class that takes a tokenized route template and creates an IHttpRoute for it. It exposes the following virtuals:
publicclass HttpRouteBuilder
{
    publicvirtual IHttpRoute BuildHttpRoute(string routeTemplate, IEnumerable<HttpMethod> httpMethods, string controllerName, string actionName);
    publicvirtual IHttpRoute BuildHttpRoute(HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, string routeTemplate);
}
You can pass in a custom HttpRouteBuilder to the MapHttpAttributeRoutes method:
config.MapHttpAttributeRoutes(new MyRouteBuilder());
Extending the route builder allows you to add or remove constraints, add a per-route message handler, and return a custom instance of IHttpRoute among other things.

The other extensibility point is IInlineConstraintResolver. This interface is responsible for taking an inline constraint and manufacturing an IHttpRouteConstraint for that parameter:
publicinterface IInlineConstraintResolver
{
    IHttpRouteConstraint ResolveConstraint(string inlineConstraint);
}
The inline constraint resolver is an argument to HttpRouteBuilder's constructor, so you could call the following:
config.MapHttpAttributeRoutes(new HttpRouteBuilder(new MyConstraintResolver()));

The default constraint resolver uses a dictionary based approach of mapping constraint keys to a particular constraint type. It contains logic to create an instance of the type based on the constraint arguments. It exposes the dictionary publicly so that you can add custom constraints without having to implement IInlineConstraintResolver. Here's an example:
DefaultInlineConstraintResolver constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("phonenumber", typeof(PhoneNumberRouteConstraint));
config.MapHttpAttributeRoutes(new HttpRouteBuilder(constraintResolver));

Updated Wiki: Attribute routing in MVC

$
0
0
WORK IN PROGRESS

Attribute routing in ASP.NET MVC

One of the limitations of ASP.NET MVC's routing system is that it requires you to configure routes on a global route table. This has several consequences:
  • It forces you to encode action-specific information like parameter names in a global route table.
  • Routes registered globally can conflict with other routes and end up matching actions they aren't supposed to match.
  • The information about what URI to use to call into a controller is kept in a completely different file from the controller itself. A developer has to look at both the controller and the global route table in configuration to understand how to call into the controller.
An attribute-based approach solves all these problems by allowing you to configure how an action would be accessed right on the action itself. For most cases, this should improve usability and make Web applications simpler to build and maintain.

Usage

Defining simple routes

publicclass HomeController : Controller
{
    [HttpGet("")]
    public ActionResult Index() { ... }

    [HttpGet("about")]
    public ActionResult About() { ... }

    [HttpGet("contact-us")]
    public ActionResult ContactUs() { ... }
}

Using route parameters

Route parameters can be specified within the route template.
publicclass GreetingsController : Controller
{
    [HttpGet("say/hello/to/{name}")]
    public ActionResult SayHelloTo(string Name) { ... }
}

Optional parameters and default values

You can specify that a parameter is optional by adding a question mark to the parameter, that is:
[HttpGet("countries/{name?}")]
public ActionResult GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter for action selection to succeed, but we can investigate lifting that restriction. (Please let us know if this is important.)

Default values can be specified in a similar way:
[HttpGet("countries/{name=USA}")]
public ActionResult GetCountry(string name) { }
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.

Defining constraints over route parameters

Route constraints can be applied to particular parameters in the route template itself. Here's an example:
[HttpGet("people/{id:int}")]
public ActionResult Get(int id) { }
This action will only match if id in the route can be converted to an integer. This allows other routes to get selected in more general cases.

The following default inline constraints are defined:


Constraint Key Description Example
bool Matches a Boolean value {x:bool}
datetime Matches a DateTime value {x:datetime}
decimal Matches a Decimal value {x:decimal}
double Matches a 64-bit floating point value {x:double}
float Matches a 32-bit floating point value {x:float}
guid Matches a GUID value {x:guid}
int Matches a 32-bit integer value {x:int}
long Matches a 64-bit integer value {x:long}
minlength Matches a string with the specified minimum length {x:minlength(4)}
maxlength Matches a string with the specified maximum length {x:maxlength(8)}
length Matches a string with the specified length or within the specified range of lengths {x:length(6)}, {x:length(4,8)}
min Matches an integer with the specified minimum value {x:min(100)}
max Matches an integer with the specified maximum value {x:max(200)}
range Matches an integer within the specified range of values {x:range(100,200)}
alpha Matches English uppercase or lowercase alphabet characters {x:alpha}
regex Matches the specified regular expression {x:regex(^\d{3}-\d{3}-\d{4}$)}

Inline constraints can have arguments specified in parentheses - this is used by several of the built-in constraints. Inline constraints can also be chained with a colon used as a separator like this:
[HttpGet("people/{id:int:min(0)}")]
public ActionResult GetPerson(int id) { }
Inline constraints must appear before the optional parameter '?' and default values

The constraint resolution is extensible. See below for details.

Overloading

publicclass UsersController : Controller
{
    [HttpGet("show-user({id:int})", RouteName = "GetUserById")]
    public ActionResult Show(int id) { ... }

    [HttpGet("show-user({username})", RouteName = "GetUserByUsername")]
    public ActionResult Show(string username) { ... }
}

Specifying other (or no) HTTP methods on a route

publicclass UsersController : Controller
{
    [HttpPost("new-user")]
    public ActionResult NewUser(string name, int age) { ... }

    [HttpRoute("any-method-would-work")]
    public ActionResult WorksWithAnyMethod() { ... }
}

Specifying multiple ways to access an action

publicclass ProductsController : Controller
{
    [HttpGet("/p-{productId}")]
    [HttpGet("/p/{title}/{productId}")]
    public ActionResult NewUser(string productId, string title) { ... }
}


Scenario 2: Versioning controllers - different controllers for different versions

[RoutePrefix("api/v1/customers")]
publicclass CustomersV1Controller : Controller { ... }
[RoutePrefix("api/v2/customers")]
publicclass CustomersV2Controller : Controller { ... }

Scenario 3: Nested controllers - hierarchical routing where one controller can be accessed as a sub-resource of another controller

publicclass MoviesController : ApiController
{
    [HttpGet("actors/{actorId}/movies")]
    public Movie Get(int actorId) { }
    [HttpGet("directors/{directorId}/movies")]
    public Movie Get(int directorId) { }
}

Scenario 4: Defining multiple ways to access a resource

publicclass PeopleController : ApiController
{
    [HttpGet("people/{id:int}")]
    public Person Get(int id) { }
    [HttpGet("people/{name}")]
    public Person Get(string name) { }
}
In the controller above, the request api/People/3 would match the first action because routes with constrained parameters are evaluated before unconstrained parameters.

Scenario 5: Defining actions with different parameter names

publicclass MyController : ApiController
{
    [HttpPost("my/action1/{param1}/{param2")]
    publicvoid Action1(string param1, string param2) { }
    [HttpPost("my/action2/{x}/{y}")]
    publicvoid Action2(string x, string y) { }
}

Scenario 6: Defining multiple ways to access a particular action

[RoutePrefix("orders")]
[RoutePrefix("customers/{customerId}/orders")]
publicclass OrdersController : ApiController
{
    [HttpGet("{orderId}")]
    [HttpGet("get/{orderId}")]
    publicvoid Get(string customerId = null, string orderId) { }
}

Design

The experience for getting started with attribute-based routing will look something like this:
  1. Annotate the action with one of our HTTP verb attributes (HttpGet/HttpPost/HttpPut/HttpDelete/HttpPatch/HttpHead/HttpOptions/AcceptVerbs), passing in the route template in the constructor.
  2. Call the HttpConfiguration.MapHttpAttributeRoutes() extension method when configuring routes.
This call will use the controller selector and the action selector from the configuration to explore all the controllers and actions available and retrieve route-defining attributes. It will use these attributes to create routes and add these to the server's route collection.

This design allows attribute-based routing to compose well with the existing routing system since you can call MapHttpAttributeRoutes and still define regular routes using MapHttpRoute. Here's an example:
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute("Default", "api/{controller}");
In most cases, MapHttpAttributeRoutes will be called first so that attribute routes are registered before the global routes (and therefore get a chance to supersede global routes).

Optional parameters and default values

You can specify that a parameter is optional by adding a question mark to the parameter, that is:
[HttpGet("countries/{name?}")]
public Country GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter for action selection to succeed, but we can investigate lifting that restriction. (Please let us know if this is important.)

Default values can be specified in a similar way:
[HttpGet("countries/{name=USA}")]
public Country GetCountry(string name) { }
The optional parameter '?' and the default values must appear after inline constraints in the parameter definition.

Inline Constraints

Route constraints can be applied to particular parameters in the route template itself. Here's an example:
[HttpGet("people/{id:int}")]
public Person Get(int id) { }
This action will only match if id in the route can be converted to an integer. This allows other routes to get selected in more general cases.

The following default inline constraints are defined:

Constraint Key Description Example
bool Matches a Boolean value {x:bool}
datetime Matches a DateTime value {x:datetime}
decimal Matches a Decimal value {x:decimal}
double Matches a 64-bit floating point value {x:double}
float Matches a 32-bit floating point value {x:float}
guid Matches a GUID value {x:guid}
int Matches a 32-bit integer value {x:int}
long Matches a 64-bit integer value {x:long}
minlength Matches a string with the specified minimum length {x:minlength(4)}
maxlength Matches a string with the specified maximum length {x:maxlength(8)}
length Matches a string with the specified length or within the specified range of lengths {x:length(6)}, {x:length(4,8)}
min Matches an integer with the specified minimum value {x:min(100)}
max Matches an integer with the specified maximum value {x:max(200)}
range Matches an integer within the specified range of values {x:range(100,200)}
alpha Matches English uppercase or lowercase alphabet characters {x:alpha}
regex Matches the specified regular expression {x:regex(^\d{3}-\d{3}-\d{4}$)}

Inline constraints can have arguments specified in parentheses - this is used by several of the built-in constraints. Inline constraints can also be chained with a colon used as a separator like this:
[HttpGet("people/{id:int:min(0)}")]
public Person Get(int id) { }
Inline constraints must appear before the optional parameter '?' and default values, and the constraint resolution is extensible. See below for details.

Route prefixes

Frequently you’ll want to specify a common prefix for an entire controller. For example:

publicclass CustomersController : ApiController
{
    [HttpGet("customers")]
    public IEnumerable<Customer> Get() { }
    [HttpGet("customers/{id}")]
    public Customer Get(int id) { }
    [HttpPost("customers")]
    publicvoid Post(Customer customer) { }
}

The [RoutePrefix] attribute lets you define a common prefix for an entire controller. So the previous controller definition is simplified as:

[RoutePrefix("customers")]
publicclass CustomersController : ApiController
{
    public IEnumerable<Customer> Get() { }
    [HttpGet("{id}")]
    public Customer Get(int id) { }
    publicvoid Post(Customer customer) { }
}

When MapHttpAttributeRoutes gets called, it goes through all the prefixes and adds a route for every action. If the action has no route provider attribute, the route prefix itself gets added. If the action does have a route provider attribute, the two templates get concatenated. Multiple route prefixes will each register their own routes for each action. So if you have two route prefixes and three actions on a controller, you would have six routes generated.

Optional parameters, default values, and inline constraints can all be applied to route prefixes as well.

Naming

Web API's routing system requires every route to have a distinct name. Route names are useful for generating links by allowing you to identify the route you want to use. You can choose to define the route name right on the attribute itself:
[HttpGet("customers/{id}", RouteName = "GetCustomerById")]

In the absence of a specified route name, Web API will generate a default route name. If there is only one attribute route for the action name on a particular controller, the route name will take the form "ControllerName.ActionName". If there are multiple attributes with the same action name on that controller, a suffix gets added to differentiate between the routes: "Customer.Get1", "Customer.Get2".

Ordering

There is an Order property on the [RoutePrefix] attribute and a RouteOrder on the HTTP verb attributes that allows you to customize the order in which the routes are evaluated. The default order is 0, and routes with a smaller order get evaluated first. Negative numbers can be used to evaluate before the default and positive numbers can be used to evaluate after the default. In addition, a default ordering is used to order routes that don't have an order specified.

Here's how the ordering works:
  1. Compare routes by prefix order. If a prefix order is smaller, it goes earlier into the route collection. If the prefix order is the same, keep going.
  2. Compare routes by the RouteOrder on the HTTP verb attribute. If an order is smaller, it goes earlier into the route collection. If the order is the same, keep going.
  3. Go through the parsed route segment by segment. Apply the following ordering for determining which route goes first:
    1. Literal segments
    2. Constrained parameter segments
    3. Unconstrained parameter segments
    4. Constrained wildcard parameter segments
    5. Unconstrained wildcard parameter segments
  4. If no ordering can be determined up to this point, use an OrdinalIgnoreCase string comparison of the two route templates to ensure that the ordering is stable and won't change if the order of the actions and attributes changes.

Extensibility

There are two main extensibility points that are built in - the HttpRouteBuilder and the IInlineConstraintResolver interface. HttpRouteBuilder is the class that takes a tokenized route template and creates an IHttpRoute for it. It exposes the following virtuals:
publicclass HttpRouteBuilder
{
    publicvirtual IHttpRoute BuildHttpRoute(string routeTemplate, IEnumerable<HttpMethod> httpMethods, string controllerName, string actionName);
    publicvirtual IHttpRoute BuildHttpRoute(HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, string routeTemplate);
}
You can pass in a custom HttpRouteBuilder to the MapHttpAttributeRoutes method:
config.MapHttpAttributeRoutes(new MyRouteBuilder());
Extending the route builder allows you to add or remove constraints, add a per-route message handler, and return a custom instance of IHttpRoute among other things.

The other extensibility point is IInlineConstraintResolver. This interface is responsible for taking an inline constraint and manufacturing an IHttpRouteConstraint for that parameter:
publicinterface IInlineConstraintResolver
{
    IHttpRouteConstraint ResolveConstraint(string inlineConstraint);
}
The inline constraint resolver is an argument to HttpRouteBuilder's constructor, so you could call the following:
config.MapHttpAttributeRoutes(new HttpRouteBuilder(new MyConstraintResolver()));

The default constraint resolver uses a dictionary based approach of mapping constraint keys to a particular constraint type. It contains logic to create an instance of the type based on the constraint arguments. It exposes the dictionary publicly so that you can add custom constraints without having to implement IInlineConstraintResolver. Here's an example:
DefaultInlineConstraintResolver constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("phonenumber", typeof(PhoneNumberRouteConstraint));
config.MapHttpAttributeRoutes(new HttpRouteBuilder(constraintResolver));
Viewing all 7925 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>