Fix #1276 - Implement WebAPI action conventions and overloading

This change adds ApplicationModel conventions that can enable WebAPI
action conventions (verb mapping) and WebAPI overloading.

The conventions activate when a controller has a marker attribute.
ApiController has this attribute, so any ported code will automatically
opt-in.

Also ported some old tests for action selection to our new functional test
framework.
This commit is contained in:
Ryan Nowak 2014-10-08 14:31:36 -07:00
parent 2578b8107f
commit 9b11c1d90f
30 changed files with 2201 additions and 25 deletions

View File

@ -5,9 +5,12 @@ using System.Security.Principal;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.WebApiCompatShim;
namespace System.Web.Http
{
[UseWebApiActionConventions]
[UseWebApiOverloading]
public abstract class ApiController : IDisposable
{
/// <summary>Gets the action context.</summary>

View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public interface IUseWebApiActionConventions
{
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public interface IUseWebApiOverloading
{
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class UseWebApiActionConventionsAttribute : Attribute, IUseWebApiActionConventions
{
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class UseWebApiOverloadingAttribute : Attribute, IUseWebApiOverloading
{
}
}

View File

@ -0,0 +1,95 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.ApplicationModel;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public class WebApiActionConventionsGlobalModelConvention : IGlobalModelConvention
{
private static readonly string[] SupportedHttpMethodConventions = new string[]
{
"GET",
"PUT",
"POST",
"DELETE",
"PATCH",
"HEAD",
"OPTIONS",
};
public void Apply(GlobalModel model)
{
foreach (var controller in model.Controllers)
{
if (IsConventionApplicable(controller))
{
Apply(controller);
}
}
}
private bool IsConventionApplicable(ControllerModel controller)
{
return controller.Attributes.OfType<IUseWebApiActionConventions>().Any();
}
private void Apply(ControllerModel model)
{
var newActions = new List<ActionModel>();
foreach (var action in model.Actions)
{
SetHttpMethodFromConvention(action);
// Action Name doesn't really come into play with attribute routed actions. However for a
// non-attribute-routed action we need to create a 'named' version and an 'unnamed' version.
if (!IsActionAttributeRouted(action))
{
var namedAction = action;
var unnamedAction = new ActionModel(namedAction);
unnamedAction.IsActionNameMatchRequired = false;
newActions.Add(unnamedAction);
}
}
model.Actions.AddRange(newActions);
}
private bool IsActionAttributeRouted(ActionModel action)
{
if (action.Controller.AttributeRoutes.Count > 0)
{
return true;
}
return action.AttributeRouteModel?.Template != null;
}
private void SetHttpMethodFromConvention(ActionModel action)
{
if (action.HttpMethods.Count > 0)
{
// If the HttpMethods are set from attributes, don't override it with the convention
return;
}
// The Method name is used to infer verb constraints. Changing the action name has not impact.
foreach (var verb in SupportedHttpMethodConventions)
{
if (action.ActionMethod.Name.StartsWith(verb, StringComparison.OrdinalIgnoreCase))
{
action.HttpMethods.Add(verb);
return;
}
}
// If no convention matches, then assume POST
action.HttpMethods.Add("POST");
}
}
}

View File

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Microsoft.AspNet.Mvc.ApplicationModel;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public class WebApiOverloadingGlobalModelConvention : IGlobalModelConvention
{
public void Apply(GlobalModel model)
{
foreach (var controller in model.Controllers)
{
if (IsConventionApplicable(controller))
{
Apply(controller);
}
}
}
private bool IsConventionApplicable(ControllerModel controller)
{
return controller.Attributes.OfType<IUseWebApiOverloading>().Any();
}
private void Apply(ControllerModel model)
{
foreach (var action in model.Actions)
{
action.ActionConstraints.Add(new OverloadActionConstraint());
}
}
}
}

View File

@ -0,0 +1,109 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Net.Http.Formatting;
using System.Text;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public static class FormDataCollectionExtensions
{
// This is a helper method to use Model Binding over a JQuery syntax.
// Normalize from JQuery to MVC keys. The model binding infrastructure uses MVC keys
// x[] --> x
// [] --> ""
// x[field] --> x.field, where field is not a number
public static string NormalizeJQueryToMvc(string key)
{
if (key == null)
{
return string.Empty;
}
StringBuilder sb = null;
var i = 0;
while (true)
{
int indexOpen = key.IndexOf('[', i);
if (indexOpen < 0)
{
// Fast path, no normalization needed.
// This skips the string conversion and allocating the string builder.
if (i == 0)
{
return key;
}
sb = sb ?? new StringBuilder();
sb.Append(key, i, key.Length - i);
break; // no more brackets
}
sb = sb ?? new StringBuilder();
sb.Append(key, i, indexOpen - i); // everything up to "["
// Find closing bracket.
var indexClose = key.IndexOf(']', indexOpen);
if (indexClose == -1)
{
throw new ArgumentException(Resources.JQuerySyntaxMissingClosingBracket, "key");
}
if (indexClose == indexOpen + 1)
{
// Empty bracket. Signifies array. Just remove.
}
else
{
if (char.IsDigit(key[indexOpen + 1]))
{
// array index. Leave unchanged.
sb.Append(key, indexOpen, indexClose - indexOpen + 1);
}
else
{
// Field name. Convert to dot notation.
sb.Append('.');
sb.Append(key, indexOpen + 1, indexClose - indexOpen - 1);
}
}
i = indexClose + 1;
if (i >= key.Length)
{
break; // end of string
}
}
return sb.ToString();
}
public static IEnumerable<KeyValuePair<string, string>> GetJQueryNameValuePairs(
[NotNull] this FormDataCollection formData)
{
var count = 0;
foreach (var kv in formData)
{
ThrowIfMaxHttpCollectionKeysExceeded(count);
var key = NormalizeJQueryToMvc(kv.Key);
var value = kv.Value ?? string.Empty;
yield return new KeyValuePair<string, string>(key, value);
count++;
}
}
private static void ThrowIfMaxHttpCollectionKeysExceeded(int count)
{
if (count >= MediaTypeFormatter.MaxHttpCollectionKeys)
{
var message = Resources.FormatMaxHttpCollectionKeyLimitReached(
MediaTypeFormatter.MaxHttpCollectionKeys,
typeof(MediaTypeFormatter));
throw new InvalidOperationException(message);
}
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.ApplicationModel;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromUriAttribute : Attribute, IParameterModelConvention
{
public string Name { get; set; }
public void Apply(ParameterModel model)
{
if (Name != null)
{
model.ParameterName = Name;
}
}
}
}

View File

@ -0,0 +1,125 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Routing;
using System.Net.Http.Formatting;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public class OverloadActionConstraint : IActionConstraint
{
public int Order { get; } = Int32.MaxValue;
public bool Accept(ActionConstraintContext context)
{
var candidates = context.Candidates.Select(c => new
{
Action = c,
Parameters = GetOverloadableParameters(c),
});
// Combined route value keys and query string keys. These are the values available for overload selection.
var requestKeys = GetCombinedKeys(context.RouteContext);
// Group candidates by the highest number of keys, and then process them until we find an action
// with all parameters satisfied.
foreach (var group in candidates.GroupBy(c => c.Parameters?.Count ?? 0).OrderByDescending(g => g.Key))
{
var foundMatch = false;
foreach (var candidate in group)
{
var allFound = true;
if (candidate.Parameters != null)
{
foreach (var parameter in candidate.Parameters)
{
if (!requestKeys.Contains(parameter.ParameterBindingInfo.Prefix))
{
if (candidate.Action.Action == context.CurrentCandidate.Action)
{
return false;
}
allFound = false;
break;
}
}
}
if (allFound)
{
foundMatch = true;
}
}
if (foundMatch)
{
return group.Any(c => c.Action.Action == context.CurrentCandidate.Action);
}
}
return false;
}
private List<ParameterDescriptor> GetOverloadableParameters(ActionSelectorCandidate candidate)
{
if (candidate.Action.Parameters == null)
{
return null;
}
var isOverloaded = false;
foreach (var constraint in candidate.Constraints)
{
if (constraint is OverloadActionConstraint)
{
isOverloaded = true;
}
}
if (!isOverloaded)
{
return null;
}
// We only consider parameters that are bound from the URL.
return candidate.Action.Parameters.Where(
p =>
p.ParameterBindingInfo != null &&
!p.IsOptional &&
ValueProviderResult.CanConvertFromString(p.ParameterBindingInfo.ParameterType))
.ToList();
}
private static ISet<string> GetCombinedKeys(RouteContext routeContext)
{
var keys = new HashSet<string>(routeContext.RouteData.Values.Keys, StringComparer.OrdinalIgnoreCase);
keys.Remove("controller");
keys.Remove("action");
var queryString = routeContext.HttpContext.Request.QueryString.ToUriComponent();
if (queryString.Length > 0)
{
// We need to chop off the leading '?'
var queryData = new FormDataCollection(queryString.Substring(1));
var queryNameValuePairs = queryData.GetJQueryNameValuePairs();
if (queryNameValuePairs != null)
{
foreach (var queryNameValuePair in queryNameValuePairs)
{
keys.Add(queryNameValuePair.Key);
}
}
}
return keys;
}
}
}

View File

@ -0,0 +1,62 @@
// <auto-generated />
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
using System.Globalization;
using System.Reflection;
using System.Resources;
internal static class Resources
{
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNet.Mvc.WebApiCompatShim.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// The key is invalid JQuery syntax because it is missing a closing bracket.
/// </summary>
internal static string JQuerySyntaxMissingClosingBracket
{
get { return GetString("JQuerySyntaxMissingClosingBracket"); }
}
/// <summary>
/// The key is invalid JQuery syntax because it is missing a closing bracket.
/// </summary>
internal static string FormatJQuerySyntaxMissingClosingBracket()
{
return GetString("JQuerySyntaxMissingClosingBracket");
}
/// <summary>
/// The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class.
/// </summary>
internal static string MaxHttpCollectionKeyLimitReached
{
get { return GetString("MaxHttpCollectionKeyLimitReached"); }
}
/// <summary>
/// The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class.
/// </summary>
internal static string FormatMaxHttpCollectionKeyLimitReached(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("MaxHttpCollectionKeyLimitReached"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
System.Diagnostics.Debug.Assert(value != null);
if (formatterNames != null)
{
for (var i = 0; i < formatterNames.Length; i++)
{
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
}
}
return value;
}
}
}

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="JQuerySyntaxMissingClosingBracket" xml:space="preserve">
<value>The key is invalid JQuery syntax because it is missing a closing bracket.</value>
</data>
<data name="MaxHttpCollectionKeyLimitReached" xml:space="preserve">
<value>The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class.</value>
</data>
</root>

View File

@ -18,7 +18,9 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
public void Invoke(MvcOptions options)
{
// Placeholder
// Add webapi behaviors to controllers with the appropriate attributes
options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention());
options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention());
}
public void Invoke(WebApiCompatShimOptions options)

