Fix #1287 - Port WebAPI parameter binding behavior

This change modifies the default parameter binding behavior for an
ApiController to use the WebAPI rules.

'simple types' default to use route data or query string
'complex types' default to use the body (formatters)

Adds ModelBindingAttribute to enabled model binding
This commit is contained in:
Ryan Nowak 2014-10-16 18:27:00 -07:00
parent 5b1bcb6079
commit df8f84b772
31 changed files with 710 additions and 158 deletions

View File

@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
using Microsoft.AspNet.Mvc.WebApiCompatShim;
using MvcSample.Web.Models;
namespace MvcSample.Web
@ -61,74 +58,11 @@ namespace MvcSample.Web
return result;
}
private class OverloadAttribute : Attribute, IActionConstraint
private class OverloadAttribute : Attribute, IActionConstraintFactory
{
public int Order { get; } = Int32.MaxValue;
public bool Accept(ActionConstraintContext context)
public IActionConstraint CreateInstance(IServiceProvider services)
{
var candidates = context.Candidates.Select(a => new
{
Action = a,
Parameters = GetOverloadableParameters(a.Action),
});
var valueProviderFactory = context.RouteContext.HttpContext.RequestServices
.GetRequiredService<ICompositeValueProviderFactory>();
var factoryContext = new ValueProviderFactoryContext(
context.RouteContext.HttpContext,
context.RouteContext.RouteData.Values);
var valueProvider = valueProviderFactory.GetValueProvider(factoryContext);
foreach (var group in candidates.GroupBy(c => c.Parameters.Count).OrderByDescending(g => g.Key))
{
var foundMatch = false;
foreach (var candidate in group)
{
var allFound = true;
foreach (var parameter in candidate.Parameters)
{
if (!(valueProvider.ContainsPrefixAsync(parameter.ParameterBindingInfo.Prefix).Result))
{
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(ActionDescriptor action)
{
if (action.Parameters == null)
{
return new List<ParameterDescriptor>();
}
return action.Parameters.Where(
p =>
p.ParameterBindingInfo != null &&
!p.IsOptional &&
ValueProviderResult.CanConvertFromString(p.ParameterBindingInfo.ParameterType))
.ToList();
return new OverloadActionConstraint();
}
}
}

View File

@ -5,6 +5,7 @@
"dependencies": {
"Kestrel": "1.0.0-*",
"Microsoft.AspNet.Mvc": "6.0.0-*",
"Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-*",
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
"Microsoft.AspNet.StaticFiles": "1.0.0-*",

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.ApplicationModel
{
@ -19,6 +20,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModel
{
Action = other.Action;
Attributes = new List<object>(other.Attributes);
BinderMarker = other.BinderMarker;
IsOptional = other.IsOptional;
ParameterInfo = other.ParameterInfo;
ParameterName = other.ParameterName;
@ -28,6 +30,8 @@ namespace Microsoft.AspNet.Mvc.ApplicationModel
public List<object> Attributes { get; private set; }
public IBinderMarker BinderMarker { get; set; }
public bool IsOptional { get; set; }
public ParameterInfo ParameterInfo { get; private set; }

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc
{
var actionBindingContext = await _bindingContextProvider.GetActionBindingContextAsync(actionContext);
var metadataProvider = actionBindingContext.MetadataProvider;
var parameters = actionContext.ActionDescriptor.Parameters;
var actionDescriptor = actionContext.ActionDescriptor as ControllerActionDescriptor;
if (actionDescriptor == null)
{
@ -37,33 +37,49 @@ namespace Microsoft.AspNet.Mvc
nameof(actionContext));
}
var actionMethodInfo = actionDescriptor.MethodInfo;
var parameterMetadatas = metadataProvider.GetMetadataForParameters(actionMethodInfo);
var parameterMetadata = new List<ModelMetadata>();
foreach (var parameter in actionDescriptor.Parameters)
{
var metadata = metadataProvider.GetMetadataForParameter(
modelAccessor: null,
methodInfo: actionDescriptor.MethodInfo,
parameterName: parameter.Name,
binderMarker: parameter.BinderMarker);
var actionArguments = new Dictionary<string, object>(StringComparer.Ordinal);
await PopulateActionArgumentsAsync(parameterMetadatas, actionBindingContext, actionArguments);
return actionArguments;
}
if (metadata != null)
{
parameterMetadata.Add(metadata);
}
}
private async Task PopulateActionArgumentsAsync(IEnumerable<ModelMetadata> modelMetadatas,
ActionBindingContext actionBindingContext,
IDictionary<string, object> invocationInfo)
{
var bodyBoundParameterCount = modelMetadatas.Count(
modelMetadata => modelMetadata.Marker is IBodyBinderMarker);
var bodyBoundParameterCount = parameterMetadata.Count(
modelMetadata => modelMetadata.Marker is IBodyBinderMarker);
if (bodyBoundParameterCount > 1)
{
throw new InvalidOperationException(Resources.MultipleBodyParametersAreNotAllowed);
}
foreach (var modelMetadata in modelMetadatas)
var actionArguments = new Dictionary<string, object>(StringComparer.Ordinal);
foreach (var parameter in parameterMetadata)
{
var modelBindingContext = GetModelBindingContext(modelMetadata, actionBindingContext);
await PopulateArgumentAsync(actionBindingContext, actionArguments, parameter);
}
if (await actionBindingContext.ModelBinder.BindModelAsync(modelBindingContext))
{
invocationInfo[modelMetadata.PropertyName] = modelBindingContext.Model;
}
return actionArguments;
}
private async Task PopulateArgumentAsync(
ActionBindingContext actionBindingContext,
IDictionary<string, object> arguments,
ModelMetadata modelMetadata)
{
var parameterType = modelMetadata.ModelType;
var modelBindingContext = GetModelBindingContext(modelMetadata, actionBindingContext);
if (await actionBindingContext.ModelBinder.BindModelAsync(modelBindingContext))
{
arguments[modelMetadata.PropertyName] = modelBindingContext.Model;
}
}

View File

@ -5,12 +5,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.ApplicationModel;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ApplicationModel;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc
@ -191,6 +191,8 @@ namespace Microsoft.AspNet.Mvc
var attributes = parameterInfo.GetCustomAttributes(inherit: true).OfType<object>().ToList();
parameterModel.Attributes.AddRange(attributes);
parameterModel.BinderMarker = attributes.OfType<IBinderMarker>().FirstOrDefault();
parameterModel.ParameterName = parameterInfo.Name;
parameterModel.IsOptional = parameterInfo.HasDefaultValue;
@ -508,8 +510,9 @@ namespace Microsoft.AspNet.Mvc
{
var parameterDescriptor = new ParameterDescriptor()
{
BinderMarker = parameter.BinderMarker,
IsOptional = parameter.IsOptional,
Name = parameter.ParameterName,
IsOptional = parameter.IsOptional
};
var isFromBody = parameter.Attributes.OfType<FromBodyAttribute>().Any();

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
@ -16,6 +17,8 @@ namespace Microsoft.AspNet.Mvc
public ParameterBindingInfo ParameterBindingInfo { get; set; }
public IBinderMarker BinderMarker { get; set; }
public BodyParameterInfo BodyParameterInfo { get; set; }
}
}

View File

@ -46,24 +46,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return CreateMetadataFromPrototype(prototype, modelAccessor);
}
public IEnumerable<ModelMetadata> GetMetadataForParameters([NotNull] MethodInfo methodInfo)
{
var parameters = methodInfo.GetParameters();
foreach (var parameter in parameters)
{
// Name can be null if the methodinfo represents an open delegate.
if (!string.IsNullOrEmpty(parameter.Name))
{
yield return GetMetadataForParameterCore(
modelAccessor: null, parameterName: parameter.Name, parameter: parameter);
}
}
}
public ModelMetadata GetMetadataForParameter(
Func<object> modelAccessor,
[NotNull] MethodInfo methodInfo,
[NotNull] string parameterName)
[NotNull] string parameterName,
IBinderMarker binderMarker)
{
var parameter = methodInfo.GetParameters().FirstOrDefault(
param => StringComparer.Ordinal.Equals(param.Name, parameterName));
@ -73,7 +60,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
throw new ArgumentException(message, nameof(parameterName));
}
return GetMetadataForParameterCore(modelAccessor, parameterName, parameter);
return GetMetadataForParameterCore(modelAccessor, parameterName, parameter, binderMarker);
}
// Override for creating the prototype metadata (without the accessor)
@ -87,12 +74,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Func<object> modelAccessor);
private ModelMetadata GetMetadataForParameterCore(Func<object> modelAccessor,
string parameterName,
ParameterInfo parameter)
ParameterInfo parameter,
IBinderMarker binderMarker)
{
var parameterInfo =
CreateParameterInfo(parameter.ParameterType,
parameter.GetCustomAttributes(),
parameterName);
parameterName,
binderMarker);
var typeInfo = GetTypeInformation(parameter.ParameterType);
UpdateMetadataWithTypeInfo(parameterInfo.Prototype, typeInfo);
@ -244,13 +233,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
}
private ParameterInformation CreateParameterInfo(Type parameterType, IEnumerable<Attribute> attributes, string parameterName)
private ParameterInformation CreateParameterInfo(
Type parameterType,
IEnumerable<Attribute> attributes,
string parameterName,
IBinderMarker binderMarker)
{
var metadataProtoType = CreateMetadataPrototype(attributes: attributes,
containerType: null,
modelType: parameterType,
propertyName: parameterName);
if (binderMarker != null)
{
metadataProtoType.Marker = binderMarker;
}
var nameProvider = binderMarker as IModelNameProvider;
if (nameProvider != null && nameProvider.Name != null)
{
metadataProtoType.ModelName = nameProvider.Name;
}
return new ParameterInformation
{
Prototype = metadataProtoType

View File

@ -15,8 +15,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelMetadata GetMetadataForType(Func<object> modelAccessor, [NotNull] Type modelType);
IEnumerable<ModelMetadata> GetMetadataForParameters([NotNull] MethodInfo methodInfo);
ModelMetadata GetMetadataForParameter(Func<object> modelAccessor, [NotNull] MethodInfo methodInfo, [NotNull] string parameterName);
ModelMetadata GetMetadataForParameter(
Func<object> modelAccessor,
[NotNull] MethodInfo methodInfo,
[NotNull] string parameterName,
IBinderMarker binderMarker);
}
}

