diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/BindingSource.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/BindingSource.cs
index dc08b98db8..87791e85da 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/BindingSource.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/BindingSource.cs
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
@@ -86,6 +87,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
isGreedy: true,
isFromRequest: false);
+ ///
+ /// A for special parameter types that are not user input.
+ ///
+ public static readonly BindingSource Special = new BindingSource(
+ "Special",
+ Resources.BindingSource_Special,
+ isGreedy: true,
+ isFromRequest: false);
+
+ ///
+ /// A for and .
+ ///
+ public static readonly BindingSource FormFile = new BindingSource(
+ "FormFile",
+ Resources.BindingSource_FormFile,
+ isGreedy: true,
+ isFromRequest: true);
+
///
/// Creates a new .
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Properties/Resources.Designer.cs
index fd85073dde..3cd4baf115 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Properties/Resources.Designer.cs
@@ -170,6 +170,38 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions
return GetString("BindingSource_Services");
}
+ ///
+ /// Special
+ ///
+ internal static string BindingSource_Special
+ {
+ get { return GetString("BindingSource_Special"); }
+ }
+
+ ///
+ /// Special
+ ///
+ internal static string FormatBindingSource_Special()
+ {
+ return GetString("BindingSource_Special");
+ }
+
+ ///
+ /// FormFile
+ ///
+ internal static string BindingSource_FormFile
+ {
+ get { return GetString("BindingSource_FormFile"); }
+ }
+
+ ///
+ /// FormFile
+ ///
+ internal static string FormatBindingSource_FormFile()
+ {
+ return GetString("BindingSource_FormFile");
+ }
+
///
/// ModelBinding
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Abstractions/Resources.resx
index 6368aa3629..224ec4161d 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/Resources.resx
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Resources.resx
@@ -168,4 +168,10 @@
The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources.
+
+ Special
+
+
+ FormFile
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs
index 91e138301b..9261c0e6f3 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs
@@ -79,6 +79,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider());
options.ModelMetadataDetailsProviders.Add(new DefaultValidationMetadataProvider());
+ options.ModelMetadataDetailsProviders.Add(new BindingSourceMetadataProvider(typeof(CancellationToken), BindingSource.Special));
+ options.ModelMetadataDetailsProviders.Add(new BindingSourceMetadataProvider(typeof(IFormFile), BindingSource.FormFile));
+ options.ModelMetadataDetailsProviders.Add(new BindingSourceMetadataProvider(typeof(IFormCollection), BindingSource.FormFile));
+
// Set up validators
options.ModelValidatorProviders.Add(new DefaultModelValidatorProvider());
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/BindingSourceMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/BindingSourceMetadataProvider.cs
new file mode 100644
index 0000000000..52f84aed6d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/BindingSourceMetadataProvider.cs
@@ -0,0 +1,50 @@
+// Copyright (c) .NET Foundation. 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.Reflection;
+using System.Threading;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
+{
+ public class BindingSourceMetadataProvider : IBindingMetadataProvider
+ {
+ ///
+ /// Creates a new for the given .
+ ///
+ ///
+ /// The . The provider sets of the given or
+ /// anything assignable to the given .
+ ///
+ ///
+ /// The to assign to the given .
+ ///
+ public BindingSourceMetadataProvider(Type type, BindingSource bindingSource)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
+
+ Type = type;
+ BindingSource = bindingSource;
+ }
+
+ public Type Type { get; }
+ public BindingSource BindingSource { get; }
+
+ ///
+ public void CreateBindingMetadata(BindingMetadataProviderContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (Type.IsAssignableFrom(context.Key.ModelType))
+ {
+ context.BindingMetadata.BindingSource = BindingSource;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/BindingSourceMetadataProviderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/BindingSourceMetadataProviderTests.cs
new file mode 100644
index 0000000000..0d255fa660
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/BindingSourceMetadataProviderTests.cs
@@ -0,0 +1,31 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
+{
+ public class BindingSourceMetadataProviderTests
+ {
+ [Fact]
+ public void CreateBindingMetadata_ForMatchingType_SetsBindingSource()
+ {
+ // Arrange
+ var provider = new BindingSourceMetadataProvider(typeof(Test), BindingSource.Special);
+
+ var key = ModelMetadataIdentity.ForType(typeof(Test));
+
+ var context = new BindingMetadataProviderContext(key, new ModelAttributes(new object[0], new object[0]));
+
+ // Act
+ provider.CreateBindingMetadata(context);
+
+ // Assert
+ Assert.Equal(BindingSource.Special, context.BindingMetadata.BindingSource);
+ }
+
+ private class Test
+ {
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BindingSourceMetadataProviderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BindingSourceMetadataProviderIntegrationTest.cs
new file mode 100644
index 0000000000..62e09f867d
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BindingSourceMetadataProviderIntegrationTest.cs
@@ -0,0 +1,152 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Internal;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.IntegrationTests
+{
+ public class BindingSourceMetadataProviderIntegrationTest
+ {
+ [Fact]
+ public async Task BindParameter_WithCancellationToken_BindingSourceSpecial()
+ {
+ // Arrange
+ var options = new MvcOptions();
+ var setup = new MvcCoreMvcOptionsSetup(new TestHttpRequestStreamReaderFactory());
+
+ options.ModelBinderProviders.Insert(0, new CancellationTokenModelBinderProvider());
+
+ setup.Configure(options);
+
+ var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(options);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "Parameter1",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(CancellationTokenBundle),
+ };
+
+ var testContext = ModelBindingTestHelper.GetTestContext(request =>
+ {
+ request.Form = new FormCollection(new Dictionary
+ {
+ { "name", new[] { "Fred" } }
+ });
+ });
+
+ var modelState = testContext.ModelState;
+ var token = testContext.HttpContext.RequestAborted;
+
+ // Act
+ var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var boundPerson = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(boundPerson);
+ Assert.Equal("Fred", boundPerson.Name);
+ Assert.Equal(token, boundPerson.Token);
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ }
+
+ private class CancellationTokenBundle
+ {
+ public string Name { get; set; }
+
+ public CancellationToken Token { get; set; }
+ }
+
+ [Fact]
+ public async Task BindParameter_WithFormFile_BindingSourceFormFile()
+ {
+ // Arrange
+ var options = new MvcOptions();
+ var setup = new MvcCoreMvcOptionsSetup(new TestHttpRequestStreamReaderFactory());
+
+ options.ModelBinderProviders.Insert(0, new FormFileModelBinderProvider());
+
+ setup.Configure(options);
+
+ var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(options);
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "Parameter1",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(FormFileBundle),
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ request.QueryString = QueryString.Create("Name", "Fred");
+ UpdateRequest(request, data, "File");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var boundPerson = Assert.IsType(modelBindingResult.Model);
+ Assert.Equal("Fred", boundPerson.Name);
+ Assert.Equal("text.txt", boundPerson.File.FileName);
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ }
+
+ private class FormFileBundle
+ {
+ public string Name { get; set; }
+
+ public IFormFile File { get; set; }
+ }
+
+ private void UpdateRequest(HttpRequest request, string data, string name)
+ {
+ const string fileName = "text.txt";
+ var fileCollection = new FormFileCollection();
+ var formCollection = new FormCollection(new Dictionary(), fileCollection);
+
+ request.Form = formCollection;
+ request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq";
+
+ if (string.IsNullOrEmpty(data) || string.IsNullOrEmpty(name))
+ {
+ // Leave the submission empty.
+ return;
+ }
+
+ request.Headers["Content-Disposition"] = $"form-data; name={name}; filename={fileName}";
+
+ var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(data));
+ fileCollection.Add(new FormFile(memoryStream, 0, data.Length, name, fileName)
+ {
+ Headers = request.Headers
+ });
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs
index 69450a51c3..2b166e10db 100644
--- a/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs
@@ -162,6 +162,24 @@ namespace Microsoft.AspNetCore.Mvc
provider => Assert.IsType(provider),
provider => Assert.IsType(provider),
provider =>
+ {
+ var specialParameter = Assert.IsType(provider);
+ Assert.Equal(typeof(CancellationToken), specialParameter.Type);
+ Assert.Equal(BindingSource.Special, specialParameter.BindingSource);
+ },
+ provider =>
+ {
+ var formFileParameter = Assert.IsType(provider);
+ Assert.Equal(typeof(IFormFile), formFileParameter.Type);
+ Assert.Equal(BindingSource.FormFile, formFileParameter.BindingSource);
+ },
+ provider =>
+ {
+ var formCollectionParameter = Assert.IsType(provider);
+ Assert.Equal(typeof(IFormCollection), formCollectionParameter.Type);
+ Assert.Equal(BindingSource.FormFile, formCollectionParameter.BindingSource);
+ },
+ provider =>
{
var excludeFilter = Assert.IsType(provider);
Assert.Equal(typeof(Type), excludeFilter.Type);