Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 19, 2025

Add contravariance to RenderFragment

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Mark TValue as contravariant in RenderFragment

Description

Marking TValue as contravariant with the in modifier enables passing render fragments that accept base types where derived types are expected, eliminating complex reflection-based adapters in generic component composition.

Core Change:

// Before
public delegate RenderFragment RenderFragment<TValue>(TValue value);

// After
public delegate RenderFragment RenderFragment<in TValue>(TValue value);

Enables:

// Non-generic fragment handling base type
RenderFragment<IList<Product>> baseTemplate = (IList<Product> items) => builder => { /* ... */ };

// Can now be assigned where specific type is expected
RenderFragment<List<Product>> specificTemplate = baseTemplate; // ✅ Works with contravariance

// DynamicComponent scenario now works directly
var parameters = new Dictionary<string, object>
{
    ["ItemsTemplate"] = baseTemplate, // ✅ No adapter needed
};
<DynamicComponent Type="typeof(Pager<Product>)" Parameters="parameters" />

Testing:

  • 7 new tests covering contravariance scenarios (base class, interface, method parameters, DynamicComponent use cases)
  • All 1,191 existing tests pass unchanged
  • Backward compatible (contravariance relaxes type restrictions)
Original prompt

This section details on the original issue you should resolve

<issue_title>RenderFragment contravariance on TValue</issue_title>
<issue_description>## Background and Motivation

RenderFragment is currently invariant, which makes passing render fragments that accept a base class awkward when composing generic components. Marking TValue as contravariant enables passing a fragment that handles a base type to places expecting a fragment for a derived type (or a nongeneric to a generic wrapper) without manual adapters.

@code {
	// Non-generic fragment that renders from the base list type
	RenderFragment<IList> ItemsTemplate = (IList models) => @<div>
		@foreach (var item in models.Items) { <span>@item.ToString()</span> }
	</div>;

	// We dynamically pick a T at runtime (e.g., via reflection/type factory)
	var itemType = actualType; // e.g., typeof(Product)
	var componentType = typeof(Pager<>).MakeGenericType(itemType);

	// But we can’t assign Found where RenderFragment<List<T>> is expected
	var parameters = new Dictionary<string, object?>
	{
		["ItemsTemplate"] = ItemsTemplate, // ❌ invariant delegate blocks this
	};

	<DynamicComponent Type="@componentType" Parameters="parameters" />
}

Proposed API

Add contravariance to TValue of RenderFragment.

public delegate RenderFragment RenderFragment<in TValue>(TValue value);

Usage Examples

The above mentioned code would run without exceptions.

Alternative Designs

The alternative is to make this adapter

  // Adapter: IList -> List<T> signature
static object CreateTypedTemplate(Type itemType, RenderFragment<IList> baseTemplate)
{
	// Make a delegate: RenderFragment<List<T>> (List<T> list) => baseTemplate(list)
	var listType = typeof(List<>).MakeGenericType(itemType);

	// Build a strongly-typed lambda via reflection
	var param = System.Linq.Expressions.Expression.Parameter(listType, "list");

	// Convert List<T> to IList for the base template call
	var convert = System.Linq.Expressions.Expression.Convert(param, typeof(System.Collections.IList));

	// baseTemplate(converted)
	var invoke = System.Linq.Expressions.Expression.Invoke(
	  System.Linq.Expressions.Expression.Constant(baseTemplate),
	  convert
	);

	var lambdaType = typeof(RenderFragment<>).MakeGenericType(listType);
	var lambda = System.Linq.Expressions.Expression.Lambda(lambdaType, invoke, param);

	return lambda.Compile();
}

Risks

Breaking assumed invariance via reflection in code/libraries that assume it.</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@dotnet-policy-service
Copy link
Contributor

Greetings! You've submitted a PR that modifies code that is shared with https://github.com/dotnet/runtime . Please make sure you synchronize this code with the changes in that repo!

Copilot AI changed the title [WIP] Update RenderFragment<TValue> to support contravariance on TValue Add contravariance to RenderFragment<TValue> Dec 19, 2025
Copilot AI requested a review from javiercn December 19, 2025 13:07
@javiercn javiercn marked this pull request as ready for review December 19, 2025 15:57
@javiercn javiercn requested a review from a team as a code owner December 19, 2025 15:57
Copilot AI review requested due to automatic review settings December 19, 2025 15:58
@javiercn
Copy link
Member

Tests are only for demonstration purposes. We will remove them as this is just a compiler restriction that we are relaxing

/// </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);
Copy link
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
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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds contravariance to the RenderFragment<TValue> delegate by marking the TValue type parameter with the in modifier. This enables passing render fragments that accept base types where derived types are expected, simplifying generic component composition and eliminating the need for complex reflection-based adapters when working with dynamic components.

Key Changes

  • Modified RenderFragment<TValue> delegate signature to make TValue contravariant
  • Added 7 comprehensive unit tests covering various contravariance scenarios (base classes, interfaces, method parameters, DynamicComponent usage)
  • Tests validate both the core functionality and the specific issue scenario described in the GitHub issue

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/Components/Components/src/RenderFragment.cs Added in modifier to TValue type parameter, making the delegate contravariant
src/Components/Components/test/RenderFragmentContravarianceTest.cs Added 4 tests covering contravariance with base classes, interfaces, method parameters, and object type
src/Components/Components/test/RenderFragmentIssueScenarioTest.cs Added 3 tests validating the exact GitHub issue scenario with DynamicComponent and demonstrating elimination of adapter code

Comment on lines 19 to 20
/// <typeparam name="TValue">The type of object.</typeparam>
/// <param name="value">The value used to build the content.</param>
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

The XML documentation for the RenderFragment delegate should be updated to mention that TValue is contravariant. This is important for API documentation because contravariance affects how developers can use the delegate. Consider adding a remark or updating the typeparam description to note that TValue is contravariant, allowing render fragments that accept base types to be used where derived types are expected.

Suggested change
/// <typeparam name="TValue">The type of object.</typeparam>
/// <param name="value">The value used to build the content.</param>
/// <typeparam name="TValue">
/// The type of object. This type parameter is contravariant, so a <see cref="RenderFragment{TValue}"/>
/// that accepts a base type can be used where a fragment for a derived type is expected.
/// </typeparam>
/// <param name="value">The value used to build the content.</param>
/// <remarks>
/// Because <typeparamref name="TValue"/> is contravariant, you can, for example, use a
/// <see cref="RenderFragment{TValue}"/> declared as <c>RenderFragment&lt;object&gt;</c> in place of
/// a <c>RenderFragment&lt;string&gt;</c>, since <c>string</c> derives from <c>object</c>.
/// </remarks>

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RenderFragment<TValue> contravariance on TValue

3 participants