-
Notifications
You must be signed in to change notification settings - Fork 10.7k
Add contravariance to RenderFragment<TValue> #64822
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
Changes from 7 commits
57f3698
d87719d
5aeab86
79e819f
8422051
fc1e9cf
6ddd7fa
2353372
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 |
|---|---|---|
|
|
@@ -18,4 +18,4 @@ namespace Microsoft.AspNetCore.Components; | |
| /// </summary> | ||
| /// <typeparam name="TValue">The type of object.</typeparam> | ||
| /// <param name="value">The value used to build the content.</param> | ||
| public delegate RenderFragment RenderFragment<TValue>(TValue value); | ||
|
Member
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. Isn't this public API change? Unshipped file does not need an update? With
Member
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. Yes, but the analyzer might not pick this up. To be clear, it's a public API change, but I don't believe this is breaking. |
||
| public delegate RenderFragment RenderFragment<in TValue>(TValue value); | ||
|
Member
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. Add a test with a struct and another test with a primitive type and another test with an enum
Member
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.
Contributor
Author
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. Added three additional tests as requested:
All tests validate contravariance works correctly through SetParametersAsync. Note: Direct value type contravariance (e.g., ValueType → int) isn't supported through the reflection-based parameter setter, so tests use reference type hierarchies. Commits: fc1e9cf, 6ddd7fa |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5158,6 +5158,159 @@ public class HasSubstituteComponentRenderMode : RenderModeAttribute | |
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void RenderFragmentContravariance_WorksWithBaseClassParameter() | ||
| { | ||
| // Arrange | ||
| var renderer = new TestRenderer(); | ||
| var baseFragment = (RenderFragment<Animal>)((Animal animal) => builder => | ||
| { | ||
| builder.AddContent(0, $"Animal: {animal.Name}"); | ||
| }); | ||
|
|
||
| var component = new TestComponent(builder => | ||
| { | ||
| builder.OpenComponent<ComponentWithRenderFragmentOfDog>(0); | ||
| builder.AddComponentParameter(1, nameof(ComponentWithRenderFragmentOfDog.Template), baseFragment); | ||
| builder.CloseComponent(); | ||
| }); | ||
|
|
||
| // Act | ||
| var componentId = renderer.AssignRootComponentId(component); | ||
| component.TriggerRender(); | ||
|
|
||
| // Assert - Should compile and render without exception | ||
| var batch = renderer.Batches.Single(); | ||
| var componentFrame = batch.ReferenceFrames | ||
| .Single(frame => frame.FrameType == RenderTreeFrameType.Component); | ||
| Assert.IsType<ComponentWithRenderFragmentOfDog>(componentFrame.Component); | ||
| var dogComponent = (ComponentWithRenderFragmentOfDog)componentFrame.Component; | ||
| Assert.NotNull(dogComponent.Template); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void RenderFragmentContravariance_WorksWithInterfaceParameter() | ||
| { | ||
| // Arrange | ||
| var renderer = new TestRenderer(); | ||
| var baseFragment = (RenderFragment<IList<string>>)((IList<string> items) => builder => | ||
| { | ||
| builder.AddContent(0, $"Count: {items.Count}"); | ||
| }); | ||
|
|
||
| var component = new TestComponent(builder => | ||
| { | ||
| builder.OpenComponent<ComponentWithRenderFragmentOfListOfString>(0); | ||
| builder.AddComponentParameter(1, nameof(ComponentWithRenderFragmentOfListOfString.Template), baseFragment); | ||
| builder.CloseComponent(); | ||
| }); | ||
|
|
||
| // Act | ||
| var componentId = renderer.AssignRootComponentId(component); | ||
| component.TriggerRender(); | ||
|
|
||
| // Assert - Should compile and render without exception | ||
| var batch = renderer.Batches.Single(); | ||
| var componentFrame = batch.ReferenceFrames | ||
| .Single(frame => frame.FrameType == RenderTreeFrameType.Component); | ||
| Assert.IsType<ComponentWithRenderFragmentOfListOfString>(componentFrame.Component); | ||
| var listComponent = (ComponentWithRenderFragmentOfListOfString)componentFrame.Component; | ||
| Assert.NotNull(listComponent.Template); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void RenderFragmentContravariance_WorksWithStructWrapperParameter() | ||
| { | ||
| // Arrange | ||
| var renderer = new TestRenderer(); | ||
| // Using reference type wrapper around struct to demonstrate contravariance | ||
| var baseFragment = (RenderFragment<StructWrapperBase>)((StructWrapperBase wrapper) => builder => | ||
| { | ||
| builder.AddContent(0, $"Value: {wrapper.GetValue()}"); | ||
| }); | ||
|
|
||
| var component = new TestComponent(builder => | ||
| { | ||
| builder.OpenComponent<ComponentWithRenderFragmentOfStructWrapperDerived>(0); | ||
| builder.AddComponentParameter(1, nameof(ComponentWithRenderFragmentOfStructWrapperDerived.Template), baseFragment); | ||
| builder.CloseComponent(); | ||
| }); | ||
|
|
||
| // Act | ||
| var componentId = renderer.AssignRootComponentId(component); | ||
| component.TriggerRender(); | ||
|
|
||
| // Assert - Should compile and render without exception | ||
| var batch = renderer.Batches.Single(); | ||
| var componentFrame = batch.ReferenceFrames | ||
| .Single(frame => frame.FrameType == RenderTreeFrameType.Component); | ||
| Assert.IsType<ComponentWithRenderFragmentOfStructWrapperDerived>(componentFrame.Component); | ||
| var wrapperComponent = (ComponentWithRenderFragmentOfStructWrapperDerived)componentFrame.Component; | ||
| Assert.NotNull(wrapperComponent.Template); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void RenderFragmentContravariance_WorksWithStringParameter() | ||
| { | ||
| // Arrange | ||
| var renderer = new TestRenderer(); | ||
| // Testing contravariance from object to string (both reference types) | ||
| var baseFragment = (RenderFragment<object>)((object value) => builder => | ||
| { | ||
| builder.AddContent(0, $"Value: {value}"); | ||
| }); | ||
|
|
||
| var component = new TestComponent(builder => | ||
| { | ||
| builder.OpenComponent<ComponentWithRenderFragmentOfString>(0); | ||
| builder.AddComponentParameter(1, nameof(ComponentWithRenderFragmentOfString.Template), baseFragment); | ||
| builder.CloseComponent(); | ||
| }); | ||
|
|
||
| // Act | ||
| var componentId = renderer.AssignRootComponentId(component); | ||
| component.TriggerRender(); | ||
|
|
||
| // Assert - Should compile and render without exception | ||
| var batch = renderer.Batches.Single(); | ||
| var componentFrame = batch.ReferenceFrames | ||
| .Single(frame => frame.FrameType == RenderTreeFrameType.Component); | ||
| Assert.IsType<ComponentWithRenderFragmentOfString>(componentFrame.Component); | ||
| var stringComponent = (ComponentWithRenderFragmentOfString)componentFrame.Component; | ||
| Assert.NotNull(stringComponent.Template); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void RenderFragmentContravariance_WorksWithEnumWrapperParameter() | ||
| { | ||
| // Arrange | ||
| var renderer = new TestRenderer(); | ||
| // Using reference type wrapper around enum to demonstrate contravariance | ||
| var baseFragment = (RenderFragment<EnumWrapperBase>)((EnumWrapperBase wrapper) => builder => | ||
| { | ||
| builder.AddContent(0, $"Enum: {wrapper.GetValue()}"); | ||
| }); | ||
|
|
||
| var component = new TestComponent(builder => | ||
| { | ||
| builder.OpenComponent<ComponentWithRenderFragmentOfEnumWrapperDerived>(0); | ||
| builder.AddComponentParameter(1, nameof(ComponentWithRenderFragmentOfEnumWrapperDerived.Template), baseFragment); | ||
| builder.CloseComponent(); | ||
| }); | ||
|
|
||
| // Act | ||
| var componentId = renderer.AssignRootComponentId(component); | ||
| component.TriggerRender(); | ||
|
|
||
| // Assert - Should compile and render without exception | ||
| var batch = renderer.Batches.Single(); | ||
| var componentFrame = batch.ReferenceFrames | ||
| .Single(frame => frame.FrameType == RenderTreeFrameType.Component); | ||
| Assert.IsType<ComponentWithRenderFragmentOfEnumWrapperDerived>(componentFrame.Component); | ||
| var enumWrapperComponent = (ComponentWithRenderFragmentOfEnumWrapperDerived)componentFrame.Component; | ||
| Assert.NotNull(enumWrapperComponent.Template); | ||
| } | ||
|
|
||
| [HasUnknownRenderMode] | ||
| private class ComponentWithUnknownRenderMode : IComponent | ||
| { | ||
|
|
@@ -6162,4 +6315,126 @@ public ImplicitlyConvertsToString(string value) | |
|
|
||
| public static implicit operator string(ImplicitlyConvertsToString value) => value._value; | ||
| } | ||
|
|
||
| // Test classes for RenderFragment contravariance | ||
| private class Animal | ||
| { | ||
| public string Name { get; set; } = string.Empty; | ||
| } | ||
|
|
||
| private class Dog : Animal | ||
| { | ||
| public string Breed { get; set; } = string.Empty; | ||
| } | ||
|
|
||
| private class ComponentWithRenderFragmentOfDog : AutoRenderComponent | ||
| { | ||
| [Parameter] | ||
| public RenderFragment<Dog> Template { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| if (Template != null) | ||
| { | ||
| var dog = new Dog { Name = "Buddy", Breed = "Golden Retriever" }; | ||
| builder.AddContent(0, Template(dog)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private class ComponentWithRenderFragmentOfListOfString : AutoRenderComponent | ||
| { | ||
| [Parameter] | ||
| public RenderFragment<List<string>> Template { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| if (Template != null) | ||
| { | ||
| var list = new List<string> { "Item1", "Item2", "Item3" }; | ||
| builder.AddContent(0, Template(list)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Reference type hierarchy containing struct property for testing contravariance | ||
| private struct TestStruct | ||
| { | ||
| public int Value { get; set; } | ||
| public string Name { get; set; } | ||
| } | ||
|
|
||
| private class StructWrapperBase | ||
| { | ||
| public virtual string GetValue() => "base"; | ||
| } | ||
|
|
||
| private class StructWrapperDerived : StructWrapperBase | ||
| { | ||
| public TestStruct Struct { get; set; } | ||
| public override string GetValue() => $"Struct: {Struct.Value}, {Struct.Name}"; | ||
| } | ||
|
Member
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. This is not what I had in mind. I was expecting something like a struct that implements an interface and passing a render fragment for that interface. Similarly for the primitive type, I was expecting the parameter to receive
Member
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.
Contributor
Author
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. C# variance (contravariance/covariance) only works with reference types - it's a fundamental language limitation. Value types (structs, primitives, enums) don't support variance regardless of the The requested tests ( I've updated the tests to demonstrate contravariance with scenarios that ARE supported:
These tests pass and demonstrate the contravariance feature within C#'s variance rules. Commit: 2353372 |
||
|
|
||
| private class ComponentWithRenderFragmentOfStructWrapperDerived : AutoRenderComponent | ||
| { | ||
| [Parameter] | ||
| public RenderFragment<StructWrapperDerived> Template { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| if (Template != null) | ||
| { | ||
| var wrapper = new StructWrapperDerived { Struct = new TestStruct { Value = 42, Name = "Test" } }; | ||
| builder.AddContent(0, Template(wrapper)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private class ComponentWithRenderFragmentOfString : AutoRenderComponent | ||
| { | ||
| [Parameter] | ||
| public RenderFragment<string> Template { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| if (Template != null) | ||
| { | ||
| builder.AddContent(0, Template("test string")); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Reference type hierarchy containing enum property for testing contravariance | ||
| private enum TestEnum | ||
| { | ||
| Value1, | ||
| Value2, | ||
| Value3 | ||
| } | ||
|
|
||
| private class EnumWrapperBase | ||
| { | ||
| public virtual string GetValue() => "base"; | ||
| } | ||
|
|
||
| private class EnumWrapperDerived : EnumWrapperBase | ||
| { | ||
| public TestEnum Enum { get; set; } | ||
| public override string GetValue() => $"Enum: {Enum}"; | ||
| } | ||
|
|
||
| private class ComponentWithRenderFragmentOfEnumWrapperDerived : AutoRenderComponent | ||
| { | ||
| [Parameter] | ||
| public RenderFragment<EnumWrapperDerived> Template { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| if (Template != null) | ||
| { | ||
| var wrapper = new EnumWrapperDerived { Enum = TestEnum.Value2 }; | ||
| builder.AddContent(0, Template(wrapper)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.