Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support binding to form and form file parameters in minimal actions #35158

Merged
merged 24 commits into from
Nov 24, 2021

Conversation

martincostello
Copy link
Member

@martincostello martincostello commented Aug 8, 2021

PR Title

Support binding to the form and files in the form from parameters in minimal actions.

PR Description

Add basic support for binding to parameters of types IFormCollection, IFormFileCollection, and IFormFile on the HttpRequest in minimal actions.

I've just done this as a draft for now as while the new and existing RequestDelegateFactory tests pass, I'm sure there's better ways to deal with some of the reflection/expression binding stuff I've put in.

TODO

Relates to #34303 and #35100.

/cc @davidfowl @jchannon

@ghost ghost added area-runtime community-contribution Indicates that the PR has been added by a community member labels Aug 8, 2021
@davidfowl
Copy link
Member

davidfowl commented Aug 8, 2021

Needs support in the EndpointMetadataApiDescriptionProvider so it shows up property in API explorer. It also needs the right request content type so it can be swagger shows the file upload UI.

Copy link
Contributor

@pranavkm pranavkm left a comment

Choose a reason for hiding this comment

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

Allowing form parameters without antiforgery leaves this open to CSRF out of the box. Has that been thought thru?

@martincostello
Copy link
Member Author

It was mentioned by @davidfowl here #34303 (comment) (I assumed forks was a typo for forms), but I haven't addressed there here other than removing the IFormCollection support that was in the initial commit.

Off the top of my head, if that needs adding in here would that "just" be the case of updating the method that is reading the form to do something similar to what the ValidateAntiforgeryTokenAuthorizationFilter class does?

@adityamandaleeka adityamandaleeka added area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels feature-minimal-actions Controller-like actions for endpoint routing and removed area-runtime labels Aug 9, 2021
@martincostello martincostello force-pushed the Minimal-APIs-IForm-Support-34303 branch 2 times, most recently from be491e0 to 177fe8f Compare August 10, 2021 19:20
@davidfowl
Copy link
Member

This change looks really good @martincostello!

@@ -709,13 +727,32 @@ private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo
return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext);
}

