Make BindingSource extensible

This is a major refactor of how IBinderMetadata interacts with model
binders and value providers. We're doing this to support better
extensibility for metadata in ApiExplorer.

You'll notice a bunch of deleted code in DefaultApiDescriptionProvider
that maps metadata marker interfaces to a fixed list of Api sources. This
is replaced now with IBindingSourceMetadata - which also replaces the
hierarchy of marker interfaces. Now user code can create an arbitrary
binding source and have a consistent API for model-binders,
value-providers and full-visibility in ApiExplorer as
well.

Additonally, there's some error checking in place that better enforces the
constraints we already have in the system. IE you can't create a 'greedy'
model binder that uses value-provider data.

Two additional enhancements are planned for followup PRs:
1. Add a BindingSource property to model-metadata. This will remove some
duplication, but I want to delay it because it would touch another 10 or
so files.

2. Add an extensibility interface for our 'special' model binders like the
file binder so these can show up in ApiExplorer as well.
This commit is contained in:
Ryan Nowak 2015-01-26 14:31:38 -08:00
parent 8c32d9e207
commit 12f8f23ccb
74 changed files with 1759 additions and 861 deletions

View File

@ -27,9 +27,9 @@ namespace Microsoft.AspNet.Mvc.Description
public ApiParameterRouteInfo RouteInfo { get; set; }
/// <summary>
/// Gets or sets the <see cref="ApiParameterSource"/>.
/// Gets or sets the <see cref="BindingSource"/>.
/// </summary>
public ApiParameterSource Source { get; set; }
public BindingSource Source { get; set; }
/// <summary>
/// Gets or sets the parameter type.

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNet.Mvc.Description
/// An optional parameter is considered optional by the routing system. This does not imply
/// that the parameter is considered optional by the action.
///
/// If the parameter uses <see cref="ApiParameterSource.ModelBinding"/> for the value of
/// If the parameter uses <see cref="ModelBinding.BindingSource.ModelBinding"/> for the value of
/// <see cref="ApiParameterDescription.Source"/> then the value may also come from the
/// URL query string or form data.
/// </remarks>

View File

@ -1,130 +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 System.Diagnostics;
using Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// A metadata description of the source of an <see cref="ApiParameterDescription"/> for an HTTP request.
/// </summary>
[DebuggerDisplay("Source: {DisplayName}")]
public class ApiParameterSource : IEquatable<ApiParameterSource>
{
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request body.
/// </summary>
public static readonly ApiParameterSource Body = new ApiParameterSource(
"Body",
Resources.ApiParameterSource_Body);
/// <summary>
/// An <see cref="ApiParameterSource"/> for a custom model binder (unknown data source).
/// </summary>
public static readonly ApiParameterSource Custom = new ApiParameterSource(
"Custom",
Resources.ApiParameterSource_Custom);
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request form-data.
/// </summary>
public static readonly ApiParameterSource Form = new ApiParameterSource(
"Form",
Resources.ApiParameterSource_Form);
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request headers.
/// </summary>
public static readonly ApiParameterSource Header = new ApiParameterSource(
"Header",
Resources.ApiParameterSource_Header);
/// <summary>
/// An <see cref="ApiParameterSource"/> for a parameter that should be hidden. Used when
/// a parameter cannot be set with user input.
/// </summary>
public static readonly ApiParameterSource Hidden = new ApiParameterSource(
"Hidden",
Resources.ApiParameterSource_Hidden);
/// <summary>
/// An <see cref="ApiParameterSource"/> for model binding. Includes form-data, query-string
/// and headers from the request.
/// </summary>
public static readonly ApiParameterSource ModelBinding = new ApiParameterSource(
"ModelBinding",
Resources.ApiParameterSource_ModelBinding);
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request url path.
/// </summary>
public static readonly ApiParameterSource Path = new ApiParameterSource(
"Path",
Resources.ApiParameterSource_Path);
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request query-string.
/// </summary>
public static readonly ApiParameterSource Query = new ApiParameterSource(
"Query",
Resources.ApiParameterSource_Query);
/// <summary>
/// Creates a new <see cref="ApiParameterSource"/>.
/// </summary>
/// <param name="id">The id. Used for comparison.</param>
/// <param name="displayName"> The display name.</param>
public ApiParameterSource([NotNull] string id, string displayName)
{
Id = id;
DisplayName = displayName;
}
/// <summary>
/// Gets the display name.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// Gets the id.
/// </summary>
public string Id { get; }
/// <inheritdoc />
public bool Equals(ApiParameterSource other)
{
return other == null ? false : string.Equals(other.Id, Id, StringComparison.Ordinal);
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return Equals(obj as ApiParameterSource);
}
/// <inheritdoc />
public override int GetHashCode()
{
return Id.GetHashCode();
}
/// <inheritdoc />
public static bool operator ==(ApiParameterSource s1, ApiParameterSource s2)
{
if (object.ReferenceEquals(s1, null))
{
return object.ReferenceEquals(s2, null); ;
}
return s1.Equals(s2);
}
/// <inheritdoc />
public static bool operator !=(ApiParameterSource s1, ApiParameterSource s2)
{
return !(s1 == s2);
}
}
}

View File

@ -140,7 +140,7 @@ namespace Microsoft.AspNet.Mvc.Description
{
// Remove any 'hidden' parameters. These are things that can't come from user input,
// so they aren't worth showing.
if (context.Results[i].Source == ApiParameterSource.Hidden)
if (!context.Results[i].Source.IsFromRequest)
{
context.Results.RemoveAt(i);
}
@ -155,9 +155,9 @@ namespace Microsoft.AspNet.Mvc.Description
foreach (var parameter in context.Results)
{
if (parameter.Source == ApiParameterSource.Path ||
parameter.Source == ApiParameterSource.ModelBinding ||
parameter.Source == ApiParameterSource.Custom)
if (parameter.Source == BindingSource.Path ||
parameter.Source == BindingSource.ModelBinding ||
parameter.Source == BindingSource.Custom)
{
ApiParameterRouteInfo routeInfo;
if (routeParameters.TryGetValue(parameter.Name, out routeInfo))
@ -165,12 +165,12 @@ namespace Microsoft.AspNet.Mvc.Description
parameter.RouteInfo = routeInfo;
routeParameters.Remove(parameter.Name);
if (parameter.Source == ApiParameterSource.ModelBinding &&
if (parameter.Source == BindingSource.ModelBinding &&
!parameter.RouteInfo.IsOptional)
{
// If we didn't see any information about the parameter, but we have
// a route parameter that matches, let's switch it to path.
parameter.Source = ApiParameterSource.Path;
parameter.Source = BindingSource.Path;
}
}
}
@ -184,7 +184,7 @@ namespace Microsoft.AspNet.Mvc.Description
{
Name = routeParameter.Key,
RouteInfo = routeParameter.Value,
Source = ApiParameterSource.Path,
Source = BindingSource.Path,
});
}
@ -449,7 +449,7 @@ namespace Microsoft.AspNet.Mvc.Description
// Attempt to find a binding source for the parameter
//
// The default is ModelBinding (aka all default value providers)
var source = ApiParameterSource.ModelBinding;
var source = BindingSource.ModelBinding;
if (!Visit(modelMetadata, source, containerName: string.Empty))
{
// If we get here, then it means we didn't find a match for any of the model. This means that it's
@ -466,7 +466,7 @@ namespace Microsoft.AspNet.Mvc.Description
/// model properties where we can definitely compute an answer.
/// </summary>
/// <param name="modelMetadata">The metadata for the model.</param>
/// <param name="ambientSource">The <see cref="ApiParameterSource"/> from the ambient context.</param>
/// <param name="ambientSource">The <see cref="BindingSource"/> from the ambient context.</param>
/// <param name="containerName">The current name prefix (to prepend to property names).</param>
/// <returns>
/// <c>true</c> if the set of <see cref="ApiParameterDescription"/> objects were created for the model.
@ -477,10 +477,10 @@ namespace Microsoft.AspNet.Mvc.Description
/// or NONE of it. If a parameter description is created for ANY sub-properties of the model, then a parameter
/// description will be created for ALL of them.
/// </remarks>
private bool Visit(ModelMetadata modelMetadata, ApiParameterSource ambientSource, string containerName)
private bool Visit(ModelMetadata modelMetadata, BindingSource ambientSource, string containerName)
{
ApiParameterSource source;
if (GetSource(modelMetadata, out source))
var source = BindingSource.GetBindingSource(modelMetadata.BinderMetadata);
if (source != null && source.IsGreedy)
{
// We have a definite answer for this model. This is a greedy source like
// [FromBody] so there's no need to consider properties.
@ -597,7 +597,7 @@ namespace Microsoft.AspNet.Mvc.Description
private ApiParameterDescription CreateResult(
ModelMetadata metadata,
ApiParameterSource source,
BindingSource source,
string containerName)
{
return new ApiParameterDescription()
@ -622,73 +622,15 @@ namespace Microsoft.AspNet.Mvc.Description
}
}
// This isn't extensible right now.
//
// Returns true if the source is greedy (means to stop exploring the model)
// Returns false if the source in unknown or known but not greedy (like [FromQuery])
private static bool GetSource(ModelMetadata metadata, out ApiParameterSource source)
{
if (metadata.BinderMetadata == null)
{
// There's nothing we can figure out.
source = null;
return false;
}
if (metadata.BinderMetadata is IFormatterBinderMetadata)
{
source = ApiParameterSource.Body;
return true;
}
else if (metadata.BinderMetadata is IHeaderBinderMetadata)
{
source = ApiParameterSource.Header;
return true;
}
else if (metadata.BinderMetadata is IServiceActivatorBinderMetadata)
{
source = ApiParameterSource.Hidden;
return true;
}
else if (metadata.BinderMetadata is IRouteDataValueProviderMetadata)
{
source = ApiParameterSource.Path;
return false;
}
else if (metadata.BinderMetadata is IQueryValueProviderMetadata)
{
source = ApiParameterSource.Query;
return false;
}
else if (metadata.BinderMetadata is IFormDataValueProviderMetadata)
{
source = ApiParameterSource.Form;
return false;
}
var binderTypeMetadata = metadata.BinderMetadata as IBinderTypeProviderMetadata;
if (binderTypeMetadata != null && binderTypeMetadata.BinderType != null)
{
// This provides it's own model binder, so we can't really make a good
// estimate of where it comes from.
source = ApiParameterSource.Custom;
return true;
}
// We're out of cases we know how to handle.
source = null;
return false;
}
private struct PropertyKey
{
public readonly Type ContainerType;
public readonly string PropertyName;
public readonly ApiParameterSource Source;
public readonly BindingSource Source;
public PropertyKey(ModelMetadata metadata, ApiParameterSource source)
public PropertyKey(ModelMetadata metadata, BindingSource source)
{
ContainerType = metadata.ContainerType;
PropertyName = metadata.PropertyName;

View File

@ -1,19 +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 System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Represents a model binder which understands <see cref="IFormatterBinderMetadata"/> and uses
/// InputFomatters to bind the model to request's body.
/// An <see cref="IModelBinder"/> which binds models from the request body using an <see cref="IInputFormatter"/>
/// when a model has the binding source <see cref="BindingSource.Body"/>/
/// </summary>
public class BodyModelBinder : MetadataAwareBinder<IFormatterBinderMetadata>
public class BodyModelBinder : BindingSourceModelBinder
{
private readonly ActionContext _actionContext;
private readonly IScopedInstance<ActionBindingContext> _bindingContext;
@ -21,11 +22,22 @@ namespace Microsoft.AspNet.Mvc
private readonly IBodyModelValidator _bodyModelValidator;
private readonly IValidationExcludeFiltersProvider _bodyValidationExcludeFiltersProvider;
/// <summary>
/// Creates a new <see cref="BodyModelBinder"/>.
/// </summary>
/// <param name="context">An accessor to the <see cref="ActionContext"/>.</param>
/// <param name="bindingContext">An accessor to the <see cref="ActionBindingContext"/>.</param>
/// <param name="selector">The <see cref="IInputFormatterSelector"/>.</param>
/// <param name="bodyModelValidator">The <see cref="IBodyModelValidator"/>.</param>
/// <param name="bodyValidationExcludeFiltersProvider">
/// The <see cref="IValidationExcludeFiltersProvider"/>.
/// </param>
public BodyModelBinder([NotNull] IScopedInstance<ActionContext> context,
[NotNull] IScopedInstance<ActionBindingContext> bindingContext,
[NotNull] IInputFormatterSelector selector,
[NotNull] IBodyModelValidator bodyModelValidator,
[NotNull] IValidationExcludeFiltersProvider bodyValidationExcludeFiltersProvider)
: base(BindingSource.Body)
{
_actionContext = context.Value;
_bindingContext = bindingContext;
@ -34,9 +46,8 @@ namespace Microsoft.AspNet.Mvc
_bodyValidationExcludeFiltersProvider = bodyValidationExcludeFiltersProvider;
}
protected override async Task<bool> BindAsync(
ModelBindingContext bindingContext,
IFormatterBinderMetadata metadata)
/// <inheritdoc />
protected async override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
var formatters = _bindingContext.Value.InputFormatters;
@ -48,9 +59,7 @@ namespace Microsoft.AspNet.Mvc
var unsupportedContentType = Resources.FormatUnsupportedContentType(
bindingContext.OperationBindingContext.HttpContext.Request.ContentType);
bindingContext.ModelState.AddModelError(bindingContext.ModelName, unsupportedContentType);
// Should always return true so that the model binding process ends here.
return true;
return;
}
bindingContext.Model = await formatter.ReadAsync(formatterContext);
@ -64,7 +73,6 @@ namespace Microsoft.AspNet.Mvc
containerMetadata: null,
excludeFromValidationFilters: _bodyValidationExcludeFiltersProvider.ExcludeFilters);
_bodyModelValidator.Validate(validationContext, bindingContext.ModelName);
return true;
}
}
}