View File

@ -20,6 +20,7 @@ namespace System.Web.Http
{
[UseWebApiRoutes]
[UseWebApiActionConventions]
[UseWebApiParameterConventions]
[UseWebApiOverloading]
public abstract class ApiController : IDisposable
{

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 IUseWebApiParameterConventions
{
}
}

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 UseWebApiParameterConventionsAttribute : Attribute, IUseWebApiParameterConventions
{
}
}

View File

@ -0,0 +1,53 @@
// 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 System.Web.Http;
using Microsoft.AspNet.Mvc.ApplicationModel;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public class WebApiParameterConventionsGlobalModelConvention : 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<IUseWebApiParameterConventions>().Any();
}
private void Apply(ControllerModel model)
{
foreach (var action in model.Actions)
{
foreach (var parameter in action.Parameters)
{
if (parameter.BinderMarker != null)
{
// This has a binding behavior configured, just leave it alone.
}
else if (ValueProviderResult.CanConvertFromString(parameter.ParameterInfo.ParameterType))
{
// Simple types are by-default from the URI.
parameter.BinderMarker = new FromUriAttribute();
}
else
{
// Complex types are by-default from the body.
parameter.BinderMarker = new FromBodyAttribute();
}
}
}
}
}
}

View File

@ -1,22 +0,0 @@
// 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

