Skip to content

Commit

Permalink
Support resolving form files in complex form mapping (#50537)
Browse files Browse the repository at this point in the history
* Support resolving IFormFile in complex form mapping

* Feedback

* Fix up FormFile integration in Blazor

* Fix up FileConverter interfaces

* Fix build

* Fix FormFileCollection initialization

* Revert "Revert "[Blazor] Update selenium versions (#50511)" (#50556)"

This reverts commit 564a94d.

* Update test for non-enhanced form

* Revert "Revert "Revert "[Blazor] Update selenium versions (#50511)" (#50556)""

This reverts commit 78bf7d8.

* Add support for IReadOnlyList<IBrowserFile>
  • Loading branch information
captainsafia authored Sep 15, 2023
1 parent 2595235 commit 2ac6452
Show file tree
Hide file tree
Showing 20 changed files with 559 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;

internal sealed class BrowserFileFromFormFile(IFormFile formFile) : IBrowserFile
{
public string Name => formFile.Name;

public DateTimeOffset LastModified => DateTimeOffset.Parse(formFile.Headers.LastModified.ToString(), CultureInfo.InvariantCulture);

public long Size => formFile.Length;

public string ContentType => formFile.ContentType;

public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default)
{
if (Size > maxAllowedSize)
{
throw new IOException($"Supplied file with size {Size} bytes exceeds the maximum of {maxAllowedSize} bytes.");
}

return formFile.OpenReadStream();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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;
#if COMPONENTS
using Microsoft.AspNetCore.Components.Forms;
#endif
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;

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

#if COMPONENTS
if (typeof(T) == typeof(IBrowserFile))
{
var targetFile = reader.FormFileCollection.GetFile(reader.CurrentPrefix.ToString());
if (targetFile != null)
{
var browserFile = new BrowserFileFromFormFile(targetFile);
result = (T)(IBrowserFile)browserFile;
found = true;
return true;
}
}

if (typeof(T) == typeof(IReadOnlyList<IBrowserFile>))
{
var targetFiles = reader.FormFileCollection.GetFiles(reader.CurrentPrefix.ToString());
var buffer = ReadOnlyCollectionBufferAdapter<IBrowserFile>.CreateBuffer();
for (var i = 0; i < targetFiles.Count; i++)
{
buffer = ReadOnlyCollectionBufferAdapter<IBrowserFile>.Add(ref buffer, new BrowserFileFromFormFile(targetFiles[i]));
}
result = (T)(IReadOnlyList<IBrowserFile>)ReadOnlyCollectionBufferAdapter<IBrowserFile>.ToResult(buffer);
found = true;
return true;
}
#endif

if (typeof(T) == typeof(IReadOnlyList<IFormFile>))
{
result = (T)reader.FormFileCollection.GetFiles(reader.CurrentPrefix.ToString());
found = true;
return true;
}

if (typeof(T) == typeof(IFormFileCollection))
{
result = (T)reader.FormFileCollection;
found = true;
return true;
}

var formFileCollection = reader.FormFileCollection;
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,31 @@
// 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;
#if COMPONENTS
using Microsoft.AspNetCore.Components.Forms;
#endif
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;

internal sealed class FileConverterFactory : IFormDataConverterFactory
{
[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
#if COMPONENTS
public bool CanConvert(Type type, FormDataMapperOptions options) => CanConvertCommon(type) || type == typeof(IBrowserFile) || type == typeof(IReadOnlyList<IBrowserFile>);
#else
public bool CanConvert(Type type, FormDataMapperOptions options) => CanConvertCommon(type);
#endif

private static bool CanConvertCommon(Type type) => type == typeof(IFormFile) || type == typeof(IFormFileCollection) || type == typeof(IReadOnlyList<IFormFile>);

[RequiresDynamicCode(FormMappingHelpers.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(FormMappingHelpers.RequiresUnreferencedCodeMessage)]
public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
{
return Activator.CreateInstance(typeof(FileConverter<>).MakeGenericType(type)) as FormDataConverter ??
throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public FormDataMapperOptions(ILoggerFactory loggerFactory)
{
_converters = new(WellKnownConverters.Converters);
_factories.Add(new ParsableConverterFactory());
_factories.Add(new FileConverterFactory());
_factories.Add(new EnumConverterFactory());
_factories.Add(new NullableConverterFactory());
_factories.Add(new DictionaryConverterFactory());
Expand Down
11 changes: 10 additions & 1 deletion src/Components/Endpoints/src/FormMapping/FormDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;
Expand Down Expand Up @@ -33,9 +34,17 @@ public FormDataReader(IReadOnlyDictionary<FormKey, StringValues> formCollection,
_prefixBuffer = buffer;
}

public FormDataReader(IReadOnlyDictionary<FormKey, StringValues> formCollection, CultureInfo culture, Memory<char> buffer, IFormFileCollection formFileCollection)
: this(formCollection, culture, buffer)
{
FormFileCollection = formFileCollection;
}

internal ReadOnlyMemory<char> CurrentPrefix => _currentPrefixBuffer;

public IFormatProvider Culture { get; internal set; }
public IFormatProvider Culture { get; }

public IFormFileCollection? FormFileCollection { get; internal set; }

public int MaxRecursionDepth { get; set; } = 64;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Components.Endpoints;
Expand All @@ -11,15 +12,19 @@ internal sealed class HttpContextFormDataProvider
{
private string? _incomingHandlerName;
private IReadOnlyDictionary<string, StringValues>? _entries;
private IFormFileCollection? _formFiles;

public string? IncomingHandlerName => _incomingHandlerName;

public IReadOnlyDictionary<string, StringValues> Entries => _entries ?? ReadOnlyDictionary<string, StringValues>.Empty;

public void SetFormData(string incomingHandlerName, IReadOnlyDictionary<string, StringValues> form)
public IFormFileCollection FormFiles => _formFiles ?? (IFormFileCollection)FormCollection.Empty;

public void SetFormData(string incomingHandlerName, IReadOnlyDictionary<string, StringValues> form, IFormFileCollection formFiles)
{
_incomingHandlerName = incomingHandlerName;
_entries = form;
_formFiles = formFiles;
}

public bool TryGetIncomingHandlerName([NotNullWhen(true)] out string? incomingHandlerName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Globalization;
using Microsoft.AspNetCore.Components.Endpoints.FormMapping;
using Microsoft.AspNetCore.Components.Forms.Mapping;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

Expand Down Expand Up @@ -85,7 +86,7 @@ public void Map(FormValueMappingContext context)

var deserializer = _cache.GetOrAdd(context.ValueType, CreateDeserializer);
Debug.Assert(deserializer != null);
deserializer.Deserialize(context, _options, _formData.Entries);
deserializer.Deserialize(context, _options, _formData.Entries, _formData.FormFiles);
}

private FormValueSupplier CreateDeserializer(Type type) =>
Expand All @@ -99,7 +100,8 @@ internal abstract class FormValueSupplier
public abstract void Deserialize(
FormValueMappingContext context,
FormDataMapperOptions options,
IReadOnlyDictionary<string, StringValues> form);
IReadOnlyDictionary<string, StringValues> form,
IFormFileCollection formFiles);
}

internal class FormValueSupplier<T> : FormValueSupplier
Expand All @@ -109,7 +111,8 @@ internal class FormValueSupplier<T> : FormValueSupplier
public override void Deserialize(
FormValueMappingContext context,
FormDataMapperOptions options,
IReadOnlyDictionary<string, StringValues> form)
IReadOnlyDictionary<string, StringValues> form,
IFormFileCollection formFiles)
{
if (form.Count == 0)
{
Expand All @@ -129,7 +132,8 @@ public override void Deserialize(
using var reader = new FormDataReader(
dictionary,
CultureInfo.InvariantCulture,
buffer.AsMemory(0, options.MaxKeyBufferSize))
buffer.AsMemory(0, options.MaxKeyBufferSize),
formFiles)
{
ErrorHandler = context.OnError,
AttachInstanceToErrorsHandler = context.MapErrorToContainer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal partial class FormDataMetadataFactory(List<IFormDataConverterFactory> f
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>().Single();
private readonly CollectionConverterFactory _collectionFactory = factories.OfType<CollectionConverterFactory>().Single();
private readonly ILogger<FormDataMetadataFactory> _logger = loggerFactory.CreateLogger<FormDataMetadataFactory>();

Expand Down Expand Up @@ -86,6 +87,12 @@ internal partial class FormDataMetadataFactory(List<IFormDataConverterFactory> f
return result;
}

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

if (_dictionaryFactory.CanConvert(type, options))
{
Log.DictionaryType(_logger, type);
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
14 changes: 13 additions & 1 deletion src/Components/Endpoints/src/FormMapping/WellKnownConverters.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if COMPONENTS
using Microsoft.AspNetCore.Components.Forms;
#endif
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;

internal static class WellKnownConverters
Expand Down Expand Up @@ -37,7 +42,14 @@ static WellKnownConverters()
{ typeof(DateTimeOffset), new ParsableConverter<DateTimeOffset>() },
{ typeof(TimeSpan), new ParsableConverter<TimeSpan>() },
{ typeof(TimeOnly), new ParsableConverter<TimeOnly>() },
{ typeof(Guid), new ParsableConverter<Guid>() }
{ typeof(Guid), new ParsableConverter<Guid>() },
{ typeof(IFormFileCollection), new FileConverter<IFormFileCollection>() },
{ typeof(IFormFile), new FileConverter<IFormFile>() },
{ typeof(IReadOnlyList<IFormFile>), new FileConverter<IReadOnlyList<IFormFile>>() },
#if COMPONENTS
{ typeof(IBrowserFile), new FileConverter<IBrowserFile>() },
{ typeof(IReadOnlyList<IBrowserFile>), new FileConverter<IReadOnlyList<IBrowserFile>>() }
#endif
};

converters.Add(typeof(char?), new NullableConverter<char>((FormDataConverter<char>)converters[typeof(char)]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<IsPackable>false</IsPackable>
<EmbeddedFilesManifestFileName>Microsoft.Extensions.FileProviders.Embedded.Manifest.xml</EmbeddedFilesManifestFileName>
<Nullable>enable</Nullable>
<DefineConstants>$(DefineConstants);COMPONENTS</DefineConstants>
</PropertyGroup>

<!-- This workaround is required when referencing FileProviders.Embedded as a project -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ internal static async Task InitializeStandardComponentServicesAsync(
if (handler != null && form != null)
{
httpContext.RequestServices.GetRequiredService<HttpContextFormDataProvider>()
.SetFormData(handler, new FormCollectionReadOnlyDictionary(form));
.SetFormData(handler, new FormCollectionReadOnlyDictionary(form), form.Files);
}

if (httpContext.RequestServices.GetService<AntiforgeryStateProvider>() is EndpointAntiforgeryStateProvider antiforgery)
Expand Down
Loading

0 comments on commit 2ac6452

Please sign in to comment.