View File

@ -1,23 +1,27 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// An <see cref="IModelBinder"/> which understands <see cref="IServiceActivatorBinderMetadata"/>
/// and activates a given model using <see cref="IServiceProvider"/>.
/// An <see cref="IModelBinder"/> which binds models from the request services when a model
/// has the binding source <see cref="BindingSource.Services"/>/
/// </summary>
public class ServicesModelBinder : MetadataAwareBinder<IServiceActivatorBinderMetadata>
public class ServicesModelBinder : BindingSourceModelBinder
{
/// <summary>
/// Creates a new <see cref="ServicesModelBinder"/>.
/// </summary>
public ServicesModelBinder()
: base(BindingSource.Services)
{
}
/// <inheritdoc />
protected override Task<bool> BindAsync(
[NotNull] ModelBindingContext bindingContext,
[NotNull] IServiceActivatorBinderMetadata metadata)
protected override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
bindingContext.Model = requestServices.GetRequiredService(bindingContext.ModelType);

View File

@ -1594,134 +1594,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ResponseCache_SpecifyDuration"), p0, p1);
}
/// <summary>
/// Body
/// </summary>
internal static string ApiParameterSource_Body
{
get { return GetString("ApiParameterSource_Body"); }
}
/// <summary>
/// Body
/// </summary>
internal static string FormatApiParameterSource_Body()
{
return GetString("ApiParameterSource_Body");
}
/// <summary>
/// Custom
/// </summary>
internal static string ApiParameterSource_Custom
{
get { return GetString("ApiParameterSource_Custom"); }
}
/// <summary>
/// Custom
/// </summary>
internal static string FormatApiParameterSource_Custom()
{
return GetString("ApiParameterSource_Custom");
}
/// <summary>
/// Header
/// </summary>
internal static string ApiParameterSource_Header
{
get { return GetString("ApiParameterSource_Header"); }
}
/// <summary>
/// Header
/// </summary>
internal static string FormatApiParameterSource_Header()
{
return GetString("ApiParameterSource_Header");
}
/// <summary>
/// Hidden
/// </summary>
internal static string ApiParameterSource_Hidden
{
get { return GetString("ApiParameterSource_Hidden"); }
}
/// <summary>
/// Hidden
/// </summary>
internal static string FormatApiParameterSource_Hidden()
{
return GetString("ApiParameterSource_Hidden");
}
/// <summary>
/// ModelBinding
/// </summary>
internal static string ApiParameterSource_ModelBinding
{
get { return GetString("ApiParameterSource_ModelBinding"); }
}
/// <summary>
/// ModelBinding
/// </summary>
internal static string FormatApiParameterSource_ModelBinding()
{
return GetString("ApiParameterSource_ModelBinding");
}
/// <summary>
/// Path
/// </summary>
internal static string ApiParameterSource_Path
{
get { return GetString("ApiParameterSource_Path"); }
}
/// <summary>
/// Path
/// </summary>
internal static string FormatApiParameterSource_Path()
{
return GetString("ApiParameterSource_Path");
}
/// <summary>
/// Query
/// </summary>
internal static string ApiParameterSource_Query
{
get { return GetString("ApiParameterSource_Query"); }
}
/// <summary>
/// Query
/// </summary>
internal static string FormatApiParameterSource_Query()
{
return GetString("ApiParameterSource_Query");
}
/// <summary>
/// Form
/// </summary>
internal static string ApiParameterSource_Form
{
get { return GetString("ApiParameterSource_Form"); }
}
/// <summary>
/// Form
/// </summary>
internal static string FormatApiParameterSource_Form()
{
return GetString("ApiParameterSource_Form");
}
/// <summary>
/// The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer.
/// </summary>

View File

@ -424,30 +424,6 @@
<data name="ResponseCache_SpecifyDuration" xml:space="preserve">
<value>If the '{0}' property is not set to true, '{1}' property must be specified.</value>
</data>
<data name="ApiParameterSource_Body" xml:space="preserve">
<value>Body</value>
</data>
<data name="ApiParameterSource_Custom" xml:space="preserve">
<value>Custom</value>
</data>
<data name="ApiParameterSource_Header" xml:space="preserve">
<value>Header</value>
</data>
<data name="ApiParameterSource_Hidden" xml:space="preserve">
<value>Hidden</value>
</data>
<data name="ApiParameterSource_ModelBinding" xml:space="preserve">
<value>ModelBinding</value>
</data>
<data name="ApiParameterSource_Path" xml:space="preserve">
<value>Path</value>
</data>
<data name="ApiParameterSource_Query" xml:space="preserve">
<value>Query</value>
</data>
<data name="ApiParameterSource_Form" xml:space="preserve">
<value>Form</value>
</data>
<data name="ApiExplorer_UnsupportedAction" xml:space="preserve">
<value>The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer.</value>
</data>

View File

@ -0,0 +1,213 @@
// 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.Diagnostics;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A metadata object representing a source of data for model binding.
/// </summary>
[DebuggerDisplay("Source: {DisplayName}")]
public class BindingSource : IEquatable<BindingSource>
{
/// <summary>
/// A <see cref="BindingSource"/> for the request body.
/// </summary>
public static readonly BindingSource Body = new BindingSource(
"Body",
Resources.BindingSource_Body,
isGreedy: true,
isFromRequest: true);
/// <summary>
/// A <see cref="BindingSource"/> for a custom model binder (unknown data source).
/// </summary>
public static readonly BindingSource Custom = new BindingSource(
"Custom",
Resources.BindingSource_Custom,
isGreedy: true,
isFromRequest: true);
/// <summary>
/// A <see cref="BindingSource"/> for the request form-data.
/// </summary>
public static readonly BindingSource Form = new BindingSource(
"Form",
Resources.BindingSource_Form,
isGreedy: false,
isFromRequest: true);
/// <summary>
/// A <see cref="BindingSource"/> for the request headers.
/// </summary>
public static readonly BindingSource Header = new BindingSource(
"Header",
Resources.BindingSource_Header,
isGreedy: true,
isFromRequest: true);
/// <summary>
/// A <see cref="BindingSource"/> for model binding. Includes form-data, query-string
/// and route data from the request.
/// </summary>
public static readonly BindingSource ModelBinding = new BindingSource(
"ModelBinding",
Resources.BindingSource_ModelBinding,
isGreedy: false,
isFromRequest: true);
/// <summary>
/// A <see cref="BindingSource"/> for the request url path.
/// </summary>
public static readonly BindingSource Path = new BindingSource(
"Path",
Resources.BindingSource_Path,
isGreedy: false,
isFromRequest: true);
/// <summary>
/// A <see cref="BindingSource"/> for the request query-string.
/// </summary>
public static readonly BindingSource Query = new BindingSource(
"Query",
Resources.BindingSource_Query,
isGreedy: false,
isFromRequest: true);
/// <summary>
/// A <see cref="BindingSource"/> for request services.
/// </summary>
public static readonly BindingSource Services = new BindingSource(
"Services",
Resources.BindingSource_Services,
isGreedy: true,
isFromRequest: false);
/// <summary>
/// Creates a new <see cref="BindingSource"/>.
/// </summary>
/// <param name="id">The id, a unique identifier.</param>
/// <param name="displayName">The display name.</param>
/// <param name="isGreedy">A value indicating whether or not the source is greedy.</param>
/// <param name="isFromRequest">
/// A value indicating whether or not the data comes from the HTTP request.
/// </param>
public BindingSource([NotNull] string id, string displayName, bool isGreedy, bool isFromRequest)
{
Id = id;
DisplayName = displayName;
IsGreedy = isGreedy;
IsFromRequest = isFromRequest;
}
/// <summary>
/// Gets the display name for the source.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// Gets the unique identifier for the source. Sources are compared based on their Id.
/// </summary>
public string Id { get; }
/// <summary>
/// Gets a value indicating whether or not a source is greedy. A greedy source will bind a model in
/// a single operation, and will not decompose the model into sub-properties.
/// </summary>
/// <remarks>
/// <para>
/// For sources based on a <see cref="IValueProvider"/>, setting <see cref="IsGreedy"/> to <c>false</c>
/// will most closely describe the behavior. This value is used inside the default model binders to
/// determine whether or not to attempt to bind properties of a model.
/// </para>
/// <para>
/// Set <see cref="IsGreedy"/> to <c>true</c> for most custom <see cref="IModelBinder"/> implementations.
/// </para>
/// <para>
/// If a source represents an <see cref="IModelBinder"/> which will recursively traverse a model's properties
/// and bind them individually using <see cref="IValueProvider"/>, then set <see cref="IsGreedy"/> to
/// <c>true</c>.
/// </para>
/// </remarks>
public bool IsGreedy { get; }
/// <summary>
/// Gets a value indicating whether or not the binding source uses input from the current HTTP request.
/// </summary>
/// <remarks>
/// Some sources (like <see cref="BindingSource.Services"/>) are based on application state and not user
/// input. These are excluded by default from ApiExplorer diagnostics.
/// </remarks>
public bool IsFromRequest { get; }
/// <summary>
/// Gets a value indicating whether or not the <see cref="BindingSource"/> can accept
/// data from <paramref name="bindingSource"/>.
/// </summary>
/// <param name="bindingSource">The <see cref="BindingSource"/> to consider as input.</param>
/// <returns><c>True</c> if the source is compatible, otherwise <c>false</c>.</returns>
/// <remarks>
/// When using this method, it is expected that the left-hand-side is metadata specified
/// on a property or parameter for model binding, and the right hand side is a source of
/// data used by a model binder or value provider.
///
/// This distinction is important as the left-hand-side may be a composite, but the right
/// may not.
/// </remarks>
public virtual bool CanAcceptDataFrom([NotNull] BindingSource bindingSource)
{
if (bindingSource is CompositeBindingSource)
{
var message = Resources.FormatBindingSource_CannotBeComposite(
bindingSource.DisplayName,
nameof(CanAcceptDataFrom));
throw new ArgumentException(message, nameof(bindingSource));
}
return this == bindingSource;
}
/// <inheritdoc />
public bool Equals(BindingSource other)
{
return other == null ? false : string.Equals(other.Id, Id, StringComparison.Ordinal);
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return Equals(obj as BindingSource);
}
/// <inheritdoc />
public override int GetHashCode()
{
return Id.GetHashCode();
}
/// <inheritdoc />
public static bool operator ==(BindingSource s1, BindingSource s2)
{
if (object.ReferenceEquals(s1, null))
{
return object.ReferenceEquals(s2, null); ;
}
return s1.Equals(s2);
}
/// <inheritdoc />
public static bool operator !=(BindingSource s1, BindingSource s2)
{
return !(s1 == s2);
}
// THIS IS TEMP CODE, this will be moved to model metadata
public static BindingSource GetBindingSource(IBinderMetadata metadata)
{
return (metadata as IBindingSourceMetadata)?.BindingSource;
}
}
}

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;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A <see cref="BindingSources"/> which can repesent multiple value-provider data sources.
/// </summary>
public class CompositeBindingSource : BindingSource
{
/// <summary>
/// Creates a new <see cref="CompositeBindingSource"/>.
/// </summary>
/// <param name="bindingSources">
/// The set of <see cref="BindingSource"/> entries.
/// Must be value-provider sources and user input.
/// </param>
/// <param name="displayName">The display name for the composite source.</param>
/// <returns>A <see cref="CompositeBindingSource"/>.</returns>
public static CompositeBindingSource Create(
[NotNull] IEnumerable<BindingSource> bindingSources,
string displayName)
{
foreach (var bindingSource in bindingSources)
{
if (bindingSource.IsGreedy)
{
var message = Resources.FormatBindingSource_CannotBeGreedy(
bindingSource.DisplayName,
nameof(CompositeBindingSource));
throw new ArgumentException(message, "bindingSources");
}
if (!bindingSource.IsFromRequest)
{
var message = Resources.FormatBindingSource_MustBeFromRequest(
bindingSource.DisplayName,
nameof(CompositeBindingSource));
throw new ArgumentException(message, "bindingSources");
}
if (bindingSource is CompositeBindingSource)
{
var message = Resources.FormatBindingSource_CannotBeComposite(
bindingSource.DisplayName,
nameof(CompositeBindingSource));
throw new ArgumentException(message, "bindingSources");
}
}
var id = string.Join("&", bindingSources.Select(s => s.Id).OrderBy(s => s, StringComparer.Ordinal));
return new CompositeBindingSource(id, displayName, bindingSources);
}
private CompositeBindingSource(
[NotNull] string id,
string displayName,
[NotNull] IEnumerable<BindingSource> bindingSources)
: base(id, displayName, isGreedy: false, isFromRequest: true)
{
BindingSources = bindingSources;
}
/// <summary>
/// Gets the set of <see cref="BindingSource"/> entries.
/// </summary>
public IEnumerable<BindingSource> BindingSources { get; }
/// <inheritdoc />
public override bool CanAcceptDataFrom([NotNull] BindingSource bindingSource)
{
if (bindingSource is CompositeBindingSource)
{
var message = Resources.FormatBindingSource_CannotBeComposite(
bindingSource.DisplayName,
nameof(CanAcceptDataFrom));
throw new ArgumentException(message, nameof(bindingSource));
}
foreach (var source in BindingSources)
{
if (source.CanAcceptDataFrom(bindingSource))
{
return true;
}
}
return false;
}
}
}

View File

@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// This attribute is used on action parameters to indicate
/// they are bound from the body of the incoming request.
/// Specifies that a parameter or property should be bound using the request body.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromBodyAttribute : Attribute, IFormatterBinderMetadata
public class FromBodyAttribute : Attribute, IBindingSourceMetadata
{
/// <inheritdoc />
public BindingSource BindingSource { get { return BindingSource.Body; } }
}
}

View File