View File

@ -0,0 +1,627 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if ASPNET50
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class WebApiCompatShimActionSelectionTest
{
private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(WebApiCompatShimWebSite));
private readonly Action<IApplicationBuilder> _app = new WebApiCompatShimWebSite.Startup().Configure;
[Theory]
[InlineData("GET", "GetItems")]
[InlineData("PUT", "PutItems")]
[InlineData("POST", "PostItems")]
[InlineData("DELETE", "DeleteItems")]
[InlineData("PATCH", "PatchItems")]
[InlineData("HEAD", "HeadItems")]
[InlineData("OPTIONS", "OptionsItems")]
public async Task WebAPIConvention_TakesHttpMethodFromPrefix_UnnamedAction(string httpMethod, string actionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod(httpMethod),
"http://localhost/api/Admin/WebAPIActionConventions");
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(actionName, result.ActionName);
}
[Theory]
[InlineData("GET", "GetItems")]
[InlineData("PUT", "PutItems")]
[InlineData("POST", "PostItems")]
[InlineData("DELETE", "DeleteItems")]
[InlineData("PATCH", "PatchItems")]
[InlineData("HEAD", "HeadItems")]
[InlineData("OPTIONS", "OptionsItems")]
public async Task WebAPIConvention_TakesHttpMethodFromPrefix_NamedAction(string httpMethod, string actionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod(httpMethod),
"http://localhost/api/Blog/WebAPIActionConventions/" + actionName);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(actionName, result.ActionName);
}
[Fact]
public async Task WebAPIConvention_TakesHttpMethodFromPrefix_NamedAction_MismatchedVerb()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("POST"),
"http://localhost/api/Blog/WebAPIActionConventions/GetItems");
// Act
var response = await client.SendAsync(request);
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task WebAPIConvention_TakesHttpMethodFromPrefix_UnnamedAction_DefaultVerbIsPost_Success()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("POST"),
"http://localhost/api/Admin/WebApiActionConventionsDefaultPost");
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("DefaultVerbIsPost", result.ActionName);
}
[Fact]
public async Task WebAPIConvention_TakesHttpMethodFromPrefix_NamedAction_DefaultVerbIsPost_Success()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("POST"),
"http://localhost/api/Blog/WebAPIActionConventionsDefaultPost/DefaultVerbIsPost");
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("DefaultVerbIsPost", result.ActionName);
}
[Fact]
public async Task WebAPIConvention_TakesHttpMethodFromPrefix_UnnamedAction_DefaultVerbIsPost_VerbMismatch()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("GET"),
"http://localhost/api/Admin/WebApiActionConventionsDefaultPost");
// Act
var response = await client.SendAsync(request);
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task WebAPIConvention_TakesHttpMethodFromPrefix_NamedAction_DefaultVerbIsPost_VerbMismatch()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("PUT"),
"http://localhost/api/Blog/WebApiActionConventionsDefaultPost/DefaultVerbIsPost");
// Act
var response = await client.SendAsync(request);
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task WebAPIConvention_TakesHttpMethodFromMethodName_NotActionName_UnnamedAction_Success()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("POST"),
"http://localhost/api/Admin/WebAPIActionConventionsActionName");
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("GetItems", result.ActionName);
}
[Fact]
public async Task WebAPIConvention_TakesHttpMethodFromMethodName_NotActionName_NamedAction_Success()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("POST"),
"http://localhost/api/Blog/WebAPIActionConventionsActionName/GetItems");
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("GetItems", result.ActionName);
}
[Fact]
public async Task WebAPIConvention_TakesHttpMethodFromMethodName_NotActionName_UnnamedAction_VerbMismatch()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("Get"),
"http://localhost/api/Admin/WebAPIActionConventionsActionName");
// Act
var response = await client.SendAsync(request);
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task WebAPIConvention_TakesHttpMethodFromMethodName_NotActionName_NamedAction_VerbMismatch()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("GET"),
"http://localhost/api/Blog/WebAPIActionConventionsActionName/GetItems");
// Act
var response = await client.SendAsync(request);
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task WebAPIConvention_HttpMethodOverride_UnnamedAction_Success()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("GET"),
"http://localhost/api/Admin/WebAPIActionConventionsVerbOverride");
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("PostItems", result.ActionName);
}
[Fact]
public async Task WebAPIConvention_HttpMethodOverride_NamedAction_Success()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("GET"),
"http://localhost/api/Blog/WebAPIActionConventionsVerbOverride/PostItems");
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("PostItems", result.ActionName);
}
[Fact]
public async Task WebAPIConvention_HttpMethodOverride_UnnamedAction_VerbMismatch()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("POST"),
"http://localhost/api/Admin/WebAPIActionConventionsVerbOverride");
// Act
var response = await client.SendAsync(request);
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task WebAPIConvention_HttpMethodOverride_NamedAction_VerbMismatch()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod("POST"),
"http://localhost/api/Blog/WebAPIActionConventionsVerbOverride/PostItems");
// Act
var response = await client.SendAsync(request);
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
// This was ported from the WebAPI 5.2 codebase. Kept the same intentionally for compatability.
[Theory]
[InlineData("GET", "api/Admin/Test", "GetUsers")]
[InlineData("GET", "api/Admin/Test/2", "GetUser")]
[InlineData("GET", "api/Admin/Test/3?name=mario", "GetUserByNameAndId")]
[InlineData("GET", "api/Admin/Test/3?name=mario&ssn=123456", "GetUserByNameIdAndSsn")]
[InlineData("GET", "api/Admin/Test?name=mario&ssn=123456", "GetUserByNameAndSsn")]
[InlineData("GET", "api/Admin/Test?name=mario&ssn=123456&age=3", "GetUserByNameAgeAndSsn")]
[InlineData("GET", "api/Admin/Test/5?random=9", "GetUser")]
[InlineData("POST", "api/Admin/Test", "PostUser")]
[InlineData("POST", "api/Admin/Test?name=mario&age=10", "PostUserByNameAndAge")]
// Note: Normally the following would not match DeleteUserByIdAndOptName because it has 'id' and 'age' as parameters while the DeleteUserByIdAndOptName action has 'id' and 'name'.
// However, because the default value is provided on action parameter 'name', having the 'id' in the request was enough to match the action.
[InlineData("DELETE", "api/Admin/Test/6?age=10", "DeleteUserByIdAndOptName")]
[InlineData("DELETE", "api/Admin/Test", "DeleteUserByOptName")]
[InlineData("DELETE", "api/Admin/Test?name=user", "DeleteUserByOptName")]
[InlineData("DELETE", "api/Admin/Test/6?email=user@test.com", "DeleteUserById_Email_OptName_OptPhone")]
[InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&name=user", "DeleteUserById_Email_OptName_OptPhone")]
[InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&name=user&phone=123456789", "DeleteUserById_Email_OptName_OptPhone")]
[InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&height=1.8", "DeleteUserById_Email_Height_OptName_OptPhone")]
[InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&height=1.8&name=user", "DeleteUserById_Email_Height_OptName_OptPhone")]
[InlineData("DELETE", "api/Admin/Test/6?email=user@test.com&height=1.8&name=user&phone=12345678", "DeleteUserById_Email_Height_OptName_OptPhone")]
[InlineData("HEAD", "api/Admin/Test/6", "Head_Id_OptSize_OptIndex")]
[InlineData("HEAD", "api/Admin/Test/6?size=2", "Head_Id_OptSize_OptIndex")]
[InlineData("HEAD", "api/Admin/Test/6?index=2", "Head_Id_OptSize_OptIndex")]
[InlineData("HEAD", "api/Admin/Test/6?index=2&size=10", "Head_Id_OptSize_OptIndex")]
[InlineData("HEAD", "api/Admin/Test/6?index=2&otherParameter=10", "Head_Id_OptSize_OptIndex")]
[InlineData("HEAD", "api/Admin/Test/6?otherQueryParameter=1234", "Head_Id_OptSize_OptIndex")]
[InlineData("HEAD", "api/Admin/Test", "Head")]
[InlineData("HEAD", "api/Admin/Test?otherParam=2", "Head")]
[InlineData("HEAD", "api/Admin/Test?index=2&size=10", "Head")]
public async Task LegacyActionSelection_OverloadedAction_WithUnnamedAction(string httpMethod, string requestUrl, string expectedActionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedActionName, result.ActionName);
}
[Theory]
[InlineData("GET", "api/Store/Test", "GetUsers")]
[InlineData("GET", "api/Store/Test/2", "GetUsersByName")]
[InlineData("GET", "api/Store/Test/luigi?ssn=123456", "GetUserByNameAndSsn")]
[InlineData("GET", "api/Store/Test/luigi?ssn=123456&id=2&ssn=12345", "GetUserByNameIdAndSsn")]
[InlineData("GET", "api/Store/Test?age=10&ssn=123456", "GetUsers")]
[InlineData("GET", "api/Store/Test?id=3&ssn=123456&name=luigi", "GetUserByNameIdAndSsn")]
[InlineData("POST", "api/Store/Test/luigi?age=20", "PostUserByNameAndAge")]
public async Task LegacyActionSelection_OverloadedAction_NonIdRouteParameter(string httpMethod, string requestUrl, string expectedActionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedActionName, result.ActionName);
}
[Theory]
[InlineData("GET", "api/Admin/Test/3?NAME=mario", "GetUserByNameAndId")]
[InlineData("GET", "api/Admin/Test/3?name=mario&SSN=123456", "GetUserByNameIdAndSsn")]
[InlineData("GET", "api/Admin/Test?nAmE=mario&ssn=123456&AgE=3", "GetUserByNameAgeAndSsn")]
[InlineData("DELETE", "api/Admin/Test/6?AGe=10", "DeleteUserByIdAndOptName")]
public async Task LegacyActionSelection_OverloadedAction_Parameter_Casing(string httpMethod, string requestUrl, string expectedActionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedActionName, result.ActionName);
}
[Theory]
[InlineData("GET", "api/Blog/Test/GetUsers", "GetUsers")]
[InlineData("GET", "api/Blog/Test/GetUser/7", "GetUser")]
[InlineData("GET", "api/Blog/Test/GetUser?id=3", "GetUser")]
[InlineData("GET", "api/Blog/Test/GetUser/4?id=3", "GetUser")]
[InlineData("GET", "api/Blog/Test/GetUserByNameAgeAndSsn?name=user&age=90&ssn=123456789", "GetUserByNameAgeAndSsn")]
[InlineData("GET", "api/Blog/Test/GetUserByNameAndSsn?name=user&ssn=123456789", "GetUserByNameAndSsn")]
[InlineData("POST", "api/Blog/Test/PostUserByNameAndAddress?name=user", "PostUserByNameAndAddress")]
public async Task LegacyActionSelection_RouteWithActionName(string httpMethod, string requestUrl, string expectedActionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedActionName, result.ActionName);
}
[Theory]
[InlineData("GET", "api/Blog/Test/getusers", "GetUsers")]
[InlineData("GET", "api/Blog/Test/getuseR/1", "GetUser")]
[InlineData("GET", "api/Blog/Test/Getuser?iD=3", "GetUser")]
[InlineData("GET", "api/Blog/Test/GetUser/4?Id=3", "GetUser")]
[InlineData("GET", "api/Blog/Test/GetUserByNameAgeandSsn?name=user&age=90&ssn=123456789", "GetUserByNameAgeAndSsn")]
[InlineData("GET", "api/Blog/Test/getUserByNameAndSsn?name=user&ssn=123456789", "GetUserByNameAndSsn")]
[InlineData("POST", "api/Blog/Test/PostUserByNameAndAddress?name=user", "PostUserByNameAndAddress")]
public async Task LegacyActionSelection_RouteWithActionName_Casing(string httpMethod, string requestUrl, string expectedActionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedActionName, result.ActionName);
}
[Theory]
[InlineData("GET", "api/Admin/Test", "GetUsers")]
[InlineData("GET", "api/Admin/Test/?name=peach", "GetUsersByName")]
[InlineData("GET", "api/Admin/Test?name=peach", "GetUsersByName")]
[InlineData("GET", "api/Admin/Test?name=peach&ssn=123456", "GetUserByNameAndSsn")]
[InlineData("GET", "api/Admin/Test?name=peach&ssn=123456&age=3", "GetUserByNameAgeAndSsn")]
public async Task LegacyActionSelection_RouteWithoutActionName(string httpMethod, string requestUrl, string expectedActionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedActionName, result.ActionName);
}
[Theory]
[InlineData("GET", "api/Admin/ParameterAttribute/2", "GetUser")]
[InlineData("GET", "api/Admin/ParameterAttribute?id=2", "GetUser")]
[InlineData("GET", "api/Admin/ParameterAttribute?myId=2", "GetUserByMyId")]
[InlineData("POST", "api/Admin/ParameterAttribute/3?name=user", "PostUserNameFromUri")]
[InlineData("POST", "api/Admin/ParameterAttribute/3", "PostUserNameFromBody")]
[InlineData("DELETE", "api/Admin/ParameterAttribute/3?name=user", "DeleteUserWithNullableIdAndName")]
[InlineData("DELETE", "api/Admin/ParameterAttribute?address=userStreet", "DeleteUser")]
public async Task LegacyActionSelection_ModelBindingParameterAttribute_AreAppliedWhenSelectingActions(string httpMethod, string requestUrl, string expectedActionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedActionName, result.ActionName);
}
[Theory]
[InlineData("GET", "api/Support/notActionParameterValue1/Test", "GetUsers")]
[InlineData("GET", "api/Support/notActionParameterValue2/Test/2", "GetUser")]
[InlineData("GET", "api/Support/notActionParameterValue1/Test?randomQueryVariable=val1", "GetUsers")]
[InlineData("GET", "api/Support/notActionParameterValue2/Test/2?randomQueryVariable=val2", "GetUser")]
public async Task LegacyActionSelection_ActionsThatHaveSubsetOfRouteParameters_AreConsideredForSelection(string httpMethod, string requestUrl, string expectedActionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedActionName, result.ActionName);
}
// This would result in ambiguous match because complex parameter is not considered for matching.
// Therefore, PostUserByNameAndAddress(string name, Address address) would conflicts with PostUserByName(string name)
[Fact]
public async Task LegacyActionSelection_RequestToAmbiguousAction_OnDefaultRoute()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod("POST"), "http://localhost/api/Admin/Test?name=mario");
// Act & Assert
await Assert.ThrowsAsync<AmbiguousActionException>(async () => await client.SendAsync(request));
}
[Theory]
[InlineData("GET", "api/Admin/EnumParameterOverloads", "Get")]
[InlineData("GET", "api/Admin/EnumParameterOverloads?scope=global", "GetWithEnumParameter")]
[InlineData("GET", "api/Admin/EnumParameterOverloads?level=off&kind=trace", "GetWithTwoEnumParameters")]
[InlineData("GET", "api/Admin/EnumParameterOverloads?level=", "GetWithNullableEnumParameter")]
public async Task LegacyActionSelection_SelectAction_ReturnsActionDescriptor_ForEnumParameterOverloads(string httpMethod, string requestUrl, string expectedActionName)
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/" + requestUrl);
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ActionSelectionResult>(body);
//Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedActionName, result.ActionName);
}
// Verify response has all the methods in its Allow header. values are unsorted.
private void AssertAllowedHeaders(HttpResponseMessage response, params HttpMethod[] allowedMethods)
{
foreach (var method in allowedMethods)
{
Assert.Contains(method.ToString(), response.Content.Headers.Allow);
}
Assert.Equal(allowedMethods.Length, response.Content.Headers.Allow.Count);
}
private class ActionSelectionResult
{
public string ActionName { get; set; }
public string ControllerName { get; set; }
}
}
}
#endif

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/BasicApi/WriteToHttpContext");
var response = await client.GetAsync("http://localhost/api/Blog/BasicApi/WriteToHttpContext");
var content = await response.Content.ReadAsStringAsync();
// Assert
@ -44,13 +44,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/BasicApi/GenerateUrl");
var response = await client.GetAsync("http://localhost/api/Blog/BasicApi/GenerateUrl");
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(
"Visited: /BasicApi/GenerateUrl",
"Visited: /api/Blog/BasicApi/GenerateUrl",
content);
}
@ -69,7 +69,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
};
// Act
var response = await client.GetAsync("http://localhost/BasicApi/GetFormatters");
var response = await client.GetAsync("http://localhost/api/Blog/BasicApi/GetFormatters");
var content = await response.Content.ReadAsStringAsync();
var formatters = JsonConvert.DeserializeObject<string[]>(content);

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.WebApiCompatShim;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.NestedProviders;
using Microsoft.Framework.OptionsModel;
@ -16,8 +17,6 @@ namespace System.Web.Http
{
public class ApiControllerActionDiscoveryTest
{
// For now we just want to verify that an ApiController is-a controller and produces
// actions. When we implement the conventions for action discovery, this test will be revised.
[Fact]
public void GetActions_ApiControllerWithControllerSuffix_IsController()
{
@ -32,9 +31,9 @@ namespace System.Web.Http
// Assert
var controllerType = typeof(TestControllers.ProductsController).GetTypeInfo();
var filtered = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray();
var actions = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray();
Assert.Equal(3, filtered.Length);
Assert.NotEmpty(actions);
}
[Fact]
@ -51,9 +50,179 @@ namespace System.Web.Http
// Assert
var controllerType = typeof(TestControllers.Blog).GetTypeInfo();
var filtered = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray();
var actions = results.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType).ToArray();
Assert.Empty(filtered);
Assert.Empty(actions);
}
[Fact]
public void GetActions_CreatesNamedAndUnnamedAction()
{
// Arrange
var provider = CreateProvider();
// Act
var context = new ActionDescriptorProviderContext();
provider.Invoke(context);
var results = context.Results.Cast<ControllerActionDescriptor>();
// Assert
var controllerType = typeof(TestControllers.StoreController).GetTypeInfo();
var actions = results
.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType)
.Where(ad => ad.MethodInfo.Name == "GetAll")
.ToArray();
Assert.Equal(2, actions.Length);
var action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "GetAll"));
Assert.Equal(
new string[] { "GET" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodConstraint>()).HttpMethods);
action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == ""));
Assert.Equal(
new string[] { "GET" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodConstraint>()).HttpMethods);
}
[Fact]
public void GetActions_CreatesNamedAndUnnamedAction_DefaultVerbIsPost()
{
// Arrange
var provider = CreateProvider();
// Act
var context = new ActionDescriptorProviderContext();
provider.Invoke(context);
var results = context.Results.Cast<ControllerActionDescriptor>();
// Assert
var controllerType = typeof(TestControllers.StoreController).GetTypeInfo();
var actions = results
.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType)
.Where(ad => ad.MethodInfo.Name == "Edit")
.ToArray();
Assert.Equal(2, actions.Length);
var action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "Edit"));
Assert.Equal(
new string[] { "POST" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodConstraint>()).HttpMethods);
action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == ""));
Assert.Equal(
new string[] { "POST" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodConstraint>()).HttpMethods);
}
[Fact]
public void GetActions_CreatesNamedAndUnnamedAction_RespectsVerbAttribute()
{
// Arrange
var provider = CreateProvider();
// Act
var context = new ActionDescriptorProviderContext();
provider.Invoke(context);
var results = context.Results.Cast<ControllerActionDescriptor>();
// Assert
var controllerType = typeof(TestControllers.StoreController).GetTypeInfo();
var actions = results
.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType)
.Where(ad => ad.MethodInfo.Name == "Delete")
.ToArray();
Assert.Equal(2, actions.Length);
var action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "Delete"));
Assert.Equal(
new string[] { "PUT" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodConstraint>()).HttpMethods);
action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == ""));
Assert.Equal(
new string[] { "PUT" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodConstraint>()).HttpMethods);
}
// The method name is used to infer a verb, not the action name
[Fact]
public void GetActions_CreatesNamedAndUnnamedAction_VerbBasedOnMethodName()
{
// Arrange
var provider = CreateProvider();
// Act
var context = new ActionDescriptorProviderContext();
provider.Invoke(context);
var results = context.Results.Cast<ControllerActionDescriptor>();
// Assert
var controllerType = typeof(TestControllers.StoreController).GetTypeInfo();
var actions = results
.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType)
.Where(ad => ad.MethodInfo.Name == "Options")
.ToArray();
Assert.Equal(2, actions.Length);
var action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == "GetOptions"));
Assert.Equal(
new string[] { "OPTIONS" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodConstraint>()).HttpMethods);
action = Assert.Single(
actions,
a => a.RouteConstraints.Any(rc => rc.RouteKey == "action" && rc.RouteValue == ""));
Assert.Equal(
new string[] { "OPTIONS" },
Assert.Single(action.ActionConstraints.OfType<HttpMethodConstraint>()).HttpMethods);
}
[Fact]
public void GetActions_AllWebApiActionsAreOverloaded()
{
// Arrange
var provider = CreateProvider();
// Act
var context = new ActionDescriptorProviderContext();
provider.Invoke(context);
var results = context.Results.Cast<ControllerActionDescriptor>();
// Assert
var controllerType = typeof(TestControllers.StoreController).GetTypeInfo();
var actions = results
.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType)
.ToArray();
Assert.NotEmpty(actions);
foreach (var action in actions)
{
Assert.Single(action.ActionConstraints, c => c is OverloadActionConstraint);
}
}
private INestedProviderManager<ActionDescriptorProviderContext> CreateProvider()
@ -70,13 +239,17 @@ namespace System.Web.Http
var conventions = new NamespaceLimitedActionDiscoveryConventions();
var options = new MvcOptions();
options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention());
options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention());
var optionsAccessor = new Mock<IOptionsAccessor<MvcOptions>>();
optionsAccessor
.SetupGet(o => o.Options)
.Returns(new MvcOptions());
.Returns(options);
var provider = new ControllerActionDescriptorProvider(
assemblyProvider.Object,
assemblyProvider.Object,
conventions,
filterProvider.Object,
optionsAccessor.Object);
@ -92,7 +265,7 @@ namespace System.Web.Http
{
public override bool IsController(TypeInfo typeInfo)
{
return
return
typeInfo.Namespace == "System.Web.Http.TestControllers" &&
base.IsController(typeInfo);
}
@ -110,16 +283,6 @@ namespace System.Web.Http.TestControllers
{
return null;
}
public IActionResult Get(int id)
{
return null;
}
public IActionResult Edit(int id)
{
return null;
}
}
// Not a controller, because there's no controller suffix
@ -130,4 +293,29 @@ namespace System.Web.Http.TestControllers
return null;
}
}
public class StoreController : ApiController
{
public IActionResult GetAll()
{
return null;
}
public IActionResult Edit(int id)
{
return null;
}
[HttpPut]
public IActionResult Delete(int id)
{
return null;
}
[ActionName("GetOptions")]
public IActionResult Options()
{
return null;
}
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Linq;
using System.Net.Http.Formatting;
using Xunit;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public class FormDataCollectionExtensionsTest
{
[Theory]
[InlineData("", null)]
[InlineData("", "")] // empty
[InlineData("x", "x")] // normal key
[InlineData("", "[]")] // trim []
[InlineData("x", "x[]")] // trim []
[InlineData("x[234]", "x[234]")] // array index
[InlineData("x.y", "x[y]")] // field lookup
[InlineData("x.y.z", "x[y][z]")] // nested field lookup
[InlineData("x.y[234].x", "x[y][234][x]")] // compound
public void TestNormalize(string expectedMvc, string jqueryString)
{
Assert.Equal(expectedMvc, FormDataCollectionExtensions.NormalizeJQueryToMvc(jqueryString));
}
[Fact]
public void TestGetJQueryNameValuePairs()
{
// Arrange
var formData = new FormDataCollection("x.y=30&x[y]=70&x[z][20]=cool");
// Act
var actual = FormDataCollectionExtensions.GetJQueryNameValuePairs(formData).ToArray();
// Assert
var arraySetter = Assert.Single(actual, kvp => kvp.Key == "x.z[20]");
Assert.Equal("cool", arraySetter.Value);
Assert.Single(actual, kvp => kvp.Key == "x.y" && kvp.Value == "30");
Assert.Single(actual, kvp => kvp.Key == "x.y" && kvp.Value == "70");
}
}
}

