From 12f8f23ccb15c1143d58170ebe233021ed217dcf Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 26 Jan 2015 14:31:38 -0800 Subject: [PATCH] Make BindingSource extensible This is a major refactor of how IBinderMetadata interacts with model binders and value providers. We're doing this to support better extensibility for metadata in ApiExplorer. You'll notice a bunch of deleted code in DefaultApiDescriptionProvider that maps metadata marker interfaces to a fixed list of Api sources. This is replaced now with IBindingSourceMetadata - which also replaces the hierarchy of marker interfaces. Now user code can create an arbitrary binding source and have a consistent API for model-binders, value-providers and full-visibility in ApiExplorer as well. Additonally, there's some error checking in place that better enforces the constraints we already have in the system. IE you can't create a 'greedy' model binder that uses value-provider data. Two additional enhancements are planned for followup PRs: 1. Add a BindingSource property to model-metadata. This will remove some duplication, but I want to delay it because it would touch another 10 or so files. 2. Add an extensibility interface for our 'special' model binders like the file binder so these can show up in ApiExplorer as well. --- .../Description/ApiParameterDescription.cs | 4 +- .../Description/ApiParameterRouteInfo.cs | 2 +- .../Description/ApiParameterSource.cs | 130 ----------- .../DefaultApiDescriptionProvider.cs | 88 ++------ .../ModelBinders/BodyModelBinder.cs | 30 ++- .../ModelBinders/ServicesModelBinder.cs | 22 +- .../Properties/Resources.Designer.cs | 128 ----------- src/Microsoft.AspNet.Mvc.Core/Resources.resx | 24 -- .../BinderMetadata/BindingSource.cs | 213 ++++++++++++++++++ .../BinderMetadata/CompositeBindingSource.cs | 95 ++++++++ .../BinderMetadata/FromBodyAttribute.cs | 7 +- .../BinderMetadata/FromFormAttribute.cs | 7 +- .../BinderMetadata/FromHeaderAttribute.cs | 8 +- .../BinderMetadata/FromQueryAttribute.cs | 7 +- .../BinderMetadata/FromRouteAttribute.cs | 7 +- .../BinderMetadata/FromServicesAttribute.cs | 7 +- .../IBinderTypeProviderMetadata.cs | 4 +- .../BinderMetadata/IBindingSourceMetadata.cs | 22 ++ .../IFormDataValueProviderMetadata.cs | 12 - .../IFormatterBinderMetadata.cs | 12 - .../BinderMetadata/IHeaderBinderMetadata.cs | 13 -- .../IQueryValueProviderMetadata.cs | 12 - .../IRouteDataValueProviderMetadata.cs | 12 - .../IServiceActivatorBinderMetadata.cs | 12 - .../BinderMetadata/IValueProviderMetadata.cs | 12 - .../BinderMetadata/ModelBinderAttribute.cs | 19 ++ .../Binders/BindingSourceModelBinder.cs | 83 +++++++ .../Binders/CompositeModelBinder.cs | 49 ++-- .../Binders/HeaderModelBinder.cs | 23 +- .../Binders/IMetadataAwareBinder.cs | 12 - .../Binders/MetadataAwareBinder.cs | 36 --- .../Binders/MutableObjectModelBinder.cs | 68 ++++-- .../BodyBindingState.cs | 4 +- .../Properties/Resources.Designer.cs | 192 ++++++++++++++++ .../Resources.resx | 36 +++ .../BindingSourceValueProvider.cs | 79 +++++++ .../ValueProviders/CompositeValueProvider.cs | 12 +- .../DictionaryBasedValueProvider.cs | 19 +- .../FormValueProviderFactory.cs | 3 +- .../IBindingSourceValueProvider.cs | 25 ++ .../IMetadataAwareValueProvider.cs | 18 -- .../MetadataAwareValueProvider.cs | 32 --- .../QueryStringValueProviderFactory.cs | 9 +- .../ReadableStringCollectionValueProvider.cs | 22 +- .../RouteValueValueProviderFactory.cs | 2 +- .../OverloadActionConstraint.cs | 12 +- .../ParameterBinding/FromUriAttribute.cs | 10 +- .../Properties/Resources.Designer.cs | 16 ++ .../Resources.resx | 57 ++--- .../BodyModelBinderTests.cs | 58 ++++- .../DefaultApiDescriptionProviderTest.cs | 74 +++--- .../ControllerActionArgumentBinderTests.cs | 6 +- .../ModelBindingHelperTest.cs | 14 +- .../TestValueProvider.cs | 26 +++ .../ApiExplorerTest.cs | 28 +-- .../Binders/BindingSourceModelBinderTest.cs | 116 ++++++++++ .../Binders/BindingSourceValueProviderTest.cs | 97 ++++++++ .../Binders/HeaderModelBinderTests.cs | 3 +- .../Binders/MutableObjectModelBinderTest.cs | 38 ++-- .../Metadata/BindingSourceTest.cs | 51 +++++ ...edDataAnnotationsMetadataAttributesTest.cs | 2 + .../CachedDataAnnotationsModelMetadataTest.cs | 2 + .../Metadata/CompositeBindingSourceTest.cs | 66 ++++++ .../Metadata/ModelBinderAttributeTest.cs | 42 ++++ .../TestValueProvider.cs | 26 +++ .../CompositeValueProviderTests.cs | 75 +++--- .../DictionaryBasedValueProviderTests.cs | 62 ++--- .../FormValueProviderFactoryTests.cs | 2 +- .../QueryStringValueProviderFactoryTest.cs | 4 +- ...adableStringCollectionValueProviderTest.cs | 69 +++--- .../ValidateBodyParameterAttribute.cs | 8 +- .../ModelBindingWebSite/FromTestAttribute.cs | 10 +- test/WebSites/ModelBindingWebSite/Startup.cs | 2 +- ...der.cs => TestBindingSourceModelBinder.cs} | 11 +- 74 files changed, 1759 insertions(+), 861 deletions(-) delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/BindingSource.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/CompositeBindingSource.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IBindingSourceMetadata.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IFormDataValueProviderMetadata.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IFormatterBinderMetadata.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IHeaderBinderMetadata.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IQueryValueProviderMetadata.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IRouteDataValueProviderMetadata.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IServiceActivatorBinderMetadata.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/IValueProviderMetadata.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/BindingSourceModelBinder.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/IMetadataAwareBinder.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MetadataAwareBinder.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/BindingSourceValueProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/IBindingSourceValueProvider.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/IMetadataAwareValueProvider.cs delete mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/MetadataAwareValueProvider.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/TestValueProvider.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BindingSourceModelBinderTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/BindingSourceValueProviderTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/BindingSourceTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/CompositeBindingSourceTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/TestValueProvider.cs rename test/WebSites/ModelBindingWebSite/{TestMetadataAwareBinder.cs => TestBindingSourceModelBinder.cs} (73%) 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 ||