@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// This attribute is used on action parameters to indicate that
/// they will be bound using form data of the incoming request.
/// Specifies that a parameter or property should be bound using form-data in the request body.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromFormAttribute : Attribute, IFormDataValueProviderMetadata
public class FromFormAttribute : Attribute, IBindingSourceMetadata
{
/// <inheritdoc />
public BindingSource BindingSource { get { return BindingSource.Form; } }
}
}

View File

@ -7,12 +7,14 @@ using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// <see cref="FromHeaderAttribute"/> can be placed on an action parameter or model property to indicate
/// that model binding should use a header value as the data source.
/// Specifies that a parameter or property should be bound using the request headers.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromHeaderAttribute : Attribute, IHeaderBinderMetadata, IModelNameProvider
public class FromHeaderAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
/// <inheritdoc />
public BindingSource BindingSource { get { return BindingSource.Header; } }
/// <inheritdoc />
public string Name { get; set; }
}

View File

@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// This attribute is used on action parameters to indicate that
/// they will be bound using query data of the incoming request.
/// Specifies that a parameter or property should be bound using the request query string.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromQueryAttribute : Attribute, IQueryValueProviderMetadata
public class FromQueryAttribute : Attribute, IBindingSourceMetadata
{
/// <inheritdoc />
public BindingSource BindingSource { get { return BindingSource.Query; } }
}
}

View File

@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// This attribute is used on action parameters to indicate that
/// they will be bound using route data of the incoming request.
/// Specifies that a parameter or property should be bound using route-data from the current request.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromRouteAttribute : Attribute, IRouteDataValueProviderMetadata
public class FromRouteAttribute : Attribute, IBindingSourceMetadata
{
/// <inheritdoc />
public BindingSource BindingSource { get { return BindingSource.Path; } }
}
}

View File

@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// This attribute is used on action parameters or model properties to indicate that
/// they will be bound using service provider.
/// Specifies that a parameter or property should be bound using the request services.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromServicesAttribute : Attribute, IServiceActivatorBinderMetadata
public class FromServicesAttribute : Attribute, IBindingSourceMetadata
{
/// <inheritdoc />
public BindingSource BindingSource { get { return BindingSource.Services; } }
}
}

View File

@ -9,12 +9,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// Provides a <see cref="Type"/> which implements <see cref="IModelBinder"/> or
/// <see cref="IModelBinderProvider"/>.
/// </summary>
public interface IBinderTypeProviderMetadata : IBinderMetadata
public interface IBinderTypeProviderMetadata : IBindingSourceMetadata
{
/// <summary>
/// A <see cref="Type"/> which implements either <see cref="IModelBinder"/> or
/// <see cref="IModelBinderProvider"/>.
/// </summary>
Type BinderType { get; set; }
Type BinderType { get; }
}
}

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;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Metadata which specificies the data source for model binding.
/// </summary>
public interface IBindingSourceMetadata : IBinderMetadata
{
/// <summary>
/// Gets the <see cref="BindingSource"/>.
/// </summary>
/// <remarks>
/// The <see cref="BindingSource"/> is metadata which can be used to determine which data
/// sources are valid for model binding of a property or parameter.
/// </remarks>
BindingSource BindingSource { get; }
}
}

View File

@ -1,12 +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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Metadata interface that indicates model binding should use only form data value providers.
/// </summary>
public interface IFormDataValueProviderMetadata : IValueProviderMetadata
{
}
}

View File

@ -1,12 +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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Metadata interface that indicates model binding should be performed by an input formatter.
/// </summary>
public interface IFormatterBinderMetadata : IBinderMetadata
{
}
}

View File

@ -1,13 +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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Metadata interface that indicates model binding should use a header value for
/// the data source of a property or parameter.
/// </summary>
public interface IHeaderBinderMetadata : IBinderMetadata
{
}
}

View File

@ -1,12 +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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Metadata interface that indicates model binding should use only query string value providers.
/// </summary>
public interface IQueryValueProviderMetadata : IValueProviderMetadata
{
}
}

View File

@ -1,12 +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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Metadata interface that indicates model binding should use only route data value providers.
/// </summary>
public interface IRouteDataValueProviderMetadata : IValueProviderMetadata
{
}
}

View File

@ -1,12 +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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Metadata interface that indicates model binding should use the service container to get the value for a model.
/// </summary>
public interface IServiceActivatorBinderMetadata : IBinderMetadata
{
}
}

View File

@ -1,12 +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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Interface for metadata related to value providers.
/// </summary>
public interface IValueProviderMetadata : IBinderMetadata
{
}
}

View File

@ -28,6 +28,7 @@ namespace Microsoft.AspNet.Mvc
public class ModelBinderAttribute : Attribute, IModelNameProvider, IBinderTypeProviderMetadata
{
private Type _binderType;
private BindingSource _bindingSource;
/// <inheritdoc />
public Type BinderType
@ -55,6 +56,24 @@ namespace Microsoft.AspNet.Mvc
}
}
/// <inheritdoc />
public BindingSource BindingSource
{
get
{
if (_bindingSource == null && _binderType != null)
{
return BindingSource.Custom;
}
return _bindingSource;
}
set
{
_bindingSource = value;
}
}
/// <inheritdoc />
public string Name { get; set; }
}

View File

@ -0,0 +1,83 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// An <see cref="IModelBinder"/> which provides data from a specific <see cref="ModelBinding.BindingSource"/>.
/// </summary>
/// <remarks>
/// <para>
/// A <see cref="BindingSourceModelBinder"/> is an <see cref="IModelBinder"/> base-implementation which
/// can provide data for all parameters and model properties which specify the corresponding
/// <see cref="ModelBinding.BindingSource"/>.
/// </para>
/// <para>
/// <see cref="BindingSourceModelBinder"/> is greedy, meaning that a given instance expects to handle all
/// parameters and properties annotated with the corresponding <see cref="ModelBinding.BindingSource"/> and
/// will short-circuit the model binding process to prevent other binders from running.
/// <see cref="ModelBinding.BindingSource.IsGreedy"/> of <see cref="BindingSource"/> must be set to <c>true.</c>
/// </para>
/// </remarks>
public abstract class BindingSourceModelBinder : IModelBinder
{
/// <summary>
/// Creates a new <see cref="BindingSourceModelBinder"/>.
/// </summary>
/// <param name="bindingSource">
/// The <see cref="ModelBinding.BindingSource"/>. Must be a single-source (non-composite) with
/// <see cref="ModelBinding.BindingSource.IsGreedy"/> equal to <c>true</c>.
/// </param>
protected BindingSourceModelBinder([NotNull] BindingSource bindingSource)
{
// This class implements a pattern that's only useful for greedy model binders. If you need
// to implement something non-greedy then don't use the base class.
if (!bindingSource.IsGreedy)
{
var message = Resources.FormatBindingSource_MustBeGreedy(
bindingSource.DisplayName,
nameof(BindingSourceModelBinder));
throw new ArgumentException(message, nameof(bindingSource));
}
BindingSource = bindingSource;
}
/// <summary>
/// Gets the corresponding <see cref="ModelBinding.BindingSource"/>.
/// </summary>
protected BindingSource BindingSource { get; }
/// <summary>
/// Binds the model. Called when the model's supported binding-source matches <see cref="BindingSource"/>.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <returns>
/// A <see cref="Task"/> which will complete when model binding has completed.
/// </returns>
protected abstract Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext);
/// <inheritdoc />
public async Task<bool> BindModelAsync(ModelBindingContext context)
{
var bindingSourceMetadata = context.ModelMetadata.BinderMetadata as IBindingSourceMetadata;
var allowedBindingSource = bindingSourceMetadata?.BindingSource;
if (allowedBindingSource == null || !allowedBindingSource.CanAcceptDataFrom(BindingSource))
{
// Binding Sources are opt-in. This model either didn't specify one or specified something
// incompatible so let other binders run.
return false;
}
await BindModelCoreAsync(context);
// Prevent other model binders from running because this model binder is the only handler for
// its binding source.
return true;
}
}
}

View File

@ -137,21 +137,32 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
newBindingContext.OperationBindingContext.BodyBindingState = GetBodyBindingState(oldBindingContext);
// look at the value providers and see if they need to be restricted.
var metadata = oldBindingContext.ModelMetadata.BinderMetadata as IValueProviderMetadata;
if (metadata != null)
// If the property has a specified data binding sources, we need to filter the set of value providers
// to just those that match. We can skip filtering when IsGreedy == true, because that can't use
// value providers.
//
// We also want to base this filtering on the - top-level value profider in case the data source
// on this property doesn't intersect with the ambient data source.
//
// Ex:
//
// public class Person
// {
// [FromQuery]
// public int Id { get; set; }
// }
//
// public IActionResult UpdatePerson([FromForm] Person person) { }
//
// In this example, [FromQuery] overrides the ambient data source (form).
var bindingSource = BindingSource.GetBindingSource(oldBindingContext.ModelMetadata.BinderMetadata);
if (bindingSource != null && !bindingSource.IsGreedy)
{
// ValueProvider property might contain a filtered list of value providers.
// While deciding to bind a particular property which is annotated with a IValueProviderMetadata,
// instead of refiltering an already filtered list, we need to filter value providers from a global
// list of all value providers. This is so that every artifact that is explicitly marked using an
// IValueProviderMetadata can restrict model binding to only use value providers which support this
// IValueProviderMetadata.
var valueProvider =
oldBindingContext.OperationBindingContext.ValueProvider as IMetadataAwareValueProvider;
oldBindingContext.OperationBindingContext.ValueProvider as IBindingSourceValueProvider;
if (valueProvider != null)
{
newBindingContext.ValueProvider = valueProvider.Filter(metadata);
newBindingContext.ValueProvider = valueProvider.Filter(bindingSource);
}
}
@ -160,27 +171,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private static BodyBindingState GetBodyBindingState(ModelBindingContext oldBindingContext)
{
var binderMetadata = oldBindingContext.ModelMetadata.BinderMetadata;
var newIsFormatterBasedMetadataFound = binderMetadata is IFormatterBinderMetadata;
var newIsFormBasedMetadataFound = binderMetadata is IFormDataValueProviderMetadata;
var currentModelNeedsToReadBody = newIsFormatterBasedMetadataFound || newIsFormBasedMetadataFound;
var bindingSource = BindingSource.GetBindingSource(oldBindingContext.ModelMetadata.BinderMetadata);
var willReadBodyWithFormatter = bindingSource == BindingSource.Body;
var willReadBodyAsFormData = bindingSource == BindingSource.Form;
var currentModelNeedsToReadBody = willReadBodyWithFormatter || willReadBodyAsFormData;
var oldState = oldBindingContext.OperationBindingContext.BodyBindingState;
// We need to throw if there are multiple models which can cause body to be read multiple times.
// Reading form data multiple times is ok since we cache form data. For the models marked to read using
// formatters, multiple reads are not allowed.
if (oldState == BodyBindingState.FormatterBased && currentModelNeedsToReadBody ||
oldState == BodyBindingState.FormBased && newIsFormatterBasedMetadataFound)
oldState == BodyBindingState.FormBased && willReadBodyWithFormatter)
{
throw new InvalidOperationException(Resources.MultipleBodyParametersOrPropertiesAreNotAllowed);
}
var state = oldBindingContext.OperationBindingContext.BodyBindingState;
if (newIsFormatterBasedMetadataFound)
if (willReadBodyWithFormatter)
{
state = BodyBindingState.FormatterBased;
}
else if (newIsFormBasedMetadataFound && oldState != BodyBindingState.FormatterBased)
else if (willReadBodyAsFormData && oldState != BodyBindingState.FormatterBased)
{
// Only update the model binding state if we have not discovered formatter based state already.
state = BodyBindingState.FormBased;

View File

@ -9,15 +9,21 @@ using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A <see cref="MetadataAwareBinder{IHeaderBinderMetadata}"/> which uses <see cref="Http.HttpRequest.Headers"/>
/// to bind the model.
/// An <see cref="IModelBinder"/> which binds models from the request headers when a model
/// has the binding source <see cref="BindingSource.Header"/>/
/// </summary>
public class HeaderModelBinder : MetadataAwareBinder<IHeaderBinderMetadata>
public class HeaderModelBinder : BindingSourceModelBinder
{
/// <summary>
/// Creates a new <see cref="HeaderModelBinder"/>.
/// </summary>
public HeaderModelBinder()
: base(BindingSource.Header)
{
}
/// <inheritdoc />
protected override Task<bool> BindAsync(
[NotNull] ModelBindingContext bindingContext,
[NotNull] IHeaderBinderMetadata metadata)
protected override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
var request = bindingContext.OperationBindingContext.HttpContext.Request;
var modelMetadata = bindingContext.ModelMetadata;
@ -38,8 +44,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var values = request.Headers.GetCommaSeparatedValues(headerName);
if (values != null)
{
bindingContext.Model =
ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, values);
bindingContext.Model = ModelBindingHelper.ConvertValuesToCollectionType(
bindingContext.ModelType,
values);
}
}

View File

@ -1,12 +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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// An <see cref="IModelBinder"/> which is aware of <see cref="IBinderMetadata"/>.
/// </summary>
public interface IMetadataAwareBinder : IModelBinder
{
}
}

View File

@ -1,36 +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.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Represents an <see cref="IMetadataAwareBinder"/> which can select itself based on the
/// <typeparamref name="TBinderMetadata"/>.
/// </summary>
/// <typeparam name="TBinderMetadata">Represents a type implementing <see cref="IBinderMetadata"/></typeparam>
public abstract class MetadataAwareBinder<TBinderMetadata> : IMetadataAwareBinder
where TBinderMetadata : IBinderMetadata
{
/// <summary>
/// Async function which does the actual binding to bind to a particular model.
/// </summary>
/// <param name="bindingContext">The binding context which has the object to be bound.</param>
/// <param name="metadata">The <see cref="IBinderMetadata"/> associated with the current binder.</param>
/// <returns>A Task with a bool implying the success or failure of the operation.</returns>
protected abstract Task<bool> BindAsync([NotNull] ModelBindingContext bindingContext,
[NotNull] TBinderMetadata metadata);
public Task<bool> BindModelAsync(ModelBindingContext context)
{
if (context.ModelMetadata.BinderMetadata is TBinderMetadata)
{
var metadata = (TBinderMetadata)context.ModelMetadata.BinderMetadata;
return BindAsync(context, metadata);
}
return Task.FromResult(false);
}
}
}

