Skip to content

Commit 116d21e

Browse files
Razor generic parameter cascading (#29767)
* Begin adding test for type parameter cascading * Cascade explicit ancestor type args * Switch to a push model for cascading generic types * Refactoring to simplify next change * Further refactoring to simplify next change * Make inferred generic types actually cascade. Still has a lot of missing cases, and is inefficient (causes repeated evaluations). * Minor refactoring (name change etc.) * Avoid multiple evaluations of the expression used for generic type inference by creating variables * Make cascaded type inference work with generic child content AFAICT the previous implementation was incorrect to treat child content as something that can "cover" a generic parameter, since it's always equivalent to an untyped lambda (i.e., doesn't specify its own types). It wouldn't have mattered before because you'd always have a different param on the same component providing the type, but now it does matter because we need to know whether to infer from an ancestor. * Handle lambdas on both provider and receiver sides * Handle inference of multiple generic parameters without duplicate evaluations * Update design time to match previous * Steps towards handling further cases * Refactoring that will simplify subsequent updates * Revert some changes I no longer want * Use the refactorings to simplify and ensure consistency about ordering * Emit _CaptureParameters variant of type inference method * Call the _CaptureParameters method to ensure single evaluation * Supply captured variables to descendants for cascading generic type inference * Update new baselines * Update comments * Step towards being able to handle unrelated generic types * Handle provision of unrelated generic types via diagnostic * Remove obsolete comment * Begin filtering which type params cascade * Now actually filter provided cascading generic types * Eliminate unnecessary parameter capturing for non-cascading-components * Update baselines * More test cases * Show we can have siblings * Show type cascading can pass through multiple levels * Clarify how we're retaining back-compat in a very obscure case * Better comments * Clean up APIs * CR: Null check * Rename attribute for consistency with other framework terminology * Add CascadingTypeParameterAttribute * Update tests to match new attribute name * CR: More tests and simplification * CR: Rename
1 parent edc1ca8 commit 116d21e

File tree

104 files changed

+4689
-260
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+4689
-260
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Components
7+
{
8+
/// <summary>
9+
/// Denotes the generic type parameter as cascading. This allows generic type inference
10+
/// to use this type parameter value automatically on descendants that also have a type
11+
/// parameter with the same name.
12+
/// </summary>
13+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
14+
public sealed class CascadingTypeParameterAttribute : Attribute
15+
{
16+
/// <summary>
17+
/// Constructs an instance of <see cref="CascadingTypeParameterAttribute"/>.
18+
/// </summary>
19+
/// <param name="name">The name of the type parameter.</param>
20+
public CascadingTypeParameterAttribute(string name)
21+
{
22+
if (name == null)
23+
{
24+
throw new ArgumentNullException(nameof(name));
25+
}
26+
27+
Name = name;
28+
}
29+
30+
/// <summary>
31+
/// Gets the name of the type parameter.
32+
/// </summary>
33+
public string Name { get; }
34+
}
35+
}

src/Components/Components/src/PublicAPI.Unshipped.txt

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Microsoft.AspNetCore.Components.DynamicComponent.Parameters.set -> void
1111
Microsoft.AspNetCore.Components.DynamicComponent.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task!
1212
Microsoft.AspNetCore.Components.DynamicComponent.Type.get -> System.Type!
1313
Microsoft.AspNetCore.Components.DynamicComponent.Type.set -> void
14+
Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute
15+
Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.CascadingTypeParameterAttribute(string! name) -> void
16+
Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute.Name.get -> string!
1417
static Microsoft.AspNetCore.Components.ParameterView.FromDictionary(System.Collections.Generic.IDictionary<string!, object?>! parameters) -> Microsoft.AspNetCore.Components.ParameterView
1518
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs) -> System.Threading.Tasks.Task!
1619
*REMOVED*readonly Microsoft.AspNetCore.Components.RenderTree.RenderTreeEdit.RemovedAttributeName -> string

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDesignTimeNodeWriter.cs

