Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
68 changes: 68 additions & 0 deletions src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProvider.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// An <see cref="IValueProvider"/> adapter for data stored in an <see cref="IFormFileCollection"/>.
/// </summary>
/// <remarks>
/// Unlike most <see cref="IValueProvider"/> instances, <see cref="FormFileValueProvider"/> does not provide any values, but
/// specifically responds to <see cref="ContainsPrefix(string)"/> queries. This allows the model binding system to
/// recurse in to deeply nested object graphs with only values for form files.
/// </remarks>
public sealed class FormFileValueProvider : IValueProvider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What motivated the choice to make this separate from FormValueProvider? Is the idea that someone could remove this value provider if they don't want it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was one of the reasons. The other reason is that FormValueProvider is meant to be derived, and it now makes user code much more complicated since they now have to track two separate prefix groups.

{
private readonly IFormFileCollection _files;
private PrefixContainer _prefixContainer;

/// <summary>
/// Creates a value provider for <see cref="IFormFileCollection"/>.
/// </summary>
/// <param name="files">The <see cref="IFormFileCollection"/>.</param>
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<string>();
var count = formFiles.Count;
for (var i = 0; i < count; i++)
{
var file = formFiles[i];

// If there is an <input type="file" ... /> 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);
}

/// <inheritdoc />
public bool ContainsPrefix(string prefix) => PrefixContainer.ContainsPrefix(prefix);

/// <inheritdoc />
public ValueProviderResult GetValue(string key) => ValueProviderResult.None;
}
}
43 changes: 43 additions & 0 deletions src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A <see cref="IValueProviderFactory"/> for <see cref="FormValueProvider"/>.
/// </summary>
public sealed class FormFileValueProviderFactory : IValueProviderFactory
{
/// <inheritdoc />
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment isn't super duper accurate. HasFormContentType will be true for both form content types, not just multipart.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I changed the code, forgot to update the comment.

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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<RootNamespace>Microsoft.AspNetCore.Mvc</RootNamespace>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks 👏 for 👏 doing 👏 this

</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FormFileValueProvider>(v));
}

private static ValueProviderFactoryContext CreateContext(string contentType)
{
var context = new DefaultHttpContext();
context.Request.ContentType = contentType;
context.Request.Form = new FormCollection(new Dictionary<string, StringValues>(), new FormFileCollection());
var actionContext = new ActionContext(context, new RouteData(), new ActionDescriptor());

return new ValueProviderFactoryContext(actionContext);
}
}
}
71 changes: 71 additions & 0 deletions src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderTest.cs
Original file line number Diff line number Diff line change
@@ -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<string, StringValues>(), 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<string, StringValues>(), 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<string, StringValues>(), formFiles);

var valueProvider = new FormFileValueProvider(formFiles);

// Act
var result = valueProvider.GetValue("file");

// Assert
Assert.Equal(ValueProviderResult.None, result);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/Mvc/Mvc/test/MvcOptionsSetupTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ public void Setup_SetsUpValueProviders()
provider => Assert.IsType<FormValueProviderFactory>(provider),
provider => Assert.IsType<RouteValueProviderFactory>(provider),
provider => Assert.IsType<QueryStringValueProviderFactory>(provider),
provider => Assert.IsType<JQueryFormValueProviderFactory>(provider));
provider => Assert.IsType<JQueryFormValueProviderFactory>(provider),
provider => Assert.IsType<FormFileValueProviderFactory>(provider));
}

[Fact]
Expand Down
Loading