View File

@ -54,16 +54,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
var hasExplicitAlias = bindingContext.ModelMetadata.BinderModelName != null;
// The fact that this has reached here,
// it is a complex object which was not directly bound by any previous model binders.
// Check if this was supposed to be handled by a non value provider based binder.
// if it was then it should be not be bound using mutable object binder.
// This check would prevent it from recursing in if a model contains a property of its own type.
// If we get here the model is a complex object which was not directly bound by any previous model binder,
// so we want to decide if we want to continue binding. This is important to get right to avoid infinite
// recursion.
//
// First, we want to make sure this object is allowed to come from a value provider source as this binder
// will always include value provider data. For instance if the model is marked with [FromBody], then we
// can just skip it. A greedy source cannot be a value provider.
//
// If the model isn't marked with ANY binding source, then we assume it's ok also.
//
// We skip this check if it is a top level object because we want to always evaluate
// the creation of top level object (this is also required for ModelBinderAttribute to work.)
var bindingSource = BindingSource.GetBindingSource(bindingContext.ModelMetadata.BinderMetadata);
if (!isTopLevelObject &&
bindingContext.ModelMetadata.BinderMetadata != null &&
!(bindingContext.ModelMetadata.BinderMetadata is IValueProviderMetadata))
bindingSource != null &&
bindingSource.IsGreedy)
{
return false;
}
@ -103,18 +109,37 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private async Task<bool> CanValueBindAnyModelProperties(MutableObjectBinderContext context)
{
// We need to enumerate the non marked properties and properties marked with IValueProviderMetadata
// instead of checking bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName)
// because there can be a case where a value provider might be willing to provide a marked property,
// which might never be bound.
// For example if person.Name is marked with FromQuery, and FormValueProvider has a key person.Name,
// and the QueryValueProvider does not, we do not want to create Person.
// We want to check to see if any of the properties of the model can be bound using the value providers,
// because that's all that MutableObjectModelBinder can handle.
//
// However, because a property might specify a custom binding source ([FromForm]), it's not correct
// for us to just try bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName),
// because that may include ALL value providers - that would lead us to mistakenly create the model
// when the data is coming from a source we should use (ex: value found in query string, but the
// model has [FromForm]).
//
// To do this we need to enumerate the properties, and see which of them provide a binding source
// through metadata, then we decide what to do.
//
// If a property has a binding source, and it's a greedy source, then it's not
// allowed to come from a value provider, so we skip it.
//
// If a property has a binding source, and it's a non-greedy source, then we'll filter the
// the value providers to just that source, and see if we can find a matching prefix
// (see CanBindValue).
//
// If a property does not have a binding source, then it's fair game for any value provider.
//
// If any property meets the above conditions and has a value from valueproviders, then we'll
// create the model and try to bind it. OR if ALL properties of the model have a greedy source,
// then we go ahead and create it.
//
var isAnyPropertyEnabledForValueProviderBasedBinding = false;
foreach (var propertyMetadata in context.PropertyMetadata)
{
// This check will skip properties which are marked explicitly using a non value binder.
if (propertyMetadata.BinderMetadata == null ||
propertyMetadata.BinderMetadata is IValueProviderMetadata)
var bindingSource = BindingSource.GetBindingSource(propertyMetadata.BinderMetadata);
if (bindingSource == null || !bindingSource.IsGreedy)
{
isAnyPropertyEnabledForValueProviderBasedBinding = true;
@ -141,15 +166,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private async Task<bool> CanBindValue(ModelBindingContext bindingContext, ModelMetadata metadata)
{
var valueProvider = bindingContext.ValueProvider;
var valueProviderMetadata = metadata.BinderMetadata as IValueProviderMetadata;
if (valueProviderMetadata != null)
var bindingSource = BindingSource.GetBindingSource(metadata.BinderMetadata);
if (bindingSource != null && !bindingSource.IsGreedy)
{
// if there is a binder metadata and since the property can be bound using a value provider.
var metadataAwareValueProvider =
bindingContext.OperationBindingContext.ValueProvider as IMetadataAwareValueProvider;
if (metadataAwareValueProvider != null)
var rootValueProvider = bindingContext.OperationBindingContext.ValueProvider as IBindingSourceValueProvider;
if (rootValueProvider != null)
{
valueProvider = metadataAwareValueProvider.Filter(valueProviderMetadata);
valueProvider = rootValueProvider.Filter(bindingSource);
}
}

View File

@ -15,13 +15,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
NotBodyBased,
/// <summary>
/// Represents if there is a <see cref="IFormatterBinderMetadata"/> that
/// Represents if there is a <see cref="BindingSource.Body"/> that
/// has been found during the current model binding process.
/// </summary>
FormatterBased,
/// <summary>
/// Represents if there is a <see cref = "IFormDataValueProviderMetadata" /> that
/// Represents if there is a <see cref = "BindingSource.Form" /> that
/// has been found during the current model binding process.
/// </summary>
FormBased

View File

@ -474,6 +474,198 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ValueInvalidGeneric"), p0);
}
/// <summary>
/// Body
/// </summary>
internal static string BindingSource_Body
{
get { return GetString("BindingSource_Body"); }
}
/// <summary>
/// Body
/// </summary>
internal static string FormatBindingSource_Body()
{
return GetString("BindingSource_Body");
}
/// <summary>
/// Custom
/// </summary>
internal static string BindingSource_Custom
{
get { return GetString("BindingSource_Custom"); }
}
/// <summary>
/// Custom
/// </summary>
internal static string FormatBindingSource_Custom()
{
return GetString("BindingSource_Custom");
}
/// <summary>
/// Form
/// </summary>
internal static string BindingSource_Form
{
get { return GetString("BindingSource_Form"); }
}
/// <summary>
/// Form
/// </summary>
internal static string FormatBindingSource_Form()
{
return GetString("BindingSource_Form");
}
/// <summary>
/// Header
/// </summary>
internal static string BindingSource_Header
{
get { return GetString("BindingSource_Header"); }
}
/// <summary>
/// Header
/// </summary>
internal static string FormatBindingSource_Header()
{
return GetString("BindingSource_Header");
}
/// <summary>
/// Services
/// </summary>
internal static string BindingSource_Services
{
get { return GetString("BindingSource_Services"); }
}
/// <summary>
/// Services
/// </summary>
internal static string FormatBindingSource_Services()
{
return GetString("BindingSource_Services");
}
/// <summary>
/// ModelBinding
/// </summary>
internal static string BindingSource_ModelBinding
{
get { return GetString("BindingSource_ModelBinding"); }
}
/// <summary>
/// ModelBinding
/// </summary>
internal static string FormatBindingSource_ModelBinding()
{
return GetString("BindingSource_ModelBinding");
}
/// <summary>
/// Path
/// </summary>
internal static string BindingSource_Path
{
get { return GetString("BindingSource_Path"); }
}
/// <summary>
/// Path
/// </summary>
internal static string FormatBindingSource_Path()
{
return GetString("BindingSource_Path");
}
/// <summary>
/// Query
/// </summary>
internal static string BindingSource_Query
{
get { return GetString("BindingSource_Query"); }
}
/// <summary>
/// Query
/// </summary>
internal static string FormatBindingSource_Query()
{
return GetString("BindingSource_Query");
}
/// <summary>
/// The provided binding source '{0}' is a composite. '{1}' requires that the source must represent a single type of input.
/// </summary>
internal static string BindingSource_CannotBeComposite
{
get { return GetString("BindingSource_CannotBeComposite"); }
}
/// <summary>
/// The provided binding source '{0}' is a composite. '{1}' requires that the source must represent a single type of input.
/// </summary>
internal static string FormatBindingSource_CannotBeComposite(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_CannotBeComposite"), p0, p1);
}
/// <summary>
/// The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request.
/// </summary>
internal static string BindingSource_MustBeFromRequest
{
get { return GetString("BindingSource_MustBeFromRequest"); }
}
/// <summary>
/// The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request.
/// </summary>
internal static string FormatBindingSource_MustBeFromRequest(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeFromRequest"), p0, p1);
}
/// <summary>
/// The provided binding source '{0}' is a greedy data source. '{1}' does not support greedy data sources.
/// </summary>
internal static string BindingSource_CannotBeGreedy
{
get { return GetString("BindingSource_CannotBeGreedy"); }
}
/// <summary>
/// The provided binding source '{0}' is a greedy data source. '{1}' does not support greedy data sources.
/// </summary>
internal static string FormatBindingSource_CannotBeGreedy(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_CannotBeGreedy"), p0, p1);
}
/// <summary>
/// The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources.
/// </summary>
internal static string BindingSource_MustBeGreedy
{
get { return GetString("BindingSource_MustBeGreedy"); }
}
/// <summary>
/// The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources.
/// </summary>
internal static string FormatBindingSource_MustBeGreedy(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeGreedy"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -204,4 +204,40 @@
<data name="ModelBinderUtil_ValueInvalidGeneric" xml:space="preserve">
<value>The supplied value is invalid for {0}.</value>
</data>
<data name="BindingSource_Body" xml:space="preserve">
<value>Body</value>
</data>
<data name="BindingSource_Custom" xml:space="preserve">
<value>Custom</value>
</data>
<data name="BindingSource_Form" xml:space="preserve">
<value>Form</value>
</data>
<data name="BindingSource_Header" xml:space="preserve">
<value>Header</value>
</data>
<data name="BindingSource_Services" xml:space="preserve">
<value>Services</value>
</data>
<data name="BindingSource_ModelBinding" xml:space="preserve">
<value>ModelBinding</value>
</data>
<data name="BindingSource_Path" xml:space="preserve">
<value>Path</value>
</data>
<data name="BindingSource_Query" xml:space="preserve">
<value>Query</value>
</data>
<data name="BindingSource_CannotBeComposite" xml:space="preserve">
<value>The provided binding source '{0}' is a composite. '{1}' requires that the source must represent a single type of input.</value>
</data>
<data name="BindingSource_MustBeFromRequest" xml:space="preserve">
<value>The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request.</value>
</data>
<data name="BindingSource_CannotBeGreedy" xml:space="preserve">
<value>The provided binding source '{0}' is a greedy data source. '{1}' does not support greedy data sources.</value>
</data>
<data name="BindingSource_MustBeGreedy" xml:space="preserve">
<value>The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources.</value>
</data>
</root>

View File

@ -0,0 +1,79 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A value provider which provides data from a specific <see cref="BindingSource"/>.
/// </summary>
/// <remarks>
/// <para>
/// A <see cref="BindingSourceValueProvider"/> is an <see cref="IValueProvider"/> base-implementation which
/// can provide data for all parameters and model properties which specify the corresponding
/// <see cref="ModelBinding.BindingSource"/>.
/// </para>
/// <para>
/// <see cref="BindingSourceValueProvider"/> implements <see cref="IBindingSourceValueProvider"/> and will
/// include or exclude itself from the set of value providers based on the model's associated
/// <see cref="ModelBinding.BindingSource"/>. Value providers are by-default included; if a model does not
/// specify a <see cref="ModelBinding.BindingSource"/> then all value providers are valid.
/// </para>
/// </remarks>
public abstract class BindingSourceValueProvider : IBindingSourceValueProvider
{
/// <summary>
/// Creates a new <see cref="BindingSourceValueProvider"/>.
/// </summary>
/// <param name="bindingSource">
/// The <see cref="ModelBinding.BindingSource"/>. Must be a single-source (non-composite) with
/// <see cref="ModelBinding.BindingSource.IsGreedy"/> equal to <c>false</c>.
/// </param>
public BindingSourceValueProvider([NotNull] BindingSource bindingSource)
{
if (bindingSource.IsGreedy)
{
var message = Resources.FormatBindingSource_CannotBeGreedy(
bindingSource.DisplayName,
nameof(BindingSourceValueProvider));
throw new ArgumentException(message, nameof(bindingSource));
}
if (bindingSource is CompositeBindingSource)
{
var message = Resources.FormatBindingSource_CannotBeComposite(
bindingSource.DisplayName,
nameof(BindingSourceValueProvider));
throw new ArgumentException(message, nameof(bindingSource));
}
BindingSource = bindingSource;
}
/// <summary>
/// Gets the corresponding <see cref="ModelBinding.BindingSource"/>.
/// </summary>
protected BindingSource BindingSource { get; }
/// <inheritdoc />
public abstract Task<bool> ContainsPrefixAsync(string prefix);
/// <inheritdoc />
public abstract Task<ValueProviderResult> GetValueAsync(string key);
/// <inheritdoc />
public virtual IValueProvider Filter(BindingSource bindingSource)
{
if (bindingSource.CanAcceptDataFrom(BindingSource))
{
return this;
}
else
{
return null;
}
}
}
}

View File

@ -12,8 +12,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <summary>
/// Represents a <see cref="IValueProvider"/> whose values come from a collection of <see cref="IValueProvider"/>s.
/// </summary>
public class CompositeValueProvider
: Collection<IValueProvider>, IEnumerableValueProvider, IMetadataAwareValueProvider
public class CompositeValueProvider :
Collection<IValueProvider>,
IEnumerableValueProvider,
IBindingSourceValueProvider
{
/// <summary>
/// Initializes a new instance of <see cref="CompositeValueProvider"/>.
@ -122,12 +124,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
/// <inheritdoc />
public IValueProvider Filter(IValueProviderMetadata valueBinderMetadata)
public IValueProvider Filter(BindingSource bindingSource)
{
var filteredValueProviders = new List<IValueProvider>();
foreach (var valueProvider in this.OfType<IMetadataAwareValueProvider>())
foreach (var valueProvider in this.OfType<IBindingSourceValueProvider>())
{
var result = valueProvider.Filter(valueBinderMetadata);
var result = valueProvider.Filter(bindingSource);
if (result != null)
{
filteredValueProviders.Add(result);

View File

@ -8,17 +8,29 @@ using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class DictionaryBasedValueProvider<TBinderMetadata> : MetadataAwareValueProvider<TBinderMetadata>
where TBinderMetadata : IValueProviderMetadata
/// <summary>
/// An <see cref="IValueProvider"/> adapter for data stored in an
/// <see cref="IDictionary{string, object}"/>.
/// </summary>
public class DictionaryBasedValueProvider: BindingSourceValueProvider
{
private readonly IDictionary<string, object> _values;
private PrefixContainer _prefixContainer;
public DictionaryBasedValueProvider(IDictionary<string, object> values)
/// <summary>
/// Creates a new <see cref="DictionaryBasedValueProvider"/>.
/// </summary>
/// <param name="bindingSource">The <see cref="BindingSource"/> of the data.</param>
/// <param name="values">The values.</param>
public DictionaryBasedValueProvider(
[NotNull] BindingSource bindingSource,
[NotNull] IDictionary<string, object> values)
: base(bindingSource)
{
_values = values;
}
/// <inheritdoc />
public override Task<bool> ContainsPrefixAsync(string key)
{
var prefixContainer = GetOrCreatePrefixContainer();
@ -35,6 +47,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return _prefixContainer;
}
/// <inheritdoc />
public override Task<ValueProviderResult> GetValueAsync([NotNull] string key)
{
object value;

View File

@ -16,7 +16,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
var culture = GetCultureInfo(request);
return new ReadableStringCollectionValueProvider<IFormDataValueProviderMetadata>(
return new ReadableStringCollectionValueProvider(
BindingSource.Form,
async () => await request.ReadFormAsync(),
culture);
}

View File

@ -0,0 +1,25 @@
// 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.ModelBinding
{
/// <summary>
/// A value provider which is which can filter its contents based on <see cref="BindingSource"/>.
/// </summary>
/// <remarks>
/// Value providers are by-default included. If a model does not specify a <see cref="BindingSource"/>
/// then all value providers are valid.
/// </remarks>
public interface IBindingSourceValueProvider : IValueProvider
{
/// <summary>
/// Filters the value provider based on <paramref name="bindingSource"/>.
/// </summary>
/// <param name="bindingSource">The <see cref="BindingSource"/> associated with a model.</param>
/// <returns>
/// The filtered value provider, or <c>null</c> if the value provider does not match
/// <paramref name="bindingSource"/>.
/// </returns>
IValueProvider Filter([NotNull] BindingSource bindingSource);
}
}

View File

@ -1,18 +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.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A value provider which is aware of <see cref="IValueProviderMetadata"/>.
/// </summary>
public interface IMetadataAwareValueProvider : IValueProvider
{
/// <summary>
/// Filters the value provider based on <paramref name="metadata"/>.
/// </summary>
/// <param name="metadata">The <see cref="IValueProviderMetadata"/> associated with a model.</param>
/// <returns>The filtered value provider.</returns>
IValueProvider Filter([NotNull] IValueProviderMetadata metadata);
}
}

View File

@ -1,32 +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.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// A <see cref="IMetadataAwareValueProvider"/> value provider which can filter
/// based on <see cref="IValueProviderMetadata"/>.
/// </summary>
/// <typeparam name="TBinderMetadata">
/// Represents a type implementing <see cref="IValueProviderMetadata"/>
/// </typeparam>
public abstract class MetadataAwareValueProvider<TBinderMetadata> : IMetadataAwareValueProvider
where TBinderMetadata : IValueProviderMetadata
{
public abstract Task<bool> ContainsPrefixAsync(string prefix);
public abstract Task<ValueProviderResult> GetValueAsync(string key);
public virtual IValueProvider Filter(IValueProviderMetadata valueBinderMetadata)
{
if (valueBinderMetadata is TBinderMetadata)
{
return this;
}
return null;
}
}
}

View File

@ -18,13 +18,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (!storage.TryGetValue(_cacheKey, out value))
{
var queryCollection = context.HttpContext.Request.Query;
provider = new ReadableStringCollectionValueProvider<IQueryValueProviderMetadata>(queryCollection,
CultureInfo.InvariantCulture);
provider = new ReadableStringCollectionValueProvider(
BindingSource.Query,
queryCollection,
CultureInfo.InvariantCulture);
storage[_cacheKey] = provider;
}
else
{
provider = (ReadableStringCollectionValueProvider<IQueryValueProviderMetadata>)value;
provider = (ReadableStringCollectionValueProvider)value;
}
return provider;
}

View File

@ -11,9 +11,10 @@ using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ReadableStringCollectionValueProvider<TBinderMetadata> :
MetadataAwareValueProvider<TBinderMetadata>, IEnumerableValueProvider
where TBinderMetadata : IValueProviderMetadata
/// <summary>
/// An <see cref="IValueProvider"/> adapter for data stored in an <see cref="IReadableStringCollection"/>.
/// </summary>
public class ReadableStringCollectionValueProvider : BindingSourceValueProvider, IEnumerableValueProvider
{
private readonly CultureInfo _culture;
private readonly Func<Task<IReadableStringCollection>> _valuesFactory;
@ -23,9 +24,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <summary>
/// Creates a provider for <see cref="IReadableStringCollection"/> wrapping an existing set of key value pairs.
/// </summary>
/// <param name="bindingSource">The <see cref="BindingSource"/> for the data.</param>
/// <param name="values">The key value pairs to wrap.</param>
/// <param name="culture">The culture to return with ValueProviderResult instances.</param>
public ReadableStringCollectionValueProvider([NotNull] IReadableStringCollection values, CultureInfo culture)
public ReadableStringCollectionValueProvider(
[NotNull] BindingSource bindingSource,
[NotNull] IReadableStringCollection values,
CultureInfo culture)
: base(bindingSource)
{
_values = values;
_culture = culture;
@ -35,10 +41,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// Creates a provider for <see cref="IReadableStringCollection"/> wrapping an
/// existing set of key value pairs provided by the delegate.
/// </summary>
/// <param name="bindingSource">The <see cref="BindingSource"/> for the data.</param>
/// <param name="values">The delegate that provides the key value pairs to wrap.</param>
/// <param name="culture">The culture to return with ValueProviderResult instances.</param>
public ReadableStringCollectionValueProvider([NotNull] Func<Task<IReadableStringCollection>> valuesFactory,
CultureInfo culture)
public ReadableStringCollectionValueProvider(
[NotNull] BindingSource bindingSource,
[NotNull] Func<Task<IReadableStringCollection>> valuesFactory,
CultureInfo culture)
: base(bindingSource)
{
_valuesFactory = valuesFactory;
_culture = culture;

View File

@ -7,7 +7,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public IValueProvider GetValueProvider([NotNull] ValueProviderFactoryContext context)
{
return new DictionaryBasedValueProvider<IRouteDataValueProviderMetadata>(context.RouteValues);
return new DictionaryBasedValueProvider(BindingSource.Path, context.RouteValues);
}
}
}

View File

@ -90,9 +90,15 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
foreach (var parameter in candidate.Action.Parameters)
{
// We only consider parameters that are bound from the URL.
if ((parameter.BinderMetadata is IRouteDataValueProviderMetadata ||
parameter.BinderMetadata is IQueryValueProviderMetadata) &&
// We only consider parameters that are marked as bound from the URL.
var source = BindingSource.GetBindingSource(parameter.BinderMetadata);
if (source == null)
{
continue;
}
if ((source.CanAcceptDataFrom(BindingSource.Path) ||
source.CanAcceptDataFrom(BindingSource.Query)) &&
ValueProviderResult.CanConvertFromString(parameter.ParameterType))
{
var optionalMetadata = parameter.BinderMetadata as IOptionalBinderMetadata;

View File

@ -3,6 +3,7 @@
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
using WebApiShimResources = Microsoft.AspNet.Mvc.WebApiCompatShim.Resources;
namespace System.Web.Http
{
@ -13,10 +14,15 @@ namespace System.Web.Http
public class FromUriAttribute :
Attribute,
IOptionalBinderMetadata,
IQueryValueProviderMetadata,
IRouteDataValueProviderMetadata,
IBindingSourceMetadata,
IModelNameProvider
{
private static readonly BindingSource FromUriSource = CompositeBindingSource.Create(
new BindingSource[] { BindingSource.Path, BindingSource.Query },
WebApiShimResources.BindingSource_URL);
public BindingSource BindingSource { get { return FromUriSource; } }
public bool IsOptional { get; set; }
/// <inheritdoc />

View File

@ -170,6 +170,22 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
return string.Format(CultureInfo.CurrentCulture, GetString("CreatedAtRoute_RouteFailed"), p0);
}
/// <summary>
/// URL
/// </summary>
internal static string BindingSource_URL
{
get { return GetString("BindingSource_URL"); }
}
/// <summary>
/// URL
/// </summary>
internal static string FormatBindingSource_URL()
{
return GetString("BindingSource_URL");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
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
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>
@ -26,36 +26,36 @@
<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
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
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
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
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
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
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
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -147,4 +147,7 @@
<data name="CreatedAtRoute_RouteFailed" xml:space="preserve">
<value>Failed to generate a URL using route '{0}'.</value>
</data>
<data name="BindingSource_URL" xml:space="preserve">
<value>URL</value>
</data>
</root>

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc
.Verifiable();
var bindingContext = GetBindingContext(typeof(Person), inputFormatter: mockInputFormatter.Object);
bindingContext.ModelMetadata.BinderMetadata = Mock.Of<IFormatterBinderMetadata>();
bindingContext.ModelMetadata.BinderMetadata = new FromBodyAttribute();
var binder = GetBodyBinder(mockInputFormatter.Object, mockValidator.Object);
@ -47,7 +47,8 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var bindingContext = GetBindingContext(typeof(Person), inputFormatter: null);
bindingContext.ModelMetadata.BinderMetadata = Mock.Of<IFormatterBinderMetadata>();
bindingContext.ModelMetadata.BinderMetadata = new FromBodyAttribute();
var binder = bindingContext.OperationBindingContext.ModelBinder;
// Act
@ -61,22 +62,61 @@ namespace Microsoft.AspNet.Mvc
Assert.True(bindingContext.ModelState.ContainsKey("someName"));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task BindModel_IsMetadataAware(bool useBody)
[Fact]
public async Task BindModel_IsGreedy()
{
// Arrange
var metadata = new Mock<IBindingSourceMetadata>();
metadata.SetupGet(m => m.BindingSource).Returns(BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), inputFormatter: null);
bindingContext.ModelMetadata.BinderMetadata = useBody ? Mock.Of<IFormatterBinderMetadata>() :
Mock.Of<IBinderMetadata>();
bindingContext.ModelMetadata.BinderMetadata = metadata.Object;
var binder = bindingContext.OperationBindingContext.ModelBinder;
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.Equal(useBody, binderResult);
Assert.True(binderResult);
}
[Fact]
public async Task BindModel_IsGreedy_IgnoresWrongSource()
{
// Arrange
var metadata = new Mock<IBindingSourceMetadata>();
metadata.SetupGet(m => m.BindingSource).Returns(BindingSource.Header);
var bindingContext = GetBindingContext(typeof(Person), inputFormatter: null);
bindingContext.ModelMetadata.BinderMetadata = metadata.Object;
var binder = bindingContext.OperationBindingContext.ModelBinder;
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
}
[Fact]
public async Task BindModel_IsGreedy_IgnoresMetadataWithNoSource()
{
// Arrange
var metadata = new Mock<IBindingSourceMetadata>();
metadata.SetupGet(m => m.BindingSource).Returns((BindingSource)null);
var bindingContext = GetBindingContext(typeof(Person), inputFormatter: null);
bindingContext.ModelMetadata.BinderMetadata = metadata.Object;
var binder = bindingContext.OperationBindingContext.ModelBinder;
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
}
private static ModelBindingContext GetBindingContext(Type modelType, IInputFormatter inputFormatter)

View File

@ -138,7 +138,7 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal(ApiParameterSource.Path, parameter.Source);
Assert.Equal(BindingSource.Path, parameter.Source);
Assert.Equal(isOptional, parameter.RouteInfo.IsOptional);
Assert.Equal("id", parameter.Name);
@ -186,7 +186,7 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal(ApiParameterSource.Path, parameter.Source);
Assert.Equal(BindingSource.Path, parameter.Source);
Assert.Equal(isOptional, parameter.RouteInfo.IsOptional);
Assert.Equal("id", parameter.Name);
@ -219,6 +219,8 @@ namespace Microsoft.AspNet.Mvc.Description
var action = CreateActionDescriptor(methodName);
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
var expected = new BindingSource(source, displayName: null, isGreedy: false, isFromRequest: false);
// Act
var descriptions = GetApiDescriptions(action);
@ -226,7 +228,7 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var parameters = description.ParameterDescriptions;
var id = Assert.Single(parameters, p => p.Source == new ApiParameterSource(source, displayName: null));
var id = Assert.Single(parameters, p => p.Source == expected);
Assert.Null(id.RouteInfo);
}
@ -248,6 +250,8 @@ namespace Microsoft.AspNet.Mvc.Description
var action = CreateActionDescriptor(methodName);
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
var expected = new BindingSource(source, displayName: null, isGreedy: false, isFromRequest: false);
// Act
var descriptions = GetApiDescriptions(action);
@ -255,7 +259,7 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var parameters = description.ParameterDescriptions;
var id = Assert.Single(parameters, p => p.Source == new ApiParameterSource(source, displayName: null));
var id = Assert.Single(parameters, p => p.Source == expected);
Assert.NotNull(id.RouteInfo);
}
@ -318,11 +322,11 @@ namespace Microsoft.AspNet.Mvc.Description
// Assert
var description = Assert.Single(descriptions);
var id1 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id1");
Assert.Equal(ApiParameterSource.Path, id1.Source);
Assert.Equal(BindingSource.Path, id1.Source);
Assert.Empty(id1.RouteInfo.Constraints);
var id2 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id2");
Assert.Equal(ApiParameterSource.Path, id2.Source);
Assert.Equal(BindingSource.Path, id2.Source);
Assert.IsType<IntRouteConstraint>(Assert.Single(id2.RouteInfo.Constraints));
}
@ -537,7 +541,7 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.ModelBinding, parameter.Source);
Assert.Same(BindingSource.ModelBinding, parameter.Source);
}
[Fact]
@ -554,7 +558,7 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("id", parameter.Name);
Assert.Same(ApiParameterSource.Path, parameter.Source);
Assert.Same(BindingSource.Path, parameter.Source);
}
[Fact]
@ -571,7 +575,7 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("id", parameter.Name);
Assert.Same(ApiParameterSource.Query, parameter.Source);
Assert.Same(BindingSource.Query, parameter.Source);
}
[Fact]
@ -588,7 +592,7 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.Body, parameter.Source);
Assert.Same(BindingSource.Body, parameter.Source);
}
[Fact]
@ -605,7 +609,7 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.Form, parameter.Source);
Assert.Same(BindingSource.Form, parameter.Source);
}
[Fact]
@ -622,7 +626,7 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("id", parameter.Name);
Assert.Same(ApiParameterSource.Header, parameter.Source);
Assert.Same(BindingSource.Header, parameter.Source);
}
// 'Hidden' parameters are hidden (not returned).
@ -654,7 +658,7 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.Custom, parameter.Source);
Assert.Same(BindingSource.Custom, parameter.Source);
}
[Fact]
@ -671,7 +675,7 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.ModelBinding, parameter.Source);
Assert.Same(BindingSource.ModelBinding, parameter.Source);
}
[Fact]
@ -689,19 +693,19 @@ namespace Microsoft.AspNet.Mvc.Description
Assert.Equal(4, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Path, id.Source);
Assert.Same(BindingSource.Path, id.Source);
Assert.Equal(typeof(int), id.Type);
var product = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product");
Assert.Same(ApiParameterSource.Body, product.Source);
Assert.Same(BindingSource.Body, product.Source);
Assert.Equal(typeof(Product), product.Type);
var userId = Assert.Single(description.ParameterDescriptions, p => p.Name == "UserId");
Assert.Same(ApiParameterSource.Header, userId.Source);
Assert.Same(BindingSource.Header, userId.Source);
Assert.Equal(typeof(string), userId.Type);
var comments = Assert.Single(description.ParameterDescriptions, p => p.Name == "Comments");
Assert.Same(ApiParameterSource.ModelBinding, comments.Source);
Assert.Same(BindingSource.ModelBinding, comments.Source);
Assert.Equal(typeof(string), comments.Type);
}
@ -721,19 +725,19 @@ namespace Microsoft.AspNet.Mvc.Description
Assert.Equal(4, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Path, id.Source);
Assert.Same(BindingSource.Path, id.Source);
Assert.Equal(typeof(int), id.Type);
var product = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product");
Assert.Same(ApiParameterSource.Body, product.Source);
Assert.Same(BindingSource.Body, product.Source);
Assert.Equal(typeof(Product), product.Type);
var userId = Assert.Single(description.ParameterDescriptions, p => p.Name == "UserId");
Assert.Same(ApiParameterSource.Header, userId.Source);
Assert.Same(BindingSource.Header, userId.Source);
Assert.Equal(typeof(string), userId.Type);
var comments = Assert.Single(description.ParameterDescriptions, p => p.Name == "Comments");
Assert.Same(ApiParameterSource.Query, comments.Source);
Assert.Same(BindingSource.Query, comments.Source);
Assert.Equal(typeof(string), comments.Type);
}
@ -752,19 +756,19 @@ namespace Microsoft.AspNet.Mvc.Description
Assert.Equal(4, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Path, id.Source);
Assert.Same(BindingSource.Path, id.Source);
Assert.Equal(typeof(int), id.Type);
var quantity = Assert.Single(description.ParameterDescriptions, p => p.Name == "Quantity");
Assert.Same(ApiParameterSource.ModelBinding, quantity.Source);
Assert.Same(BindingSource.ModelBinding, quantity.Source);
Assert.Equal(typeof(int), quantity.Type);
var productId = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product.Id");
Assert.Same(ApiParameterSource.ModelBinding, productId.Source);
Assert.Same(BindingSource.ModelBinding, productId.Source);
Assert.Equal(typeof(int), productId.Type);
var price = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product.Price");
Assert.Same(ApiParameterSource.Query, price.Source);
Assert.Same(BindingSource.Query, price.Source);
Assert.Equal(typeof(decimal), price.Type);
}
@ -784,15 +788,15 @@ namespace Microsoft.AspNet.Mvc.Description
Assert.Equal(3, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Path, id.Source);
Assert.Same(BindingSource.Path, id.Source);
Assert.Equal(typeof(int), id.Type);
var quantity = Assert.Single(description.ParameterDescriptions, p => p.Name == "Quantity");
Assert.Same(ApiParameterSource.Query, quantity.Source);
Assert.Same(BindingSource.Query, quantity.Source);
Assert.Equal(typeof(int), quantity.Type);
var product = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product");
Assert.Same(ApiParameterSource.Query, product.Source);
Assert.Same(BindingSource.Query, product.Source);
Assert.Equal(typeof(OrderProductDTO), product.Type);
}
@ -810,7 +814,7 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var c = Assert.Single(description.ParameterDescriptions);
Assert.Same(ApiParameterSource.Query, c.Source);
Assert.Same(BindingSource.Query, c.Source);
Assert.Equal("C.C", c.Name);
Assert.Equal(typeof(Cycle1), c.Type);
}
@ -829,7 +833,7 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var products = Assert.Single(description.ParameterDescriptions);
Assert.Same(ApiParameterSource.Query, products.Source);
Assert.Same(BindingSource.Query, products.Source);
Assert.Equal("Products", products.Name);
Assert.Equal(typeof(Product[]), products.Type);
}
@ -849,7 +853,7 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var c = Assert.Single(description.ParameterDescriptions);
Assert.Same(ApiParameterSource.ModelBinding, c.Source);
Assert.Same(BindingSource.ModelBinding, c.Source);
Assert.Equal("c", c.Name);
Assert.Equal(typeof(HasCollection_Complex), c.Type);
}
@ -868,7 +872,7 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var r = Assert.Single(description.ParameterDescriptions);
Assert.Same(ApiParameterSource.Query, r.Source);
Assert.Same(BindingSource.Query, r.Source);
Assert.Equal("r", r.Name);
Assert.Equal(typeof(RedundentMetadata), r.Type);
}
@ -887,11 +891,11 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var name = Assert.Single(description.ParameterDescriptions, p => p.Name == "Name");
Assert.Same(ApiParameterSource.Header, name.Source);
Assert.Same(BindingSource.Header, name.Source);
Assert.Equal(typeof(string), name.Type);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Form, id.Source);
Assert.Same(BindingSource.Form, id.Source);
Assert.Equal(typeof(int), id.Type);
}

