Skip to content

Commit

Permalink
Support resolving IFormFile in complex form mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
captainsafia committed Sep 6, 2023
1 parent 2772a78 commit f050a94
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;

internal sealed class FileConverter<T>(HttpContext? httpContext) : FormDataConverter<T>, ISingleValueConverter
{
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found)
{
if (httpContext == null)
{
result = default;
found = false;
return true;
}

if (typeof(T) == typeof(IFormFileCollection))
{
result = (T)httpContext.Request.Form.Files;
found = true;
return true;
}

var formFileCollection = httpContext.Request.Form.Files;
if (formFileCollection.Count == 0)
{
result = default;
found = false;
return true;
}

var file = formFileCollection.GetFile(reader.CurrentPrefix.ToString());
if (file != null)
{
result = (T)file;
found = true;
return true;
}

result = default;
found = false;
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;

internal sealed class FileConverterFactory(IHttpContextAccessor? httpContextAccessor = null) : IFormDataConverterFactory
{
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
public bool CanConvert(Type type, FormDataMapperOptions options) => type == typeof(IFormFile) || type == typeof(IFormFileCollection);

[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
{
return Activator.CreateInstance(typeof(FileConverter<>).MakeGenericType(type), httpContextAccessor?.HttpContext) as FormDataConverter ??
throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;

namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
Expand All @@ -25,6 +26,21 @@ public FormDataMapperOptions()
_factories.Add(new ComplexTypeConverterFactory(this));
}

[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
internal FormDataMapperOptions(IHttpContextAccessor httpContextAccessor)
{
// We don't use the base constructor here since the ordering of the factories is important.
_converters = new(WellKnownConverters.Converters);
_factories.Add(new ParsableConverterFactory());
_factories.Add(new EnumConverterFactory());
_factories.Add(new FileConverterFactory(httpContextAccessor));
_factories.Add(new NullableConverterFactory());
_factories.Add(new DictionaryConverterFactory());
_factories.Add(new CollectionConverterFactory());
_factories.Add(new ComplexTypeConverterFactory(this));
}

// Not configurable for now, this is the max number of elements we will bind. This is important for
// security reasons, as we don't want to bind a huge collection and cause perf issues.
// Some examples of this are:
Expand All @@ -35,7 +51,7 @@ public FormDataMapperOptions()
// MVC uses 32, JSON uses 64. Let's stick to STJ default.
internal int MaxRecursionDepth = 64;

// This is normally 200 (similar to ModelStateDictionary.DefaultMaxAllowedErrors in MVC)
// This is normally 200 (similar to ModelStateDictionary.DefaultMaxAllowedErrors in MVC)
internal int MaxErrorCount = 200;

internal int MaxKeyBufferSize = FormReader.DefaultKeyLengthLimit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal class FormDataMetadataFactory(List<IFormDataConverterFactory> factories
private readonly FormMetadataContext _context = new();
private readonly ParsableConverterFactory _parsableFactory = factories.OfType<ParsableConverterFactory>().Single();
private readonly DictionaryConverterFactory _dictionaryFactory = factories.OfType<DictionaryConverterFactory>().Single();
private readonly FileConverterFactory? _fileConverterFactory = factories.OfType<FileConverterFactory>().SingleOrDefault();
private readonly CollectionConverterFactory _collectionFactory = factories.OfType<CollectionConverterFactory>().Single();

[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
Expand Down Expand Up @@ -67,6 +68,12 @@ public FormDataTypeMetadata GetOrCreateMetadataFor(Type type, FormDataMapperOpti
return result;
}

if (_fileConverterFactory?.CanConvert(type, options) == true)
{
result.Kind = FormDataTypeKind.File;
return result;
}

if (_dictionaryFactory.CanConvert(type, options))
{
result.Kind = FormDataTypeKind.Dictionary;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping.Metadata;
internal enum FormDataTypeKind
{
Primitive,
File,
Collection,
Dictionary,
Object,
Expand Down
8 changes: 6 additions & 2 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat
var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance;
var endpointBuilder = options?.EndpointBuilder ?? new RdfEndpointBuilder(serviceProvider);
var jsonSerializerOptions = serviceProvider.GetService<IOptions<JsonOptions>>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions;
var formDataMapperOptions = serviceProvider.GetService<IHttpContextAccessor>() is {} httpContextAccessor
? new FormDataMapperOptions(httpContextAccessor)
: new FormDataMapperOptions();

var factoryContext = new RequestDelegateFactoryContext
{
Expand All @@ -288,6 +291,7 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat
EndpointBuilder = endpointBuilder,
MetadataAlreadyInferred = metadataResult is not null,
JsonSerializerOptions = jsonSerializerOptions,
FormDataMapperOptions = formDataMapperOptions
};

return factoryContext;
Expand Down Expand Up @@ -2054,7 +2058,7 @@ private static Expression BindComplexParameterFromFormItem(
return formArgument;
}

var formDataMapperOptions = new FormDataMapperOptions();
var formDataMapperOptions = factoryContext.FormDataMapperOptions;
var formMappingOptionsMetadatas = factoryContext.EndpointBuilder.Metadata.OfType<FormMappingOptionsMetadata>();
foreach (var formMappingOptionsMetadata in formMappingOptionsMetadatas)
{
Expand All @@ -2073,7 +2077,7 @@ private static Expression BindComplexParameterFromFormItem(

// ProcessForm(context.Request.Form, form_dict, form_buffer);
var processFormExpr = Expression.Call(ProcessFormMethod, FormExpr, Expression.Constant(formDataMapperOptions.MaxKeyBufferSize), formDict, formBuffer);
// name_reader = new FormDataReader(form_dict, CultureInfo.InvariantCulture, form_buffer.AsMemory(0, FormDataMapperOptions.MaxKeyBufferSize));
// name_reader = new FormDataReader(form_dict, CultureInfo.InvariantCulture, form_buffer.AsMemory(0, formDataMapperOptions.MaxKeyBufferSize));
var initializeReaderExpr = Expression.Assign(
formReader,
Expression.New(FormDataReaderConstructor,
Expand Down
3 changes: 3 additions & 0 deletions src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Reflection;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Endpoints.FormMapping;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -59,4 +60,6 @@ internal sealed class RequestDelegateFactoryContext

// Grab these options upfront to avoid the per request DI scope that would be made otherwise to get the options when writing Json
public required JsonSerializerOptions JsonSerializerOptions { get; set; }

public required FormDataMapperOptions FormDataMapperOptions { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;

Expand Down Expand Up @@ -278,7 +280,58 @@ public async Task SupportsRecursivePropertiesWithRecursionLimit()
var exception = await Assert.ThrowsAsync<BadHttpRequestException>(async () => await requestDelegate(httpContext));

Assert.Equal("The maximum recursion depth of '3' was exceeded for 'Manager.Manager.Manager.Name'.", exception.Message);
}

[Fact]
public async Task SupportsFormFileSourcesInDto()
{
FormFileDto capturedArgument = default;
void TestAction([FromForm] FormFileDto args) { capturedArgument = args; };
var httpContext = CreateHttpContext();
var formFiles = new FormFileCollection
{
new FormFile(Stream.Null, 0, 10, "file", "file.txt"),
};
httpContext.Request.Form = new FormCollection(new() { { "Description", "A test file" } }, formFiles);
var serviceCollection = new ServiceCollection();
serviceCollection.TryAddSingleton<IHttpContextAccessor>(new HttpContextAccessor(httpContext));
var options = new RequestDelegateFactoryOptions
{
ServiceProvider = serviceCollection.BuildServiceProvider()
};

var factoryResult = RequestDelegateFactory.Create(TestAction, options);
var requestDelegate = factoryResult.RequestDelegate;

await requestDelegate(httpContext);
Assert.Equal("A test file", capturedArgument.Description);
Assert.Equal(formFiles["file"], capturedArgument.File);
}

[Fact]
public async Task SupportsFormFileCollectionSourcesInDto()
{
FormFileCollectionDto capturedArgument = default;
void TestAction([FromForm] FormFileCollectionDto args) { capturedArgument = args; };
var httpContext = CreateHttpContext();
var formFiles = new FormFileCollection
{
new FormFile(Stream.Null, 0, 10, "file", "file.txt"),
};
httpContext.Request.Form = new FormCollection(new() { { "Description", "A test file" } }, formFiles);
var serviceCollection = new ServiceCollection();
serviceCollection.TryAddSingleton<IHttpContextAccessor>(new HttpContextAccessor(httpContext));
var options = new RequestDelegateFactoryOptions
{
ServiceProvider = serviceCollection.BuildServiceProvider()
};

var factoryResult = RequestDelegateFactory.Create(TestAction, options);
var requestDelegate = factoryResult.RequestDelegate;

await requestDelegate(httpContext);
Assert.Equal("A test file", capturedArgument.Description);
Assert.Equal(formFiles, capturedArgument.FileCollection);
}

private record TodoRecord(int Id, string Name, bool IsCompleted);
Expand All @@ -288,4 +341,26 @@ private class Employee
public string Name { get; set; }
public Employee Manager { get; set; }
}

private class FormFileDto
{
public string Description { get; set; }
public IFormFile File { get; set; }
}

private class FormFileCollectionDto
{
public string Description { get; set; }
public IFormFileCollection FileCollection { get; set; }
}

private class HttpContextAccessor(HttpContext httpContext) : IHttpContextAccessor
{

public HttpContext HttpContext
{
get;
set;
} = httpContext;
}
}

0 comments on commit f050a94

Please sign in to comment.