-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Add contravariance to RenderFragment<TValue> #64822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
57f3698
d87719d
5aeab86
79e819f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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> | ||
| public delegate RenderFragment RenderFragment<TValue>(TValue value); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| 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; | ||
| } | ||
| } |
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.