View File

@ -393,12 +393,14 @@ namespace Microsoft.AspNet.Mvc.Core.Test
public string Name { get; set; }
}
private class NonValueProviderBinderMetadataAttribute : Attribute, IBinderMetadata
private class NonValueProviderBinderMetadataAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource { get { return BindingSource.Body; } }
}
private class ValueProviderMetadataAttribute : Attribute, IValueProviderMetadata
private class ValueProviderMetadataAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource { get { return BindingSource.Query; } }
}
[Bind(new string[] { nameof(IncludedExplicitly1), nameof(IncludedExplicitly2) })]

View File

@ -70,7 +70,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
{ "", null }
};
var valueProvider = new DictionaryBasedValueProvider<TestValueBinderMetadata>(values);
var valueProvider = new TestValueProvider(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
@ -108,7 +108,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{ "", null },
{ "MyProperty", "MyPropertyValue" }
};
var valueProvider = new DictionaryBasedValueProvider<TestValueBinderMetadata>(values);
var valueProvider = new TestValueProvider(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
@ -193,7 +193,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
string.Equals(propertyName, "IncludedProperty", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "MyProperty", StringComparison.OrdinalIgnoreCase);
var valueProvider = new DictionaryBasedValueProvider<TestValueBinderMetadata>(values);
var valueProvider = new TestValueProvider(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
@ -276,7 +276,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{ "ExcludedProperty", "ExcludedPropertyValue" }
};
var valueProvider = new DictionaryBasedValueProvider<TestValueBinderMetadata>(values);
var valueProvider = new TestValueProvider(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
@ -326,7 +326,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{ "ExcludedProperty", "ExcludedPropertyValue" }
};
var valueProvider = new DictionaryBasedValueProvider<TestValueBinderMetadata>(values);
var valueProvider = new TestValueProvider(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
@ -509,10 +509,6 @@ namespace Microsoft.AspNet.Mvc.Core.Test
public string ExcludedProperty { get; set; }
}
private class TestValueBinderMetadata : IValueProviderMetadata
{
}
}
}
#endif