View File

@ -0,0 +1,444 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.Routing;
using Xunit;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public class OverloadActionConstraintTest
{
[Fact]
public void Accept_RejectsActionMatchWithMissingParameter()
{
// Arrange
var action = new ActionDescriptor();
action.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext();
// Act & Assert
Assert.False(constraint.Accept(context));
}
[Fact]
public void Accept_AcceptsActionWithSatisfiedParameters()
{
// Arrange
var action = new ActionDescriptor();
action.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17});
// Act & Assert
Assert.True(constraint.Accept(context));
}
[Fact]
public void Accept_AcceptsActionWithSatisfiedParameters_QueryStringOnly()
{
// Arrange
var action = new ActionDescriptor();
action.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?quantity=5&id=7", new { });
// Act & Assert
Assert.True(constraint.Accept(context));
}
[Fact]
public void Accept_AcceptsActionWithSatisfiedParameters_RouteDataOnly()
{
// Arrange
var action = new ActionDescriptor();
action.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?", new { quantity = 9, id = 17 });
// Act & Assert
Assert.True(constraint.Accept(context));
}
[Fact]
public void Accept_AcceptsActionWithUnsatisfiedOptionalParameter()
{
// Arrange
var action = new ActionDescriptor();
action.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
IsOptional = true,
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?store=5", new { id = 17 });
// Act & Assert
Assert.True(constraint.Accept(context));
}
[Fact]
public void Accept_AcceptsOneAndRejectsAnother()
{
// Arrange
var action1 = new ActionDescriptor();
action1.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var action2 = new ActionDescriptor();
action2.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity_ordered",
ParameterBindingInfo = new ParameterBindingInfo("quantity_ordered", typeof(int)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action1, new [] { constraint }),
new ActionSelectorCandidate(action2, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17 });
// Act & Assert
Assert.True(constraint.Accept(context));
context.CurrentCandidate = context.Candidates[1];
Assert.False(constraint.Accept(context));
}
[Fact]
public void Accept_RejectsWorseMatch()
{
// Arrange
var action1 = new ActionDescriptor();
action1.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
};
var action2 = new ActionDescriptor();
action2.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action1, new [] { constraint }),
new ActionSelectorCandidate(action2, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17 });
// Act & Assert
Assert.False(constraint.Accept(context));
}
[Fact]
public void Accept_RejectsWorseMatch_OptionalParameter()
{
// Arrange
var action1 = new ActionDescriptor();
action1.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
IsOptional = true,
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var action2 = new ActionDescriptor();
action2.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action1, new [] { constraint }),
new ActionSelectorCandidate(action2, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17 });
// Act & Assert
Assert.False(constraint.Accept(context));
}
[Fact]
public void Accept_AcceptsActionsOnSameTier()
{
// Arrange
var action1 = new ActionDescriptor();
action1.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var action2 = new ActionDescriptor();
action2.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "price",
ParameterBindingInfo = new ParameterBindingInfo("price", typeof(decimal)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action1, new [] { constraint }),
new ActionSelectorCandidate(action2, new [] { constraint }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?quantity=5&price=5.99", new { id = 17 });
// Act & Assert
Assert.True(constraint.Accept(context));
context.CurrentCandidate = context.Candidates[1];
Assert.True(constraint.Accept(context));
}
[Fact]
public void Accept_AcceptsAction_WithFewerParameters_WhenOtherIsNotOverloaded()
{
// Arrange
var action1 = new ActionDescriptor();
action1.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
};
var action2 = new ActionDescriptor();
action2.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
};
var constraint = new OverloadActionConstraint();
var context = new ActionConstraintContext();
context.Candidates = new List<ActionSelectorCandidate>()
{
new ActionSelectorCandidate(action1, new [] { constraint }),
new ActionSelectorCandidate(action2, new IActionConstraint[] { }),
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17 });
// Act & Assert
Assert.True(constraint.Accept(context));
}
private static RouteContext CreateRouteContext(string queryString = null, object routeValues = null)
{
var httpContext = new DefaultHttpContext();
if (queryString != null)
{
httpContext.Request.QueryString = new QueryString(queryString);
}
var routeContext = new RouteContext(httpContext);
routeContext.RouteData = new RouteData()
{
Values = new RouteValueDictionary(routeValues),
};
return routeContext;
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace WebApiCompatShimWebSite
{
public class ActionSelectionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
var action = (ControllerActionDescriptor)context.ActionDescriptor;
context.Result = new JsonResult(new
{
ActionName = action.Name,
ControllerName = action.ControllerName
});
}
}
}

