diff --git a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs
index 170ebbbbe38e..45990e19ff48 100644
--- a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs
+++ b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs
@@ -258,7 +258,7 @@ await Client.HubConnection.InvokeAsync(
await ValidateClientKeepsWorking(Client, batches);
}
- [Fact]
+ [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12962")]
public async Task LogsJSInteropCompletionsCallbacksAndContinuesWorkingInAllSituations()
{
// Arrange
diff --git a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs
index 9f3472518dc4..edcfcd4271c5 100644
--- a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs
+++ b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs
@@ -2560,6 +2560,17 @@ public partial class EmptyModelMetadataProvider : Microsoft.AspNetCore.Mvc.Model
{
public EmptyModelMetadataProvider() : base (default(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ICompositeMetadataDetailsProvider)) { }
}
+ public sealed partial class FormFileValueProvider : Microsoft.AspNetCore.Mvc.ModelBinding.IValueProvider
+ {
+ public FormFileValueProvider(Microsoft.AspNetCore.Http.IFormFileCollection files) { }
+ public bool ContainsPrefix(string prefix) { throw null; }
+ public Microsoft.AspNetCore.Mvc.ModelBinding.ValueProviderResult GetValue(string key) { throw null; }
+ }
+ public sealed partial class FormFileValueProviderFactory : Microsoft.AspNetCore.Mvc.ModelBinding.IValueProviderFactory
+ {
+ public FormFileValueProviderFactory() { }
+ public System.Threading.Tasks.Task CreateValueProviderAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ValueProviderFactoryContext context) { throw null; }
+ }
public partial class FormValueProvider : Microsoft.AspNetCore.Mvc.ModelBinding.BindingSourceValueProvider, Microsoft.AspNetCore.Mvc.ModelBinding.IEnumerableValueProvider, Microsoft.AspNetCore.Mvc.ModelBinding.IValueProvider
{
public FormValueProvider(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource bindingSource, Microsoft.AspNetCore.Http.IFormCollection values, System.Globalization.CultureInfo culture) : base (default(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource)) { }
diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs
index 1f246bdbd5f1..a2c0dbdcaebb 100644
--- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs
+++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs
@@ -96,6 +96,7 @@ public void Configure(MvcOptions options)
options.ValueProviderFactories.Add(new RouteValueProviderFactory());
options.ValueProviderFactories.Add(new QueryStringValueProviderFactory());
options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory());
+ options.ValueProviderFactories.Add(new FormFileValueProviderFactory());
// Set up metadata providers
ConfigureAdditionalModelMetadataDetailsProviders(options.ModelMetadataDetailsProviders);
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProvider.cs
new file mode 100644
index 000000000000..bf976f5b03e3
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProvider.cs
@@ -0,0 +1,68 @@
+// 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.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding
+{
+ ///
+ /// An adapter for data stored in an .
+ ///
+ ///
+ /// Unlike most instances, does not provide any values, but
+ /// specifically responds to queries. This allows the model binding system to
+ /// recurse in to deeply nested object graphs with only values for form files.
+ ///
+ public sealed class FormFileValueProvider : IValueProvider
+ {
+ private readonly IFormFileCollection _files;
+ private PrefixContainer _prefixContainer;
+
+ ///
+ /// Creates a value provider for .
+ ///
+ /// The .
+ public FormFileValueProvider(IFormFileCollection files)
+ {
+ _files = files ?? throw new ArgumentNullException(nameof(files));
+ }
+
+ private PrefixContainer PrefixContainer
+ {
+ get
+ {
+ _prefixContainer ??= CreatePrefixContainer(_files);
+ return _prefixContainer;
+ }
+ }
+
+ private static PrefixContainer CreatePrefixContainer(IFormFileCollection formFiles)
+ {
+ var fileNames = new List();
+ var count = formFiles.Count;
+ for (var i = 0; i < count; i++)
+ {
+ var file = formFiles[i];
+
+ // If there is an in the form and is left blank.
+ // This matches the filtering behavior from FormFileModelBinder
+ if (file.Length == 0 && string.IsNullOrEmpty(file.FileName))
+ {
+ continue;
+ }
+
+ fileNames.Add(file.Name);
+ }
+
+ return new PrefixContainer(fileNames);
+ }
+
+ ///
+ public bool ContainsPrefix(string prefix) => PrefixContainer.ContainsPrefix(prefix);
+
+ ///
+ public ValueProviderResult GetValue(string key) => ValueProviderResult.None;
+ }
+}
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs
new file mode 100644
index 000000000000..0f3ae8b8ff89
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs
@@ -0,0 +1,43 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding
+{
+ ///
+ /// A for .
+ ///
+ public sealed class FormFileValueProviderFactory : IValueProviderFactory
+ {
+ ///
+ public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var request = context.ActionContext.HttpContext.Request;
+ if (request.HasFormContentType)
+ {
+ // Allocating a Task only when the body is multipart form.
+ return AddValueProviderAsync(context, request);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static async Task AddValueProviderAsync(ValueProviderFactoryContext context, HttpRequest request)
+ {
+ var formCollection = await request.ReadFormAsync();
+ if (formCollection.Files.Count > 0)
+ {
+ var valueProvider = new FormFileValueProvider(formCollection.Files);
+ context.ValueProviders.Add(valueProvider);
+ }
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj
index 39df066d8802..1552b9c6e49d 100644
--- a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj
+++ b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj
@@ -2,6 +2,7 @@
netcoreapp3.0
+ Microsoft.AspNetCore.Mvc
diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs
new file mode 100644
index 000000000000..c0030a0ba522
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs
@@ -0,0 +1,73 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding
+{
+ public class FormFileValueProviderFactoryTest
+ {
+ [Fact]
+ public async Task CreateValueProviderAsync_DoesNotAddValueProvider_IfRequestDoesNotHaveFormContent()
+ {
+ // Arrange
+ var factory = new FormFileValueProviderFactory();
+ var context = CreateContext("application/json");
+
+ // Act
+ await factory.CreateValueProviderAsync(context);
+
+ // Assert
+ Assert.Empty(context.ValueProviders);
+ }
+
+ [Fact]
+ public async Task CreateValueProviderAsync_DoesNotAddValueProvider_IfFileCollectionIsEmpty()
+ {
+ // Arrange
+ var factory = new FormFileValueProviderFactory();
+ var context = CreateContext("multipart/form-data");
+
+ // Act
+ await factory.CreateValueProviderAsync(context);
+
+ // Assert
+ Assert.Empty(context.ValueProviders);
+ }
+
+ [Fact]
+ public async Task CreateValueProviderAsync_AddsValueProvider()
+ {
+ // Arrange
+ var factory = new FormFileValueProviderFactory();
+ var context = CreateContext("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq");
+ var files = (FormFileCollection)context.ActionContext.HttpContext.Request.Form.Files;
+ files.Add(new FormFile(Stream.Null, 0, 10, "some-name", "some-name"));
+
+ // Act
+ await factory.CreateValueProviderAsync(context);
+
+ // Assert
+ Assert.Collection(
+ context.ValueProviders,
+ v => Assert.IsType(v));
+ }
+
+ private static ValueProviderFactoryContext CreateContext(string contentType)
+ {
+ var context = new DefaultHttpContext();
+ context.Request.ContentType = contentType;
+ context.Request.Form = new FormCollection(new Dictionary(), new FormFileCollection());
+ var actionContext = new ActionContext(context, new RouteData(), new ActionDescriptor());
+
+ return new ValueProviderFactoryContext(actionContext);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderTest.cs
new file mode 100644
index 000000000000..7be36b91ff97
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderTest.cs
@@ -0,0 +1,71 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding
+{
+ public class FormFileValueProviderTest
+ {
+ [Fact]
+ public void ContainsPrefix_ReturnsFalse_IfFileIs0LengthAndFileNameIsEmpty()
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.ContentType = "multipart/form-data";
+ var formFiles = new FormFileCollection();
+ formFiles.Add(new FormFile(Stream.Null, 0, 0, "file", fileName: null));
+ httpContext.Request.Form = new FormCollection(new Dictionary(), formFiles);
+
+ var valueProvider = new FormFileValueProvider(formFiles);
+
+ // Act
+ var result = valueProvider.ContainsPrefix("file");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_ReturnsTrue_IfFileExists()
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.ContentType = "multipart/form-data";
+ var formFiles = new FormFileCollection();
+ formFiles.Add(new FormFile(Stream.Null, 0, 10, "file", "file"));
+ httpContext.Request.Form = new FormCollection(new Dictionary(), formFiles);
+
+ var valueProvider = new FormFileValueProvider(formFiles);
+
+ // Act
+ var result = valueProvider.ContainsPrefix("file");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void GetValue_ReturnsNoneResult()
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.ContentType = "multipart/form-data";
+ var formFiles = new FormFileCollection();
+ formFiles.Add(new FormFile(Stream.Null, 0, 10, "file", "file"));
+ httpContext.Request.Form = new FormCollection(new Dictionary(), formFiles);
+
+ var valueProvider = new FormFileValueProvider(formFiles);
+
+ // Act
+ var result = valueProvider.GetValue("file");
+
+ // Assert
+ Assert.Equal(ValueProviderResult.None, result);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs
index a615d9267513..48f74b2113ea 100644
--- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs
+++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs
@@ -1,5 +1,4 @@
-
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.Text.Encodings.Web;
diff --git a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs
index 00eda811ead3..6b686831183b 100644
--- a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs
+++ b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs
@@ -82,7 +82,8 @@ public void Setup_SetsUpValueProviders()
provider => Assert.IsType(provider),
provider => Assert.IsType(provider),
provider => Assert.IsType(provider),
- provider => Assert.IsType(provider));
+ provider => Assert.IsType(provider),
+ provider => Assert.IsType(provider));
}
[Fact]
diff --git a/src/Mvc/test/Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs
index a7ff6eeba30d..92ea35e3193f 100644
--- a/src/Mvc/test/Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs
+++ b/src/Mvc/test/Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@@ -75,6 +76,397 @@ public async Task BindProperty_WithData_WithEmptyPrefix_GetsBound()
Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState);
}
+ [Fact]
+ public async Task BindProperty_WithOnlyFormFile_WithEmptyPrefix()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "Parameter1",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(Person)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ UpdateRequest(request, data, "Address.File");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var boundPerson = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(boundPerson.Address);
+ var file = Assert.IsAssignableFrom(boundPerson.Address.File);
+ Assert.Equal("form-data; name=Address.File; filename=text.txt", file.ContentDisposition);
+ using var reader = new StreamReader(boundPerson.Address.File.OpenReadStream());
+ Assert.Equal(data, reader.ReadToEnd());
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Collection(
+ modelState.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("Address.File", kvp.Key);
+ Assert.Null(value.RawValue);
+ Assert.Empty(value.Errors);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ });
+ }
+
+ [Fact]
+ public async Task BindProperty_WithOnlyFormFile_WithPrefix()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "Parameter1",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(Person)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ UpdateRequest(request, data, "Parameter1.Address.File");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var boundPerson = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(boundPerson.Address);
+ var file = Assert.IsAssignableFrom(boundPerson.Address.File);
+ Assert.Equal("form-data; name=Parameter1.Address.File; filename=text.txt", file.ContentDisposition);
+ using var reader = new StreamReader(boundPerson.Address.File.OpenReadStream());
+ Assert.Equal(data, reader.ReadToEnd());
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Collection(
+ modelState.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("Parameter1.Address.File", kvp.Key);
+ Assert.Null(value.RawValue);
+ Assert.Empty(value.Errors);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ });
+ }
+
+ private class Group
+ {
+ public string GroupName { get; set; }
+
+ public Person Person { get; set; }
+ }
+
+ [Fact]
+ public async Task BindProperty_OnFormFileInNestedSubClass_AtSecondLevel_WhenSiblingPropertyIsSpecified()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "Parameter1",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(Group)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ request.QueryString = QueryString.Create("Person.Address.Zip", "98056");
+ UpdateRequest(request, data, "Person.Address.File");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var group = Assert.IsType(modelBindingResult.Model);
+ Assert.Null(group.GroupName);
+ var boundPerson = group.Person;
+ Assert.NotNull(boundPerson);
+ Assert.NotNull(boundPerson.Address);
+ var file = Assert.IsAssignableFrom(boundPerson.Address.File);
+ Assert.Equal("form-data; name=Person.Address.File; filename=text.txt", file.ContentDisposition);
+ using var reader = new StreamReader(boundPerson.Address.File.OpenReadStream());
+ Assert.Equal(data, reader.ReadToEnd());
+ Assert.Equal(98056, boundPerson.Address.Zip);
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Collection(
+ modelState.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("Person.Address.File", kvp.Key);
+ Assert.Null(value.RawValue);
+ Assert.Empty(value.Errors);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ },
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("Person.Address.Zip", kvp.Key);
+ Assert.Equal("98056", value.RawValue);
+ Assert.Empty(value.Errors);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ });
+ }
+
+ private class Fleet
+ {
+ public int? Id { get; set; }
+
+ public FleetGarage Garage { get; set; }
+ }
+
+ public class FleetGarage
+ {
+ public string Name { get; set; }
+
+ public FleetVehicle[] Vehicles { get; set; }
+ }
+
+ public class FleetVehicle
+ {
+ public string Name { get; set; }
+
+ public IFormFile Spec { get; set; }
+
+ public FleetVehicle BackupVehicle { get; set; }
+ }
+
+ [Fact]
+ public async Task BindProperty_OnFormFileInNestedSubClass_AtSecondLevel_RecursiveModel()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "fleet",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(Fleet)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ request.QueryString = QueryString.Create("fleet.Garage.Name", "WestEnd");
+ UpdateRequest(request, data, "fleet.Garage.Vehicles[0].Spec");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var fleet = Assert.IsType(modelBindingResult.Model);
+ Assert.Null(fleet.Id);
+
+ Assert.NotNull(fleet.Garage);
+ Assert.NotNull(fleet.Garage.Vehicles);
+
+ var vehicle = Assert.Single(fleet.Garage.Vehicles);
+ var file = Assert.IsAssignableFrom(vehicle.Spec);
+
+ using var reader = new StreamReader(file.OpenReadStream());
+ Assert.Equal(data, reader.ReadToEnd());
+ Assert.Null(vehicle.Name);
+ Assert.Null(vehicle.BackupVehicle);
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Collection(
+ modelState.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("fleet.Garage.Name", kvp.Key);
+ Assert.Equal("WestEnd", value.RawValue);
+ Assert.Empty(value.Errors);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ },
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("fleet.Garage.Vehicles[0].Spec", kvp.Key);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ });
+ }
+
+ [Fact]
+ public async Task BindProperty_OnFormFileInNestedSubClass_AtThirdLevel_RecursiveModel()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "fleet",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(Fleet)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ request.QueryString = QueryString.Create("fleet.Garage.Name", "WestEnd");
+ UpdateRequest(request, data, "fleet.Garage.Vehicles[0].BackupVehicle.Spec");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var fleet = Assert.IsType(modelBindingResult.Model);
+ Assert.Null(fleet.Id);
+
+ Assert.NotNull(fleet.Garage);
+ Assert.NotNull(fleet.Garage.Vehicles);
+
+ var vehicle = Assert.Single(fleet.Garage.Vehicles);
+ Assert.Null(vehicle.Spec);
+ Assert.NotNull(vehicle.BackupVehicle);
+ var file = Assert.IsAssignableFrom(vehicle.BackupVehicle.Spec);
+
+ using var reader = new StreamReader(file.OpenReadStream());
+ Assert.Equal(data, reader.ReadToEnd());
+ Assert.Null(vehicle.Name);
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Collection(
+ modelState.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("fleet.Garage.Name", kvp.Key);
+ Assert.Equal("WestEnd", value.RawValue);
+ Assert.Empty(value.Errors);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ },
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("fleet.Garage.Vehicles[0].BackupVehicle.Spec", kvp.Key);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ });
+ }
+
+ [Fact]
+ public async Task BindProperty_OnFormFileInNestedSubClass_AtSecondLevel_WhenSiblingPropertiesAreNotSpecified()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "Parameter1",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(Group)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ request.QueryString = QueryString.Create("GroupName", "TestGroup");
+ UpdateRequest(request, data, "Person.Address.File");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var group = Assert.IsType(modelBindingResult.Model);
+ Assert.Equal("TestGroup", group.GroupName);
+ var boundPerson = group.Person;
+ Assert.NotNull(boundPerson);
+ Assert.NotNull(boundPerson.Address);
+ var file = Assert.IsAssignableFrom(boundPerson.Address.File);
+ Assert.Equal("form-data; name=Person.Address.File; filename=text.txt", file.ContentDisposition);
+ using var reader = new StreamReader(boundPerson.Address.File.OpenReadStream());
+ Assert.Equal(data, reader.ReadToEnd());
+ Assert.Equal(0, boundPerson.Address.Zip);
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Collection(
+ modelState.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("GroupName", kvp.Key);
+ Assert.Equal("TestGroup", value.RawValue);
+ Assert.Empty(value.Errors);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ },
+ kvp =>
+ {
+ var (key, value) = kvp;
+ Assert.Equal("Person.Address.File", kvp.Key);
+ Assert.Null(value.RawValue);
+ Assert.Empty(value.Errors);
+ Assert.Equal(ModelValidationState.Valid, value.ValidationState);
+ });
+ }
+
private class ListContainer1
{
[ModelBinder(Name = "files")]
@@ -354,15 +746,526 @@ public async Task BindProperty_WithData_WithPrefix_GetsBound()
Assert.Single(modelState, e => e.Key == "p.Specs");
}
- private void UpdateRequest(HttpRequest request, string data, string name)
+ private class House
{
- const string fileName = "text.txt";
- var fileCollection = new FormFileCollection();
- var formCollection = new FormCollection(new Dictionary(), fileCollection);
+ public Garage Garage { get; set; }
+ }
+
+ private class Garage
+ {
+ public List Cars { get; set; }
+ }
+
+ [Fact]
+ public async Task BindProperty_FormFileCollectionInCollection_WithPrefix()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor
+ {
+ Name = "house",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(House)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ request.QueryString = QueryString.Create("house.Garage.Cars[0].Name", "Accord");
+ UpdateRequest(request, data + 1, "house.Garage.Cars[0].Specs");
+ AddFormFile(request, data + 2, "house.Garage.Cars[1].Specs");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var house = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(house.Garage);
+ Assert.NotNull(house.Garage.Cars);
+ Assert.Collection(
+ house.Garage.Cars,
+ car =>
+ {
+ Assert.Equal("Accord", car.Name);
+
+ var file = Assert.Single(car.Specs);
+ using var reader = new StreamReader(file.OpenReadStream());
+ Assert.Equal(data + 1, reader.ReadToEnd());
+ },
+ car =>
+ {
+ Assert.Null(car.Name);
+
+ var file = Assert.Single(car.Specs);
+ using var reader = new StreamReader(file.OpenReadStream());
+ Assert.Equal(data + 2, reader.ReadToEnd());
+ });
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Equal(3, modelState.Count);
+
+ var entry = Assert.Single(modelState, e => e.Key == "house.Garage.Cars[0].Name").Value;
+ Assert.Equal("Accord", entry.AttemptedValue);
+ Assert.Equal("Accord", entry.RawValue);
+
+ Assert.Single(modelState, e => e.Key == "house.Garage.Cars[0].Specs");
+ Assert.Single(modelState, e => e.Key == "house.Garage.Cars[1].Specs");
+ }
+
+ [Fact]
+ public async Task BindProperty_FormFileCollectionInCollection_OnlyFiles()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor
+ {
+ Name = "house",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(House)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ UpdateRequest(request, data + 1, "house.Garage.Cars[0].Specs");
+ AddFormFile(request, data + 2, "house.Garage.Cars[1].Specs");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var house = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(house.Garage);
+ Assert.NotNull(house.Garage.Cars);
+ Assert.Collection(
+ house.Garage.Cars,
+ car =>
+ {
+ Assert.Null(car.Name);
+
+ var file = Assert.Single(car.Specs);
+ using var reader = new StreamReader(file.OpenReadStream());
+ Assert.Equal(data + 1, reader.ReadToEnd());
+ },
+ car =>
+ {
+ Assert.Null(car.Name);
+
+ var file = Assert.Single(car.Specs);
+ using var reader = new StreamReader(file.OpenReadStream());
+ Assert.Equal(data + 2, reader.ReadToEnd());
+ });
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Equal(2, modelState.Count);
+
+ Assert.Single(modelState, e => e.Key == "house.Garage.Cars[0].Specs");
+ Assert.Single(modelState, e => e.Key == "house.Garage.Cars[1].Specs");
+ }
+
+ [Fact]
+ public async Task BindProperty_FormFileCollectionInCollection_OutOfOrderFile()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor
+ {
+ Name = "house",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(House)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ UpdateRequest(request, data + 1, "house.Garage.Cars[800].Specs");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var house = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(house.Garage);
+ Assert.Empty(house.Garage.Cars);
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Empty(modelState);
+ }
+
+ [Fact]
+ public async Task BindProperty_FormFileCollectionInCollection_MultipleFiles()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor
+ {
+ Name = "house",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(House)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ UpdateRequest(request, data + 1, "house.Garage.Cars[0].Specs");
+ AddFormFile(request, data + 2, "house.Garage.Cars[0].Specs");
+ });
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var house = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(house.Garage);
+ Assert.NotNull(house.Garage.Cars);
+ Assert.Collection(
+ house.Garage.Cars,
+ car =>
+ {
+ Assert.Null(car.Name);
+ Assert.Collection(
+ car.Specs,
+ file =>
+ {
+ using var reader = new StreamReader(file.OpenReadStream());
+ Assert.Equal(data + 1, reader.ReadToEnd());
+
+ },
+ file =>
+ {
+ using var reader = new StreamReader(file.OpenReadStream());
+ Assert.Equal(data + 2, reader.ReadToEnd());
+
+ });
+ });
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ var kvp = Assert.Single(modelState);
+
+ Assert.Equal("house.Garage.Cars[0].Specs", kvp.Key);
+ }
+
+ [Fact]
+ public async Task BindProperty_FormFile_AsAPropertyOnNestedColection()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor
+ {
+ Name = "p",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(Car1)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ request.QueryString = QueryString.Create("p.Name", "Accord");
+ UpdateRequest(request, data, "p.Specs");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var car = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(car.Specs);
+ var file = Assert.Single(car.Specs);
+ Assert.Equal("form-data; name=p.Specs; filename=text.txt", file.ContentDisposition);
+ var reader = new StreamReader(file.OpenReadStream());
+ Assert.Equal(data, reader.ReadToEnd());
+
+ // ModelState
+ Assert.True(modelState.IsValid);
+ Assert.Equal(2, modelState.Count);
+
+ var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value;
+ Assert.Equal("Accord", entry.AttemptedValue);
+ Assert.Equal("Accord", entry.RawValue);
+
+ Assert.Single(modelState, e => e.Key == "p.Specs");
+ }
+
+ public class MultiDimensionalFormFileContainer
+ {
+ public IFormFile[][] FormFiles { get; set; }
+ }
+
+ [Fact]
+ public async Task BindModelAsync_MultiDimensionalFormFile_Works()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor
+ {
+ Name = "p",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(MultiDimensionalFormFileContainer)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ UpdateRequest(request, data + 1, "FormFiles[0]");
+ AddFormFile(request, data + 2, "FormFiles[1]");
+ AddFormFile(request, data + 3, "FormFiles[1]");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var container = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(container.FormFiles);
+ Assert.Collection(
+ container.FormFiles,
+ item =>
+ {
+ Assert.Collection(
+ item,
+ file => Assert.Equal(data + 1, ReadFormFile(file)));
+ },
+ item =>
+ {
+ Assert.Collection(
+ item,
+ file => Assert.Equal(data + 2, ReadFormFile(file)),
+ file => Assert.Equal(data + 3, ReadFormFile(file)));
+ });
+ }
+
+ [Fact]
+ public async Task BindModelAsync_MultiDimensionalFormFile_WithArrayNotation()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor
+ {
+ Name = "p",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(MultiDimensionalFormFileContainer)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ UpdateRequest(request, data + 1, "FormFiles[0][0]");
+ AddFormFile(request, data + 2, "FormFiles[1][0]");
+ AddFormFile(request, data + 3, "FormFiles[1][0]");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+ var container = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(container.FormFiles);
+ Assert.Empty(container.FormFiles);
+ }
+
+ public class MultiDimensionalFormFileContainerLevel2
+ {
+ public MultiDimensionalFormFileContainerLevel1 Level1 { get; set; }
+ }
+
+ public class MultiDimensionalFormFileContainerLevel1
+ {
+ public MultiDimensionalFormFileContainer Container { get; set; }
+ }
+
+ [Fact]
+ public async Task BindModelAsync_DeeplyNestedMultiDimensionalFormFile_Works()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor
+ {
+ Name = "p",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(MultiDimensionalFormFileContainerLevel2)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ UpdateRequest(request, data + 1, "p.Level1.Container.FormFiles[0]");
+ AddFormFile(request, data + 2, "p.Level1.Container.FormFiles[1]");
+ AddFormFile(request, data + 3, "p.Level1.Container.FormFiles[1]");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var level2 = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(level2.Level1);
+ var container = level2.Level1.Container;
+ Assert.NotNull(container);
+ Assert.NotNull(container.FormFiles);
+ Assert.Collection(
+ container.FormFiles,
+ item =>
+ {
+ Assert.Collection(
+ item,
+ file => Assert.Equal(data + 1, ReadFormFile(file)));
+ },
+ item =>
+ {
+ Assert.Collection(
+ item,
+ file => Assert.Equal(data + 2, ReadFormFile(file)),
+ file => Assert.Equal(data + 3, ReadFormFile(file)));
+ });
+ }
+
+ public class DictionaryContainer
+ {
+ public Dictionary Dictionary { get; set; }
+ }
+
+ [Fact]
+ public async Task BindModelAsync_DictionaryOfFormFiles()
+ {
+ // Arrange
+ var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
+ var parameter = new ParameterDescriptor
+ {
+ Name = "p",
+ BindingInfo = new BindingInfo(),
+ ParameterType = typeof(DictionaryContainer)
+ };
+
+ var data = "Some Data Is Better Than No Data.";
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request =>
+ {
+ request.QueryString = QueryString.Create(new Dictionary
+ {
+ { "p.Dictionary[0].Key", "key0" },
+ { "p.Dictionary[1].Key", "key1" },
+ { "p.Dictionary[4000].Key", "key1" },
+ });
+ UpdateRequest(request, data + 1, "p.Dictionary[0].Value");
+ AddFormFile(request, data + 2, "p.Dictionary[1].Value");
+ });
+
+ var modelState = testContext.ModelState;
+
+ // Act
+ var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
+
+ // Assert
+
+ // ModelBindingResult
+ Assert.True(modelBindingResult.IsModelSet);
+
+ // Model
+ var container = Assert.IsType(modelBindingResult.Model);
+ Assert.NotNull(container.Dictionary);
+ Assert.Collection(
+ container.Dictionary.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ Assert.Equal("key0", kvp.Key);
+ Assert.Equal(data + 1, ReadFormFile(kvp.Value));
+ },
+ kvp =>
+ {
+ Assert.Equal("key1", kvp.Key);
+ Assert.Equal(data + 2, ReadFormFile(kvp.Value));
+ });
+ }
+
+ private static string ReadFormFile(IFormFile file)
+ {
+ using var reader = new StreamReader(file.OpenReadStream());
+ return reader.ReadToEnd();
+ }
+
+ private void UpdateRequest(HttpRequest request, string data, string name)
+ {
+ var formCollection = new FormCollection(new Dictionary(), new FormFileCollection());
request.Form = formCollection;
+
request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq";
+ AddFormFile(request, data, name);
+ }
+
+ private void AddFormFile(HttpRequest request, string data, string name)
+ {
+ const string fileName = "text.txt";
+
if (string.IsNullOrEmpty(data) || string.IsNullOrEmpty(name))
{
// Leave the submission empty.
@@ -371,6 +1274,7 @@ private void UpdateRequest(HttpRequest request, string data, string name)
request.Headers["Content-Disposition"] = $"form-data; name={name}; filename={fileName}";
+ var fileCollection = (FormFileCollection)request.Form.Files;
var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(data));
fileCollection.Add(new FormFile(memoryStream, 0, data.Length, name, fileName)
{
@@ -378,4 +1282,4 @@ private void UpdateRequest(HttpRequest request, string data, string name)
});
}
}
-}
\ No newline at end of file
+}