View File

@ -0,0 +1,26 @@
// 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;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class TestValueProvider : DictionaryBasedValueProvider
{
public static readonly BindingSource TestBindingSource = new BindingSource(
id: "Test",
displayName: "Test",
isGreedy: false,
isFromRequest: true);
public TestValueProvider(IDictionary<string, object> values)
: base(TestBindingSource, values)
{
}
public TestValueProvider(BindingSource bindingSource, IDictionary<string, object> values)
: base(bindingSource, values)
{
}
}
}

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Xml;
using Microsoft.AspNet.TestHost;
using Newtonsoft.Json;
@ -690,11 +690,11 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(2, parameters.Count);
var i = Assert.Single(parameters, p => p.Name == "i");
Assert.Equal(ApiParameterSource.ModelBinding.Id, i.Source);
Assert.Equal(BindingSource.ModelBinding.Id, i.Source);
Assert.Equal(typeof(int).FullName, i.Type);
var s = Assert.Single(parameters, p => p.Name == "s");
Assert.Equal(ApiParameterSource.ModelBinding.Id, s.Source);
Assert.Equal(BindingSource.ModelBinding.Id, s.Source);
Assert.Equal(typeof(string).FullName, s.Type);
}
@ -718,11 +718,11 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(2, parameters.Count);
var i = Assert.Single(parameters, p => p.Name == "i");
Assert.Equal(ApiParameterSource.Query.Id, i.Source);
Assert.Equal(BindingSource.Query.Id, i.Source);
Assert.Equal(typeof(int).FullName, i.Type);
var s = Assert.Single(parameters, p => p.Name == "s");
Assert.Equal(ApiParameterSource.Path.Id, s.Source);
Assert.Equal(BindingSource.Path.Id, s.Source);
Assert.Equal(typeof(string).FullName, s.Type);
}
@ -746,7 +746,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(1, parameters.Count);
var product = Assert.Single(parameters, p => p.Name == "product");
Assert.Equal(ApiParameterSource.ModelBinding.Id, product.Source);
Assert.Equal(BindingSource.ModelBinding.Id, product.Source);
Assert.Equal(typeof(ApiExplorerWebSite.Product).FullName, product.Type);
}
@ -770,11 +770,11 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(2, parameters.Count);
var id = Assert.Single(parameters, p => p.Name == "id");
Assert.Equal(ApiParameterSource.Path.Id, id.Source);
Assert.Equal(BindingSource.Path.Id, id.Source);
Assert.Equal(typeof(int).FullName, id.Type);
var product = Assert.Single(parameters, p => p.Name == "product");
Assert.Equal(ApiParameterSource.Body.Id, product.Source);
Assert.Equal(BindingSource.Body.Id, product.Source);
Assert.Equal(typeof(ApiExplorerWebSite.Product).FullName, product.Type);
}
@ -798,27 +798,27 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(6, parameters.Count);
var customerId = Assert.Single(parameters, p => p.Name == "CustomerId");
Assert.Equal(ApiParameterSource.Query.Id, customerId.Source);
Assert.Equal(BindingSource.Query.Id, customerId.Source);
Assert.Equal(typeof(string).FullName, customerId.Type);
var referrer = Assert.Single(parameters, p => p.Name == "Referrer");
Assert.Equal(ApiParameterSource.Header.Id, referrer.Source);
Assert.Equal(BindingSource.Header.Id, referrer.Source);
Assert.Equal(typeof(string).FullName, referrer.Type);
var quantity = Assert.Single(parameters, p => p.Name == "Details.Quantity");
Assert.Equal(ApiParameterSource.Form.Id, quantity.Source);
Assert.Equal(BindingSource.Form.Id, quantity.Source);
Assert.Equal(typeof(int).FullName, quantity.Type);
var product = Assert.Single(parameters, p => p.Name == "Details.Product");
Assert.Equal(ApiParameterSource.Form.Id, product.Source);
Assert.Equal(BindingSource.Form.Id, product.Source);
Assert.Equal(typeof(ApiExplorerWebSite.Product).FullName, product.Type);
var shippingInstructions = Assert.Single(parameters, p => p.Name == "Comments.ShippingInstructions");
Assert.Equal(ApiParameterSource.Query.Id, shippingInstructions.Source);
Assert.Equal(BindingSource.Query.Id, shippingInstructions.Source);
Assert.Equal(typeof(string).FullName, shippingInstructions.Type);
var feedback = Assert.Single(parameters, p => p.Name == "Comments.Feedback");
Assert.Equal(ApiParameterSource.Form.Id, feedback.Source);
Assert.Equal(BindingSource.Form.Id, feedback.Source);
Assert.Equal(typeof(string).FullName, feedback.Type);
}

