-
Notifications
You must be signed in to change notification settings - Fork 10.7k
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
Changes from 4 commits
57f3698
d87719d
5aeab86
79e819f
8422051
fc1e9cf
6ddd7fa
2353372
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); | ||
|
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. @copilot Remove the current 2 test files and instead perform the tests using test components. Look at RendererTests to see how some of the tests are done. We must validate that
Contributor
Author
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. Done. Removed the standalone test files and added integration tests to RendererTest.cs that validate contravariance works with SetParametersAsync. The tests use test components with RenderFragment and RenderFragment<List> parameters, and verify that passing RenderFragment and RenderFragment<IList> respectively works correctly through the component parameter setting pipeline. Commit: 8422051 |
| 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; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.