-
Notifications
You must be signed in to change notification settings - Fork 190
Less alloc/wrapping/boxing for Query, Forms, Cookies #411
Conversation
Hi @benaadams, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution! The agreement was validated by .NET Foundation and real humans are currently evaluating your PR. TTYL, DNFBOT; |
May need changes to Kestrel's custom IDictionaries dealing with empty keys |
@@ -12,7 +12,7 @@ public class HttpRequestFeature : IHttpRequestFeature | |||
{ | |||
public HttpRequestFeature() |
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.
FYI: This implementation is used exclusively in unit tests, no optimizations required. We should add a doc comment to that effect.
@benaadams What you're getting at with much of this is that our multiple levels of abstraction are inefiicent and we should scale back. That's legit, but I think we need to choose which collection we use carefully. We used Dictionaries internally because they were convenient storage, but as fowler pointed out we hate that their index throws, and they also have a significantly larger surface area than we need. Rather than pushing IDictionary up to the surface APIs, let's try pushing a reduced IHeaderCollection down into the features/servers. we only really need |
The same goes for query, form, etc.. |
@Tratcher we can't push the form and query etc down to the server but that's not what you mean I assume. |
@davidfowl Not down to the server, but down to their respective storage/features. |
@Tratcher yes. However, after a large amount of changes, and some iterations to maintain a close analogue to flow and constraints, I mostly ended up in a similar place to where the code started; including wrapping on get for headers! There are a few tweaks but they really end up independent to the abstraction changes (LazyDict and its inherited collections, KeyValueAccumulator). Which was't where I expected to be; and would have likely been the exact same place if I wasn't trying to avoid it. 😿 However, I did ponder along the journey... Questions
I had a few more, but I think they are artefacts of the above two and what implementation the answers encourage you down. Not necessarily looking for answers. |
IDictionary's contract is that the indexer throws, that's not something we can/should change in our stack as it would case a lot of unpleasant surprises. If we want a different semantic then we need a different contract / interface. I don't think we even need IDictionary, it was just the closest match to what wanted. Now that we're cleaning things up I think we can come up with a slimmed down type that has the semantics we want and use that in both the surface API (HttpRequest) and internally (IHttpRequestFeature). The request data should be mutable. There are common scenarios where middleware will alter the request to meet downstream requirements like applying x-forwarded headers to their respective fields. |
Different Contract makes sense; also with IgnoreCase on keys? If you did something like this (inheritance-wise) everyone will want some: interface IReadOnly<TKey, TValue>
{
TValue Item { get; }
}
interface IReadWrite<TKey, TValue> : IReadOnly<TKey, TValue>
{
new TValue Item { get; set; }
} It seems to work, though you have to implement the get twice: class Thing<TKey, TValue> : IReadWrite<TKey, TValue>
{
public TValue Item
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
TValue IReadOnly<TKey, TValue>.Item
{
get
{
return Item;
}
}
} Then you can create a concrete type; use it read/write via interface and expose as readonly without any var thing = Thing();
thing["that"] = that;
IReadWrite iThing = thing;
iThing["other"] = other;
return (IReadOnly)iThing; |
Rebased rather than merged |
Hi @benaadams, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution! The agreement was validated by .NET Foundation and real humans are currently evaluating your PR. TTYL, DNFBOT; |
@rynowak How badly will this break MVC? |
|
||
switch (values.Count) | ||
{ | ||
case 0: |
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.
values.Count == 0 was already checked in the prior if block. Might as well make this if..else if...else
.
@Tratcher - I'd expect much more breakage in the tests which frequently new up collections for testing. We actually read these very few places in the product code. |
} | ||
} | ||
|
||
StringValues IRequestCookieCollection.this[string key] |
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.
Something is strange here, there shouldn't be two indexers.
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.
string and StringValues
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.
Ah, right, this collection really is different from the others. It doesn't need to use StringValues anywhere, it only had that because of the old common IReadableStringCollection. We can just make this all <string, string>
now.
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.
Ah ok, makes things simpiler :)
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.
Hmm... how does that fit with RequestCookieFeature? https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs#L62
Multiple entries for the same value as via headers? Concat?
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.
nvm; I think I've got it
This is pretty much done. I'll rebase and merge it when I get a chance later today. Any of the minor remaining comments you don't get to by then I'll just take care of. Thanks |
Done, however quick check am returning |
Should be fine. If they really care they can use TryGetValue |
Rebased, squashed, merged. |
👏 :) |
@@ -25,7 +25,7 @@ public static class UriHelper | |||
FragmentString fragment = new FragmentString()) | |||
{ | |||
string combinePath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/"; | |||
return combinePath + query + fragment; | |||
return $"{combinePath}{query.ToString()}{fragment.ToString()}"; |
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.
Since this has just 3 components, it would be more efficient reverting to the String.Concat()
a+b+c format it was in before. While maintaining equal readability, it's ~3-4x faster on most machines (I've tested this on a variety locally and on the latest servers).
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.
Send a PR 😄
This adds additional performance improvements (namely string.Concat overloads) on top of aspnet#411.
HeaderDictionary
,QueryCollection
,FormCollection
,RequestCookies
IReadableStringCollection
,ReadableStringCollection
->IQueryCollection
,IRequestCookies
,IFormCollection
each implementingIDictionary<string, StringValues>
IQueryCollection
,IRequestCookies
,IFormCollection
doesn't throw on get contract, non-boxing enumerator in Empty paths.KeyValueAccumulator
fromclass
tostruct
ToUriComponent
orParseQuery
)Equality
forStringValues
(see PR Allow non-boxing equality for StringValues dotnet/extensions#55)Also cleaned up and added some intellisense code comments; but they still need work, so this is not "done"