View File

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Web.Http;
using Microsoft.AspNet.Mvc.WebApiCompatShim;
namespace WebApiCompatShimWebSite
{
// This was ported from the WebAPI 5.2 codebase. Kept the same intentionally for compatability.
[ActionSelectionFilter]
public class EnumParameterOverloadsController : ApiController
{
public IEnumerable<string> Get()
{
return new string[] { "get" };
}
public string GetWithEnumParameter(UserKind scope)
{
return scope.ToString();
}
public string GetWithTwoEnumParameters([FromUri]UserKind level, UserKind kind)
{
return level.ToString() + kind.ToString();
}
public string GetWithNullableEnumParameter(TraceLevel? level)
{
return level.ToString();
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Web.Http;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.WebApiCompatShim;
namespace WebApiCompatShimWebSite
{
// This was ported from the WebAPI 5.2 codebase. Kept the same intentionally for compatability.
[ActionSelectionFilter]
public class ParameterAttributeController : ApiController
{
public User GetUserByMyId(int myId) { return null; }
public User GetUser([FromUri(Name = "id")] int myId) { return null; }
public List<User> PostUserNameFromUri(int id, [FromUri]string name) { return null; }
public List<User> PostUserNameFromBody(int id, [FromBody] string name) { return null; }
public void DeleteUserWithNullableIdAndName(int? id, string name) { }
public void DeleteUser(string address) { }
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Web.Http;
using Microsoft.AspNet.Mvc;
namespace WebApiCompatShimWebSite
{
// This was ported from the WebAPI 5.2 codebase. Kept the same intentionally for compatability.
[ActionSelectionFilter]
public class TestController : ApiController
{
public User GetUser(int id) { return null; }
public List<User> GetUsers() { return null; }
public List<User> GetUsersByName(string name) { return null; }
[AcceptVerbs("PATCH")]
public void PutUser(User user) { }
public User GetUserByNameAndId(string name, int id) { return null; }
public User GetUserByNameAndAge(string name, int age) { return null; }
public User GetUserByNameAgeAndSsn(string name, int age, int ssn) { return null; }
public User GetUserByNameIdAndSsn(string name, int id, int ssn) { return null; }
public User GetUserByNameAndSsn(string name, int ssn) { return null; }
public User PostUser(User user) { return null; }
public User PostUserByNameAndAge(string name, int age) { return null; }
public User PostUserByName(string name) { return null; }
public User PostUserByNameAndAddress(string name, UserAddress address) { return null; }
public User DeleteUserByOptName(string name = null) { return null; }
public User DeleteUserByIdAndOptName(int id, string name = "DefaultName") { return null; }
public User DeleteUserByIdNameAndAge(int id, string name, int age) { return null; }
public User DeleteUserById_Email_OptName_OptPhone(int id, string email, string name = null, int phone = 0) { return null; }
public User DeleteUserById_Email_Height_OptName_OptPhone(int id, string email, double height, string name = "DefaultName", int? phone = null) { return null; }
public void Head_Id_OptSize_OptIndex(int id, int size = 10, int index = 0) { }
public void Head() { }
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Web.Http;
using Microsoft.AspNet.Mvc;
namespace WebApiCompatShimWebSite
{
// The verb is still inferred by the METHOD NAME not the action name.
[ActionSelectionFilter]
public class WebAPIActionConventionsActionNameController : ApiController
{
[ActionName("GetItems")]
public void PostItems()
{
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Web.Http;
namespace WebApiCompatShimWebSite
{
// Each of these is mapped to an unnamed action with the corresponding http verb, and also
// a named action with the corresponding http verb.
[ActionSelectionFilter]
public class WebAPIActionConventionsController : ApiController
{
public void GetItems()
{
}
public void PostItems()
{
}
public void PutItems()
{
}
public void DeleteItems()
{
}
public void PatchItems()
{
}
public void HeadItems()
{
}
public void OptionsItems()
{
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Web.Http;
namespace WebApiCompatShimWebSite
{
// This action only accepts POST by default
[ActionSelectionFilter]
public class WebAPIActionConventionsDefaultPostController : ApiController
{
public void DefaultVerbIsPost()
{
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Web.Http;
using Microsoft.AspNet.Mvc;
namespace WebApiCompatShimWebSite
{
// The verb is overridden by the attribute
[ActionSelectionFilter]
public class WebAPIActionConventionsVerbOverrideController : ApiController
{
[HttpGet]
public void PostItems()
{
}
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace WebApiCompatShimWebSite
{
public class User
{
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace WebApiCompatShimWebSite
{
public class UserAddress
{
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace WebApiCompatShimWebSite
{
public enum UserKind
{
Normal,
Admin,
SuperAdmin,
}
}

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
namespace WebApiCompatShimWebSite
@ -19,7 +21,15 @@ namespace WebApiCompatShimWebSite
services.AddWebApiConventions();
});
app.UseMvc();
app.UseMvc(routes =>
{
// Tests include different styles of WebAPI conventional routing and action selection - the prefix keeps
// them from matching too eagerly.
routes.MapRoute("named-action", "api/Blog/{controller}/{action}/{id?}");
routes.MapRoute("unnamed-action", "api/Admin/{controller}/{id?}");
routes.MapRoute("name-as-parameter", "api/Store/{controller}/{name?}");
routes.MapRoute("extra-parameter", "api/Support/{extra}/{controller}/{id?}");
});
}
}
}