Skip to content

RenderFragment<TValue> contravariance on TValue #63961

@HrvojeJuric

Description

@HrvojeJuric

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-blazorIncludes: Blazor, Razor Componentscopilot-candidate

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions