-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Make PropertySetter a concrete type #25054
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
Conversation
| #else | ||
| false; | ||
| #endif | ||
| static bool PoolForBrowserLogs = true; |
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.
Is this intentional?
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.
Yup. We had it turned off because running the benchmarks would bury you logs from the GC. But that's all gone now and it actually helps to see the benchmark progress when you're running it in a container.
| /// Specifies the field for which validation messages should be displayed. | ||
| /// </summary> | ||
| [Parameter] public Expression<Func<TValue>>? For { get; set; } | ||
| [Parameter(Required = true)] public Expression<Func<TValue>> For { get; set; } = default!; |
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.
Should we set CurrentEditContext to be a required cascading parameter as well?
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.
With cascading parameters, we're not going to be able any tooling hints. Right now, the component produces a much more contextual error message than the generic one you get if we apply the attribute.
| /// Gets or sets the content to display when a match is found for the requested route. | ||
| /// </summary> | ||
| [Parameter] public RenderFragment<RouteData> Found { get; set; } | ||
| [Parameter(Required = true)] public RenderFragment<RouteData> Found { get; set; } |
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.
These two seem a bit aggressive, don't we currently handle them when they are null?
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.
Currently, we throw exceptions when they are not provided. I like using Required parameters to indicate that they are needed.
@pranavkm Javier's comment made me realize that we might be able to remove the exceptions we throw in the Router component when these values are not provided. Thoughts?
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.
Yup, I could remove the checks from this type. They're about as generic as they come.
| } | ||
|
|
||
| [Fact] | ||
| public void RequiredParameter_NoneSpecified() |
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.
nit: Naming SetParameterProperties_ThrowsWhen_ARequiredParameterIsNotSpecified
With all the cases that are here it makes it hard to see at a glance you are covering everything.
javiercn
left a comment
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.
Looks great!
Some minor comments, but that's it.
|
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
|
|
|
||
| public string? CaptureUnmatchedValuesPropertyName { get; } | ||
|
|
||
| public IReadOnlyDictionary<string, IPropertySetter> Writers => _underlyingWriters; |
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.
This is a bit nitpicky, but I'd have a preference for not exposing the dictionary completely like this, because it makes it really easy for someone to make a mistake in the future and do lookups against Writers instead of going through TryGetValue which has the perf optimization.
Since this is only used during the ThrowRequiredParametersNotSet method, what would you think about changing this to something like:
| public IReadOnlyDictionary<string, IPropertySetter> Writers => _underlyingWriters; | |
| public IEnumerable<string> GetKnownPropertyNamesInOrder() | |
| => _underlyingWriters.Keys.OrderBy(kvp => kvp.Key, StringComparer.Ordinal); |
... so as to make it really scenario-specific?
| _cachedWritersByType[targetType] = writers; | ||
| } | ||
|
|
||
| var requiredParametersWritten = 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.
I definitely like counting as a way of making this cheap, and it's great to see the benchmarks showing this works out well!
However, the current implementation leaves open an issue whereby the "required" checks could be bypassed by the parent component, either accidentally or deliberately. If we're going to document that "required" checks are truly enforced, then we should look at closing this loophole. The issue is that the parent component isn't required to provide only a single value for each parameter. A ParameterView isn't a dictionary; it can contain repeated keys. So if parameters A and B were both required, the parent could provide two values for A and none for B, and bypass the check.
The approach I had in mind for locking this down is a bit elaborate but TBH I can't think of anything simpler that remains O(1) and don't do any string hashing. Maybe you can! Here's one possibility:
- Imagine that
IPropertySetteralso had anint RequiredParameterIndexfield, and whenWritersForTypebuilt its dictionary, it gave an incrementing value to each required parameter (starting from 0 for eachWritersForTypeinstance). The ordering is completely arbitrary.WritersForTypealso keeps track of the total number of required parameters like you already do here. - Then, inside
SetProperties, westackallocauint[]of lengthceil(numRequiredParameters/32). And each time we write a required parameters, we set to 1 the bit corresponding to itsRequiredParameterIndex. For example, if the index was 58, then we'd want to set the 58th bit, which is the 58-32 = 26th bit in the 2nduintin the array. Ignoring my possible off-by-one errors or similar, this would be something like doing a logical OR onthatArray[writer.RequiredParameterIndex / 32]with value1 << (writer.RequiredParameterIndex & 31). - At the end of the
SetPropertiesprocess, we would have tofor(var i = 0; i < writer.RequiredParameterIndex / 32; i++)and check that each entry inthatArrayhas the expected value, i.e., it equalsuint.MaxValuefor all entries except the last, which equals whatever expected sequence of bits is left over. Strictly speaking that's not O(1) but it basically is because virtually no components have > 32 required parameters and even then the extra cost is negligible.
If we wanted, this could be simplified by having the rule that you're just not allowed to have > 32 required parameters, and then the IPropertySetter could hold the 1 << index value precomputed and various bits of the above algorithm could be simplified to work on a single uint instead of an array. Instead of storing "number of required parameters" we could store the precomputed "expected sum of the bits" and then just do a direct equality comparison at the end of SetProperties.
Of course, if you have a different idea that's great too! I just haven't thought of a way of checking all the incoming parameter names are distinct without using some kind of hashset/dictionary-like structure, which we'd want to avoid due to the cost of hashing.
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.
Sidenote: I have no idea why IPropertySetter is actually defined as an interface. Since it's purely internal, it seems like it could be turned into a concrete type. Not a big deal and obviously orthogonal to this PR, so please don't feel pressured to do anything with it here. Just a thought in case you actually wanted to make a change in that area.
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.
A ParameterView isn't a dictionary; it can contain repeated keys. So if parameters A and B were both required, the parent could provide two values for A and none for B, and bypass the check.
Yup. I'd based it on the observation that the compiler \ parser does not allow duplicate attributes. But if you think it's compelling we can definitely guard against it.
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.
I know it could be argued as an edge case. If our docs say that "required" is just a hint that isn't really enforced, then we could skip the robustness. But I suspect we want to document that "required" means "we really enforce this for reals".
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.
BTW I really do think the "you can't have > 32 required parameters" would be a very reasonable restriction if this simplifies things significantly.
|
Updated numbers. ComplexTable-from-blank appears to have bumped up (it hovered around that number in a few runs), but the rest appear to be about the same.
|
|
|
||
| var setMethod = property.SetMethod; | ||
|
|
||
| var propertySetterAsAction = |
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.
This is identical to what PropertyHelper does: https://github.com/dotnet/aspnetcore/blob/master/src/Shared/PropertyHelper/PropertyHelper.cs#L312-L321. I wasn't sure if we intentionally avoided using that type in here for some reason, so I copied it over. It avoids a Activator.CreateInstance call that we previously had per property which would be an improvement in my books
| /// Gets or sets the content to display when a match is found for the requested route. | ||
| /// </summary> | ||
| [Parameter] public RenderFragment<RouteData> Found { get; set; } | ||
| [Parameter(Required = true)] public RenderFragment<RouteData> Found { get; set; } |
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.
Yup, I could remove the checks from this type. They're about as generic as they come.
56d3bbb to
7d5aa19
Compare
* Use the pattern from PropertyHelpers to set property values * Tweaks to Blazor WebAssembly tests to allow running tests locally
7d5aa19 to
3eb9546
Compare
|
@SteveSandersonMS I yanked out all the Required parameters support. I would still like to take the change to use a concrete PropertySetter type and the other benchmark infrastructure changes |
SteveSandersonMS
left a comment
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.
Cool
|
Hello @pranavkm! Because this pull request has the p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (
|
captainsafia
left a comment
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.
Changes look legit.
Uh oh!
There was an error while loading. Please reload this page.