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 @@ public class BindingSource : IEquatable 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 @@ internal static string FormatBindingSource_Services() 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 @@ public void Configure(MvcOptions options) 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 @@ public void Setup_SetsUpMetadataDetailsProviders() 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);