@ -37,7 +37,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
foreach (var parameter in candidate.Parameters)
{
if (!requestKeys.Contains(parameter.ParameterBindingInfo.Prefix))
if (!requestKeys.Contains(parameter.Prefix))
{
if (candidate.Action.Action == context.CurrentCandidate.Action)
{
@ -65,7 +65,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
return false;
}
private List<ParameterDescriptor> GetOverloadableParameters(ActionSelectorCandidate candidate)
private List<OverloadedParameter> GetOverloadableParameters(ActionSelectorCandidate candidate)
{
if (candidate.Action.Parameters == null)
{
@ -86,13 +86,26 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
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();
var parameters = new List<OverloadedParameter>();
foreach (var parameter in candidate.Action.Parameters)
{
// We only consider parameters that are bound from the URL.
if ((parameter.BinderMarker is IRouteDataMarker || parameter.BinderMarker is IQueryBinderMarker) &&
!parameter.IsOptional &&
ValueProviderResult.CanConvertFromString(parameter.ParameterBindingInfo.ParameterType))
{
var prefix = (parameter.BinderMarker as IModelNameProvider).Name ?? parameter.Name;
parameters.Add(new OverloadedParameter()
{
ParameterDescriptor = parameter,
Prefix = prefix,
});
}
}
return parameters;
}
private static ISet<string> GetCombinedKeys(RouteContext routeContext)
@ -121,5 +134,12 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
return keys;
}
private class OverloadedParameter
{
public ParameterDescriptor ParameterDescriptor { get; set; }
public string Prefix { get; set; }
}
}
}

