Skip to content
2 changes: 1 addition & 1 deletion src/Components/Components/src/RenderFragment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Comment thread
javiercn marked this conversation as resolved.
public delegate RenderFragment RenderFragment<TValue>(TValue value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 *REMOVED* etc

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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);
101 changes: 101 additions & 0 deletions src/Components/Components/test/RendererTest.cs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added three additional tests as requested:

  1. Struct scenario: Using reference type hierarchy with struct property (StructWrapperBase → StructWrapperDerived)
  2. Primitive type scenario: Testing object → string contravariance
  3. Enum scenario: Using reference type hierarchy with enum property (EnumWrapperBase → EnumWrapperDerived)

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
Expand Up @@ -5158,6 +5158,66 @@ 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);
}

[HasUnknownRenderMode]
private class ComponentWithUnknownRenderMode : IComponent
{
Expand Down Expand Up @@ -6162,4 +6222,45 @@ 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));
}
}
}
}
Loading