private static Expression BindParameterFromFormFile(ParameterInfo parameter, string key, FactoryContext factoryContext)
{
if (factoryContext.JsonRequestBodyType is not null)
Copy link
Member

Choose a reason for hiding this comment

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

I would add a computed property on the factoryContext HasBody => JsonRequestBodyType is not null || ReadForm and use that here and in the JSON logic

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem with doing exactly that is it wouldn't allow you to have more than one IFormFile parameter as trying to bind the second one would find the first and then throw.

@martincostello
Copy link
Member Author

Off the top of my head, if that needs adding in here would that "just" be the case of updating the method that is reading the form to do something similar to what the ValidateAntiforgeryTokenAuthorizationFilter class does?

I had a quick look at this just now with regards to CSRF and discovered for myself that IAntiforgery is defined in Microsoft.AspNetCore.Antiforgery, which then depends on Microsoft.AspNetCore.Http.Extensions, so it's definitely not a case of "just" using that 😄

I'll defer to the experts as to what the implications this feature would have on that and what (if anything) can/should be done.

@martincostello martincostello force-pushed the Minimal-APIs-IForm-Support-34303 branch 3 times, most recently from 4110b72 to 606d1d6 Compare August 18, 2021 11:28
@martincostello
Copy link
Member Author

martincostello commented Aug 18, 2021

Going to mark this ready for review as I can't make any further progress on this without input from the team about CSRF and the new metadata API suggestion. I also have to keep rebasing it 😄

I guess possible outcomes include:

  1. Can't implement as-is due to CSRF concerns
  2. Gets punted back to 7.0 and CSRF and metadata dealt with when the team has time to come back to it.

@martincostello martincostello marked this pull request as ready for review August 18, 2021 11:31
@davidfowl
Copy link
Member

@martincostello this is something we're willing to take for .NET 7 and with the latest changes, it'll be possible to simplify this change.

@martincostello
Copy link
Member Author

@davidfowl - Ah cool - I stopped rebasing as I saw the team was still putting lots of fit and finish on things so was stepping back waiting for the dust to settle. If/when the majority of the changes you have planned are in for 6.0, let me know and I'll rebase this 👍

@martincostello martincostello force-pushed the Minimal-APIs-IForm-Support-34303 branch from 461f293 to 54f8a53 Compare September 26, 2021 15:12
Fix tests that wouldn't compile after the rename in 9a0e5e2 that was done from another solution file.
Add TODO for how to correctly handle IFormFileCollection with IFromFormMetadata.
@martincostello
Copy link
Member Author

I've applied the feedback from #35175 (comment), there's just a few TODOs regarding what binding sources to use for which types I'm unsure of which is best.

Comment on lines 119 to 122
var alreadyHasAnyBodyParameter = apiDescription.ParameterDescriptions.Any(x =>
x.Source == BindingSource.Body ||
x.Source == BindingSource.Form ||
x.Source == BindingSource.FormFile);
Copy link
Member

Choose a reason for hiding this comment

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

Alternatively I could remove this "default" behaviour for the body by removing the skip for BindingSource.Body and having it dealt with explicitly in CreateApiParameterDescription() instead.

Let's do this for consistency. The main thing that we want to have keep working is if there is no BindingSource.Body or BindingSource.FormFile parameter by "default" and someone added IAcceptsMetadata explicitly with .Accepts<TRequest>(...) or [Consumes(...)] indicating they're manually reading the body, we should continue to create this "fake" ApiParameterDescription from IAcceptsMetadata .

If we know we're consuming the body on behalf of the app, we can just assume that the IAcceptsMetadata is what we added by default. If someone tries to add custom IAcceptsMetadata despite that, it's certainly wrong, so no big loss.

Also nit:

Suggested change
var alreadyHasAnyBodyParameter = apiDescription.ParameterDescriptions.Any(x =>
x.Source == BindingSource.Body ||
x.Source == BindingSource.Form ||
x.Source == BindingSource.FormFile);
var alreadyHasAnyBodyParameter = apiDescription.ParameterDescriptions.Any(x =>
x.Source == BindingSource.Body ||
x.Source == BindingSource.FormFile);

I don't see the point in looking for BindingSources we'll never create at this point.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed nit.

Still thinking about the best way to tackle this, as I've tried a few different ways to remove the default/skip body parameter but they all seemed to break something in the tests.

Copy link
Member

Choose a reason for hiding this comment

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

What tests break? I'm okay with changing some of the tests if they're asserting something nonsensical like custom IAcceptsMetadata being used to generate an ApiParameterDescription when we're consuming an application/json request body on behalf of the app. After this change, custom IAcceptsMetadata won't be used to generate an ApiParameterDescription when we're consuming a multipart/form-data on behalf of the app, so at least this is consistent.

Copy link
Member Author

Choose a reason for hiding this comment

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

There were some tests checking there was no body parameter at all, where there was now a default. There were others that would fail because there was no implicit accept metadata where it was expected because the unit tests don't run through the real RequestDelegateFactory code because it's all mocked routing.

For the latter I was torn between changing it to do so to make it more realistic vs. the fact the two implementation details were becoming aware of each other.

I'll come back to it with fresh eyes tomorrow and maybe it'll all click how best to refactor it.

Revert changes from 599e72c.
Remove unused optional parameter.
Throw a NotSupportedException if [FromForm]/IFromFormMetadata is used for parameters of types other than IFormFile and IFormFileCollection.
Update IFormFileCollection to bind as BindingSource.FormFile.
Remove check for BindingSource.Form as that's not a used value with Minimal APIs.
Update the default parameter handling for inferred body parameters so the default parameter is only added via IAcceptsMetadata if there were no other parameters already defined.
Add missing >.
@halter73 halter73 merged commit 6080213 into dotnet:main Nov 24, 2021
@ghost ghost added this to the 7.0-preview1 milestone Nov 24, 2021
@halter73
Copy link
Member

Thanks @martincostello!! 🎉

@martincostello martincostello deleted the Minimal-APIs-IForm-Support-34303 branch November 25, 2021 07:42
@martincostello
Copy link
Member Author

Thanks @halter73 - if you let me know when the agreed CSRF support lands in main, I'd be happy to do a follow-up PR to take out the guards for the authorized requests added in 45b30d2.

@ghost
Copy link

ghost commented Nov 25, 2021

Hi @martincostello. It looks like you just commented on a closed PR. The team will most probably miss it. If you'd like to bring something important up to their attention, consider filing a new issue and add enough details to build context.

@BrennanConroy BrennanConroy added Docs This issue tracks updating documentation and removed Docs This issue tracks updating documentation labels Feb 3, 2022
@amcasey amcasey added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Jun 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels community-contribution Indicates that the PR has been added by a community member feature-minimal-actions Controller-like actions for endpoint routing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants