Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 on lines 19 to 20
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.
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.

public delegate RenderFragment RenderFragment<in TValue>(TValue value);
107 changes: 107 additions & 0 deletions src/Components/Components/test/RenderFragmentContravarianceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components.Test;

public class RenderFragmentContravarianceTest
{
[Fact]
public void RenderFragment_SupportsContravariance_WithBaseClass()
{
// Arrange
var builder = new RenderTreeBuilder();
RenderFragment<Animal> animalFragment = (Animal animal) => innerBuilder =>
{
innerBuilder.AddContent(0, $"Animal: {animal.Name}");
};

// Act - Assign to a variable expecting a more derived type (contravariance)
RenderFragment<Dog> dogFragment = animalFragment;
var dog = new Dog { Name = "Buddy", Breed = "Golden Retriever" };
var result = dogFragment(dog);

// Assert - Should compile and work without exception
result(builder);
Assert.NotNull(result);
}

[Fact]
public void RenderFragment_SupportsContravariance_WithInterface()
{
// Arrange
var builder = new RenderTreeBuilder();
RenderFragment<IList<string>> listFragment = (IList<string> items) => innerBuilder =>
{
foreach (var item in items)
{
innerBuilder.AddContent(0, item);
}
};

// Act - Assign to a variable expecting a more specific type (contravariance)
RenderFragment<List<string>> specificListFragment = listFragment;
var list = new List<string> { "Item1", "Item2", "Item3" };
var result = specificListFragment(list);

// Assert - Should compile and work without exception
result(builder);
Assert.NotNull(result);
}

[Fact]
public void RenderFragment_SupportsContravariance_InMethodParameter()
{
// Arrange
RenderFragment<Animal> animalFragment = (Animal animal) => innerBuilder =>
{
innerBuilder.AddContent(0, $"Animal: {animal.Name}");
};

var dog = new Dog { Name = "Max", Breed = "Labrador" };
var builder = new RenderTreeBuilder();

// Act - Pass base type fragment to method expecting derived type fragment
ProcessDogFragment(animalFragment, dog, builder);

// Assert - Should compile and work without exception
Assert.True(true); // If we got here, contravariance worked
}

[Fact]
public void RenderFragment_SupportsContravariance_WithObject()
{
// Arrange
var builder = new RenderTreeBuilder();
RenderFragment<object> objectFragment = (object obj) => innerBuilder =>
{
innerBuilder.AddContent(0, obj?.ToString() ?? "null");
};

// Act - Assign to a variable expecting a more specific type (contravariance)
RenderFragment<string> stringFragment = objectFragment;
var result = stringFragment("test string");

// Assert - Should compile and work without exception
result(builder);
Assert.NotNull(result);
}

private void ProcessDogFragment(RenderFragment<Dog> fragment, Dog dog, RenderTreeBuilder builder)
{
var result = fragment(dog);
result(builder);
}

// Test classes
private class Animal
{
public string Name { get; set; } = string.Empty;
}

private class Dog : Animal
{
public string Breed { get; set; } = string.Empty;
}
}
123 changes: 123 additions & 0 deletions src/Components/Components/test/RenderFragmentIssueScenarioTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components.Test;

/// <summary>
/// Tests to validate the exact scenario described in the GitHub issue.
/// This demonstrates using RenderFragment contravariance with DynamicComponent.
/// </summary>
public class RenderFragmentIssueScenarioTest
{
[Fact]
public void RenderFragment_Contravariance_EnablesDynamicComponentScenario()
{
// This test validates the exact scenario from the issue:
// Using a RenderFragment<IList> where RenderFragment<List<T>> is expected

// Arrange - Non-generic fragment that renders from the base list type
RenderFragment<IList<string>> itemsTemplate = (IList<string> models) => innerBuilder =>
{
foreach (var item in models)
{
innerBuilder.AddContent(0, $"Item: {item}");
}
};

// Simulate dynamically picking a T at runtime
var itemType = typeof(string);
var listType = typeof(List<>).MakeGenericType(itemType); // List<string>

// Act - Create parameters as would be done with DynamicComponent
// Before contravariance, this would fail because RenderFragment<IList<string>>
// couldn't be assigned where RenderFragment<List<string>> was expected
var parameters = new Dictionary<string, object>
{
["ItemsTemplate"] = itemsTemplate, // ✅ Now works with contravariance!
};

// Validate we can cast to the expected type
var typedFragment = parameters["ItemsTemplate"] as RenderFragment<List<string>>;

// Assert
Assert.NotNull(typedFragment);

// Verify it actually works
var list = new List<string> { "Product1", "Product2", "Product3" };
var builder = new RenderTreeBuilder();
var result = typedFragment(list);
result(builder);
}

[Fact]
public void RenderFragment_Contravariance_WorksWithPagerComponent()
{
// This test simulates a more complete scenario with a pager component
// that expects RenderFragment<List<Product>> but we provide RenderFragment<IList<Product>>

// Arrange - Create a base template that works with any IList<Product>
RenderFragment<IList<Product>> baseTemplate = (IList<Product> items) => innerBuilder =>
{
innerBuilder.OpenElement(0, "div");
foreach (var item in items)
{
innerBuilder.OpenElement(1, "span");
innerBuilder.AddContent(2, item.Name);
innerBuilder.CloseElement();
}
innerBuilder.CloseElement();
};

// Act - Use it where List<Product> is expected (contravariance)
RenderFragment<List<Product>> specificTemplate = baseTemplate;

var products = new List<Product>
{
new Product { Name = "Product 1" },
new Product { Name = "Product 2" },
new Product { Name = "Product 3" }
};

var builder = new RenderTreeBuilder();
var result = specificTemplate(products);

// Assert - Should compile and execute without error
result(builder);
Assert.NotNull(result);
}

[Fact]
public void RenderFragment_Contravariance_EliminatesNeedForAdapter()
{
// This test demonstrates that we no longer need the complex adapter
// shown in the issue's "Alternative Designs" section

// Arrange - Base template
RenderFragment<IList<string>> baseTemplate = (IList<string> items) => innerBuilder =>
{
innerBuilder.AddContent(0, $"Count: {items.Count}");
};

// Before contravariance, you'd need CreateTypedTemplate adapter (complex reflection code)
// Now, direct assignment just works:
RenderFragment<List<string>> typedTemplate = baseTemplate; // ✅ Simple!

// Act
var list = new List<string> { "A", "B", "C" };
var builder = new RenderTreeBuilder();
var result = typedTemplate(list);
result(builder);

// Assert
Assert.NotNull(result);
// The fact that this compiles and runs is the success - no adapter needed!
}

// Test classes
private class Product
{
public string Name { get; set; } = string.Empty;
}
}
Loading