View File

@ -0,0 +1,17 @@
// 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;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace System.Web.Http
{
/// <summary>
/// An attribute that specifies that the value can be bound from the query string or route data.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromUriAttribute : Attribute, IQueryBinderMarker, IRouteDataMarker, IModelNameProvider
{
public string Name { get; set; }
}
}

View File

@ -0,0 +1,15 @@
// 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.ModelBinding;
namespace System.Web.Http
{
/// <summary>
/// An attribute that specifies that the value can be bound by a model binder.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class ModelBinderAttribute : Attribute, IBinderMarker
{
}
}

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
// Add webapi behaviors to controllers with the appropriate attributes
options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention());
options.ApplicationModelConventions.Add(new WebApiParameterConventionsGlobalModelConvention());
options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention());
options.ApplicationModelConventions.Add(new WebApiRoutesGlobalModelConvention(area: DefaultAreaName));

View File

@ -6,6 +6,7 @@
"dependencies": {
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
"Microsoft.AspNet.Mvc.Core": "6.0.0-*",
"Microsoft.AspNet.Mvc.ModelBinding": "6.0.0-*",
"Microsoft.AspNet.WebApi.Client": "5.2.2"
},
"frameworks": {

View File

@ -17,6 +17,7 @@ namespace Microsoft.AspNet.Mvc.ApplicationModel
parameter.Action = new ActionModel(typeof(TestController).GetMethod("Edit"));
parameter.Attributes.Add(new FromBodyAttribute());
parameter.BinderMarker = new FromBodyAttribute();
parameter.IsOptional = true;
parameter.ParameterName = "id";

View File

@ -59,7 +59,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = metadataProvider.GetMetadataForParameter(modelAccessor: null,
methodInfo: methodInfo,
parameterName: "foo");
parameterName: "foo",
binderMarker: null);
var actionBindingContext = new ActionBindingContext(actionContext,
@ -93,7 +94,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = metadataProvider.GetMetadataForParameter(modelAccessor: null,
methodInfo: methodInfo,
parameterName: "foo1");
parameterName: "foo1",
binderMarker: null);
var actionBindingContext = new ActionBindingContext(actionContext,

View File

@ -1,7 +1,6 @@
// 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.Collections.Generic;
using System.Net;
@ -377,5 +376,4 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.PaymentRequired, response.StatusCode);
}
}
}
#endif
}

View File

@ -0,0 +1,227 @@
// 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;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class WebApiCompatShimParameterBindingTest
{
private readonly IServiceProvider _provider = TestHelper.CreateServices(nameof(WebApiCompatShimWebSite));
private readonly Action<IApplicationBuilder> _app = new WebApiCompatShimWebSite.Startup().Configure;
[Theory]
[InlineData("http://localhost/api/Blog/Employees/PostByIdDefault/5")]
[InlineData("http://localhost/api/Blog/Employees/PostByIdDefault?id=5")]
public async Task ApiController_SimpleParameter_Default_ReadsFromUrl(string url)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, url);
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("5", content);
}
[Fact]
public async Task ApiController_SimpleParameter_Default_DoesNotReadFormData()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var url = "http://localhost/api/Blog/Employees/PostByIdDefault";
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>()
{
{ "id", "5" },
});
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("-1", content);
}
[Theory]
[InlineData("http://localhost/api/Blog/Employees/PostByIdModelBinder/5")]
[InlineData("http://localhost/api/Blog/Employees/PostByIdModelBinder?id=5")]
public async Task ApiController_SimpleParameter_ModelBinder_ReadsFromUrl(string url)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, url);
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("5", content);
}
[Fact]
public async Task ApiController_SimpleParameter_ModelBinder_ReadsFromFormData()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var url = "http://localhost/api/Blog/Employees/PostByIdModelBinder";
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>()
{
{ "id", "5" },
});
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("5", content);
}
[Theory]
[InlineData("http://localhost/api/Blog/Employees/PostByIdFromQuery/5", "-1")]
[InlineData("http://localhost/api/Blog/Employees/PostByIdFromQuery?id=5", "5")]
public async Task ApiController_SimpleParameter_FromQuery_ReadsFromQueryNotRouteData(string url, string expected)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, url);
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expected, content);
}
[Fact]
public async Task ApiController_SimpleParameter_FromQuery_DoesNotReadFormData()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var url = "http://localhost/api/Blog/Employees/PostByIdFromQuery";
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>()
{
{ "id", "5" },
});
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("-1", content);
}
[Fact]
public async Task ApiController_ComplexParameter_Default_ReadsFromBody()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var url = "http://localhost/api/Blog/Employees/PutEmployeeDefault";
var request = new HttpRequestMessage(HttpMethod.Put, url);
request.Content = new StringContent(JsonConvert.SerializeObject(new
{
Id = 5,
Name = "Test Employee",
}));
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("{\"Id\":5,\"Name\":\"Test Employee\"}", content);
}
[Fact]
public async Task ApiController_ComplexParameter_ModelBinder_ReadsFormAndUrl()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var url = "http://localhost/api/Blog/Employees/PutEmployeeModelBinder/5";
var request = new HttpRequestMessage(HttpMethod.Put, url);
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>()
{
{ "name", "Test Employee" },
});
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("{\"Id\":5,\"Name\":\"Test Employee\"}", content);
}
// name is read from the url - and the rest from the body (formatters)
[Fact]
public async Task ApiController_TwoParameters_DefaultSources()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var url = "http://localhost/api/Blog/Employees/PutEmployeeBothDefault?name=Name_Override";
var request = new HttpRequestMessage(HttpMethod.Put, url);
request.Content = new StringContent(JsonConvert.SerializeObject(new
{
Id = 5,
Name = "Test Employee",
}));
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
// Act
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("{\"Id\":5,\"Name\":\"Name_Override\"}", content);
}
}
}

