Skip to content

Avoid allocating FormFeature in HttpRequest.HasFormContentType #50830

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

Closed
wants to merge 2 commits into from

Conversation

captainsafia
Copy link
Member

@captainsafia captainsafia commented Sep 20, 2023

Closes #50153.

public class FormFeatureBenchmark
{
    private HttpRequest _request;

    [IterationSetup]
    public void Setup()
    {
        _request = new DefaultHttpRequest(new DefaultHttpContext());
    }

    [Benchmark]
    public void Get_HttpRequestHasFormContentType()
    {
        _ = _request.HasFormContentType;
    }
}

Baseline

Method Mean Error StdDev Median Op/s Gen 0 Gen 1 Gen 2 Allocated
Get_HttpRequestHasFormContentType 10.69 us 1.879 us 5.390 us 9.208 us 93,577.6 - - - 776 B

With Changeset

Method Mean Error StdDev Op/s Gen 0 Gen 1 Gen 2 Allocated
Get_HttpRequestHasFormContentType 8.959 us 2.259 us 6.445 us 111,616.9 - - - 672 B

@ghost ghost added the area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions label Sep 20, 2023
{
return true;
}

var contentType = ContentType;
return HasApplicationFormContentType(contentType) || HasMultipartFormContentType(contentType);
return HttpExtensions.HasApplicationFormContentType(_request.ContentType) || HttpExtensions.HasMultipartFormContentType(_request.ContentType);
Copy link
Member

@JamesNK JamesNK Sep 20, 2023

Choose a reason for hiding this comment

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

This is done a couple of times.

Add a HasFormContentType(...) to HttpExtensions that calls these methods for you? Eliminates the chance of logic going out of sync.

{
var encoding = FilterEncoding(contentType.Encoding);
var encoding = FilterEncoding(ContentType!.Encoding);
Copy link
Member

Choose a reason for hiding this comment

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

nit: I don't like that the ContentType property allocates a MediaTypeHeaderValue. IMO it should be a method.

Copy link
Member

Choose a reason for hiding this comment

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

The type is more reliable, methods just re-invent the parsers.

{
get { return FormFeature.HasFormContentType; }
}
public override bool HasFormContentType => HttpExtensions.HasApplicationFormContentType(ContentType) || HttpExtensions.HasMultipartFormContentType(ContentType);
Copy link
Member

Choose a reason for hiding this comment

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

Use new HasFormContentType(...) method here.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a behavior change mentioned here: #50016 (comment)
Before if the FormFeature.Form was set manually this would also return true independent of the headers.
Now it would return false because only the headers are checked.
DefaultHttpRequest.HasFormContentType needs to be exactly the same as FormFeature.HasFormContentType for backward compatibility.

I guess this should also check if the form is already set e.g.

_features.Cache.Form?._form is not null || ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Now it would return false because only the headers are checked.
DefaultHttpRequest.HasFormContentType needs to be exactly the same as FormFeature.HasFormContentType for backward compatibility.

Yeah, this is a total drag. I almost considered a breaking change here. However, it seems like there's a few components in MVC's model binding that rely on the behavior of form set = form content type is true. At best, here we can avoid constructing a new FormFeature if it has already been set.

@ghost
Copy link

ghost commented Sep 28, 2023

Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime.
To make sure no conflicting changes have occurred, please rerun validation before merging. You can do this by leaving an /azp run comment here (requires commit rights), or by simply closing and reopening.

@ghost ghost added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Sep 28, 2023
internal static bool HasApplicationFormContentType([NotNullWhen(true)] string? contentType)
{
// Content-Type: application/x-www-form-urlencoded; charset=utf-8
return contentType != null && contentType.Contains(UrlEncodedFormContentType, StringComparison.OrdinalIgnoreCase);
Copy link
Member

Choose a reason for hiding this comment

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

This is less precise / robust than the prior implementation. It's ok in the normal scenario, but it can cause strange random bugs for other inputs. E.g. this now matches "web-application/x-www-form-urlencoded-foo".

We have tools that can get the same precision as before without allocating:
https://github.com/dotnet/aspnetcore/blob/main/src/Shared/MediaType/ReadOnlyMediaTypeHeaderValue.cs

Copy link
Member Author

Choose a reason for hiding this comment

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

We have tools that can get the same precision as before without allocating:

Ah, perfect! This is exactly what I was looking for.

get { return FormFeature.HasFormContentType; }
}
public override bool HasFormContentType => HttpExtensions.HasAnyFormContentType(ContentType)
|| _features.Fetch(ref _features.Cache.Form, _features.Collection, collection => collection.Get<IFormFeature>()) != null;
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this just checking for the presence of the form feature? If present, it should check the form feature's HasFormContentType.

The current form feature should also take precedence if it exists.

@captainsafia captainsafia deleted the safia/form-contenttype-check branch November 29, 2023 00:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Avoid computing FormFeature when calling HttpRequest.HasFormContentType
4 participants