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);
275 changes: 275 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,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
{
Expand Down Expand Up @@ -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}";
}

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.

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 RenderFragment<int> but pass in RenderFragment<object>

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.

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 in modifier.

The requested tests (RenderFragment<IComparable>RenderFragment<struct>, RenderFragment<object>RenderFragment<int>, RenderFragment<IConvertible>RenderFragment<enum>) cannot compile in C# because the target types are value types.

I've updated the tests to demonstrate contravariance with scenarios that ARE supported:

  • Interface hierarchy with reference types (IComparable → string, IComparable → Version)
  • Base type to derived reference type (object → string)

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));
}
}
}
}
Loading