diff --git a/src/Components/Components/src/RenderFragment.cs b/src/Components/Components/src/RenderFragment.cs index e5016ddfeae1..f1c28f34a624 100644 --- a/src/Components/Components/src/RenderFragment.cs +++ b/src/Components/Components/src/RenderFragment.cs @@ -18,4 +18,4 @@ namespace Microsoft.AspNetCore.Components; /// /// The type of object. /// The value used to build the content. -public delegate RenderFragment RenderFragment(TValue value); +public delegate RenderFragment RenderFragment(TValue value); diff --git a/src/Components/Components/test/RenderFragmentContravarianceTest.cs b/src/Components/Components/test/RenderFragmentContravarianceTest.cs new file mode 100644 index 000000000000..3aefe197b505 --- /dev/null +++ b/src/Components/Components/test/RenderFragmentContravarianceTest.cs @@ -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 animalFragment = (Animal animal) => innerBuilder => + { + innerBuilder.AddContent(0, $"Animal: {animal.Name}"); + }; + + // Act - Assign to a variable expecting a more derived type (contravariance) + RenderFragment 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> listFragment = (IList items) => innerBuilder => + { + foreach (var item in items) + { + innerBuilder.AddContent(0, item); + } + }; + + // Act - Assign to a variable expecting a more specific type (contravariance) + RenderFragment> specificListFragment = listFragment; + var list = new List { "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 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 objectFragment = (object obj) => innerBuilder => + { + innerBuilder.AddContent(0, obj?.ToString() ?? "null"); + }; + + // Act - Assign to a variable expecting a more specific type (contravariance) + RenderFragment stringFragment = objectFragment; + var result = stringFragment("test string"); + + // Assert - Should compile and work without exception + result(builder); + Assert.NotNull(result); + } + + private void ProcessDogFragment(RenderFragment 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; + } +} diff --git a/src/Components/Components/test/RenderFragmentIssueScenarioTest.cs b/src/Components/Components/test/RenderFragmentIssueScenarioTest.cs new file mode 100644 index 000000000000..a8e9cf272261 --- /dev/null +++ b/src/Components/Components/test/RenderFragmentIssueScenarioTest.cs @@ -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; + +/// +/// Tests to validate the exact scenario described in the GitHub issue. +/// This demonstrates using RenderFragment contravariance with DynamicComponent. +/// +public class RenderFragmentIssueScenarioTest +{ + [Fact] + public void RenderFragment_Contravariance_EnablesDynamicComponentScenario() + { + // This test validates the exact scenario from the issue: + // Using a RenderFragment where RenderFragment> is expected + + // Arrange - Non-generic fragment that renders from the base list type + RenderFragment> itemsTemplate = (IList 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 + + // Act - Create parameters as would be done with DynamicComponent + // Before contravariance, this would fail because RenderFragment> + // couldn't be assigned where RenderFragment> was expected + var parameters = new Dictionary + { + ["ItemsTemplate"] = itemsTemplate, // ✅ Now works with contravariance! + }; + + // Validate we can cast to the expected type + var typedFragment = parameters["ItemsTemplate"] as RenderFragment>; + + // Assert + Assert.NotNull(typedFragment); + + // Verify it actually works + var list = new List { "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> but we provide RenderFragment> + + // Arrange - Create a base template that works with any IList + RenderFragment> baseTemplate = (IList 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 is expected (contravariance) + RenderFragment> specificTemplate = baseTemplate; + + var products = new List + { + 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> baseTemplate = (IList items) => innerBuilder => + { + innerBuilder.AddContent(0, $"Count: {items.Count}"); + }; + + // Before contravariance, you'd need CreateTypedTemplate adapter (complex reflection code) + // Now, direct assignment just works: + RenderFragment> typedTemplate = baseTemplate; // ✅ Simple! + + // Act + var list = new List { "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; + } +}