-
Notifications
You must be signed in to change notification settings - Fork 191
Proposal: Form parsing improvements #413
Changes from 5 commits
a1440dc
5e84962
6c908d7
22f2e98
44fb6c5
4354dbb
eb4ac33
871b9f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,8 @@ namespace Microsoft.AspNet.Http.Features.Internal | |
public class FormFeature : IFormFeature | ||
{ | ||
private readonly HttpRequest _request; | ||
private Task<IFormCollection> _parsedFormTask; | ||
private IFormCollection _form; | ||
|
||
public FormFeature(IFormCollection form) | ||
{ | ||
|
@@ -63,7 +65,15 @@ public bool HasFormContentType | |
} | ||
} | ||
|
||
public IFormCollection Form { get; set; } | ||
public IFormCollection Form | ||
{ | ||
get { return _form; } | ||
set | ||
{ | ||
_parsedFormTask = null; | ||
_form = value; | ||
} | ||
} | ||
|
||
public IFormCollection ReadForm() | ||
{ | ||
|
@@ -77,17 +87,32 @@ public IFormCollection ReadForm() | |
throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); | ||
} | ||
|
||
// TODO: Avoid Sync-over-Async http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx | ||
// TODO: How do we prevent thread exhaustion? | ||
return ReadFormAsync(CancellationToken.None).GetAwaiter().GetResult(); | ||
return ReadFormAsync().GetAwaiter().GetResult(); | ||
} | ||
|
||
public async Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken) | ||
public Task<IFormCollection> ReadFormAsync() => ReadFormAsync(CancellationToken.None); | ||
|
||
public Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken) | ||
{ | ||
if (Form != null) | ||
// Avoid state machine and task allocation for repeated reads | ||
if (_parsedFormTask == null) | ||
{ | ||
return Form; | ||
if (Form != null) | ||
{ | ||
_parsedFormTask = Task.FromResult(Form); | ||
} | ||
else | ||
{ | ||
_parsedFormTask = InnerReadFormAsync(cancellationToken); | ||
} | ||
} | ||
return _parsedFormTask; | ||
} | ||
|
||
private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancellationToken) | ||
{ | ||
if (!HasFormContentType) | ||
{ | ||
throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); | ||
|
@@ -98,7 +123,7 @@ public async Task<IFormCollection> ReadFormAsync(CancellationToken cancellationT | |
_request.EnableRewind(); | ||
|
||
IDictionary<string, StringValues> formFields = null; | ||
var files = new FormFileCollection(); | ||
FormFileCollection files = null; | ||
|
||
// Some of these code paths use StreamReader which does not support cancellation tokens. | ||
using (cancellationToken.Register(_request.HttpContext.Abort)) | ||
|
@@ -119,7 +144,7 @@ public async Task<IFormCollection> ReadFormAsync(CancellationToken cancellationT | |
var section = await multipartReader.ReadNextSectionAsync(cancellationToken); | ||
while (section != null) | ||
{ | ||
var headers = new HeaderDictionary(section.Headers); | ||
var headers = section.Headers; | ||
ContentDispositionHeaderValue contentDisposition; | ||
ContentDispositionHeaderValue.TryParse(headers[HeaderNames.ContentDisposition], out contentDisposition); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. regression: this will throw if Content-Disposition is missing. You could just use section.ContentDisposition instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which would wrap; so delay wrapping down a few lines for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed as suggested |
||
if (HasFileContentDisposition(contentDisposition)) | ||
|
@@ -129,8 +154,12 @@ public async Task<IFormCollection> ReadFormAsync(CancellationToken cancellationT | |
|
||
var file = new FormFile(_request.Body, section.BaseStreamOffset.Value, section.Body.Length) | ||
{ | ||
Headers = headers, | ||
Headers = new HeaderDictionary(headers), | ||
}; | ||
if (files == null) | ||
{ | ||
files = new FormFileCollection(); | ||
} | ||
files.Add(file); | ||
} | ||
else if (HasFormDataContentDisposition(contentDisposition)) | ||
|
@@ -157,14 +186,32 @@ public async Task<IFormCollection> ReadFormAsync(CancellationToken cancellationT | |
section = await multipartReader.ReadNextSectionAsync(cancellationToken); | ||
} | ||
|
||
formFields = formAccumulator.GetResults(); | ||
if (formAccumulator.HasValues) | ||
{ | ||
formFields = formAccumulator.GetResults(); | ||
} | ||
} | ||
} | ||
|
||
// Rewind so later readers don't have to. | ||
_request.Body.Seek(0, SeekOrigin.Begin); | ||
|
||
Form = new FormCollection(formFields, files); | ||
if (formFields == null || formFields.Count == 0) | ||
{ | ||
if (files == null || files.Count == 0) | ||
{ | ||
Form = FormCollection.Empty; | ||
} | ||
else | ||
{ | ||
Form = new FormCollection(files); | ||
} | ||
} | ||
else | ||
{ | ||
Form = new FormCollection(formFields, files); | ||
} | ||
|
||
return Form; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,17 +7,16 @@ | |
|
||
namespace Microsoft.AspNet.WebUtilities | ||
{ | ||
public class KeyValueAccumulator | ||
public struct KeyValueAccumulator | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this even need an inner dictionary? It could be just have a callback OnKeyValuePair(string key, string value) or something. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could do but would need the allocation of an outer dictionary, a closure; or some code rearranging? For example its used in the extension method You can avoid alloc with these changes by checking |
||
{ | ||
private Dictionary<string, List<string>> _accumulator; | ||
|
||
public KeyValueAccumulator() | ||
{ | ||
_accumulator = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); | ||
} | ||
|
||
public void Append(string key, string value) | ||
{ | ||
if (_accumulator == null) | ||
{ | ||
_accumulator = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); | ||
} | ||
List<string> values; | ||
if (_accumulator.TryGetValue(key, out values)) | ||
{ | ||
|
@@ -29,12 +28,27 @@ public void Append(string key, string value) | |
} | ||
} | ||
|
||
public bool HasValues => _accumulator != null; | ||
|
||
public IDictionary<string, StringValues> GetResults() | ||
{ | ||
var results = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase); | ||
if (_accumulator == null) | ||
{ | ||
return new SafeLazyDictionary<StringValues>(); | ||
} | ||
|
||
var results = new SafeLazyDictionary<StringValues>(_accumulator.Count); | ||
|
||
foreach (var kv in _accumulator) | ||
{ | ||
results.Add(kv.Key, kv.Value.ToArray()); | ||
if (kv.Value.Count == 1) | ||
{ | ||
results.Add(kv.Key, new StringValues(kv.Value[0])); | ||
} | ||
else | ||
{ | ||
results.Add(kv.Key, new StringValues(kv.Value.ToArray())); | ||
} | ||
} | ||
return results; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not set the cached task here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You've got the non-async method; so didn't want to set the task if that's the only path used