View File

@ -0,0 +1,116 @@
// 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.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class BindingSourceModelBinderTest
{
[Fact]
public void BindingSourceModelBinder_ThrowsOnNonGreedySource()
{
// Arrange
var expected =
"The provided binding source 'Test Source' is not a greedy data source. " +
"'BindingSourceModelBinder' only supports greedy data sources." + Environment.NewLine +
"Parameter name: bindingSource";
var bindingSource = new BindingSource(
"Test",
displayName: "Test Source",
isGreedy: false,
isFromRequest: true);
// Act & Assert
var exception = Assert.Throws<ArgumentException>(
() => new TestableBindingSourceModelBinder(bindingSource));
Assert.Equal(expected, exception.Message);
}
[Fact]
public async Task BindingSourceModelBinder_ReturnsFalse_WithNoSource()
{
// Arrange
var context = new ModelBindingContext();
context.ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(
modelAccessor: null,
modelType: typeof(string));
var binder = new TestableBindingSourceModelBinder(BindingSource.Body);
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.False(result);
Assert.False(binder.WasBindModelCoreCalled);
}
[Fact]
public async Task BindingSourceModelBinder_ReturnsFalse_NonMatchingSource()
{
// Arrange
var context = new ModelBindingContext();
context.ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(
modelAccessor: null,
modelType: typeof(string));
context.ModelMetadata.BinderMetadata = new ModelBinderAttribute()
{
BindingSource = BindingSource.Query,
};
var binder = new TestableBindingSourceModelBinder(BindingSource.Body);
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.False(result);
Assert.False(binder.WasBindModelCoreCalled);
}
[Fact]
public async Task BindingSourceModelBinder_ReturnsTrue_MatchingSource()
{
// Arrange
var context = new ModelBindingContext();
context.ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(
modelAccessor: null,
modelType: typeof(string));
context.ModelMetadata.BinderMetadata = new ModelBinderAttribute()
{
BindingSource = BindingSource.Body,
};
var binder = new TestableBindingSourceModelBinder(BindingSource.Body);
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.True(result);
Assert.True(binder.WasBindModelCoreCalled);
}
private class TestableBindingSourceModelBinder : BindingSourceModelBinder
{
public bool WasBindModelCoreCalled { get; private set; }
public TestableBindingSourceModelBinder(BindingSource source)
: base(source)
{
}
protected override Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
WasBindModelCoreCalled = true;
return Task.FromResult(true);
}
}
}
}

View File

@ -0,0 +1,97 @@
// 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.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class BindingSourceValueProviderTest
{
[Fact]
public void BindingSourceValueProvider_ThrowsOnNonGreedySource()
{
// Arrange
var expected =
"The provided binding source 'Test Source' is a greedy data source. " +
"'BindingSourceValueProvider' does not support greedy data sources." + Environment.NewLine +
"Parameter name: bindingSource";
var bindingSource = new BindingSource(
"Test",
displayName: "Test Source",
isGreedy: true,
isFromRequest: true);
// Act & Assert
var exception = Assert.Throws<ArgumentException>(
() => new TestableBindingSourceValueProvider(bindingSource));
Assert.Equal(expected, exception.Message);
}
[Fact]
public void BindingSourceValueProvider_ThrowsOnCompositeSource()
{
// Arrange
var expected =
"The provided binding source 'Test Source' is a composite. " +
"'BindingSourceValueProvider' requires that the source must represent a single type of input." +
Environment.NewLine +
"Parameter name: bindingSource";
var bindingSource = CompositeBindingSource.Create(
bindingSources: new BindingSource[] { BindingSource.Query, BindingSource.Form },
displayName: "Test Source");
// Act & Assert
var exception = Assert.Throws<ArgumentException>(
() => new TestableBindingSourceValueProvider(bindingSource));
Assert.Equal(expected, exception.Message);
}
[Fact]
public void BindingSourceValueProvider_ReturnsNull_WithNonMatchingSource()
{
// Arrange
var valueProvider = new TestableBindingSourceValueProvider(BindingSource.Query);
// Act
var result = valueProvider.Filter(BindingSource.Body);
// Assert
Assert.Null(result);
}
[Fact]
public void BindingSourceValueProvider_ReturnsSelf_WithMatchingSource()
{
// Arrange
var valueProvider = new TestableBindingSourceValueProvider(BindingSource.Query);
// Act
var result = valueProvider.Filter(BindingSource.Query);
// Assert
Assert.Same(valueProvider, result);
}
private class TestableBindingSourceValueProvider : BindingSourceValueProvider
{
public TestableBindingSourceValueProvider(BindingSource source)
: base(source)
{
}
public override Task<bool> ContainsPrefixAsync(string prefix)
{
throw new NotImplementedException();
}
public override Task<ValueProviderResult> GetValueAsync(string key)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -91,8 +91,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
return bindingContext;
}
public class TestFromHeader : IHeaderBinderMetadata
public class TestFromHeader : IBindingSourceMetadata
{
public BindingSource BindingSource { get; } = BindingSource.Header;
}
}
}

View File

@ -236,26 +236,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
[Theory]
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), false)]
[InlineData(typeof(TypeWithAtLeastOnePropertyMarkedUsingValueBinderMetadata), true)]
public async Task CanCreateModel_ForExplicitValueProviderMetadata_UsesOriginalValueProvider(Type modelType, bool originalValueProviderProvidesValue)
public async Task CanCreateModel_ForExplicitValueProviderMetadata_UsesOriginalValueProvider(
Type modelType,
bool originalValueProviderProvidesValue)
{
var mockValueProvider = new Mock<IValueProvider>();
mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(false));
var mockOriginalValueProvider = new Mock<IMetadataAwareValueProvider>();
mockOriginalValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(originalValueProviderProvidesValue));
mockOriginalValueProvider.Setup(o => o.Filter(It.IsAny<IValueProviderMetadata>()))
.Returns<IValueProviderMetadata>(
valueProviderMetadata =>
{
if (valueProviderMetadata is ValueBinderMetadataAttribute)
{
return mockOriginalValueProvider.Object;
}
var mockOriginalValueProvider = new Mock<IBindingSourceValueProvider>();
mockOriginalValueProvider
.Setup(o => o.ContainsPrefixAsync(It.IsAny<string>()))
.Returns(Task.FromResult(originalValueProviderProvidesValue));
return null;
});
mockOriginalValueProvider
.Setup(o => o.Filter(It.IsAny<BindingSource>()))
.Returns<BindingSource>(source =>
{
if (source == BindingSource.Query)
{
return mockOriginalValueProvider.Object;
}
return null;
});
var bindingContext = new MutableObjectBinderContext
{
@ -1542,12 +1546,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public Document SubDocument { get; set; }
}
private class NonValueBinderMetadataAttribute : Attribute, IBinderMetadata
private class NonValueBinderMetadataAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource { get { return BindingSource.Body; } }
}
private class ValueBinderMetadataAttribute : Attribute, IValueProviderMetadata
private class ValueBinderMetadataAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource { get { return BindingSource.Query; } }
}
public class ExcludedProvider : IPropertyBindingPredicateProvider

View File

@ -0,0 +1,51 @@
// 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 Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class BindingSourceTest
{
[Fact]
public void BindingSource_CanAcceptDataFrom_ThrowsOnComposite()
{
// Arrange
var expected =
"The provided binding source 'Test Source' is a composite. " +
"'CanAcceptDataFrom' requires that the source must represent a single type of input." +
Environment.NewLine +
"Parameter name: bindingSource";
var bindingSource = CompositeBindingSource.Create(
bindingSources: new BindingSource[] { BindingSource.Query, BindingSource.Form },
displayName: "Test Source");
// Act & Assert
var exception = Assert.Throws<ArgumentException>(
() => BindingSource.Query.CanAcceptDataFrom(bindingSource));
Assert.Equal(expected, exception.Message);
}
[Fact]
public void BindingSource_CanAcceptDataFrom_Match()
{
// Act
var result = BindingSource.Query.CanAcceptDataFrom(BindingSource.Query);
// Assert
Assert.True(result);
}
[Fact]
public void BindingSource_CanAcceptDataFrom_NoMatch()
{
// Act
var result = BindingSource.Query.CanAcceptDataFrom(BindingSource.Path);
// Assert
Assert.False(result);
}
}
}

View File

@ -145,6 +145,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private class TestBinderTypeProvider : IBinderTypeProviderMetadata
{
public Type BinderType { get; set; }
public BindingSource BindingSource { get; set; }
}
private class TestPredicateProvider : IPropertyBindingPredicateProvider

View File

@ -525,6 +525,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private class TestBinderTypeProvider : IBinderTypeProviderMetadata
{
public Type BinderType { get; set; }
public BindingSource BindingSource { get; set; }
}
private class DataTypeWithCustomDisplayFormat : DataTypeAttribute

View File

@ -0,0 +1,66 @@
// 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 Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class CompositeBindingSourceTest
{
[Fact]
public void CompositeBindingSourceTest_CanAcceptDataFrom_ThrowsOnComposite()
{
// Arrange
var expected =
"The provided binding source 'Test Source2' is a composite. " +
"'CanAcceptDataFrom' requires that the source must represent a single type of input." +
Environment.NewLine +
"Parameter name: bindingSource";
var composite1 = CompositeBindingSource.Create(
bindingSources: new BindingSource[] { BindingSource.Query, BindingSource.Form },
displayName: "Test Source1");
var composite2 = CompositeBindingSource.Create(
bindingSources: new BindingSource[] { BindingSource.Query, BindingSource.Form },
displayName: "Test Source2");
// Act & Assert
var exception = Assert.Throws<ArgumentException>(
() => composite1.CanAcceptDataFrom(composite2));
Assert.Equal(expected, exception.Message);
}
[Fact]
public void CompositeBindingSourceTest_CanAcceptDataFrom_Match()
{
// Arrange
var composite = CompositeBindingSource.Create(
bindingSources: new BindingSource[] { BindingSource.Query, BindingSource.Form },
displayName: "Test Source1");
// Act
var result = composite.CanAcceptDataFrom(BindingSource.Query);
// Assert
Assert.True(result);
}
[Fact]
public void CompositeBindingSourceTest_CanAcceptDataFrom_NoMatch()
{
// Arrange
var composite = CompositeBindingSource.Create(
bindingSources: new BindingSource[] { BindingSource.Query, BindingSource.Form },
displayName: "Test Source1");
// Act
var result = composite.CanAcceptDataFrom(BindingSource.Path);
// Assert
Assert.False(result);
}
}
}

View File

@ -25,5 +25,47 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Assert
Assert.Equal(expected, ex.Message);
}
[Fact]
public void NoBinderType_NoBindingSource()
{
// Arrange
var attribute = new ModelBinderAttribute();
// Act
var source = attribute.BindingSource;
// Assert
Assert.Null(source);
}
[Fact]
public void BinderType_DefaultCustomBindingSource()
{
// Arrange
var attribute = new ModelBinderAttribute();
attribute.BinderType = typeof(ByteArrayModelBinder);
// Act
var source = attribute.BindingSource;
// Assert
Assert.Equal(BindingSource.Custom, source);
}
[Fact]
public void BinderType_SettingBindingSource_OverridesDefaultCustomBindingSource()
{
// Arrange
var attribute = new ModelBinderAttribute();
attribute.BindingSource = BindingSource.Query;
attribute.BinderType = typeof(ByteArrayModelBinder);
// Act
var source = attribute.BindingSource;
// Assert
Assert.Equal(BindingSource.Query, source);
}
}
}

View File

@ -0,0 +1,26 @@
// 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;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class TestValueProvider : DictionaryBasedValueProvider
{
public static readonly BindingSource TestBindingSource = new BindingSource(
id: "Test",
displayName: "Test",
isGreedy: false,
isFromRequest: true);
public TestValueProvider(IDictionary<string, object> values)
: base(TestBindingSource, values)
{
}
public TestValueProvider(BindingSource bindingSource, IDictionary<string, object> values)
: base(bindingSource, values)
{
}
}
}

View File

