diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs index 31ec6028f3..14935089b7 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs @@ -27,9 +27,9 @@ namespace Microsoft.AspNet.Mvc.Description public ApiParameterRouteInfo RouteInfo { get; set; } /// - /// Gets or sets the . + /// Gets or sets the . /// - public ApiParameterSource Source { get; set; } + public BindingSource Source { get; set; } /// /// Gets or sets the parameter type. diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterRouteInfo.cs b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterRouteInfo.cs index 7379f42048..12caf8d4ad 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterRouteInfo.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterRouteInfo.cs @@ -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 for the value of + /// If the parameter uses for the value of /// then the value may also come from the /// URL query string or form data. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs deleted file mode 100644 index 8cd5f9886b..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs +++ /dev/null @@ -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 -{ - /// - /// A metadata description of the source of an for an HTTP request. - /// - [DebuggerDisplay("Source: {DisplayName}")] - public class ApiParameterSource : IEquatable - { - /// - /// An for the request body. - /// - public static readonly ApiParameterSource Body = new ApiParameterSource( - "Body", - Resources.ApiParameterSource_Body); - - /// - /// An for a custom model binder (unknown data source). - /// - public static readonly ApiParameterSource Custom = new ApiParameterSource( - "Custom", - Resources.ApiParameterSource_Custom); - - /// - /// An for the request form-data. - /// - public static readonly ApiParameterSource Form = new ApiParameterSource( - "Form", - Resources.ApiParameterSource_Form); - - /// - /// An for the request headers. - /// - public static readonly ApiParameterSource Header = new ApiParameterSource( - "Header", - Resources.ApiParameterSource_Header); - - /// - /// An for a parameter that should be hidden. Used when - /// a parameter cannot be set with user input. - /// - public static readonly ApiParameterSource Hidden = new ApiParameterSource( - "Hidden", - Resources.ApiParameterSource_Hidden); - - /// - /// An for model binding. Includes form-data, query-string - /// and headers from the request. - /// - public static readonly ApiParameterSource ModelBinding = new ApiParameterSource( - "ModelBinding", - Resources.ApiParameterSource_ModelBinding); - - /// - /// An for the request url path. - /// - public static readonly ApiParameterSource Path = new ApiParameterSource( - "Path", - Resources.ApiParameterSource_Path); - - /// - /// An for the request query-string. - /// - public static readonly ApiParameterSource Query = new ApiParameterSource( - "Query", - Resources.ApiParameterSource_Query); - - /// - /// Creates a new . - /// - /// The id. Used for comparison. - /// The display name. - public ApiParameterSource([NotNull] string id, string displayName) - { - Id = id; - DisplayName = displayName; - } - - /// - /// Gets the display name. - /// - public string DisplayName { get; } - - /// - /// Gets the id. - /// - public string Id { get; } - - /// - public bool Equals(ApiParameterSource other) - { - return other == null ? false : string.Equals(other.Id, Id, StringComparison.Ordinal); - } - - /// - public override bool Equals(object obj) - { - return Equals(obj as ApiParameterSource); - } - - /// - public override int GetHashCode() - { - return Id.GetHashCode(); - } - - /// - public static bool operator ==(ApiParameterSource s1, ApiParameterSource s2) - { - if (object.ReferenceEquals(s1, null)) - { - return object.ReferenceEquals(s2, null); ; - } - - return s1.Equals(s2); - } - - /// - public static bool operator !=(ApiParameterSource s1, ApiParameterSource s2) - { - return !(s1 == s2); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs index 3f7e297942..6f60cfb80f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs @@ -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. /// /// The metadata for the model. - /// The from the ambient context. + /// The from the ambient context. /// The current name prefix (to prepend to property names). /// /// true if the set of 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. /// - 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; diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs index aff83a06c5..e6f23cf57b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs @@ -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 { /// - /// Represents a model binder which understands and uses - /// InputFomatters to bind the model to request's body. + /// An which binds models from the request body using an + /// when a model has the binding source / /// - public class BodyModelBinder : MetadataAwareBinder + public class BodyModelBinder : BindingSourceModelBinder { private readonly ActionContext _actionContext; private readonly IScopedInstance _bindingContext; @@ -21,11 +22,22 @@ namespace Microsoft.AspNet.Mvc private readonly IBodyModelValidator _bodyModelValidator; private readonly IValidationExcludeFiltersProvider _bodyValidationExcludeFiltersProvider; + /// + /// Creates a new . + /// + /// An accessor to the . + /// An accessor to the . + /// The . + /// The . + /// + /// The . + /// public BodyModelBinder([NotNull] IScopedInstance context, [NotNull] IScopedInstance 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 BindAsync( - ModelBindingContext bindingContext, - IFormatterBinderMetadata metadata) + /// + 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; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinders/ServicesModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/ServicesModelBinder.cs index d12a81c8ef..07f4f809b4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinders/ServicesModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/ServicesModelBinder.cs @@ -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 { /// - /// An which understands - /// and activates a given model using . + /// An which binds models from the request services when a model + /// has the binding source / /// - public class ServicesModelBinder : MetadataAwareBinder + public class ServicesModelBinder : BindingSourceModelBinder { + /// + /// Creates a new . + /// + public ServicesModelBinder() + : base(BindingSource.Services) + { + } + /// - protected override Task 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); diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index ba86b70e93..2199f4decc 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1594,134 +1594,6 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("ResponseCache_SpecifyDuration"), p0, p1); } - /// - /// Body - /// - internal static string ApiParameterSource_Body - { - get { return GetString("ApiParameterSource_Body"); } - } - - /// - /// Body - /// - internal static string FormatApiParameterSource_Body() - { - return GetString("ApiParameterSource_Body"); - } - - /// - /// Custom - /// - internal static string ApiParameterSource_Custom - { - get { return GetString("ApiParameterSource_Custom"); } - } - - /// - /// Custom - /// - internal static string FormatApiParameterSource_Custom() - { - return GetString("ApiParameterSource_Custom"); - } - - /// - /// Header - /// - internal static string ApiParameterSource_Header - { - get { return GetString("ApiParameterSource_Header"); } - } - - /// - /// Header - /// - internal static string FormatApiParameterSource_Header() - { - return GetString("ApiParameterSource_Header"); - } - - /// - /// Hidden - /// - internal static string ApiParameterSource_Hidden - { - get { return GetString("ApiParameterSource_Hidden"); } - } - - /// - /// Hidden - /// - internal static string FormatApiParameterSource_Hidden() - { - return GetString("ApiParameterSource_Hidden"); - } - - /// - /// ModelBinding - /// - internal static string ApiParameterSource_ModelBinding - { - get { return GetString("ApiParameterSource_ModelBinding"); } - } - - /// - /// ModelBinding - /// - internal static string FormatApiParameterSource_ModelBinding() - { - return GetString("ApiParameterSource_ModelBinding"); - } - - /// - /// Path - /// - internal static string ApiParameterSource_Path - { - get { return GetString("ApiParameterSource_Path"); } - } - - /// - /// Path - /// - internal static string FormatApiParameterSource_Path() - { - return GetString("ApiParameterSource_Path"); - } - - /// - /// Query - /// - internal static string ApiParameterSource_Query - { - get { return GetString("ApiParameterSource_Query"); } - } - - /// - /// Query - /// - internal static string FormatApiParameterSource_Query() - { - return GetString("ApiParameterSource_Query"); - } - - /// - /// Form - /// - internal static string ApiParameterSource_Form - { - get { return GetString("ApiParameterSource_Form"); } - } - - /// - /// Form - /// - internal static string FormatApiParameterSource_Form() - { - return GetString("ApiParameterSource_Form"); - } - /// /// The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 2c556ba58d..78a8b4faf5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -424,30 +424,6 @@ If the '{0}' property is not set to true, '{1}' property must be specified. - - Body - - - Custom - - - Header - - - Hidden - - - ModelBinding - - - Path - - - Query - - - Form - The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/BindingSource.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/BindingSource.cs new file mode 100644 index 0000000000..caa895d429 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/BindingSource.cs @@ -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 +{ + /// + /// A metadata object representing a source of data for model binding. + /// + [DebuggerDisplay("Source: {DisplayName}")] + public class BindingSource : IEquatable + { + /// + /// A for the request body. + /// + public static readonly BindingSource Body = new BindingSource( + "Body", + Resources.BindingSource_Body, + isGreedy: true, + isFromRequest: true); + + /// + /// A for a custom model binder (unknown data source). + /// + public static readonly BindingSource Custom = new BindingSource( + "Custom", + Resources.BindingSource_Custom, + isGreedy: true, + isFromRequest: true); + + /// + /// A for the request form-data. + /// + public static readonly BindingSource Form = new BindingSource( + "Form", + Resources.BindingSource_Form, + isGreedy: false, + isFromRequest: true); + + /// + /// A for the request headers. + /// + public static readonly BindingSource Header = new BindingSource( + "Header", + Resources.BindingSource_Header, + isGreedy: true, + isFromRequest: true); + + /// + /// A for model binding. Includes form-data, query-string + /// and route data from the request. + /// + public static readonly BindingSource ModelBinding = new BindingSource( + "ModelBinding", + Resources.BindingSource_ModelBinding, + isGreedy: false, + isFromRequest: true); + + /// + /// A for the request url path. + /// + public static readonly BindingSource Path = new BindingSource( + "Path", + Resources.BindingSource_Path, + isGreedy: false, + isFromRequest: true); + + /// + /// A for the request query-string. + /// + public static readonly BindingSource Query = new BindingSource( + "Query", + Resources.BindingSource_Query, + isGreedy: false, + isFromRequest: true); + + /// + /// A for request services. + /// + public static readonly BindingSource Services = new BindingSource( + "Services", + Resources.BindingSource_Services, + isGreedy: true, + isFromRequest: false); + + /// + /// Creates a new . + /// + /// The id, a unique identifier. + /// The display name. + /// A value indicating whether or not the source is greedy. + /// + /// A value indicating whether or not the data comes from the HTTP request. + /// + public BindingSource([NotNull] string id, string displayName, bool isGreedy, bool isFromRequest) + { + Id = id; + DisplayName = displayName; + IsGreedy = isGreedy; + IsFromRequest = isFromRequest; + } + + /// + /// Gets the display name for the source. + /// + public string DisplayName { get; } + + /// + /// Gets the unique identifier for the source. Sources are compared based on their Id. + /// + public string Id { get; } + + /// + /// 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. + /// + /// + /// + /// For sources based on a , setting to false + /// 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. + /// + /// + /// Set to true for most custom implementations. + /// + /// + /// If a source represents an which will recursively traverse a model's properties + /// and bind them individually using , then set to + /// true. + /// + /// + public bool IsGreedy { get; } + + /// + /// Gets a value indicating whether or not the binding source uses input from the current HTTP request. + /// + /// + /// Some sources (like ) are based on application state and not user + /// input. These are excluded by default from ApiExplorer diagnostics. + /// + public bool IsFromRequest { get; } + + /// + /// Gets a value indicating whether or not the can accept + /// data from . + /// + /// The to consider as input. + /// True if the source is compatible, otherwise false. + /// + /// 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. + /// + 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; + } + + /// + public bool Equals(BindingSource other) + { + return other == null ? false : string.Equals(other.Id, Id, StringComparison.Ordinal); + } + + /// + public override bool Equals(object obj) + { + return Equals(obj as BindingSource); + } + + /// + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + /// + public static bool operator ==(BindingSource s1, BindingSource s2) + { + if (object.ReferenceEquals(s1, null)) + { + return object.ReferenceEquals(s2, null); ; + } + + return s1.Equals(s2); + } + + /// + 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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/CompositeBindingSource.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/CompositeBindingSource.cs new file mode 100644 index 0000000000..4ef74c0702 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/CompositeBindingSource.cs @@ -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 +{ + /// + /// A which can repesent multiple value-provider data sources. + /// + public class CompositeBindingSource : BindingSource + { + /// + /// Creates a new . + /// + /// + /// The set of entries. + /// Must be value-provider sources and user input. + /// + /// The display name for the composite source. + /// A . + public static CompositeBindingSource Create( + [NotNull] IEnumerable 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 bindingSources) + : base(id, displayName, isGreedy: false, isFromRequest: true) + { + BindingSources = bindingSources; + } + + /// + /// Gets the set of entries. + /// + public IEnumerable BindingSources { get; } + + /// + 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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromBodyAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromBodyAttribute.cs index bd7c644df3..24b90cc6c1 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromBodyAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromBodyAttribute.cs @@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { /// - /// 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. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromBodyAttribute : Attribute, IFormatterBinderMetadata + public class FromBodyAttribute : Attribute, IBindingSourceMetadata { + /// + public BindingSource BindingSource { get { return BindingSource.Body; } } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs index 79191054fb..891a1af3de 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs @@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { /// - /// 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. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromFormAttribute : Attribute, IFormDataValueProviderMetadata + public class FromFormAttribute : Attribute, IBindingSourceMetadata { + /// + public BindingSource BindingSource { get { return BindingSource.Form; } } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromHeaderAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromHeaderAttribute.cs index b77d402fad..de30b842c7 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromHeaderAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromHeaderAttribute.cs @@ -7,12 +7,14 @@ using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { /// - /// 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. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromHeaderAttribute : Attribute, IHeaderBinderMetadata, IModelNameProvider + public class FromHeaderAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider { + /// + public BindingSource BindingSource { get { return BindingSource.Header; } } + /// public string Name { get; set; } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs index 8268a74ec2..3ae9be21ed 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs @@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { /// - /// 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. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromQueryAttribute : Attribute, IQueryValueProviderMetadata + public class FromQueryAttribute : Attribute, IBindingSourceMetadata { + /// + public BindingSource BindingSource { get { return BindingSource.Query; } } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs index 613667dc1e..ecbcf923dd 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs @@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { /// - /// 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. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromRouteAttribute : Attribute, IRouteDataValueProviderMetadata + public class FromRouteAttribute : Attribute, IBindingSourceMetadata { + /// + public BindingSource BindingSource { get { return BindingSource.Path; } } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromServicesAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromServicesAttribute.cs index ba84b2fe81..b10f85771d 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromServicesAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromServicesAttribute.cs @@ -7,11 +7,12 @@ using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { /// - /// 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. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromServicesAttribute : Attribute, IServiceActivatorBinderMetadata + public class FromServicesAttribute : Attribute, IBindingSourceMetadata { + /// + public BindingSource BindingSource { get { return BindingSource.Services; } } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBinderTypeProviderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBinderTypeProviderMetadata.cs index 4b1b680afa..43fad56e6e 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBinderTypeProviderMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBinderTypeProviderMetadata.cs @@ -9,12 +9,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// Provides a which implements or /// . /// - public interface IBinderTypeProviderMetadata : IBinderMetadata + public interface IBinderTypeProviderMetadata : IBindingSourceMetadata { /// /// A which implements either or /// . /// - Type BinderType { get; set; } + Type BinderType { get; } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBindingSourceMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBindingSourceMetadata.cs new file mode 100644 index 0000000000..271ae88f69 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBindingSourceMetadata.cs @@ -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 +{ + /// + /// Metadata which specificies the data source for model binding. + /// + public interface IBindingSourceMetadata : IBinderMetadata + { + /// + /// Gets the . + /// + /// + /// The is metadata which can be used to determine which data + /// sources are valid for model binding of a property or parameter. + /// + BindingSource BindingSource { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IFormDataValueProviderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IFormDataValueProviderMetadata.cs deleted file mode 100644 index f4ee5cbde7..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IFormDataValueProviderMetadata.cs +++ /dev/null @@ -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 -{ - /// - /// Metadata interface that indicates model binding should use only form data value providers. - /// - public interface IFormDataValueProviderMetadata : IValueProviderMetadata - { - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IFormatterBinderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IFormatterBinderMetadata.cs deleted file mode 100644 index 3a5c8399ca..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IFormatterBinderMetadata.cs +++ /dev/null @@ -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 -{ - /// - /// Metadata interface that indicates model binding should be performed by an input formatter. - /// - public interface IFormatterBinderMetadata : IBinderMetadata - { - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IHeaderBinderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IHeaderBinderMetadata.cs deleted file mode 100644 index 6954375006..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IHeaderBinderMetadata.cs +++ /dev/null @@ -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 -{ - /// - /// Metadata interface that indicates model binding should use a header value for - /// the data source of a property or parameter. - /// - public interface IHeaderBinderMetadata : IBinderMetadata - { - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IQueryValueProviderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IQueryValueProviderMetadata.cs deleted file mode 100644 index 7e81a950f6..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IQueryValueProviderMetadata.cs +++ /dev/null @@ -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 -{ - /// - /// Metadata interface that indicates model binding should use only query string value providers. - /// - public interface IQueryValueProviderMetadata : IValueProviderMetadata - { - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IRouteDataValueProviderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IRouteDataValueProviderMetadata.cs deleted file mode 100644 index 2ee89fcb17..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IRouteDataValueProviderMetadata.cs +++ /dev/null @@ -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 -{ - /// - /// Metadata interface that indicates model binding should use only route data value providers. - /// - public interface IRouteDataValueProviderMetadata : IValueProviderMetadata - { - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IServiceActivatorBinderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IServiceActivatorBinderMetadata.cs deleted file mode 100644 index 5b18b12fc2..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IServiceActivatorBinderMetadata.cs +++ /dev/null @@ -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 -{ - /// - /// Metadata interface that indicates model binding should use the service container to get the value for a model. - /// - public interface IServiceActivatorBinderMetadata : IBinderMetadata - { - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IValueProviderMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IValueProviderMetadata.cs deleted file mode 100644 index 825387df99..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IValueProviderMetadata.cs +++ /dev/null @@ -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 -{ - /// - /// Interface for metadata related to value providers. - /// - public interface IValueProviderMetadata : IBinderMetadata - { - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/ModelBinderAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/ModelBinderAttribute.cs index f8a3f848e6..bd94966bdb 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/ModelBinderAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/ModelBinderAttribute.cs @@ -28,6 +28,7 @@ namespace Microsoft.AspNet.Mvc public class ModelBinderAttribute : Attribute, IModelNameProvider, IBinderTypeProviderMetadata { private Type _binderType; + private BindingSource _bindingSource; /// public Type BinderType @@ -55,6 +56,24 @@ namespace Microsoft.AspNet.Mvc } } + /// + public BindingSource BindingSource + { + get + { + if (_bindingSource == null && _binderType != null) + { + return BindingSource.Custom; + } + + return _bindingSource; + } + set + { + _bindingSource = value; + } + } + /// public string Name { get; set; } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingSourceModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingSourceModelBinder.cs new file mode 100644 index 0000000000..b89d479c98 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingSourceModelBinder.cs @@ -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 +{ + /// + /// An which provides data from a specific . + /// + /// + /// + /// A is an base-implementation which + /// can provide data for all parameters and model properties which specify the corresponding + /// . + /// + /// + /// is greedy, meaning that a given instance expects to handle all + /// parameters and properties annotated with the corresponding and + /// will short-circuit the model binding process to prevent other binders from running. + /// of must be set to true. + /// + /// + public abstract class BindingSourceModelBinder : IModelBinder + { + /// + /// Creates a new . + /// + /// + /// The . Must be a single-source (non-composite) with + /// equal to true. + /// + 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; + } + + /// + /// Gets the corresponding . + /// + protected BindingSource BindingSource { get; } + + /// + /// Binds the model. Called when the model's supported binding-source matches . + /// + /// The . + /// + /// A which will complete when model binding has completed. + /// + protected abstract Task BindModelCoreAsync([NotNull] ModelBindingContext bindingContext); + + /// + public async Task 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; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs index 58607e18ae..a39638b1cc 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs @@ -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; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs index 24c8a6dff5..89c0192458 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs @@ -9,15 +9,21 @@ using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { /// - /// A which uses - /// to bind the model. + /// An which binds models from the request headers when a model + /// has the binding source / /// - public class HeaderModelBinder : MetadataAwareBinder + public class HeaderModelBinder : BindingSourceModelBinder { + /// + /// Creates a new . + /// + public HeaderModelBinder() + : base(BindingSource.Header) + { + } + /// - protected override Task 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); } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/IMetadataAwareBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/IMetadataAwareBinder.cs deleted file mode 100644 index 15005ff4f8..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/IMetadataAwareBinder.cs +++ /dev/null @@ -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 -{ - /// - /// An which is aware of . - /// - public interface IMetadataAwareBinder : IModelBinder - { - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MetadataAwareBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MetadataAwareBinder.cs deleted file mode 100644 index 0bf3c4cfbe..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MetadataAwareBinder.cs +++ /dev/null @@ -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 -{ - /// - /// Represents an which can select itself based on the - /// . - /// - /// Represents a type implementing - public abstract class MetadataAwareBinder : IMetadataAwareBinder - where TBinderMetadata : IBinderMetadata - { - /// - /// Async function which does the actual binding to bind to a particular model. - /// - /// The binding context which has the object to be bound. - /// The associated with the current binder. - /// A Task with a bool implying the success or failure of the operation. - protected abstract Task BindAsync([NotNull] ModelBindingContext bindingContext, - [NotNull] TBinderMetadata metadata); - - public Task BindModelAsync(ModelBindingContext context) - { - if (context.ModelMetadata.BinderMetadata is TBinderMetadata) - { - var metadata = (TBinderMetadata)context.ModelMetadata.BinderMetadata; - return BindAsync(context, metadata); - } - - return Task.FromResult(false); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 9101233e1a..7c6a4458d8 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -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 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 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); } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BodyBindingState.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BodyBindingState.cs index f621c8ebbd..35e19993bb 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/BodyBindingState.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BodyBindingState.cs @@ -15,13 +15,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding NotBodyBased, /// - /// Represents if there is a that + /// Represents if there is a that /// has been found during the current model binding process. /// FormatterBased, /// - /// Represents if there is a that + /// Represents if there is a that /// has been found during the current model binding process. /// FormBased diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs index 7a071820bf..ccb8a02331 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs @@ -474,6 +474,198 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderUtil_ValueInvalidGeneric"), p0); } + /// + /// Body + /// + internal static string BindingSource_Body + { + get { return GetString("BindingSource_Body"); } + } + + /// + /// Body + /// + internal static string FormatBindingSource_Body() + { + return GetString("BindingSource_Body"); + } + + /// + /// Custom + /// + internal static string BindingSource_Custom + { + get { return GetString("BindingSource_Custom"); } + } + + /// + /// Custom + /// + internal static string FormatBindingSource_Custom() + { + return GetString("BindingSource_Custom"); + } + + /// + /// Form + /// + internal static string BindingSource_Form + { + get { return GetString("BindingSource_Form"); } + } + + /// + /// Form + /// + internal static string FormatBindingSource_Form() + { + return GetString("BindingSource_Form"); + } + + /// + /// Header + /// + internal static string BindingSource_Header + { + get { return GetString("BindingSource_Header"); } + } + + /// + /// Header + /// + internal static string FormatBindingSource_Header() + { + return GetString("BindingSource_Header"); + } + + /// + /// Services + /// + internal static string BindingSource_Services + { + get { return GetString("BindingSource_Services"); } + } + + /// + /// Services + /// + internal static string FormatBindingSource_Services() + { + return GetString("BindingSource_Services"); + } + + /// + /// ModelBinding + /// + internal static string BindingSource_ModelBinding + { + get { return GetString("BindingSource_ModelBinding"); } + } + + /// + /// ModelBinding + /// + internal static string FormatBindingSource_ModelBinding() + { + return GetString("BindingSource_ModelBinding"); + } + + /// + /// Path + /// + internal static string BindingSource_Path + { + get { return GetString("BindingSource_Path"); } + } + + /// + /// Path + /// + internal static string FormatBindingSource_Path() + { + return GetString("BindingSource_Path"); + } + + /// + /// Query + /// + internal static string BindingSource_Query + { + get { return GetString("BindingSource_Query"); } + } + + /// + /// Query + /// + internal static string FormatBindingSource_Query() + { + return GetString("BindingSource_Query"); + } + + /// + /// The provided binding source '{0}' is a composite. '{1}' requires that the source must represent a single type of input. + /// + internal static string BindingSource_CannotBeComposite + { + get { return GetString("BindingSource_CannotBeComposite"); } + } + + /// + /// The provided binding source '{0}' is a composite. '{1}' requires that the source must represent a single type of input. + /// + internal static string FormatBindingSource_CannotBeComposite(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_CannotBeComposite"), p0, p1); + } + + /// + /// The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request. + /// + internal static string BindingSource_MustBeFromRequest + { + get { return GetString("BindingSource_MustBeFromRequest"); } + } + + /// + /// The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request. + /// + internal static string FormatBindingSource_MustBeFromRequest(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeFromRequest"), p0, p1); + } + + /// + /// The provided binding source '{0}' is a greedy data source. '{1}' does not support greedy data sources. + /// + internal static string BindingSource_CannotBeGreedy + { + get { return GetString("BindingSource_CannotBeGreedy"); } + } + + /// + /// The provided binding source '{0}' is a greedy data source. '{1}' does not support greedy data sources. + /// + internal static string FormatBindingSource_CannotBeGreedy(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_CannotBeGreedy"), p0, p1); + } + + /// + /// The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources. + /// + internal static string BindingSource_MustBeGreedy + { + get { return GetString("BindingSource_MustBeGreedy"); } + } + + /// + /// The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources. + /// + 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); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx index dbb8698d8f..fa74ff2293 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx @@ -204,4 +204,40 @@ The supplied value is invalid for {0}. + + Body + + + Custom + + + Form + + + Header + + + Services + + + ModelBinding + + + Path + + + Query + + + The provided binding source '{0}' is a composite. '{1}' requires that the source must represent a single type of input. + + + The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request. + + + The provided binding source '{0}' is a greedy data source. '{1}' does not support greedy data sources. + + + The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/BindingSourceValueProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/BindingSourceValueProvider.cs new file mode 100644 index 0000000000..8416654d2d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/BindingSourceValueProvider.cs @@ -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 +{ + /// + /// A value provider which provides data from a specific . + /// + /// + /// + /// A is an base-implementation which + /// can provide data for all parameters and model properties which specify the corresponding + /// . + /// + /// + /// implements and will + /// include or exclude itself from the set of value providers based on the model's associated + /// . Value providers are by-default included; if a model does not + /// specify a then all value providers are valid. + /// + /// + public abstract class BindingSourceValueProvider : IBindingSourceValueProvider + { + /// + /// Creates a new . + /// + /// + /// The . Must be a single-source (non-composite) with + /// equal to false. + /// + 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; + } + + /// + /// Gets the corresponding . + /// + protected BindingSource BindingSource { get; } + + /// + public abstract Task ContainsPrefixAsync(string prefix); + + /// + public abstract Task GetValueAsync(string key); + + /// + public virtual IValueProvider Filter(BindingSource bindingSource) + { + if (bindingSource.CanAcceptDataFrom(BindingSource)) + { + return this; + } + else + { + return null; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/CompositeValueProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/CompositeValueProvider.cs index 71d8a14916..02a4fc3a29 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/CompositeValueProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/CompositeValueProvider.cs @@ -12,8 +12,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// Represents a whose values come from a collection of s. /// - public class CompositeValueProvider - : Collection, IEnumerableValueProvider, IMetadataAwareValueProvider + public class CompositeValueProvider : + Collection, + IEnumerableValueProvider, + IBindingSourceValueProvider { /// /// Initializes a new instance of . @@ -122,12 +124,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } /// - public IValueProvider Filter(IValueProviderMetadata valueBinderMetadata) + public IValueProvider Filter(BindingSource bindingSource) { var filteredValueProviders = new List(); - foreach (var valueProvider in this.OfType()) + foreach (var valueProvider in this.OfType()) { - var result = valueProvider.Filter(valueBinderMetadata); + var result = valueProvider.Filter(bindingSource); if (result != null) { filteredValueProviders.Add(result); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/DictionaryBasedValueProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/DictionaryBasedValueProvider.cs index 22307ffad9..cf55d74584 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/DictionaryBasedValueProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/DictionaryBasedValueProvider.cs @@ -8,17 +8,29 @@ using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { - public class DictionaryBasedValueProvider : MetadataAwareValueProvider - where TBinderMetadata : IValueProviderMetadata + /// + /// An adapter for data stored in an + /// . + /// + public class DictionaryBasedValueProvider: BindingSourceValueProvider { private readonly IDictionary _values; private PrefixContainer _prefixContainer; - public DictionaryBasedValueProvider(IDictionary values) + /// + /// Creates a new . + /// + /// The of the data. + /// The values. + public DictionaryBasedValueProvider( + [NotNull] BindingSource bindingSource, + [NotNull] IDictionary values) + : base(bindingSource) { _values = values; } + /// public override Task ContainsPrefixAsync(string key) { var prefixContainer = GetOrCreatePrefixContainer(); @@ -35,6 +47,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return _prefixContainer; } + /// public override Task GetValueAsync([NotNull] string key) { object value; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs index 84212ec7ad..7b3f372d0a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs @@ -16,7 +16,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { var culture = GetCultureInfo(request); - return new ReadableStringCollectionValueProvider( + return new ReadableStringCollectionValueProvider( + BindingSource.Form, async () => await request.ReadFormAsync(), culture); } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/IBindingSourceValueProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/IBindingSourceValueProvider.cs new file mode 100644 index 0000000000..cec9fadda7 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/IBindingSourceValueProvider.cs @@ -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 +{ + /// + /// A value provider which is which can filter its contents based on . + /// + /// + /// Value providers are by-default included. If a model does not specify a + /// then all value providers are valid. + /// + public interface IBindingSourceValueProvider : IValueProvider + { + /// + /// Filters the value provider based on . + /// + /// The associated with a model. + /// + /// The filtered value provider, or null if the value provider does not match + /// . + /// + IValueProvider Filter([NotNull] BindingSource bindingSource); + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/IMetadataAwareValueProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/IMetadataAwareValueProvider.cs deleted file mode 100644 index bb3832d0e5..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/IMetadataAwareValueProvider.cs +++ /dev/null @@ -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 -{ - /// - /// A value provider which is aware of . - /// - public interface IMetadataAwareValueProvider : IValueProvider - { - /// - /// Filters the value provider based on . - /// - /// The associated with a model. - /// The filtered value provider. - IValueProvider Filter([NotNull] IValueProviderMetadata metadata); - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/MetadataAwareValueProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/MetadataAwareValueProvider.cs deleted file mode 100644 index 6d27eb6768..0000000000 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/MetadataAwareValueProvider.cs +++ /dev/null @@ -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 -{ - /// - /// A value provider which can filter - /// based on . - /// - /// - /// Represents a type implementing - /// - public abstract class MetadataAwareValueProvider : IMetadataAwareValueProvider - where TBinderMetadata : IValueProviderMetadata - { - public abstract Task ContainsPrefixAsync(string prefix); - - public abstract Task GetValueAsync(string key); - - public virtual IValueProvider Filter(IValueProviderMetadata valueBinderMetadata) - { - if (valueBinderMetadata is TBinderMetadata) - { - return this; - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/QueryStringValueProviderFactory.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/QueryStringValueProviderFactory.cs index a55a97a33f..8afd649abb 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/QueryStringValueProviderFactory.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/QueryStringValueProviderFactory.cs @@ -18,13 +18,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding if (!storage.TryGetValue(_cacheKey, out value)) { var queryCollection = context.HttpContext.Request.Query; - provider = new ReadableStringCollectionValueProvider(queryCollection, - CultureInfo.InvariantCulture); + provider = new ReadableStringCollectionValueProvider( + BindingSource.Query, + queryCollection, + CultureInfo.InvariantCulture); + storage[_cacheKey] = provider; } else { - provider = (ReadableStringCollectionValueProvider)value; + provider = (ReadableStringCollectionValueProvider)value; } return provider; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ReadableStringCollectionValueProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ReadableStringCollectionValueProvider.cs index c6e6136120..ef4727b7cf 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ReadableStringCollectionValueProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ReadableStringCollectionValueProvider.cs @@ -11,9 +11,10 @@ using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { - public class ReadableStringCollectionValueProvider : - MetadataAwareValueProvider, IEnumerableValueProvider - where TBinderMetadata : IValueProviderMetadata + /// + /// An adapter for data stored in an . + /// + public class ReadableStringCollectionValueProvider : BindingSourceValueProvider, IEnumerableValueProvider { private readonly CultureInfo _culture; private readonly Func> _valuesFactory; @@ -23,9 +24,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// Creates a provider for wrapping an existing set of key value pairs. /// + /// The for the data. /// The key value pairs to wrap. /// The culture to return with ValueProviderResult instances. - 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 wrapping an /// existing set of key value pairs provided by the delegate. /// + /// The for the data. /// The delegate that provides the key value pairs to wrap. /// The culture to return with ValueProviderResult instances. - public ReadableStringCollectionValueProvider([NotNull] Func> valuesFactory, - CultureInfo culture) + public ReadableStringCollectionValueProvider( + [NotNull] BindingSource bindingSource, + [NotNull] Func> valuesFactory, + CultureInfo culture) + : base(bindingSource) { _valuesFactory = valuesFactory; _culture = culture; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/RouteValueValueProviderFactory.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/RouteValueValueProviderFactory.cs index b70f6551f8..1ec0265785 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/RouteValueValueProviderFactory.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/RouteValueValueProviderFactory.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public IValueProvider GetValueProvider([NotNull] ValueProviderFactoryContext context) { - return new DictionaryBasedValueProvider(context.RouteValues); + return new DictionaryBasedValueProvider(BindingSource.Path, context.RouteValues); } } } diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/OverloadActionConstraint.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/OverloadActionConstraint.cs index d8efebb3bf..c77a9994e7 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/OverloadActionConstraint.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/OverloadActionConstraint.cs @@ -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; diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ParameterBinding/FromUriAttribute.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ParameterBinding/FromUriAttribute.cs index 9c78de7ce5..54928897fe 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ParameterBinding/FromUriAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ParameterBinding/FromUriAttribute.cs @@ -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; } /// diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs index 986908598c..51d9fdff57 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Properties/Resources.Designer.cs @@ -170,6 +170,22 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim return string.Format(CultureInfo.CurrentCulture, GetString("CreatedAtRoute_RouteFailed"), p0); } + /// + /// URL + /// + internal static string BindingSource_URL + { + get { return GetString("BindingSource_URL"); } + } + + /// + /// URL + /// + internal static string FormatBindingSource_URL() + { + return GetString("BindingSource_URL"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx index 7ca73030e8..f4448a3035 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Resources.resx @@ -1,17 +1,17 @@  - @@ -147,4 +147,7 @@ Failed to generate a URL using route '{0}'. + + URL + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/BodyModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/BodyModelBinderTests.cs index a144a3f65e..29e86d034b 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/BodyModelBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/BodyModelBinderTests.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc .Verifiable(); var bindingContext = GetBindingContext(typeof(Person), inputFormatter: mockInputFormatter.Object); - bindingContext.ModelMetadata.BinderMetadata = Mock.Of(); + 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(); + 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(); + metadata.SetupGet(m => m.BindingSource).Returns(BindingSource.Body); + var bindingContext = GetBindingContext(typeof(Person), inputFormatter: null); - bindingContext.ModelMetadata.BinderMetadata = useBody ? Mock.Of() : - Mock.Of(); + 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(); + 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(); + 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) diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs index 5117d68c81..87f118a533 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs @@ -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(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); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs index 23c83e57f9..9eda87caa1 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs @@ -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) })] diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs index 293c6bb8e0..d918ec44d8 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs @@ -70,7 +70,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test { { "", null } }; - var valueProvider = new DictionaryBasedValueProvider(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(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(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(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(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 diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/TestValueProvider.cs b/test/Microsoft.AspNet.Mvc.Core.Test/TestValueProvider.cs new file mode 100644 index 0000000000..b63fce6502 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/TestValueProvider.cs @@ -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 values) + : base(TestBindingSource, values) + { + } + + public TestValueProvider(BindingSource bindingSource, IDictionary values) + : base(bindingSource, values) + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs index c832cfa5e0..7b5a102bc1 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs @@ -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); } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BindingSourceModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BindingSourceModelBinderTest.cs new file mode 100644 index 0000000000..df559821ab --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BindingSourceModelBinderTest.cs @@ -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( + () => 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); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BindingSourceValueProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BindingSourceValueProviderTest.cs new file mode 100644 index 0000000000..bae6afb0ff --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BindingSourceValueProviderTest.cs @@ -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( + () => 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( + () => 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 ContainsPrefixAsync(string prefix) + { + throw new NotImplementedException(); + } + + public override Task GetValueAsync(string key) + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/HeaderModelBinderTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/HeaderModelBinderTests.cs index baa92719bf..aedc02d95d 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/HeaderModelBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/HeaderModelBinderTests.cs @@ -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; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs index 67c2d6cf97..a2cfb48043 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs @@ -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(); mockValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) .Returns(Task.FromResult(false)); - var mockOriginalValueProvider = new Mock(); - mockOriginalValueProvider.Setup(o => o.ContainsPrefixAsync(It.IsAny())) - .Returns(Task.FromResult(originalValueProviderProvidesValue)); - mockOriginalValueProvider.Setup(o => o.Filter(It.IsAny())) - .Returns( - valueProviderMetadata => - { - if (valueProviderMetadata is ValueBinderMetadataAttribute) - { - return mockOriginalValueProvider.Object; - } + var mockOriginalValueProvider = new Mock(); + mockOriginalValueProvider + .Setup(o => o.ContainsPrefixAsync(It.IsAny())) + .Returns(Task.FromResult(originalValueProviderProvidesValue)); - return null; - }); + mockOriginalValueProvider + .Setup(o => o.Filter(It.IsAny())) + .Returns(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 diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/BindingSourceTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/BindingSourceTest.cs new file mode 100644 index 0000000000..a99d693190 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/BindingSourceTest.cs @@ -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( + () => 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); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsMetadataAttributesTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsMetadataAttributesTest.cs index b26ac03f34..fda9e0d1f0 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsMetadataAttributesTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsMetadataAttributesTest.cs @@ -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 diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs index 9fe52fc77d..3adf5b448d 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CachedDataAnnotationsModelMetadataTest.cs @@ -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 diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CompositeBindingSourceTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CompositeBindingSourceTest.cs new file mode 100644 index 0000000000..d183ddfbcd --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CompositeBindingSourceTest.cs @@ -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( + () => 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); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelBinderAttributeTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelBinderAttributeTest.cs index 612d9b3926..ee720736de 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelBinderAttributeTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelBinderAttributeTest.cs @@ -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); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/TestValueProvider.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/TestValueProvider.cs new file mode 100644 index 0000000000..b63fce6502 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/TestValueProvider.cs @@ -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 values) + : base(TestBindingSource, values) + { + } + + public TestValueProvider(BindingSource bindingSource, IDictionary values) + : base(bindingSource, values) + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/CompositeValueProviderTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/CompositeValueProviderTests.cs index f59b3f2cef..a01133fc9e 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/CompositeValueProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/CompositeValueProviderTests.cs @@ -13,21 +13,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public class CompositeValueProviderTests { - public static IEnumerable 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(); - var dictionary = new Dictionary(StringComparer.Ordinal) + var dictionary = new Dictionary(StringComparer.Ordinal) { { "prefix-test", "some-value" }, }; @@ -62,19 +53,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.Empty(values); } + public static IEnumerable 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(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() { valueProvider1.Object, valueProvider2.Object }); // Act - var result = provider.Filter(metadata); + var result = provider.Filter(metadata.BindingSource); // Assert var valueProvider = Assert.IsType(result); @@ -84,23 +85,45 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.Same(valueProvider1.Object, filteredProvider); } - private Mock GetMockValueProvider(IValueProviderMetadata metadata) + private Mock GetMockValueProvider(string bindingSourceId) { - var valueProvider = new Mock(); - valueProvider.Setup(o => o.Filter(metadata)) - .Returns(valueProvider.Object); + var valueProvider = new Mock(MockBehavior.Strict); + + valueProvider + .Setup(o => o.Filter(It.Is(s => s.Id == bindingSourceId))) + .Returns(valueProvider.Object); + + valueProvider + .Setup(o => o.Filter(It.Is(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); + } + } } } } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/DictionaryBasedValueProviderTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/DictionaryBasedValueProviderTests.cs index 837587fff8..3c22974459 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/DictionaryBasedValueProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/DictionaryBasedValueProviderTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { { "test-key", "value" } }; - var provider = new DictionaryBasedValueProvider(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(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(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(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(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(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(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(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 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(StringComparer.OrdinalIgnoreCase); - var provider = new DictionaryBasedValueProvider(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>(result); + Assert.Same(result, provider); } - private class TestValueProviderMetadata : IValueProviderMetadata + [Fact] + public void FilterExclude() { - } + // Arrange + var values = new Dictionary(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); } } } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs index 22a591eb3c..164ee849bc 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs @@ -45,7 +45,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var result = factory.GetValueProvider(context); // Assert - var valueProvider = Assert.IsType>(result); + var valueProvider = Assert.IsType(result); Assert.Equal(CultureInfo.CurrentCulture, valueProvider.Culture); } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/QueryStringValueProviderFactoryTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/QueryStringValueProviderFactoryTest.cs index 55132645a2..a40693d0c0 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/QueryStringValueProviderFactoryTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/QueryStringValueProviderFactoryTest.cs @@ -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(); @@ -34,7 +34,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var result = _factory.GetValueProvider(factoryContext); // Assert - var valueProvider = Assert.IsType>(result); + var valueProvider = Assert.IsType(result); Assert.Equal(CultureInfo.InvariantCulture, valueProvider.Culture); } #endif diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ReadableStringCollectionValueProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ReadableStringCollectionValueProviderTest.cs index bb637d710b..88d8e51729 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ReadableStringCollectionValueProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/ReadableStringCollectionValueProviderTest.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test { // Arrange var backingStore = new ReadableStringCollection(new Dictionary()); - var valueProvider = new ReadableStringCollectionValueProvider(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(_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(_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(_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(_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(_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(_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(_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(_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(_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(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(_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 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(_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>(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); } } } diff --git a/test/WebSites/FormatterWebSite/ValidateBodyParameterAttribute.cs b/test/WebSites/FormatterWebSite/ValidateBodyParameterAttribute.cs index e8bbc43271..5bf33467e1 100644 --- a/test/WebSites/FormatterWebSite/ValidateBodyParameterAttribute.cs +++ b/test/WebSites/FormatterWebSite/ValidateBodyParameterAttribute.cs @@ -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; + } } } diff --git a/test/WebSites/ModelBindingWebSite/FromTestAttribute.cs b/test/WebSites/ModelBindingWebSite/FromTestAttribute.cs index 5cf413fd77..ca4264fa7f 100644 --- a/test/WebSites/ModelBindingWebSite/FromTestAttribute.cs +++ b/test/WebSites/ModelBindingWebSite/FromTestAttribute.cs @@ -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; } } } \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Startup.cs b/test/WebSites/ModelBindingWebSite/Startup.cs index d300caa990..9d1931cb04 100644 --- a/test/WebSites/ModelBindingWebSite/Startup.cs +++ b/test/WebSites/ModelBindingWebSite/Startup.cs @@ -22,7 +22,7 @@ namespace ModelBindingWebSite .Configure(m => { m.MaxModelValidationErrors = 8; - m.ModelBinders.Insert(0, typeof(TestMetadataAwareBinder)); + m.ModelBinders.Insert(0, typeof(TestBindingSourceModelBinder)); m.AddXmlDataContractSerializerFormatter(); }); diff --git a/test/WebSites/ModelBindingWebSite/TestMetadataAwareBinder.cs b/test/WebSites/ModelBindingWebSite/TestBindingSourceModelBinder.cs similarity index 73% rename from test/WebSites/ModelBindingWebSite/TestMetadataAwareBinder.cs rename to test/WebSites/ModelBindingWebSite/TestBindingSourceModelBinder.cs index c87a39fc3e..abb7512c59 100644 --- a/test/WebSites/ModelBindingWebSite/TestMetadataAwareBinder.cs +++ b/test/WebSites/ModelBindingWebSite/TestBindingSourceModelBinder.cs @@ -8,10 +8,16 @@ using Microsoft.AspNet.Mvc.ModelBinding; namespace ModelBindingWebSite { - public class TestMetadataAwareBinder : MetadataAwareBinder + public class TestBindingSourceModelBinder : BindingSourceModelBinder { - protected override Task 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 ||