aspnetcore: [8.0.100-preview.5.23276.4]Unable to cast object of type when passing data(custom type) to a component in Blazor Apps after building
Application Name: SPOT Client OS: Windows10 21h2 CPU: x64 .NET Build: 8.0.100-preview.5.23276.4 App, App Source or detailed repro information checking at: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1829070/
Verify Scenarios:
- Windows10 21h2 x64 + dotnet-sdk-8.0.100-preview.5.23276.4: Fail
- Windows10 21h2 x64 + dotnet-sdk-8.0.100-preview.3.23178.7: Pass
- Windows10 21h2 x64 + 7.0.302: Pass
- Windows10 21h2 x64 + 6.0.408 Pass
App repro steps on repro machine: 1.Build and Publish the app against .NET 8 Preview 5 build. 2.Execute the app. 3.Launch url: https://localhost:5005. 4.Click “Login” button. 5.Type 9191 for pin. 6.Click “Continue” button. 7.Click “Cancel” button. 8.Click “Not Now” button. 9.Type “Brown” in the search box. 10.Select “Brown, Alex”. 11.Click “Pickup” button. 12.Click “Card” button. 13.Click “Add another card” button.
Expected Result: Add card window opens. Actual Result: Window doesn’t open.
Findings
For this app, we were able to build and publish successfully, but there is an error at runtime in one of our cases. This issue only happens after building the app with.NET 8 Preview 5 SDK , it does not repro if it was built/published with the .NET 8 Preview 3, .NET 7, or .NET 6 SDK build.
in a page, app passes data to a component (DateModalInput) like this:
<DateModalInput PlaceHolder="@(S.ExpirationPlaceholderCaption)" />
Component receives data like this:
[Parameter] public string Placeholder { get; set; }
S.ExpirationPlaceholderCaption data type is not string, but it works when we build and run it with .NET6, .NET7 and .NET 8 Preview 3 SDK.
For .NET8 Preview 5 SDK, we need to change Parameter data type, so it doesn’t need to cast.
Error :
System.InvalidOperationException: Unable to set property 'PlaceHolder' on object of type 'XYZ.Platform.Client.UI.Components.DateModalInput'. The error was: Unable to cast object of type 'XYZ.Platform.StringProperty' to type 'System.String'.
---> System.InvalidCastException: Unable to cast object of type 'XYZ.Platform.StringProperty' to type 'System.String'.
at Microsoft.AspNetCore.Components.Reflection.PropertySetter.CallPropertySetter[TTarget,TValue](Action`2 setter, Object target, Object value)
at Microsoft.AspNetCore.Components.Reflection.ComponentProperties.<SetProperties>g__SetProperty|2_0(Object target, PropertySetter writer, String parameterName, Object value)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Components.Reflection.ComponentProperties.<SetProperties>g__SetProperty|2_0(Object target, PropertySetter writer, String parameterName, Object value)
at Microsoft.AspNetCore.Components.Reflection.ComponentProperties.SetProperties(ParameterView& parameters, Object target)
at Microsoft.AspNetCore.Components.ComponentBase.SetParametersAsync(ParameterView parameters)
at Microsoft.AspNetCore.Components.Rendering.ComponentState.SetDirectParameters(ParameterView parameters)
at Microsoft.AspNetCore.Components.
About this issue
- Original URL
- State: closed
- Created a year ago
- Comments: 18 (17 by maintainers)
This does tie in with a broader class of requests we’ve had for a “strict parameter passing” mode where you can only pass declared parameters to a component. I could imagine that such a mode would also impose the limit that everything you pass has to be named exactly (case sensitively).
One related issue is https://github.com/dotnet/aspnetcore/issues/45667 - I’m sure there was another one that went into more detail but can’t find it now.
The concept for doing this nonbreakingly is that people would opt in on a per-component or per-project level (e.g., in
_Imports.razor
). Then it could be enforced at compile time, at least when using any components that don’t haveCaptureUnmatchedAttributes
. It doesn’t really work for ones that do useCaptureUnmatchedAttributes
because of the forward-compat issue mentioned above - people would be unable to add any new parameters without risking a breaking change.No, but it is very HTML-y, and we’re dealing with a syntax that you can equally think of as being HTML-like as it is C#-like. There isn’t really an objectively right choice for this.
In one sense I wish we had sided with the stricter rule in the beginning, but there are arguments against it. Part of our original rationale was being able to override standard HTML attributes nonbreakingly. For example consider
QuickGrid
, which has a parameterClass
. If it didn’t originally haveClass
but did support passthrough of arbitrary HTML attributes, people may have been writing<QuickGrid class="my-css-class">
. Then later, we want to add a “class” parameter so we can do programmatic stuff with it. Would we call itClass
orclass
? The latter would preserve back-compat, but is also completely inconsistent with our style rules. And some people might already have been writingClass
(or to make an absurd but technically valid argument, evencLaSs
) since that’s legal in HTML. Being case-insensitive solves all of that.Even if we can get over the design problem mentiond above, I am nervous about the extent of the breaking change that would cause.
Good breaking changes are ones where affected code is immediately obvious and can be detected by the compiler. This is at the opposite extreme, since nonmatching parameters would start going into
CaptureUnmatchedAttributes
, changing the behavior of components in subtle ways instead of failing at compile time or even giving a clear runtime exception. People affected by this would have almost no way to know why their app was now broken, so it would be very hard to know they should look for a compat flag.@SteveSandersonMS @danroth27 Ugh, apologies GitHub notifications failed me here and I didn’t see your responses.
I like the idea of the generic
AddComponentParameter
call, but I can’t remember right now if the compiler actually has access to the expected component type at the point where we emit this call.If we do, we could presumably just insert a cast to the target type in the
AddAttribute
version. Given that we didn’t do that it makes me think we might not. @jjonescz do you know if we have that info at the point we emit these calls?If we don’t, then I don’t think we can easily fix this: we have to decide to either allow implicit casting or we don’t. Given that we know it causes reported issues I would lean on the side of keeping the new behavior and noting breaking change, but lets see if we can do the target typing version before deciding on it.
@mkArtakMSFT @danroth27 @jaredpar @jjonescz
This is caused by the following change: https://github.com/dotnet/razor/pull/8286, which comes from https://github.com/dotnet/aspnetcore/pull/46562
Previously, having an implicit cast to e.g.
string
on an object component parameter could cause the wrongAddAttribute
overload to be chosen for component params, which later caused type errors when the thing was looked up and found to be astring
rather than the expected object type.We fixed this by calling the new
AddComponentParameter(...)
method, or adding an(object)
cast if the runtime doesn’t have the new method, forcing the correct overload to be called.In this case here we’re hitting the
(object)
version of the code gen: the object in question was castable tostring
so we previously chose thestring
overload ofAddAttribute
. Because the underlying object on the parameter is actually a string, it doesn’t have the issue at runtime, because the types match, and the application functioned correctly.In the new code gen we cast the underlying object directly to
object
so it never undergoes the explicit string conversion. When we then try and assign that to a string field at runtime it fails.I think it’s fair to say that the previously observed behavior worked unintentionally, and we’ve ‘fixed’ it with that PR, but I’m not sure how many other cases of this are in the wild and if we want to try and think about how we can maintain the existing behavior. Note that even though the code path will be different on a runtime with
AddComponentParameter
the outcome will be the same.For reference:
Previous code:
New code:
@mkArtakMSFT @danroth27 Not sure of anything that intentionally changed here. I’ll take a look.
Can we get more details on the type of
ExpirationPlaceholderCaption
? How was that property getting cast to a string previously? Can we see the type?