View File

@ -927,7 +927,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private static ModelMetadata GetMetadataForParameter(MethodInfo methodInfo, string parameterName)
{
var metadataProvider = new DataAnnotationsModelMetadataProvider();
return metadataProvider.GetMetadataForParameter(null, methodInfo, parameterName);
return metadataProvider.GetMetadataForParameter(
modelAccessor: null,
methodInfo: methodInfo,
parameterName: parameterName,
binderMarker: null);
}
private class Person

View File

@ -63,7 +63,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
"Property3", "Property4", "IncludedAndExcludedExplicitly1", "ExcludedExplicitly1" };
// Act
var metadata = provider.GetMetadataForParameter(null, methodInfo, "param");
var metadata = provider.GetMetadataForParameter(
modelAccessor: null,
methodInfo: methodInfo,
parameterName: "param",
binderMarker: null);
// Assert
Assert.Equal(expectedIncludedPropertyNames.ToList(), metadata.IncludedProperties);
@ -79,7 +83,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var provider = new DataAnnotationsModelMetadataProvider();
// Act
var metadata = provider.GetMetadataForParameter(null, methodInfo, "param");
var metadata = provider.GetMetadataForParameter(
modelAccessor: null,
methodInfo: methodInfo,
parameterName: "param",
binderMarker: null);
// Assert
Assert.Equal("ParameterPrefix", metadata.ModelName);
@ -108,7 +116,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var provider = new DataAnnotationsModelMetadataProvider();
// Act
var metadata = provider.GetMetadataForParameter(null, methodInfo, "param");
var metadata = provider.GetMetadataForParameter(
modelAccessor: null,
methodInfo: methodInfo,
parameterName: "param",
binderMarker: null);
// Assert
Assert.Equal("ParameterPrefix", metadata.ModelName);

View File