@ -13,21 +13,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class CompositeValueProviderTests
{
public static IEnumerable<object[]> RegisteredAsMetadataClasses
{
get
{
yield return new object[] { new TestValueProviderMetadata() };
yield return new object[] { new DerivedValueBinderMetadata() };
}
}
[Fact]
public async Task GetKeysFromPrefixAsync_ReturnsResultFromFirstValueProviderThatReturnsValues()
{
// Arrange
var provider1 = Mock.Of<IValueProvider>();
var dictionary = new Dictionary<string, string>(StringComparer.Ordinal)
var dictionary = new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "prefix-test", "some-value" },
};
@ -62,19 +53,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Empty(values);
}
public static IEnumerable<object[]> BinderMetadata
{
get
{
yield return new object[] { new TestValueProviderMetadata() };
yield return new object[] { new DerivedValueProviderMetadata() };
}
}
[Theory]
[MemberData(nameof(RegisteredAsMetadataClasses))]
public void FilterReturnsItself_ForAnyClassRegisteredAsGenericParam(IValueProviderMetadata metadata)
[MemberData(nameof(BinderMetadata))]
public void FilterReturnsItself_ForAnyClassRegisteredAsGenericParam(IBindingSourceMetadata metadata)
{
// Arrange
var values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
var unrelatedMetadata = new UnrelatedValueBinderMetadata();
var valueProvider1 = GetMockValueProvider(metadata);
var valueProvider2 = GetMockValueProvider(unrelatedMetadata);
var valueProvider1 = GetMockValueProvider("Test");
var valueProvider2 = GetMockValueProvider("Unrelated");
var provider = new CompositeValueProvider(new List<IValueProvider>() { valueProvider1.Object, valueProvider2.Object });
// Act
var result = provider.Filter(metadata);
var result = provider.Filter(metadata.BindingSource);
// Assert
var valueProvider = Assert.IsType<CompositeValueProvider>(result);
@ -84,23 +85,45 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Same(valueProvider1.Object, filteredProvider);
}
private Mock<IMetadataAwareValueProvider> GetMockValueProvider(IValueProviderMetadata metadata)
private Mock<IBindingSourceValueProvider> GetMockValueProvider(string bindingSourceId)
{
var valueProvider = new Mock<IMetadataAwareValueProvider>();
valueProvider.Setup(o => o.Filter(metadata))
.Returns(valueProvider.Object);
var valueProvider = new Mock<IBindingSourceValueProvider>(MockBehavior.Strict);
valueProvider
.Setup(o => o.Filter(It.Is<BindingSource>(s => s.Id == bindingSourceId)))
.Returns(valueProvider.Object);
valueProvider
.Setup(o => o.Filter(It.Is<BindingSource>(s => s.Id != bindingSourceId)))
.Returns((IBindingSourceValueProvider)null);
return valueProvider;
}
private class TestValueProviderMetadata : IValueProviderMetadata
private class TestValueProviderMetadata : IBindingSourceMetadata
{
public BindingSource BindingSource
{
get
{
return new BindingSource("Test", displayName: null, isGreedy: true, isFromRequest: true);
}
}
}
private class DerivedValueProviderMetadata : TestValueProviderMetadata
{
}
private class DerivedValueBinderMetadata : TestValueProviderMetadata
{
}
private class UnrelatedValueBinderMetadata : IValueProviderMetadata
private class UnrelatedValueBinderMetadata : IBindingSourceMetadata
{
public BindingSource BindingSource
{
get
{
return new BindingSource("Unrelated", displayName: null, isGreedy: true, isFromRequest: true);
}
}
}
}
}

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
{ "test-key", "value" }
};
var provider = new DictionaryBasedValueProvider<TestValueProviderMetadata>(values);
var provider = new DictionaryBasedValueProvider(BindingSource.Query, values);
// Act
var result = await provider.GetValueAsync("not-test-key");
@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
{ "test-key", "test-value" }
};
var provider = new DictionaryBasedValueProvider<TestValueProviderMetadata>(values);
var provider = new DictionaryBasedValueProvider(BindingSource.Query, values);
// Act
var result = await provider.GetValueAsync("test-key");
@ -52,7 +52,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
{ "test-key", null }
};
var provider = new DictionaryBasedValueProvider<TestValueProviderMetadata>(values);
var provider = new DictionaryBasedValueProvider(BindingSource.Query, values);
// Act
var result = await provider.GetValueAsync("test-key");
@ -76,7 +76,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ "bar.baz", 1 },
};
var valueProvider = new DictionaryBasedValueProvider<TestValueProviderMetadata>(values);
var valueProvider = new DictionaryBasedValueProvider(BindingSource.Query, values);
// Act
var result = await valueProvider.ContainsPrefixAsync(prefix);
@ -97,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ "bar.baz", 2 },
};
var valueProvider = new DictionaryBasedValueProvider<TestValueProviderMetadata>(values);
var valueProvider = new DictionaryBasedValueProvider(BindingSource.Query, values);
// Act
var result = await valueProvider.GetValueAsync(prefix);
@ -115,7 +115,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ "bar.baz", 2 },
};
var valueProvider = new DictionaryBasedValueProvider<TestValueProviderMetadata>(values);
var valueProvider = new DictionaryBasedValueProvider(BindingSource.Query, values);
// Act
var result = await valueProvider.GetValueAsync("bar");
@ -132,7 +132,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
{ "test-key", "test-value" }
};
var provider = new DictionaryBasedValueProvider<TestValueProviderMetadata>(values);
var provider = new DictionaryBasedValueProvider(BindingSource.Query, values);
// Act
var result = await provider.ContainsPrefixAsync("not-test-key");
@ -149,7 +149,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
{ "test-key", "test-value" }
};
var provider = new DictionaryBasedValueProvider<TestValueProviderMetadata>(values);
var provider = new DictionaryBasedValueProvider(BindingSource.Query, values);
// Act
var result = await provider.ContainsPrefixAsync("test-key");
@ -158,37 +158,45 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.True(result);
}
public static IEnumerable<object[]> RegisteredAsMetadataClasses
{
get
{
yield return new object[] { new TestValueProviderMetadata() };
yield return new object[] { new DerivedValueProviderMetadata() };
}
}
[Theory]
[MemberData(nameof(RegisteredAsMetadataClasses))]
public void FilterReturnsItself_ForAnyClassRegisteredAsGenericParam(IValueProviderMetadata metadata)
[Fact]
public void FilterInclude()
{
// Arrange
var values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
var provider = new DictionaryBasedValueProvider<TestValueProviderMetadata>(values);
var provider = new DictionaryBasedValueProvider(BindingSource.Query, values);
var bindingSource = new BindingSource(
BindingSource.Query.Id,
displayName: null,
isGreedy: true,
isFromRequest: true);
// Act
var result = provider.Filter(metadata);
var result = provider.Filter(bindingSource);
// Assert
Assert.NotNull(result);
Assert.IsType<DictionaryBasedValueProvider<TestValueProviderMetadata>>(result);
Assert.Same(result, provider);
}
private class TestValueProviderMetadata : IValueProviderMetadata
[Fact]
public void FilterExclude()
{
}
// Arrange
var values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
var provider = new DictionaryBasedValueProvider(BindingSource.Query, values);
private class DerivedValueProviderMetadata : TestValueProviderMetadata
{
var bindingSource = new BindingSource(
"Test",
displayName: null,
isGreedy: true,
isFromRequest: true);
// Act
var result = provider.Filter(bindingSource);
// Assert
Assert.Null(result);
}
}
}

View File

@ -45,7 +45,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var result = factory.GetValueProvider(context);
// Assert
var valueProvider = Assert.IsType<ReadableStringCollectionValueProvider<IFormDataValueProviderMetadata>>(result);
var valueProvider = Assert.IsType<ReadableStringCollectionValueProvider>(result);
Assert.Equal(CultureInfo.CurrentCulture, valueProvider.Culture);
}

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
#if ASPNET50
[Fact]
public void GetValueProvider_ReturnsQueryStringValueProviderInstaceWithInvariantCulture()
public void GetValueProvider_ReturnsQueryStringValueProviderInstanceWithInvariantCulture()
{
// Arrange
var request = new Mock<HttpRequest>();
@ -34,7 +34,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var result = _factory.GetValueProvider(factoryContext);
// Assert
var valueProvider = Assert.IsType<ReadableStringCollectionValueProvider<IQueryValueProviderMetadata>>(result);
var valueProvider = Assert.IsType<ReadableStringCollectionValueProvider>(result);
Assert.Equal(CultureInfo.InvariantCulture, valueProvider.Culture);
}
#endif

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
// Arrange
var backingStore = new ReadableStringCollection(new Dictionary<string, string[]>());
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(backingStore, null);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, backingStore, null);
// Act
var result = await valueProvider.ContainsPrefixAsync("");
@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public async Task ContainsPrefixAsync_WithNonEmptyCollection_ReturnsTrueForEmptyPrefix()
{
// Arrange
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, null);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act
var result = await valueProvider.ContainsPrefixAsync("");
@ -53,7 +53,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public async Task ContainsPrefixAsync_WithNonEmptyCollection_ReturnsTrueForKnownPrefixes()
{
// Arrange
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, null);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act & Assert
Assert.True(await valueProvider.ContainsPrefixAsync("foo"));
@ -65,7 +65,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public async Task ContainsPrefixAsync_WithNonEmptyCollection_ReturnsFalseForUnknownPrefix()
{
// Arrange
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, null);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act
var result = await valueProvider.ContainsPrefixAsync("biff");
@ -85,7 +85,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ "null_value", "null_value" },
{ "prefix", "prefix" }
};
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, culture: null);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture: null);
// Act
var result = await valueProvider.GetKeysFromPrefixAsync("");
@ -98,7 +98,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public async Task GetKeysFromPrefixAsync_UnknownPrefix_ReturnsEmptyDictionary()
{
// Arrange
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, null);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act
var result = await valueProvider.GetKeysFromPrefixAsync("abc");
@ -111,7 +111,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public async Task GetKeysFromPrefixAsync_KnownPrefix_ReturnsMatchingItems()
{
// Arrange
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, null);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act
var result = await valueProvider.GetKeysFromPrefixAsync("bar");
@ -127,7 +127,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
// Arrange
var culture = new CultureInfo("fr-FR");
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, culture);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture);
// Act
var vpResult = await valueProvider.GetValueAsync("bar.baz");
@ -144,7 +144,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
// Arrange
var culture = new CultureInfo("fr-FR");
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, culture);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture);
// Act
var vpResult = await valueProvider.GetValueAsync("foo");
@ -163,7 +163,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
// Arrange
var culture = new CultureInfo("fr-FR");
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, culture);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, culture);
// Act
var result = await valueProvider.GetValueAsync(key);
@ -182,7 +182,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ "key", new string[] { null, null, "value" } }
});
var culture = new CultureInfo("fr-FR");
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(backingStore, culture);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, backingStore, culture);
// Act
var vpResult = await valueProvider.GetValueAsync("key");
@ -196,7 +196,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public async Task GetValueAsync_ReturnsNullIfKeyNotFound()
{
// Arrange
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, null);
var valueProvider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
// Act
var vpResult = await valueProvider.GetValueAsync("bar");
@ -205,36 +205,43 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Null(vpResult);
}
public static IEnumerable<object[]> RegisteredAsMetadataClasses
{
get
{
yield return new object[] { new TestValueProviderMetadata() };
yield return new object[] { new DerivedValueProviderMetadata() };
}
}
[Theory]
[MemberData(nameof(RegisteredAsMetadataClasses))]
public void FilterReturnsItself_ForAnyClassRegisteredAsGenericParam(IValueProviderMetadata metadata)
[Fact]
public void FilterInclude()
{
// Arrange
var valueProvider = new ReadableStringCollectionValueProvider<TestValueProviderMetadata>(_backingStore, null);
var provider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
var bindingSource = new BindingSource(
BindingSource.Query.Id,
displayName: null,
isGreedy: true,
isFromRequest: true);
// Act
var result = valueProvider.Filter(metadata);
var result = provider.Filter(bindingSource);
// Assert
Assert.NotNull(result);
Assert.IsType<ReadableStringCollectionValueProvider<TestValueProviderMetadata>>(result);
Assert.Same(result, provider);
}
private class TestValueProviderMetadata : IValueProviderMetadata
[Fact]
public void FilterExclude()
{
}
// Arrange
var provider = new ReadableStringCollectionValueProvider(BindingSource.Query, _backingStore, null);
private class DerivedValueProviderMetadata : TestValueProviderMetadata
{
var bindingSource = new BindingSource(
"Test",
displayName: null,
isGreedy: true,
isFromRequest: true);
// Act
var result = provider.Filter(bindingSource);
// Assert
Assert.Null(result);
}
}
}

View File

@ -15,7 +15,7 @@ namespace FormatterWebSite
{
var bodyParameter = context.ActionDescriptor
.Parameters
.FirstOrDefault(parameter => parameter.BinderMetadata is IFormatterBinderMetadata);
.FirstOrDefault(parameter => IsBodyBindingSource(parameter.BinderMetadata));
if (bodyParameter != null)
{
var parameterBindingErrors = context.ModelState[bodyParameter.Name].Errors;
@ -36,5 +36,11 @@ namespace FormatterWebSite
base.OnActionExecuting(context);
}
private bool IsBodyBindingSource(IBinderMetadata binderMetadata)
{
var bindingSource = (binderMetadata as IBindingSourceMetadata)?.BindingSource;
return bindingSource?.CanAcceptDataFrom(BindingSource.Body) ?? false;
}
}
}

View File

@ -6,8 +6,16 @@ using Microsoft.AspNet.Mvc.ModelBinding;
namespace ModelBindingWebSite
{
public class FromTestAttribute : Attribute, IBinderMetadata
public class FromTestAttribute : Attribute, IBindingSourceMetadata
{
public static readonly BindingSource TestBindingSource = new BindingSource(
"Test",
displayName: null,
isGreedy: true,
isFromRequest: true);
public BindingSource BindingSource { get { return TestBindingSource; } }
public object Value { get; set; }
}
}

View File

@ -22,7 +22,7 @@ namespace ModelBindingWebSite
.Configure<MvcOptions>(m =>
{
m.MaxModelValidationErrors = 8;
m.ModelBinders.Insert(0, typeof(TestMetadataAwareBinder));
m.ModelBinders.Insert(0, typeof(TestBindingSourceModelBinder));
m.AddXmlDataContractSerializerFormatter();
});

View File

@ -8,10 +8,16 @@ using Microsoft.AspNet.Mvc.ModelBinding;
namespace ModelBindingWebSite
{
public class TestMetadataAwareBinder : MetadataAwareBinder<FromTestAttribute>
public class TestBindingSourceModelBinder : BindingSourceModelBinder
{
protected override Task<bool> BindAsync(ModelBindingContext bindingContext, FromTestAttribute metadata)
public TestBindingSourceModelBinder()
: base(FromTestAttribute.TestBindingSource)
{
}
protected override Task BindModelCoreAsync(ModelBindingContext bindingContext)
{
var metadata = (FromTestAttribute)bindingContext.ModelMetadata.BinderMetadata;
bindingContext.Model = metadata.Value;
if (!IsSimpleType(bindingContext.ModelType))
@ -22,7 +28,6 @@ namespace ModelBindingWebSite
return Task.FromResult(true);
}
private bool IsSimpleType(Type type)
{
return type.GetTypeInfo().IsPrimitive ||