diff --git a/src/Components/Components/src/DynamicComponent.cs b/src/Components/Components/src/DynamicComponent.cs
index 3e5e932a8bef..387d98f8fc0b 100644
--- a/src/Components/Components/src/DynamicComponent.cs
+++ b/src/Components/Components/src/DynamicComponent.cs
@@ -32,6 +32,7 @@ public DynamicComponent()
///
[Parameter]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
+ [EditorRequired]
public Type Type { get; set; } = default!;
///
diff --git a/src/Components/Components/src/EditorRequiredAttribute.cs b/src/Components/Components/src/EditorRequiredAttribute.cs
new file mode 100644
index 000000000000..38a23937668b
--- /dev/null
+++ b/src/Components/Components/src/EditorRequiredAttribute.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Components
+{
+ ///
+ /// Specifies that the component parameter is required to be provided by the user when authoring it in the editor.
+ ///
+ /// If a value for this parameter is not provided, editors or build tools may provide warnings indicating the user to
+ /// specify a value. This attribute is only valid on properties marked with .
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+ public sealed class EditorRequiredAttribute : Attribute
+ {
+ }
+}
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index 4058101d38ad..50c92b7e30c5 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -9,6 +9,8 @@ Microsoft.AspNetCore.Components.ComponentApplicationState.PersistAsJson(
Microsoft.AspNetCore.Components.ComponentApplicationState.PersistState(string! key, byte[]! value) -> void
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson(string! key, out TValue? instance) -> bool
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakePersistedState(string! key, out byte[]? value) -> bool
+Microsoft.AspNetCore.Components.EditorRequiredAttribute
+Microsoft.AspNetCore.Components.EditorRequiredAttribute.EditorRequiredAttribute() -> void
Microsoft.AspNetCore.Components.ErrorBoundaryBase
Microsoft.AspNetCore.Components.ErrorBoundaryBase.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment?
Microsoft.AspNetCore.Components.ErrorBoundaryBase.ChildContent.set -> void
diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs
index d94ca1083384..76f9f47d311e 100644
--- a/src/Components/Components/src/RouteView.cs
+++ b/src/Components/Components/src/RouteView.cs
@@ -25,6 +25,7 @@ public class RouteView : IComponent
/// displayed and the parameter values that will be supplied to the page.
///
[Parameter]
+ [EditorRequired]
public RouteData RouteData { get; set; }
///
diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs
index bc8bd0b3d6c0..3a05e7589bea 100644
--- a/src/Components/Components/src/Routing/Router.cs
+++ b/src/Components/Components/src/Routing/Router.cs
@@ -47,7 +47,9 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary
///
/// Gets or sets the assembly that should be searched for components matching the URI.
///
- [Parameter] public Assembly AppAssembly { get; set; }
+ [Parameter]
+ [EditorRequired]
+ public Assembly AppAssembly { get; set; }
///
/// Gets or sets a collection of additional assemblies that should be searched for components
@@ -58,12 +60,16 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary
///
/// Gets or sets the content to display when no match is found for the requested route.
///
- [Parameter] public RenderFragment NotFound { get; set; }
+ [Parameter]
+ [EditorRequired]
+ public RenderFragment NotFound { get; set; }
///
/// Gets or sets the content to display when a match is found for the requested route.
///
- [Parameter] public RenderFragment Found { get; set; }
+ [Parameter]
+ [EditorRequired]
+ public RenderFragment Found { get; set; }
///
/// Get or sets the content to display when asynchronous navigation is in progress.
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptor.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptor.cs
index ecfd96105f54..6a11b3aba45e 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptor.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptor.cs
@@ -29,6 +29,8 @@ protected BoundAttributeDescriptor(string kind)
public bool IsBooleanProperty { get; protected set; }
+ internal bool IsEditorRequired { get; set; }
+
public string Name { get; protected set; }
public string IndexerNamePrefix { get; protected set; }
@@ -81,4 +83,4 @@ public override int GetHashCode()
return BoundAttributeDescriptorComparer.Default.GetHashCode(this);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptorBuilder.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptorBuilder.cs
index b6fb01d7ad01..051f2655d67b 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptorBuilder.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptorBuilder.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -28,6 +28,8 @@ public abstract class BoundAttributeDescriptorBuilder
public abstract RazorDiagnosticCollection Diagnostics { get; }
+ internal bool IsEditorRequired { get; set; }
+
public virtual IReadOnlyList BoundAttributeParameters { get; }
public virtual void BindAttributeParameter(Action configure)
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentLoweringPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentLoweringPass.cs
index 3a7e3c748e28..fa0c73783502 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentLoweringPass.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentLoweringPass.cs
@@ -67,7 +67,7 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentInte
}
}
- private ComponentIntermediateNode RewriteAsComponent(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper)
+ private static ComponentIntermediateNode RewriteAsComponent(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper)
{
var component = new ComponentIntermediateNode()
{
@@ -89,13 +89,54 @@ private ComponentIntermediateNode RewriteAsComponent(TagHelperIntermediateNode n
// because we see the nodes in the wrong order.
foreach (var childContent in component.ChildContents)
{
- childContent.ParameterName = childContent.ParameterName ?? component.ChildContentParameterName ?? ComponentMetadata.ChildContent.DefaultParameterName;
+ childContent.ParameterName ??= component.ChildContentParameterName ?? ComponentMetadata.ChildContent.DefaultParameterName;
}
+ ValidateRequiredAttributes(node, tagHelper, component);
+
return component;
}
- private MarkupElementIntermediateNode RewriteAsElement(TagHelperIntermediateNode node)
+ private static void ValidateRequiredAttributes(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper, ComponentIntermediateNode intermediateNode)
+ {
+ if (intermediateNode.Children.Any(c => c is TagHelperDirectiveAttributeIntermediateNode node && (node.TagHelper?.IsSplatTagHelper() ?? false)))
+ {
+ // If there are any splat attributes, assume the user may have provided all values.
+ // This pass runs earlier than ComponentSplatLoweringPass, so we cannot rely on the presence of SplatIntermediateNode to make this check.
+ return;
+ }
+
+ foreach (var requiredAttribute in tagHelper.EditorRequiredAttributes)
+ {
+ if (!IsPresentAsAttribute(requiredAttribute.Name, intermediateNode))
+ {
+ intermediateNode.Diagnostics.Add(
+ RazorDiagnosticFactory.CreateComponent_EditorRequiredParameterNotSpecified(
+ node.Source ?? SourceSpan.Undefined,
+ intermediateNode.TagName,
+ requiredAttribute.Name));
+ }
+ }
+
+ static bool IsPresentAsAttribute(string attributeName, ComponentIntermediateNode intermediateNode)
+ {
+ foreach (var child in intermediateNode.Children)
+ {
+ if (child is ComponentAttributeIntermediateNode attributeNode && attributeName == attributeNode.AttributeName)
+ {
+ return true;
+ }
+ else if (child is ComponentChildContentIntermediateNode childContent && attributeName == childContent.AttributeName)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ private static MarkupElementIntermediateNode RewriteAsElement(TagHelperIntermediateNode node)
{
var result = new MarkupElementIntermediateNode()
{
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeDescriptorBuilder.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeDescriptorBuilder.cs
index 52f152b33f8d..446bd56b7460 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeDescriptorBuilder.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeDescriptorBuilder.cs
@@ -125,7 +125,10 @@ public BoundAttributeDescriptor Build()
CaseSensitive,
parameters,
new Dictionary(Metadata),
- diagnostics.ToArray());
+ diagnostics.ToArray())
+ {
+ IsEditorRequired = IsEditorRequired,
+ };
return descriptor;
}
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs
index d88879c15960..2e3c31b4eadf 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language.Legacy;
namespace Microsoft.AspNetCore.Razor.Language
@@ -588,6 +589,17 @@ public static RazorDiagnostic CreateTagHelper_InconsistentTagStructure(SourceSpa
return RazorDiagnostic.Create(TagHelper_InconsistentTagStructure, location, firstDescriptor, secondDescriptor, tagName, nameof(TagMatchingRuleDescriptor.TagStructure));
}
+ internal static readonly RazorDiagnosticDescriptor Component_EditorRequiredParameterNotSpecified =
+ new RazorDiagnosticDescriptor(
+ $"{DiagnosticPrefix}2012",
+ () => Resources.Component_EditorRequiredParameterNotSpecified,
+ RazorDiagnosticSeverity.Warning);
+
+ public static RazorDiagnostic CreateComponent_EditorRequiredParameterNotSpecified(SourceSpan location, string tagName, string parameterName)
+ {
+ return RazorDiagnostic.Create(Component_EditorRequiredParameterNotSpecified, location, tagName, parameterName);
+ }
+
#endregion
#region TagHelper Errors
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx
index 8fa60e30c6e6..6d541a509f83 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx
@@ -1,4 +1,4 @@
-
+