@ -252,6 +252,87 @@ namespace System.Web.Http
}
}
[Fact]
public void GetActions_Parameters_SimpleTypeFromUriByDefault()
{
// Arrange
var provider = CreateProvider();
// Act
var context = new ActionDescriptorProviderContext();
provider.Invoke(context);
var results = context.Results.Cast<ControllerActionDescriptor>();
// Assert
var controllerType = typeof(TestControllers.EmployeesController).GetTypeInfo();
var actions = results
.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType)
.Where(ad => ad.Name == "Get")
.ToArray();
Assert.NotEmpty(actions);
foreach (var action in actions)
{
var parameter = Assert.Single(action.Parameters);
Assert.IsType<FromUriAttribute>(parameter.BinderMarker);
}
}
[Fact]
public void GetActions_Parameters_ComplexTypeFromBodyByDefault()
{
// Arrange
var provider = CreateProvider();
// Act
var context = new ActionDescriptorProviderContext();
provider.Invoke(context);
var results = context.Results.Cast<ControllerActionDescriptor>();
// Assert
var controllerType = typeof(TestControllers.EmployeesController).GetTypeInfo();
var actions = results
.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType)
.Where(ad => ad.Name == "Put")
.ToArray();
Assert.NotEmpty(actions);
foreach (var action in actions)
{
var parameter = Assert.Single(action.Parameters);
Assert.IsType<FromBodyAttribute>(parameter.BinderMarker);
}
}
[Fact]
public void GetActions_Parameters_BinderMarker()
{
// Arrange
var provider = CreateProvider();
// Act
var context = new ActionDescriptorProviderContext();
provider.Invoke(context);
var results = context.Results.Cast<ControllerActionDescriptor>();
// Assert
var controllerType = typeof(TestControllers.EmployeesController).GetTypeInfo();
var actions = results
.Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType)
.Where(ad => ad.Name == "Post")
.ToArray();
Assert.NotEmpty(actions);
foreach (var action in actions)
{
var parameter = Assert.Single(action.Parameters);
Assert.IsType<ModelBinderAttribute>(parameter.BinderMarker);
}
}
private INestedProviderManager<ActionDescriptorProviderContext> CreateProvider()
{
var assemblyProvider = new Mock<IAssemblyProvider>();
@ -346,5 +427,27 @@ namespace System.Web.Http.TestControllers
return null;
}
}
public class EmployeesController : ApiController
{
public IActionResult Get(int id)
{
return null;
}
public IActionResult Put(Employee employee)
{
return null;
}
public IActionResult Post([ModelBinder] Employee employee)
{
return null;
}
}
public class Employee
{
}
}
#endif

View File

@ -2,6 +2,7 @@
// 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.Http;
using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.Routing;
@ -20,6 +21,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
@ -49,11 +51,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
@ -68,7 +72,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
};
context.CurrentCandidate = context.Candidates[0];
context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17});
context.RouteContext = CreateRouteContext("?quantity=5", new { id = 17 });
// Act & Assert
Assert.True(constraint.Accept(context));
@ -83,11 +87,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
@ -117,11 +123,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
@ -151,11 +159,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity",
IsOptional = true,
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
@ -186,11 +196,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
@ -201,11 +213,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity_ordered",
ParameterBindingInfo = new ParameterBindingInfo("quantity_ordered", typeof(int)),
},
@ -239,6 +253,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
@ -249,11 +264,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
@ -284,11 +301,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity",
IsOptional = true,
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
@ -300,11 +319,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
@ -335,11 +356,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},
@ -350,11 +373,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "price",
ParameterBindingInfo = new ParameterBindingInfo("price", typeof(decimal)),
},
@ -388,6 +413,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
@ -398,11 +424,61 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
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));
}
[Fact]
public void Accept_AcceptsAction_WithFewerParameters_WhenOtherIsNotOverloaded_FromBodyAttribute()
{
// Arrange
var action1 = new ActionDescriptor();
action1.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
};
var action2 = new ActionDescriptor();
action2.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
BinderMarker = new FromUriAttribute(),
Name = "id",
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
BinderMarker = new FromBodyAttribute(),
Name = "quantity",
ParameterBindingInfo = new ParameterBindingInfo("quantity", typeof(int)),
},

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Builder
{
public static Configuration GetTestConfiguration(this IApplicationBuilder app)
{
var configurationProvider = app.ApplicationServices.GetRequiredService<ITestConfigurationProvider>();
var configurationProvider = app.ApplicationServices.GetService<ITestConfigurationProvider>();
var configuration = configurationProvider == null
? new Configuration()
: configurationProvider.Configuration;

View File

@ -8,13 +8,13 @@
<PropertyGroup Label="Globals">
<ProjectGuid>24b59501-5f37-4129-96e6-f02ec34c7e2c</ProjectGuid>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>55571</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,42 @@
// 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.Controllers.ParameterBinding
{
public class EmployeesController : ApiController
{
public IActionResult PostByIdDefault(int id = -1)
{
return Ok(id);
}
public IActionResult PostByIdModelBinder([ModelBinder] int id = -1)
{
return Ok(id);
}
public IActionResult PostByIdFromQuery([FromQuery] int id = -1)
{
return Ok(id);
}
public IActionResult PutEmployeeDefault(Employee employee)
{
return Ok(employee);
}
public IActionResult PutEmployeeModelBinder([ModelBinder] Employee employee)
{
return Ok(employee);
}
public IActionResult PutEmployeeBothDefault(string name, Employee employee)
{
employee.Name = name;
return Ok(employee);
}
}
}

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 class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -8,5 +8,6 @@
"frameworks": {
"aspnet50": { },
"aspnetcore50": { }
}
},
"webroot": "wwwroot"
}