-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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
[Blazor] Take cascading parameter attribute type into account when supplying cascading values #48554
[Blazor] Take cascading parameter attribute type into account when supplying cascading values #48554
Conversation
src/Components/Components/src/ICascadingValueSupplierFactory.cs
Outdated
Show resolved
Hide resolved
src/Components/Components/src/Binding/IFormValueCascadingParameterAttribute.cs
Outdated
Show resolved
Hide resolved
var cascadingParameterAttribute = prop.GetCustomAttributes() | ||
.OfType<ICascadingParameterAttribute>().SingleOrDefault(); | ||
if (cascadingParameterAttribute != 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.
We need to think about what happens when you have [SupplyParameterFromQuery]
and [SupplyParameterFromFrom]
attribute. There are some cases that might be "incompatible", but there are other cases where we might want both attributes to apply. For example, if we do [SupplyParameterFromPrerenderedState]
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.
Hm, true. I wonder if it would be fine to modify this code slightly to add a ReflectedCascadingParameterInfo
for each attribute on the parameter property. Maybe we could also add something like an Order
property to ICascadingParameterAttribute
so that, for example, if [SupplyParameterFromQuery]
and [SupplyParameterFromForm]
are both present, we prioritize supplying a value from a form when possible, but fall back to supplying a parameter from the query if form data is not available.
Is this something you think should be addressed by this PR?
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.
Great points to raise. It would definitely be consistent with other parts of the framework if we had an Order
property.
I don't think you should feel required to implement that in this PR but if there's broad agreement about the direction, it would be good to file an issue summarizing the design then we can hopefully get on and implement that soon.
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 we likely end up with a fairly clear, straightforwards design if:
CascadingParameterAttribute
is unsealed- ... and gains a new
Order
property - ... and we change it to
AllowMultiple = true
Then we can cheaply have form/query subclasses in a different assembly, and the core doesn't need to know or care about anything but the base class.
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.
CascadingParameterAttribute
I'd rather not open up the ability for third-parties to create their own definitions of Cascading values by extending/overriding CascadingParameterAttribute.
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 tried to address that in #48554 (comment). Can you give more details about what you're concerned about?
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.
Then we can cheaply have form/query subclasses in a different assembly, and the core doesn't need to know or care about anything but the base class.
I like this, but wouldn't CascadingModelBinder
and CascadingQueryValueProvider
still need to know what attribute type was used so that, in the case of only one attribute being present, they don't supply values for each other (or for CascadingValue
) by accident? Or would we be okay with, for example, a parameter annotated with [CascadingValue]
receiving values from the query or a form request?
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 would expect CascadingModelBinder
and CascadingQueryValueProvider
to be in layers where they are able to know about the corresponding CascadingParameterAttribute
subclass and hence can do the same type check that you already have.
internal interface ICascadingParameterAttribute | ||
{ | ||
public string? Name { 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.
I would rather change CascadingParameterAttribute
to be nonsealed and thereby not require a marker interface. The forms and query ones can then just inherit it.
This does mean that developers can make their own subclasses but I don't see any reason to object to that. Unless we make ICascadingValueSupplier
public, subclassing CascadingParameterAttribute
would not allow people to do anything different from just using CascadingParameterAttribute
as they already do today.
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 does mean that developers can make their own subclasses but I don't see any reason to object to that.
There's no benefit to it either, is it?
The consequences of doing it that way is that we have created an "infinite set" of attributes that can mean "I am a cascading value", and that's something we'll have to account for in the future.
For example, if we do source generation based on the attribute, now we need to pay the cost of performing semantic analysis over all the parameters, as we need to inspect each attribute and their base class for determining its a target, instead of limiting the scope to the concrete set of attributes we care about.
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.
The benefit is just that this relationship corresponds naturally to inheritance, as we want to share code across these types anyway, and they literally are variations on a core thing. Is there a problem with using Roslyn's semantic model to tell us if a type inherits from another? Seems like that's exactly what it's designed for, and is no different from hundreds of other places in the codebase that allow subclassing.
It's not that I'm hugely fixated on using a subclass here, but I am getting concerned that our codebase is getting increasingly niche.
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.
It's not that I'm hugely fixated on using a subclass here, but I am getting concerned that our codebase is getting increasingly niche.
Can you clarify what you mean by this?
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.
The specific example here is making a nonobvious design choice that introduces extra concepts (marker interfaces), with a rationale based on a future possibility of optimizing source generation.
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.
That it's just a practical sample.
The rationale here is opening up a potential extensibility point without a clear benefit to it. Marker interfaces are a well-known concept that is used across our codebase.
The principle in contrast here is minimizing API surface and extensibility points unless there are clear scenarios that users can exercise.
src/Components/Components/src/ICascadingValueSupplierFactory.cs
Outdated
Show resolved
Hide resolved
builder.OpenComponent(0, RouteData.PageType); | ||
|
||
foreach (var kvp in RouteData.RouteValues) | ||
builder.OpenComponent<CascadingQueryValueProvider>(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'd love to reduce the number of implicit, system-created layers in the component hierarchy. Each layer is a nontrivial cost as we've been observing with @DamianEdwards's recent benchmarking (e.g., when we changed to have an App
component and a Router
).
Not suggesting this should change in this PR, but the approach I have in mind is:
- We define a new mechanism within the cascading parameter lookup flow where, if we're scanning up the hierarchy and don't find any match for a cascading parameter, we also call some new API on the
Renderer
in case it wants to supply a value. - We define a new protected virtual API on
Renderer
so that subclasses can plug in their own set ofICascadingValueSupplier
instances for this - We update
EndpointHtmlRenderer
and others to have implementations for form/query/etc as applicable based on the renderer type.
This will also let us remove the CascadingModelBinder
layer, and means we can add as many new built-in cascaded parameter types as we want without incurring the cost of an extra layer in the component hierarchy for each one.
I'm happy to do this sort of thing as a follow-up.
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'd love to reduce the number of implicit, system-created layers in the component hierarchy. Each layer is a nontrivial cost as we've been observing with @DamianEdwards's recent benchmarking (e.g., when we changed to have an App component and a Router).
This will also let us remove the CascadingModelBinder layer, and means we can add as many new built-in cascaded parameter types as we want without incurring the cost of an extra layer in the component hierarchy for each one.
I would say that we shouldn't jump to conclusions with regards to perf here without more data. The data we have so far compares two very different pipelines, and we would need data with and without the component to make a proper judgement call. Otherwise, it's premature optimization.
CascadingModelBinder
exists because the app must have the ability to influence the context in which form dispatching (and in the future, passing prerendered state from the server to the client), happens.
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 would say that we shouldn't jump to conclusions with regards to perf here without more data. The data we have so far compares two very different pipelines, and we would need data with and without the component to make a proper judgement call. Otherwise, it's premature optimization.
Collecting data is great. No objection to that. At the same time it does come at a time cost, and there isn't innately a guarantee that any particular microbenchmark we set up is actually measuring the right thing. In this particular case my sense is that we'd be removing one set of complexities (special hidden built-in components) and adding a different set (a new renderer feature) so there wouldn't necessarily be a net increase in complexity here, while there would definitely be less going on at runtime.
the app must have the ability to influence the context in which form dispatching
Just to be clear, I'm only talking about at the root level. Ideally there would be a built-in default behavior that does the most natural form binding, so we can reduce the number of out-of-box concepts we make people understand. People would only need to add them manually where they want to introduce a scope boundary. Is that what you're already thinking? If not, can we find a way to design the system that way?
I'm still working on tests for this, but I've updated the implementation to not rely on separating |
|
||
namespace Microsoft.AspNetCore.Components; | ||
|
||
internal sealed class CascadingQueryValueProvider : IComponent, ICascadingValueSupplier, IDisposable |
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 would have expected this to be done directly within CascadingModelBinder
and not as a separate component. CascadingModelBinder
in my view, is responsible for all the binding operations related to [SupplyParameterFromXXX]
It looks like the big remaining Q's here are:
And there are some other sub-questions too, like "Should we use a marker interface for cascading parameter attributes or unseal I'm going to propose a set of design decisions in this comment that hopefully provides an answer to all those questions, and we can iterate from there as necessary. Layering:@SteveSandersonMS proposed in #48554 (comment) an approach where we add a new API on
This wouldn't prevent customers from still using Here's what I propose the specific layering looks like:
We could register one or more services under a new interface (maybe
I could go either way here, but I'm leaning toward using the marker interface. In addition to the points made in earlier discussions, subclassing Handling multiple cascading parameter attributes on a property:As mentioned in #48554 (comment), we could add an "Order" property to
@SteveSandersonMS @javiercn What are your thoughts on this? |
@MackinnonBuck That looks like a great analysis and I'm pretty much convinced by the tradeoffs you're proposing. I do wonder if we can get away without needing to support multiple |
Okay, I think this PR is ready to have another look. I believe I've addressed everything except the following:
|
/// <summary> | ||
/// Provides values that get supplied to cascading parameters with <see cref="CascadingModelBinder"/>. | ||
/// </summary> | ||
public abstract class CascadingModelBindingProvider |
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.
Later on, rather than this being a general DI service with a public API, we might change this into a Renderer API so it's not publicly visible or extensible (except for people implementing a renderer). It potentially leads to a whole bunch of simplifications and reductions in API surface. This is something we discussed before, but I think you're right not to add it to this PR.
I'm totally happy with this PR proceeding as-is, since right now this works and we have other things to focus on for preview 6. But just wanted to raise this so it doesn't seem surprising if I try to move around a bunch of this machinery and possible constrain how much we have to support in preview 7. If you have any concerns about that please let me know!
src/Components/Components/test/Routing/QueryParameterValueSupplierTest.cs
Show resolved
Hide resolved
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.
Nice one, @MackinnonBuck!
There is a lot of implementation change/extension here, probably beyond what I can be confident that I've understood all the implications of. Everything I've read looks reasonable but it is tough to fully visualize if this would lead to any meaningful perf differences or other behavior changes.
I'll be digging into this area in more detail in preview 7 so by that stage this area will have had close attention from @javiercn, me, and you. Hopefully by that point we can all be pretty confident in it :)
[Blazor] Take cascading parameter attribute type into account when supplying cascading values
This PR allows cascading value suppliers to choose not to supply a value to a cascading parameter property depending on the type of the attribute used to annotate the property.
Description
This PR allows
[SupplyParameterFromForm]
,[SupplyParameterFromQuery]
and[CascadingParameter]
to work indpendently. This prevents, for example, a query parameter being interpreted as a form request entry.WIP:
[SupplyParameterFromQuery]
to work in non-page componentsSupport multiple cascading value attributes on a single component parameterFixes #46981
Fixes #48770