dotnet / dotnet/aspnetcore

Inconsistent Form model-binding behaviour whether an `IFormFileCollection` property exists or not.

Open
#54,130 2 comments 5 reactions 0 assignees View on GitHub
area-minimal
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

Calling an endpoint with a model _without_ an `IFormFileCollection` without providing any valid fields causes the handler to receive a `null` value:

```sh
curl --location --request POST 'https://localhost:7173/withForm' \
--header 'Content-Type: application/x-www-form-urlencoded'
```

Calling an endpoint with a model _with_ an `IFormFileCollection` without providing any valid fields correctly validates the model and promptly throws a `FormDataMappingException` (`Missing required value for property 'Name'`)

```sh
curl --location --request POST 'https://localhost:7173/withoutForm' \
--header 'Content-Type: application/x-www-form-urlencoded'
```

If, however, I pass in a single valid field (even with an empty value), then all works as expected:

```sh
curl --location 'https://localhost:7173/withFileEnumerable' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'name='
```

Note: This is specifically for `IFormFileCollection`; `IEnumerable`, `List` (and presumably other collection interfaces) behaves similarly to not having one (i.e. `null` value being passed). `application/x-www-urlencoded` and `multipart/form-data` behaves identically.

### Expected Behavior

Calling an endpoint - with or without an `IFormFileCollection` - without providing any valid fields correctly validates the model and promptly throws a `FormDataMappingException`.

Alternatively (same meaning, different wording): Calling an endpoint without a single valid field should work (In terms of model binding) identically to calling an endpoint with one or more valid fields

### Steps To Reproduce

```c#
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("withoutFiles", ( [FromForm] WithoutFiles request) =>
{
return request switch
{
null => Results.BadRequest(), // This can happen for some reason.
_ => Results.Ok()
};
}).DisableAntiforgery();

app.MapPost("withFileCollection", ( [FromForm] WithFileCollection request) =>
{
return request switch
{
null => Results.BadRequest(), // This can never happen (as expected).
_ => Results.Ok()
};
}).DisableAntiforgery();

app.MapPost("withFileList", ( [FromForm] WithFileList request) =>
{
return request switch
{
null => Results.BadRequest(), // This can happen for some reason.
_ => Results.Ok()
};
}).DisableAntiforgery();

app.MapPost("withFileEnumerable", ( [FromForm] WithFileEnumerable request) =>
{
return request switch
{
null => Results.BadRequest(), // This can happen for some reason.
_ => Results.Ok()
};
}).DisableAntiforgery();

app.Run();

public class WithoutFiles
{
public required string Name { get; init; }
public required string Description { get; init; }
}

public class WithFileCollection
{
public required string Name { get; init; }
public required string Description { get; init; }

public IFormFileCollection Attachments { get; init; } = default!;
}

public class WithFileEnumerable
{
public required string Name { get; init; }
public required string Description { get; init; }

public IEnumerable Attachments { get; init; } = default!;
}

public class WithFileList
{
public required string Name { get; init; }
public required string Description { get; init; }

public List Attachments { get; init; } = default!;
}
```

### Exceptions (if any)

This is the exception I get in the `IFormFileCollection` case - the one I'm expecting in all cases.

```
Microsoft.AspNetCore.Http.BadHttpRequestException: Missing required value for property 'Name'.
---> Microsoft.AspNetCore.Components.Endpoints.FormMapping.FormDataMappingException: An error occurred while trying to map a value from form data. For more details, see the 'Error' property and the 'InnerException' property.
at Microsoft.AspNetCore.Components.Endpoints.FormMapping.FormDataReader.AddMappingError(Exception exception, String attemptedValue)
at Microsoft.AspNetCore.Components.Endpoints.FormMapping.CompiledComplexTypeConverter`1.TryRead(FormDataReader& context, Type type, FormDataMapperOptions options, T& result, Boolean& found)
at Microsoft.AspNetCore.Components.Endpoints.FormMapping.FormDataMapper.Map[T](FormDataReader reader, FormDataMapperOptions options)
at lambda_method1(Closure, Object, HttpContext, Object)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Http.RequestDelegateFactory.Log.FormDataMappingFailed(HttpContext httpContext, String parameterTypeName, String parameterName, FormDataMappingException exception, Boolean shouldThrow)
at lambda_method1(Closure, Object, HttpContext, Object)
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass104_2.<b__2>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
```

### .NET Version

`8.0.201` (but was also broke in `8.0.102` and possibly earlier)

### Anything else?

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.