dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.23k stars 9.95k forks source link

IFormFile incompatible with FromForm in Minimal APIs for .NET 8 Preview 6 #49526

Closed gavilanch closed 10 months ago

gavilanch commented 1 year ago

Is there an existing issue for this?

Describe the bug

In ASP.NET Core 8 - Preview 6. When applying a FromForm attribute to a class with an IFormFile property, when doing the HTTP request, the app throws an InvalidOperationException: No converter registered for type 'Microsoft.AspNetCore.Http.IFormFile'.

Expected Behavior

This shouldn't throw an exception, but assign the value to the corresponding IFormFile property.

Steps To Reproduce

Github repo: https://github.com/gavilanch/TestFromFormDotNet8Preview6/tree/master

The repo basically have 2 important files: Program.cs and CreateActorDTO.cs. The second one is a DTO with a string and IFormFile property. I have included an endpoint with a form so that you can easily test the behavior.

Exceptions (if any)

System.InvalidOperationException: No converter registered for type 'Microsoft.AspNetCore.Http.IFormFile'.
   at Microsoft.AspNetCore.Components.Endpoints.Binding.FormDataMapperOptions.CreateConverter(Type type, FormDataMapperOptions options)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd[TArg](TKey key, Func`3 valueFactory, TArg factoryArgument)
   at Microsoft.AspNetCore.Components.Endpoints.Binding.ComplexTypeConverterFactory.CanConvert(Type type, FormDataMapperOptions options)
   at Microsoft.AspNetCore.Components.Endpoints.Binding.FormDataMapperOptions.<>c.<.ctor>b__2_4(Type type, FormDataMapperOptions options)
   at Microsoft.AspNetCore.Components.Endpoints.Binding.FormDataMapperOptions.CreateConverter(Type type, FormDataMapperOptions options)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd[TArg](TKey key, Func`3 valueFactory, TArg factoryArgument)
   at Microsoft.AspNetCore.Components.Endpoints.Binding.FormDataMapperOptions.ResolveConverter[T]()
   at lambda_method3(Closure, Object, HttpContext, Object)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass102_2.<<HandleRequestBodyAndCompileRequestDelegateForForm>b__2>d.MoveNext()
--- End of stack trace from previous location ---
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: */*
Connection: keep-alive
Host: localhost:7085
User-Agent: PostmanRuntime/7.32.3
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=--------------------------393630075424756012793498
Content-Length: 208566
Postman-Token: dcac1cc6-03f4-4bd5-9a75-3a4be032efff

.NET Version

8.0.100-preview.6.23330.14

Anything else?

ASP.NET Core Version: 8.0.0-preview.6.23329.11

IDE: Microsoft Visual Studio Community 2022 (64-bit) - Preview Version 17.7.0 Preview 4.0 Windows 10

dotnet --info:

.NET SDK:
 Version:   8.0.100-preview.6.23330.14
 Commit:    ba97796b8f

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.19043
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\8.0.100-preview.6.23330.14\

.NET workloads installed:
There are no installed workloads to display.

Host:
  Version:      8.0.0-preview.6.23329.7
  Architecture: x64
  Commit:       5340be2ccc

.NET SDKs installed:
  5.0.408 [C:\Program Files\dotnet\sdk]
  7.0.101 [C:\Program Files\dotnet\sdk]
  7.0.304 [C:\Program Files\dotnet\sdk]
  7.0.400-preview.23330.10 [C:\Program Files\dotnet\sdk]
  8.0.100-preview.6.23330.14 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.18 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.19 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 8.0.0-preview.6.23329.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.18 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.19 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.0-preview.6.23329.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 5.0.17 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.18 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.19 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.1 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.7 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 8.0.0-preview.6.23329.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  Not set

global.json file:
  Not found
captainsafia commented 1 year ago

@gavilanch Thanks for reporting this bug!

We recently added support for complex form binding to minimal APIs. It kicks in when a parameter has a FromForm attribute. We should probably add another check to verify that it only kicks in on complex types that aren't IFormFile/IFormCollection/etc.

captainsafia commented 1 year ago

As I was looking at this, I missed a key detail in the repro which is the fact that the IFormFile type is encapsulated in another type. As a result of this, the description that I gave earlier for this bug isn't totally valid.

I believe that we might need to modify the form-binding behavior here to provide more direct converters for certain form-related primitives (e.g. IFormFile, IFormCollection, etc)

cc: @javiercn

Markz878 commented 11 months ago

My ticket 50614 was closed as a dupe of this, but in RC2 I still can't bind a Blazor SSR form to an IFormFile property. Could someone clarify this? @mkArtakMSFT @captainsafia

captainsafia commented 11 months ago

@Markz878 Sure! This was closed as a dupe because the napping logic is shared between minimal APIs and Blazor here. The PR referenced above contains tests showcasing Blazor usage. Can you share a repro of the code that's not working as desired?

Markz878 commented 11 months ago

@captainsafia Thanks for response, I tried the code in the tests and in the comments but I didn't get it to work. Here is (one version of) the code for a Blazor RC2 SSR Home page that I tried:

@page "/"

<PageTitle>Home</PageTitle>

<EditForm method="post" Model="NewCustomer" OnValidSubmit="SubmitForm" FormName="customer" Enhance>
    <input type="file" name="NewCustomer.File" />
    <button>Submit</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    public Customer? NewCustomer { get; set; }

    protected override void OnInitialized()
    {
        NewCustomer ??= new();
    }

    public async Task SubmitForm()
    {
        ArgumentNullException.ThrowIfNull(NewCustomer?.File);
        using FileStream fileStream = File.OpenWrite(NewCustomer.File.Name);
        using Stream submittedFileStream = NewCustomer.File.OpenReadStream();
        await submittedFileStream.CopyToAsync(fileStream);
    }

    public class Customer
    {
        public IFormFile? File { get; set; }
    }
}
captainsafia commented 11 months ago

@Markz878 Can you clarify what the intended behavior is? I see that you are trying to copy the contents of the form file to another stream. I'm observing that the Customer.File property is populated correctly here which is the intended behavior. Are you seeing something else on your end?

image
Markz878 commented 11 months ago

I just noticed I had javascript disabled, which made the form submission fail. After I enabled js it started working again. I also tried this without the Enhance attribute, and that made the submit fail too. Should it work without that, or without javascript? But it at least works with them, so we can use it, thanks!

captainsafia commented 10 months ago

Closing as resolved for Minimal APIs.