+81-67
Original file line numberDiff line numberDiff line change
@@ -418,22 +418,49 @@ public override void WriteComponent(CodeRenderingContext context, ComponentInter
418418
}
419419
else
420420
{
421+
var parameters = GetTypeInferenceMethodParameters(node.TypeInferenceNode);
422+
423+
// If this component is going to cascade any of its generic types, we have to split its type inference
424+
// into two parts. First we call an inference method that captures all the parameters in local variables,
425+
// then we use those to call the real type inference method that emits the component. The reason for this
426+
// is so the captured variables can be used by descendants without re-evaluating the expressions.
427+
CodeWriterExtensions.CSharpCodeWritingScope? typeInferenceCaptureScope = null;
428+
if (node.Component.SuppliesCascadingGenericParameters())
429+
{
430+
typeInferenceCaptureScope = context.CodeWriter.BuildScope();
431+
context.CodeWriter.Write(node.TypeInferenceNode.FullTypeName);
432+
context.CodeWriter.Write(".");
433+
context.CodeWriter.Write(node.TypeInferenceNode.MethodName);
434+
context.CodeWriter.Write("_CaptureParameters(");
435+
var isFirst = true;
436+
foreach (var parameter in parameters.Where(p => p.UsedForTypeInference))
437+
{
438+
if (isFirst)
439+
{
440+
isFirst = false;
441+
}
442+
else
443+
{
444+
context.CodeWriter.Write(", ");
445+
}
446+
447+
WriteTypeInferenceMethodParameterInnards(context, parameter);
448+
context.CodeWriter.Write(", out var ");
449+
450+
var variableName = $"__typeInferenceArg_{_scopeStack.Depth}_{parameter.ParameterName}";
451+
context.CodeWriter.Write(variableName);
452+
453+
UseCapturedCascadingGenericParameterVariable(node, parameter, variableName);
454+
}
455+
context.CodeWriter.WriteLine(");");
456+
}
457+
421458
// When we're doing type inference, we can't write all of the code inline to initialize
422459
// the component on the builder. We generate a method elsewhere, and then pass all of the information
423460
// to that method. We pass in all of the attribute values + the sequence numbers.
424461
//
425462
// __Blazor.MyComponent.TypeInference.CreateMyComponent_0(__builder, 0, 1, ..., 2, ..., 3, ....);
426463

427-
// Preserve order of attributes + splats
428-
var attributes = node.Children.Where(s =>
429-
{
430-
return s is ComponentAttributeIntermediateNode || s is SplatIntermediateNode;
431-
}).ToList();
432-
var childContents = node.ChildContents.ToList();
433-
var captures = node.Captures.ToList();
434-
var setKeys = node.SetKeys.ToList();
435-
var remaining = attributes.Count + childContents.Count + captures.Count + setKeys.Count;
436-
437464
context.CodeWriter.Write(node.TypeInferenceNode.FullTypeName);
438465
context.CodeWriter.Write(".");
439466
context.CodeWriter.Write(node.TypeInferenceNode.MethodName);
@@ -443,76 +470,27 @@ public override void WriteComponent(CodeRenderingContext context, ComponentInter
443470
context.CodeWriter.Write(", ");
444471

445472
context.CodeWriter.Write("-1");
446-
context.CodeWriter.Write(", ");
447473

448-
for (var i = 0; i < attributes.Count; i++)
474+
foreach (var parameter in parameters)
449475
{
450-
context.CodeWriter.Write("-1");
451476
context.CodeWriter.Write(", ");
452477

453-
// Don't type check generics, since we can't actually write the type name.
454-
// The type checking with happen anyway since we defined a method and we're generating
455-
// a call to it.
456-
if (attributes[i] is ComponentAttributeIntermediateNode attribute)
457-
{
458-
WriteComponentAttributeInnards(context, attribute, canTypeCheck: false);
459-
}
460-
else if (attributes[i] is SplatIntermediateNode splat)
461-
{
462-
WriteSplatInnards(context, splat, canTypeCheck: false);
463-
}
464-
465-
remaining--;
466-
if (remaining > 0)
478+
if (!string.IsNullOrEmpty(parameter.SeqName))
467479
{
480+
context.CodeWriter.Write("-1");
468481
context.CodeWriter.Write(", ");
469482
}
470-
}
471483

472-
for (var i = 0; i < childContents.Count; i++)
473-
{
474-
context.CodeWriter.Write("-1");
475-
context.CodeWriter.Write(", ");
476-
477-
WriteComponentChildContentInnards(context, childContents[i]);
478-
479-
remaining--;
480-
if (remaining > 0)
481-
{
482-
context.CodeWriter.Write(", ");
483-
}
484+
WriteTypeInferenceMethodParameterInnards(context, parameter);
484485
}
485486

486-
for (var i = 0; i < setKeys.Count; i++)
487-
{
488-
context.CodeWriter.Write("-1");
489-
context.CodeWriter.Write(", ");
490-
491-
WriteSetKeyInnards(context, setKeys[i]);
492-
493-
remaining--;
494-
if (remaining > 0)
495-
{
496-
context.CodeWriter.Write(", ");
497-
}
498-
}
487+
context.CodeWriter.Write(");");
488+
context.CodeWriter.WriteLine();
499489

500-
for (var i = 0; i < captures.Count; i++)
490+
if (typeInferenceCaptureScope.HasValue)
501491
{
502-
context.CodeWriter.Write("-1");
503-
context.CodeWriter.Write(", ");
504-
505-
WriteReferenceCaptureInnards(context, captures[i], shouldTypeCheck: false);
506-
507-
remaining--;
508-
if (remaining > 0)
509-
{
510-
context.CodeWriter.Write(", ");
511-
}
492+
typeInferenceCaptureScope.Value.Dispose();
512493
}
513-
514-
context.CodeWriter.Write(");");
515-
context.CodeWriter.WriteLine();
516494
}
517495

518496
// We want to generate something that references the Component type to avoid
@@ -540,6 +518,42 @@ public override void WriteComponent(CodeRenderingContext context, ComponentInter
540518
}
541519
}
542520

521+
private void WriteTypeInferenceMethodParameterInnards(CodeRenderingContext context, TypeInferenceMethodParameter parameter)
522+
{
523+
switch (parameter.Source)
524+
{
525+
case ComponentAttributeIntermediateNode attribute:
526+
// Don't type check generics, since we can't actually write the type name.
527+
// The type checking with happen anyway since we defined a method and we're generating
528+
// a call to it.
529+
WriteComponentAttributeInnards(context, attribute, canTypeCheck: false);
530+
break;
531+
case SplatIntermediateNode splat:
532+
WriteSplatInnards(context, splat, canTypeCheck: false);
533+
break;
534+
case ComponentChildContentIntermediateNode childNode:
535+
WriteComponentChildContentInnards(context, childNode);
536+
break;
537+
case SetKeyIntermediateNode setKey:
538+
WriteSetKeyInnards(context, setKey);
539+
break;
540+
case ReferenceCaptureIntermediateNode capture:
541+
WriteReferenceCaptureInnards(context, capture, shouldTypeCheck: false);
542+
break;
543+
case CascadingGenericTypeParameter syntheticArg:
544+
// The value should be populated before we use it, because we emit code for creating ancestors
545+
// first, and that's where it's populated. However if this goes wrong somehow, we don't want to
546+
// throw, so use a fallback
547+
context.CodeWriter.Write(syntheticArg.ValueExpression ?? "default");
548+
break;
549+
case TypeInferenceCapturedVariable capturedVariable:
550+
context.CodeWriter.Write(capturedVariable.VariableName);
551+
break;
552+
default:
553+
throw new InvalidOperationException($"Not implemented: type inference method parameter from source {parameter.Source}");
554+
}
555+
}
556+
543557
public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeIntermediateNode node)
544558
{
545559
if (context == null)

0 commit comments

Comments
 (0)