diff --git a/Blazor.sln b/Blazor.sln
index f9581e909f..68b8a2f64f 100644
--- a/Blazor.sln
+++ b/Blazor.sln
@@ -95,6 +95,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorLibrary-CSharp", "src\Microsoft.AspNetCore.Blazor.Templates\content\BlazorLibrary-CSharp\BlazorLibrary-CSharp.csproj", "{3A457B14-D91B-4FFF-A81A-8F350BDB911F}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers", "src\Microsoft.AspNetCore.Blazor.Analyzers\Microsoft.AspNetCore.Blazor.Analyzers.csproj", "{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers.Test", "test\Microsoft.AspNetCore.Blazor.Analyzers.Test\Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj", "{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -330,6 +334,22 @@ Global
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
+ {6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
+ {6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
+ {6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
+ {6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
+ {CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
+ {CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
+ {CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
+ {CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -372,6 +392,8 @@ Global
{C57382BC-EE93-49D5-BC40-5C98AF8AA048} = {4AE0D35B-D97A-44D0-8392-C9240377DCCE}
{50F6820F-D058-4E68-9E15-801F893F514E} = {36A7DEB7-5F88-4BFB-B57E-79EEC9950E25}
{3A457B14-D91B-4FFF-A81A-8F350BDB911F} = {E8EBA72C-D555-43AE-BC98-F0B2D05F6A07}
+ {6DDD6A29-0A3E-417F-976C-5FE3FDA74055} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
+ {CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
diff --git a/samples/StandaloneApp/Shared/MainLayout.cshtml b/samples/StandaloneApp/Shared/MainLayout.cshtml
index 7202cfb4a7..c38b136e7d 100644
--- a/samples/StandaloneApp/Shared/MainLayout.cshtml
+++ b/samples/StandaloneApp/Shared/MainLayout.cshtml
@@ -1,4 +1,4 @@
-@implements ILayoutComponent
+@inherits BlazorLayoutComponent
@@ -13,8 +13,3 @@
@Body
-
-@functions {
- [Parameter]
- public RenderFragment Body { get; set; }
-}
diff --git a/src/Microsoft.AspNetCore.Blazor.Analyzers/ComponentParametersShouldNotBePublicAnalyzer.cs b/src/Microsoft.AspNetCore.Blazor.Analyzers/ComponentParametersShouldNotBePublicAnalyzer.cs
new file mode 100644
index 0000000000..0297461f12
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Analyzers/ComponentParametersShouldNotBePublicAnalyzer.cs
@@ -0,0 +1,66 @@
+// 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 Microsoft.AspNetCore.Blazor.Shared;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Blazor.Analyzers
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class ComponentParametersShouldNotBePublicAnalyzer : DiagnosticAnalyzer
+ {
+ public const string DiagnosticId = "BL9993";
+ private const string Category = "Encapsulation";
+
+ private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Title), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Format), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Description), Resources.ResourceManager, typeof(Resources));
+ private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
+ DiagnosticId,
+ Title,
+ MessageFormat,
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: Description);
+
+ public override ImmutableArray SupportedDiagnostics
+ => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.PropertyDeclaration);
+ }
+
+ private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
+ {
+ var semanticModel = context.SemanticModel;
+ var declaration = (PropertyDeclarationSyntax)context.Node;
+
+ var parameterAttribute = declaration.AttributeLists
+ .SelectMany(list => list.Attributes)
+ .Where(attr => semanticModel.GetTypeInfo(attr).Type?.ToDisplayString() == BlazorApi.ParameterAttribute.FullTypeName)
+ .FirstOrDefault();
+
+ if (parameterAttribute != null && IsPublic(declaration))
+ {
+ var identifierText = declaration.Identifier.Text;
+ if (!string.IsNullOrEmpty(identifierText))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ Rule,
+ declaration.GetLocation(),
+ identifierText));
+ }
+ }
+ }
+
+ private static bool IsPublic(PropertyDeclarationSyntax declaration)
+ => declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword));
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Analyzers/ComponentParametersShouldNotBePublicCodeFixProvider.cs b/src/Microsoft.AspNetCore.Blazor.Analyzers/ComponentParametersShouldNotBePublicCodeFixProvider.cs
new file mode 100644
index 0000000000..1226bd1809
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Analyzers/ComponentParametersShouldNotBePublicCodeFixProvider.cs
@@ -0,0 +1,73 @@
+// 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 Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Blazor.Analyzers
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ComponentParametersShouldNotBePublicCodeFixProvider)), Shared]
+ public class ComponentParametersShouldNotBePublicCodeFixProvider : CodeFixProvider
+ {
+ private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_FixTitle), Resources.ResourceManager, typeof(Resources));
+
+ public override ImmutableArray FixableDiagnosticIds
+ => ImmutableArray.Create(ComponentParametersShouldNotBePublicAnalyzer.DiagnosticId);
+
+ public sealed override FixAllProvider GetFixAllProvider()
+ {
+ // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ var diagnostic = context.Diagnostics.First();
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ // Find the type declaration identified by the diagnostic.
+ var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First();
+
+ // Register a code action that will invoke the fix.
+ var title = Title.ToString();
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: title,
+ createChangedDocument: c => GetTransformedDocumentAsync(context.Document, root, declaration),
+ equivalenceKey: title),
+ diagnostic);
+ }
+
+ private Task GetTransformedDocumentAsync(
+ Document document,
+ SyntaxNode root,
+ PropertyDeclarationSyntax declarationNode)
+ {
+ var updatedDeclarationNode = HandlePropertyDeclaration(declarationNode);
+ var newSyntaxRoot = root.ReplaceNode(declarationNode, updatedDeclarationNode);
+ return Task.FromResult(document.WithSyntaxRoot(newSyntaxRoot));
+ }
+
+ private SyntaxNode HandlePropertyDeclaration(PropertyDeclarationSyntax node)
+ {
+ TypeSyntax type = node.Type;
+ if (type == null || type.IsMissing)
+ {
+ return null;
+ }
+
+ var publicModifier = node.Modifiers.FirstOrDefault(m => m.IsKind(SyntaxKind.PublicKeyword));
+ node = node.WithModifiers(
+ node.Modifiers.Remove(publicModifier));
+ return node;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Analyzers/Microsoft.AspNetCore.Blazor.Analyzers.csproj b/src/Microsoft.AspNetCore.Blazor.Analyzers/Microsoft.AspNetCore.Blazor.Analyzers.csproj
new file mode 100644
index 0000000000..92557afc55
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Analyzers/Microsoft.AspNetCore.Blazor.Analyzers.csproj
@@ -0,0 +1,36 @@
+
+
+
+ netstandard1.3
+ false
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNetCore.Blazor.Analyzers/Resources.Designer.cs b/src/Microsoft.AspNetCore.Blazor.Analyzers/Resources.Designer.cs
new file mode 100644
index 0000000000..32ae5ee7a7
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Analyzers/Resources.Designer.cs
@@ -0,0 +1,100 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.AspNetCore.Blazor.Analyzers {
+ using System;
+ using System.Reflection;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.Blazor.Analyzers.Resources", typeof(Resources).GetTypeInfo().Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Component parameters should not be public..
+ ///
+ internal static string ComponentParametersShouldNotBePublic_Description {
+ get {
+ return ResourceManager.GetString("ComponentParametersShouldNotBePublic_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Make component parameter private.
+ ///
+ internal static string ComponentParametersShouldNotBePublic_FixTitle {
+ get {
+ return ResourceManager.GetString("ComponentParametersShouldNotBePublic_FixTitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Component parameter '{0}' is marked public, but component parameters should not be public..
+ ///
+ internal static string ComponentParametersShouldNotBePublic_Format {
+ get {
+ return ResourceManager.GetString("ComponentParametersShouldNotBePublic_Format", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Component parameter is marked public.
+ ///
+ internal static string ComponentParametersShouldNotBePublic_Title {
+ get {
+ return ResourceManager.GetString("ComponentParametersShouldNotBePublic_Title", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Analyzers/Resources.resx b/src/Microsoft.AspNetCore.Blazor.Analyzers/Resources.resx
new file mode 100644
index 0000000000..64ed4fa4a5
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Analyzers/Resources.resx
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Component parameters should not be public.
+
+
+ Make component parameter private
+
+
+ Component parameter '{0}' is marked public, but component parameters should not be public.
+
+
+ Component parameter is marked public
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.nuspec b/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.nuspec
index 9ddff75c9f..76232e7959 100644
--- a/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.nuspec
+++ b/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.nuspec
@@ -7,6 +7,7 @@
Build mechanism for Blazor applications.
+
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindLoweringPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindLoweringPass.cs
index 706283549a..ce0ab1026e 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindLoweringPass.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindLoweringPass.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.Linq;
+using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindTagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindTagHelperDescriptorProvider.cs
index ecf3bbd097..e7d8634c8a 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindTagHelperDescriptorProvider.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BindTagHelperDescriptorProvider.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs
index 2fd884ad35..8b90ffcabf 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDesignTimeNodeWriter.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs
index 6e5190af7c..fc92a6c7df 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs
@@ -170,6 +170,8 @@ namespace Microsoft.AspNetCore.Blazor.Razor
() => "Script tags should not be placed inside components because they cannot be updated dynamically. To fix this, move the script tag to the 'index.html' file or another static location. For more information see https://go.microsoft.com/fwlink/?linkid=872131",
RazorDiagnosticSeverity.Error);
+ // Reserved: BL9993 Component parameters should not be public
+
public static RazorDiagnostic Create_DisallowedScriptTag(SourceSpan? source)
{
var diagnostic = RazorDiagnostic.Create(DisallowedScriptTag, source ?? SourceSpan.Undefined);
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs
index a6a0da3f7e..aab6d37186 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRuntimeNodeWriter.cs
@@ -8,6 +8,7 @@ using System.Text;
using AngleSharp;
using AngleSharp.Html;
using AngleSharp.Parser.Html;
+using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs
index 9d363b914b..9ec80b81e3 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs
@@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Linq;
using System.Text;
+using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs
index 79df0b3840..2f82f4aca9 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs
@@ -4,8 +4,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
+using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.AspNetCore.Blazor.Razor
@@ -17,6 +20,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
.WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
+ private static MethodInfo WithMetadataImportOptionsMethodInfo =
+ typeof(CSharpCompilationOptions)
+ .GetMethod("WithMetadataImportOptions", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+
public bool IncludeDocumentation { get; set; }
public int Order { get; set; }
@@ -35,6 +42,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor
return;
}
+ // We need to see private members too
+ compilation = WithMetadataImportOptionsAll(compilation);
+
var componentSymbol = compilation.GetTypeByMetadataName(BlazorApi.IComponent.MetadataName);
if (componentSymbol == null)
{
@@ -49,6 +59,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor
return;
}
+ var blazorComponentSymbol = compilation.GetTypeByMetadataName(BlazorApi.BlazorComponent.FullTypeName);
+ if (blazorComponentSymbol == null)
+ {
+ // No definition for BlazorComponent, nothing to do.
+ return;
+ }
+
var types = new List();
var visitor = new ComponentTypeVisitor(componentSymbol, types);
@@ -67,11 +84,18 @@ namespace Microsoft.AspNetCore.Blazor.Razor
for (var i = 0; i < types.Count; i++)
{
var type = types[i];
- context.Results.Add(CreateDescriptor(type, parameterSymbol));
+ context.Results.Add(CreateDescriptor(type, parameterSymbol, blazorComponentSymbol));
}
}
- private TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol)
+ private Compilation WithMetadataImportOptionsAll(Compilation compilation)
+ {
+ var newCompilationOptions = (CSharpCompilationOptions)WithMetadataImportOptionsMethodInfo
+ .Invoke(compilation.Options, new object[] { /* All */ (byte)2 });
+ return compilation.WithOptions(newCompilationOptions);
+ }
+
+ private TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol, INamedTypeSymbol blazorComponentSymbol)
{
if (type == null)
{
@@ -102,7 +126,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
// Components have very simple matching rules. The type name (short) matches the tag name.
builder.TagMatchingRule(r => r.TagName = type.Name);
- foreach (var property in GetProperties(type, parameterSymbol))
+ foreach (var property in GetProperties(type, parameterSymbol, blazorComponentSymbol))
{
if (property.kind == PropertyKind.Ignored)
{
@@ -138,19 +162,27 @@ namespace Microsoft.AspNetCore.Blazor.Razor
return descriptor;
}
- // Does a walk up the inheritance chain to determine the set of 'visible' properties by using
+ // Does a walk up the inheritance chain to determine the set of parameters by using
// a dictionary keyed on property name.
//
- // Note that we're only interested in a property if all of the above are true:
- // - visible (not shadowed)
- // - has public getter
- // - has public setter
- // - is not an indexer
- private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol)
+ // We consider parameters to be defined by properties satisfying all of the following:
+ // - are visible (not shadowed)
+ // - have the [Parameter] attribute
+ // - have a setter, even if private
+ // - are not indexers
+ private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol, INamedTypeSymbol blazorComponentSymbol)
{
var properties = new Dictionary(StringComparer.Ordinal);
do
{
+ if (type == blazorComponentSymbol)
+ {
+ // The BlazorComponent base class doesn't have any [Parameter].
+ // Bail out now to avoid walking through its many members, plus the members
+ // of the System.Object base class.
+ break;
+ }
+
var members = type.GetMembers();
for (var i = 0; i < members.Length; i++)
{
@@ -174,15 +206,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor
kind = PropertyKind.Ignored;
}
- if (property.GetMethod?.DeclaredAccessibility != Accessibility.Public)
+ if (property.SetMethod == null)
{
- // Non-public getter or no getter
+ // No setter
kind = PropertyKind.Ignored;
}
- if (property.SetMethod?.DeclaredAccessibility != Accessibility.Public)
+ if (property.IsStatic)
{
- // Non-public setter or no setter
kind = PropertyKind.Ignored;
}
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EventHandlerLoweringPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EventHandlerLoweringPass.cs
index 44bd8bb311..7a843d43e0 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EventHandlerLoweringPass.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EventHandlerLoweringPass.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.Linq;
+using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EventHandlerTagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EventHandlerTagHelperDescriptorProvider.cs
index 17d07d9cdb..457d31465e 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EventHandlerTagHelperDescriptorProvider.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EventHandlerTagHelperDescriptorProvider.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirectivePass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirectivePass.cs
index cc66cf171d..d4f3acb0eb 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirectivePass.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/LayoutDirectivePass.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj
index a13c2e59b1..37cb51d9ad 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj
@@ -38,4 +38,8 @@
Resources.Designer.cs
+
+
+
+
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RefTagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RefTagHelperDescriptorProvider.cs
index 0fe2bf7c51..49f0932123 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RefTagHelperDescriptorProvider.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RefTagHelperDescriptorProvider.cs
@@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using System;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RouteAttributeExtensionNode.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RouteAttributeExtensionNode.cs
index 2760803d18..0f36b5f3c2 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RouteAttributeExtensionNode.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RouteAttributeExtensionNode.cs
@@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs
index d6437f2a28..61047dd20e 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs
@@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using System;
diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/Shared/MainLayout.cshtml b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/Shared/MainLayout.cshtml
index 86c509609a..8b8a5d6226 100644
--- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/Shared/MainLayout.cshtml
+++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/Shared/MainLayout.cshtml
@@ -1,4 +1,4 @@
-@implements ILayoutComponent
+@inherits BlazorLayoutComponent
@@ -13,7 +13,3 @@
@Body
-
-@functions {
- public RenderFragment Body { get; set; }
-}
diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/Shared/SurveyPrompt.cshtml b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/Shared/SurveyPrompt.cshtml
index f1f7ffebac..8bd8fb995b 100644
--- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/Shared/SurveyPrompt.cshtml
+++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorHosted-CSharp/BlazorHosted-CSharp.Client/Shared/SurveyPrompt.cshtml
@@ -11,9 +11,7 @@
and tell us what you think.
-@functions
-{
- // This is to demonstrate how a parent component can supply parameters
+@functions {
[Parameter]
- public string Title { get; set; }
+ string Title { get; set; } // Demonstrates how a parent component can supply parameters
}
diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/Shared/MainLayout.cshtml b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/Shared/MainLayout.cshtml
index 985a2ec2e6..c38b136e7d 100644
--- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/Shared/MainLayout.cshtml
+++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/Shared/MainLayout.cshtml
@@ -1,4 +1,4 @@
-@implements ILayoutComponent
+@inherits BlazorLayoutComponent
@@ -13,7 +13,3 @@
@Body
-
-@functions {
- public RenderFragment Body { get; set; }
-}
diff --git a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/Shared/SurveyPrompt.cshtml b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/Shared/SurveyPrompt.cshtml
index f1f7ffebac..8bd8fb995b 100644
--- a/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/Shared/SurveyPrompt.cshtml
+++ b/src/Microsoft.AspNetCore.Blazor.Templates/content/BlazorStandalone-CSharp/Shared/SurveyPrompt.cshtml
@@ -11,9 +11,7 @@
and tell us what you think.
-@functions
-{
- // This is to demonstrate how a parent component can supply parameters
+@functions {
[Parameter]
- public string Title { get; set; }
+ string Title { get; set; } // Demonstrates how a parent component can supply parameters
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs b/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs
index ffcdd4828b..e0b35b2351 100644
--- a/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Components/ComponentFactory.cs
@@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.Blazor.Reflection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -56,26 +57,14 @@ namespace Microsoft.AspNetCore.Blazor.Components
{
// Do all the reflection up front
var injectableProperties =
- GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags)
+ MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags)
.Where(p => p.GetCustomAttribute() != null);
var injectables = injectableProperties.Select(property =>
- {
- if (property.SetMethod == null)
- {
- throw new InvalidOperationException($"Cannot provide a value for property " +
- $"'{property.Name}' on type '{type.FullName}' because the property " +
- $"has no setter.");
- }
-
- return
- (
- propertyName: property.Name,
- propertyType: property.PropertyType,
- setter: (IPropertySetter)Activator.CreateInstance(
- typeof(PropertySetter<,>).MakeGenericType(type, property.PropertyType),
- property.SetMethod)
- );
- }).ToArray();
+ (
+ propertyName: property.Name,
+ propertyType: property.PropertyType,
+ setter: MemberAssignment.CreatePropertySetter(type, property)
+ )).ToArray();
// Return an action whose closure can write all the injected properties
// without any further reflection calls (just typecasts)
@@ -95,40 +84,5 @@ namespace Microsoft.AspNetCore.Blazor.Components
}
};
}
-
- private interface IPropertySetter
- {
- void SetValue(object target, object value);
- }
-
- private class PropertySetter : IPropertySetter
- {
- private readonly Action _setterDelegate;
-
- public PropertySetter(MethodInfo setMethod)
- {
- _setterDelegate = (Action)Delegate.CreateDelegate(
- typeof(Action), setMethod);
- }
-
- public void SetValue(object target, object value)
- => _setterDelegate((TTarget)target, (TValue)value);
- }
-
- private static IEnumerable GetPropertiesIncludingInherited(
- Type type, BindingFlags bindingFlags)
- {
- while (type != null)
- {
- var properties = type.GetProperties(bindingFlags)
- .Where(prop => prop.DeclaringType == type);
- foreach (var property in properties)
- {
- yield return property;
- }
-
- type = type.BaseType;
- }
- }
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/Parameter.cs b/src/Microsoft.AspNetCore.Blazor/Components/Parameter.cs
index e875dfdc49..6c4e54e02f 100644
--- a/src/Microsoft.AspNetCore.Blazor/Components/Parameter.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Components/Parameter.cs
@@ -31,5 +31,11 @@ namespace Microsoft.AspNetCore.Blazor.Components
///
public object Value
=> _frames[_frameIndex].AttributeValue;
+
+ ///
+ /// Gets the that holds the parameter name and value.
+ ///
+ internal ref RenderTreeFrame Frame
+ => ref _frames[_frameIndex];
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollectionExtensions.cs b/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollectionExtensions.cs
index 9ef5a2ea1d..d0d9f9cb04 100644
--- a/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollectionExtensions.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Components/ParameterCollectionExtensions.cs
@@ -1,7 +1,12 @@
// 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 Microsoft.AspNetCore.Blazor.Reflection;
+using Microsoft.AspNetCore.Blazor.RenderTree;
using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
using System.Reflection;
namespace Microsoft.AspNetCore.Blazor.Components
@@ -11,6 +16,13 @@ namespace Microsoft.AspNetCore.Blazor.Components
///
public static class ParameterCollectionExtensions
{
+ private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
+
+ private delegate void WriteParameterAction(ref RenderTreeFrame frame, object target);
+
+ private readonly static IDictionary> _cachedParameterWriters
+ = new ConcurrentDictionary>();
+
///
/// Iterates through the , assigning each parameter
/// to a property of the same name on .
@@ -26,54 +38,89 @@ namespace Microsoft.AspNetCore.Blazor.Components
throw new ArgumentNullException(nameof(target));
}
+ var targetType = target.GetType();
+ if (!_cachedParameterWriters.TryGetValue(targetType, out var parameterWriters))
+ {
+ parameterWriters = CreateParameterWriters(targetType);
+ _cachedParameterWriters[targetType] = parameterWriters;
+ }
+
foreach (var parameter in parameterCollection)
{
- AssignToProperty(target, parameter);
+ var parameterName = parameter.Name;
+ if (!parameterWriters.TryGetValue(parameterName, out var parameterWriter))
+ {
+ ThrowForUnknownIncomingParameterName(targetType, parameterName);
+ }
+
+ try
+ {
+ parameterWriter(ref parameter.Frame, target);
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException(
+ $"Unable to set property '{parameterName}' on object of " +
+ $"type '{target.GetType().FullName}'. The error was: {ex.Message}", ex);
+ }
}
}
- private static void AssignToProperty(object target, Parameter parameter)
+ private static IDictionary CreateParameterWriters(Type targetType)
{
- // TODO: Don't just use naive reflection like this. Possible ways to make it faster:
- // (a) Create and cache a property-assigning open delegate for each (target type,
- // property name) pair, e.g., using propertyInfo.GetSetMethod().CreateDelegate(...)
- // That's much faster than caching the PropertyInfo, at least on JIT-enabled platforms.
- // (b) Or possibly just code-gen an IComponent.SetParameters implementation for each
- // Razor component. However that might not work well with code-behind inheritance,
- // because the code-behind wouldn't be able to override it.
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
- var propertyInfo = GetPropertyInfo(target.GetType(), parameter.Name);
- try
+ foreach (var propertyInfo in GetBindableProperties(targetType))
{
- propertyInfo.SetValue(target, parameter.Value);
- }
- catch (Exception ex)
- {
- throw new InvalidOperationException(
- $"Unable to set property '{parameter.Name}' on object of " +
- $"type '{target.GetType().FullName}'. The error was: {ex.Message}", ex);
+ var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo);
+
+ var propertyName = propertyInfo.Name;
+ if (result.ContainsKey(propertyName))
+ {
+ throw new InvalidOperationException(
+ $"The type '{targetType.FullName}' declares more than one parameter matching the " +
+ $"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.");
+ }
+
+ result.Add(propertyName, (ref RenderTreeFrame frame, object target) =>
+ {
+ propertySetter.SetValue(target, frame.AttributeValue);
+ });
}
+
+ return result;
}
- private static PropertyInfo GetPropertyInfo(Type targetType, string propertyName)
+ private static IEnumerable GetBindableProperties(Type targetType)
+ => MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags)
+ .Where(property => property.IsDefined(typeof(ParameterAttribute)));
+
+ private static void ThrowForUnknownIncomingParameterName(Type targetType, string parameterName)
{
- var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
- var property = targetType.GetProperty(propertyName, flags);
- if (property == null)
+ // We know we're going to throw by this stage, so it doesn't matter that the following
+ // reflection code will be slow. We're just trying to help developers see what they did wrong.
+ var propertyInfo = targetType.GetProperty(parameterName, _bindablePropertyFlags);
+ if (propertyInfo != null)
+ {
+ if (!propertyInfo.IsDefined(typeof(ParameterAttribute)))
+ {
+ throw new InvalidOperationException(
+ $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
+ $"but it does not have [{nameof(ParameterAttribute)}] applied.");
+ }
+ else
+ {
+ // This should not happen
+ throw new InvalidOperationException(
+ $"No writer was cached for the property '{propertyInfo.Name}' on type '{targetType.FullName}'.");
+ }
+ }
+ else
{
throw new InvalidOperationException(
$"Object of type '{targetType.FullName}' does not have a property " +
- $"matching the name '{propertyName}'.");
+ $"matching the name '{parameterName}'.");
}
-
- if (!property.IsDefined(typeof(ParameterAttribute)))
- {
- throw new InvalidOperationException(
- $"Object of type '{targetType.FullName}' has a property matching the name '{propertyName}', " +
- $"but it does not have [{nameof(ParameterAttribute)}] applied.");
- }
-
- return property;
}
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Layouts/BlazorLayoutComponent.cs b/src/Microsoft.AspNetCore.Blazor/Layouts/BlazorLayoutComponent.cs
new file mode 100644
index 0000000000..7eabf7726c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/Layouts/BlazorLayoutComponent.cs
@@ -0,0 +1,23 @@
+// 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 Microsoft.AspNetCore.Blazor.Components;
+
+namespace Microsoft.AspNetCore.Blazor.Layouts
+{
+ ///
+ /// Optional base class for components that represent a layout.
+ /// Alternatively, Blazor components may implement directly
+ /// and declare their own parameter named .
+ ///
+ public abstract class BlazorLayoutComponent : BlazorComponent
+ {
+ internal const string BodyPropertyName = nameof(Body);
+
+ ///
+ /// Gets the content to be rendered inside the layout.
+ ///
+ [Parameter]
+ protected RenderFragment Body { get; private set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor/Layouts/ILayoutComponent.cs b/src/Microsoft.AspNetCore.Blazor/Layouts/ILayoutComponent.cs
deleted file mode 100644
index 24c2b5a27c..0000000000
--- a/src/Microsoft.AspNetCore.Blazor/Layouts/ILayoutComponent.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// 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 Microsoft.AspNetCore.Blazor.Components;
-using Microsoft.AspNetCore.Blazor.RenderTree;
-
-namespace Microsoft.AspNetCore.Blazor.Layouts
-{
- ///
- /// Indicates that the type represents a layout.
- ///
- public interface ILayoutComponent : IComponent
- {
- ///
- /// Gets or sets the content to be rendered inside the layout.
- ///
- RenderFragment Body { get; set; }
- }
-}
diff --git a/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutAttribute.cs b/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutAttribute.cs
index 3518ec22fe..e2c891d180 100644
--- a/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutAttribute.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutAttribute.cs
@@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.Blazor.Components;
using System;
namespace Microsoft.AspNetCore.Blazor.Layouts
@@ -12,23 +13,29 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
public class LayoutAttribute : Attribute
{
///
- /// The type of the layout. The type always implements .
+ /// The type of the layout. The type myst implement
+ /// and must accept a parameter with the name 'Body'.
///
public Type LayoutType { get; private set; }
///
/// Constructs an instance of .
///
- /// The type of the layout. This must implement .
+ /// The type of the layout.
public LayoutAttribute(Type layoutType)
{
LayoutType = layoutType ?? throw new ArgumentNullException(nameof(layoutType));
- if (!typeof(ILayoutComponent).IsAssignableFrom(layoutType))
+ if (!typeof(IComponent).IsAssignableFrom(layoutType))
{
throw new ArgumentException($"Invalid layout type: {layoutType.FullName} " +
- $"does not implement {typeof(ILayoutComponent).FullName}.");
+ $"does not implement {typeof(IComponent).FullName}.");
}
+
+ // Note that we can't validate its acceptance of a 'Body' parameter at this stage,
+ // because the contract doesn't force them to be known statically. However it will
+ // be a runtime error if the referenced component type rejects the 'Body' parameter
+ // when it gets used.
}
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutDisplay.cs b/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutDisplay.cs
index a7a938ba74..c37404d6f7 100644
--- a/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutDisplay.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutDisplay.cs
@@ -15,6 +15,9 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
///
public class LayoutDisplay : IComponent
{
+ internal const string NameOfPage = nameof(Page);
+ internal const string NameOfPageParameters = nameof(PageParameters);
+
private RenderHandle _renderHandle;
///
@@ -22,13 +25,13 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
/// The type must implement .
///
[Parameter]
- public Type Page { get; set; }
+ Type Page { get; set; }
///
/// Gets or sets the parameters to pass to the page.
///
[Parameter]
- public IDictionary PageParameters { get; set; }
+ IDictionary PageParameters { get; set; }
///
public void Init(RenderHandle renderHandle)
@@ -64,7 +67,7 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
builder.OpenComponent(0, componentType);
if (bodyParam != null)
{
- builder.AddAttribute(1, nameof(ILayoutComponent.Body), bodyParam);
+ builder.AddAttribute(1, BlazorLayoutComponent.BodyPropertyName, bodyParam);
}
else
{
diff --git a/src/Microsoft.AspNetCore.Blazor/Reflection/IPropertySetter.cs b/src/Microsoft.AspNetCore.Blazor/Reflection/IPropertySetter.cs
new file mode 100644
index 0000000000..a106a4d7d7
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/Reflection/IPropertySetter.cs
@@ -0,0 +1,12 @@
+// 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.Blazor.Reflection
+{
+ internal interface IPropertySetter
+ {
+ void SetValue(object target, object value);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor/Reflection/MemberAssignment.cs b/src/Microsoft.AspNetCore.Blazor/Reflection/MemberAssignment.cs
new file mode 100644
index 0000000000..10fdb47bdf
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/Reflection/MemberAssignment.cs
@@ -0,0 +1,57 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace Microsoft.AspNetCore.Blazor.Reflection
+{
+ internal class MemberAssignment
+ {
+ public static IEnumerable GetPropertiesIncludingInherited(
+ Type type, BindingFlags bindingFlags)
+ {
+ while (type != null)
+ {
+ var properties = type.GetProperties(bindingFlags)
+ .Where(prop => prop.DeclaringType == type);
+ foreach (var property in properties)
+ {
+ yield return property;
+ }
+
+ type = type.BaseType;
+ }
+ }
+
+ public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property)
+ {
+ if (property.SetMethod == null)
+ {
+ throw new InvalidOperationException($"Cannot provide a value for property " +
+ $"'{property.Name}' on type '{targetType.FullName}' because the property " +
+ $"has no setter.");
+ }
+
+ return (IPropertySetter)Activator.CreateInstance(
+ typeof(PropertySetter<,>).MakeGenericType(targetType, property.PropertyType),
+ property.SetMethod);
+ }
+
+ class PropertySetter : IPropertySetter
+ {
+ private readonly Action _setterDelegate;
+
+ public PropertySetter(MethodInfo setMethod)
+ {
+ _setterDelegate = (Action)Delegate.CreateDelegate(
+ typeof(Action), setMethod);
+ }
+
+ public void SetValue(object target, object value)
+ => _setterDelegate((TTarget)target, (TValue)value);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/NavLink.cs b/src/Microsoft.AspNetCore.Blazor/Routing/NavLink.cs
index bf34012707..6e77ffa147 100644
--- a/src/Microsoft.AspNetCore.Blazor/Routing/NavLink.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/NavLink.cs
@@ -39,13 +39,13 @@ namespace Microsoft.AspNetCore.Blazor.Routing
/// current route matches the NavLink href.
///
[Parameter]
- public string ActiveClass { get; set; }
+ string ActiveClass { get; set; }
///
/// Gets or sets a value representing the URL matching behavior.
///
[Parameter]
- public NavLinkMatch Match { get; set; }
+ NavLinkMatch Match { get; set; }
[Inject] private IUriHelper UriHelper { get; set; }
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/Router.cs b/src/Microsoft.AspNetCore.Blazor/Routing/Router.cs
index 5909ee297f..ade8441fe8 100644
--- a/src/Microsoft.AspNetCore.Blazor/Routing/Router.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/Router.cs
@@ -29,8 +29,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
/// Gets or sets the assembly that should be searched, along with its referenced
/// assemblies, for components matching the URI.
///
- [Parameter]
- public Assembly AppAssembly { get; set; }
+ [Parameter] private Assembly AppAssembly { get; set; }
private RouteTable Routes { get; set; }
@@ -70,8 +69,8 @@ namespace Microsoft.AspNetCore.Blazor.Routing
protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary parameters)
{
builder.OpenComponent(0, typeof(LayoutDisplay));
- builder.AddAttribute(1, nameof(LayoutDisplay.Page), handler);
- builder.AddAttribute(2, nameof(LayoutDisplay.PageParameters), parameters);
+ builder.AddAttribute(1, LayoutDisplay.NameOfPage, handler);
+ builder.AddAttribute(2, LayoutDisplay.NameOfPageParameters, parameters);
builder.CloseComponent();
}
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs b/src/shared/BlazorApi.cs
similarity index 98%
rename from src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs
rename to src/shared/BlazorApi.cs
index 8961ea5791..a033baad9b 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs
+++ b/src/shared/BlazorApi.cs
@@ -1,7 +1,7 @@
// 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.
-namespace Microsoft.AspNetCore.Blazor.Razor
+namespace Microsoft.AspNetCore.Blazor.Shared
{
// Constants for method names used in code-generation
// Keep these in sync with the actual definitions
diff --git a/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/ComponentParametersShouldNotBePublicTest.cs b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/ComponentParametersShouldNotBePublicTest.cs
new file mode 100644
index 0000000000..4b229e8a3c
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/ComponentParametersShouldNotBePublicTest.cs
@@ -0,0 +1,119 @@
+// 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 Microsoft.AspNetCore.Blazor.Components;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Blazor.Analyzers.Test
+{
+ public class ComponentParametersShouldNotBePublic : CodeFixVerifier
+ {
+ static string BlazorParameterSource = $@"
+ namespace {typeof(ParameterAttribute).Namespace}
+ {{
+ public class {typeof(ParameterAttribute).Name} : System.Attribute
+ {{
+ }}
+ }}
+";
+
+ [Fact]
+ public void IgnoresPublicPropertiesWithoutParameterAttribute()
+ {
+ var test = @"
+ namespace ConsoleApplication1
+ {
+ class TypeName
+ {
+ public string MyProperty { get; set; }
+ }
+ }" + BlazorParameterSource;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresNonpublicPropertiesWithParameterAttribute()
+ {
+ var test = @"
+ namespace ConsoleApplication1
+ {
+ using " + typeof(ParameterAttribute).Namespace + @";
+
+ class TypeName
+ {
+ [Parameter] string MyPropertyNoModifer { get; set; }
+ [Parameter] private string MyPropertyPrivate { get; set; }
+ [Parameter] protected string MyPropertyProtected { get; set; }
+ [Parameter] internal string MyPropertyInternal { get; set; }
+ }
+ }" + BlazorParameterSource;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void AddsDiagnosticAndFixForPublicPropertiesWithParameterAttribute()
+ {
+ var test = @"
+ namespace ConsoleApplication1
+ {
+ using " + typeof(ParameterAttribute).Namespace + @";
+
+ class TypeName
+ {
+ [Parameter] public string BadProperty1 { get; set; }
+ [Parameter] public object BadProperty2 { get; set; }
+ }
+ }" + BlazorParameterSource;
+
+ VerifyCSharpDiagnostic(test,
+ new DiagnosticResult
+ {
+ Id = "BL9993",
+ Message = "Component parameter 'BadProperty1' is marked public, but component parameters should not be public.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[]
+ {
+ new DiagnosticResultLocation("Test0.cs", 8, 13)
+ }
+ },
+ new DiagnosticResult
+ {
+ Id = "BL9993",
+ Message = "Component parameter 'BadProperty2' is marked public, but component parameters should not be public.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[]
+ {
+ new DiagnosticResultLocation("Test0.cs", 9, 13)
+ }
+ });
+
+ VerifyCSharpFix(test, @"
+ namespace ConsoleApplication1
+ {
+ using " + typeof(ParameterAttribute).Namespace + @";
+
+ class TypeName
+ {
+ [Parameter] string BadProperty1 { get; set; }
+ [Parameter] object BadProperty2 { get; set; }
+ }
+ }" + BlazorParameterSource);
+ }
+
+ protected override CodeFixProvider GetCSharpCodeFixProvider()
+ {
+ return new ComponentParametersShouldNotBePublicCodeFixProvider();
+ }
+
+ protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
+ {
+ return new ComponentParametersShouldNotBePublicAnalyzer();
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs
new file mode 100644
index 0000000000..b523daa09b
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs
@@ -0,0 +1,90 @@
+// 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.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.Simplification;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+
+namespace TestHelper
+{
+ ///
+ /// Diagnostic Producer class with extra methods dealing with applying codefixes
+ /// All methods are static
+ ///
+ public abstract partial class CodeFixVerifier : DiagnosticVerifier
+ {
+ ///
+ /// Apply the inputted CodeAction to the inputted document.
+ /// Meant to be used to apply codefixes.
+ ///
+ /// The Document to apply the fix on
+ /// A CodeAction that will be applied to the Document.
+ /// A Document with the changes from the CodeAction
+ private static Document ApplyFix(Document document, CodeAction codeAction)
+ {
+ var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result;
+ var solution = operations.OfType().Single().ChangedSolution;
+ return solution.GetDocument(document.Id);
+ }
+
+ ///
+ /// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection.
+ /// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row,
+ /// this method may not necessarily return the new one.
+ ///
+ /// The Diagnostics that existed in the code before the CodeFix was applied
+ /// The Diagnostics that exist in the code after the CodeFix was applied
+ /// A list of Diagnostics that only surfaced in the code after the CodeFix was applied
+ private static IEnumerable GetNewDiagnostics(IEnumerable diagnostics, IEnumerable newDiagnostics)
+ {
+ var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
+ var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
+
+ int oldIndex = 0;
+ int newIndex = 0;
+
+ while (newIndex < newArray.Length)
+ {
+ if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id)
+ {
+ ++oldIndex;
+ ++newIndex;
+ }
+ else
+ {
+ yield return newArray[newIndex++];
+ }
+ }
+ }
+
+ ///
+ /// Get the existing compiler diagnostics on the inputted document.
+ ///
+ /// The Document to run the compiler diagnostic analyzers on
+ /// The compiler diagnostics that were found in the code
+ private static IEnumerable GetCompilerDiagnostics(Document document)
+ {
+ return document.GetSemanticModelAsync().Result.GetDiagnostics();
+ }
+
+ ///
+ /// Given a document, turn it into a string based on the syntax root
+ ///
+ /// The Document to be converted to a string
+ /// A string containing the syntax of the Document after formatting
+ private static string GetStringFromDocument(Document document)
+ {
+ var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result;
+ var root = simplifiedDoc.GetSyntaxRootAsync().Result;
+ root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace);
+ return root.GetText().ToString();
+ }
+ }
+}
+
diff --git a/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Helpers/DiagnosticResult.cs b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Helpers/DiagnosticResult.cs
new file mode 100644
index 0000000000..4e3349ae25
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Helpers/DiagnosticResult.cs
@@ -0,0 +1,92 @@
+// 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.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using Microsoft.CodeAnalysis;
+using System;
+
+namespace TestHelper
+{
+ ///
+ /// Location where the diagnostic appears, as determined by path, line number, and column number.
+ ///
+ public struct DiagnosticResultLocation
+ {
+ public DiagnosticResultLocation(string path, int line, int column)
+ {
+ if (line < -1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1");
+ }
+
+ if (column < -1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1");
+ }
+
+ this.Path = path;
+ this.Line = line;
+ this.Column = column;
+ }
+
+ public string Path { get; }
+ public int Line { get; }
+ public int Column { get; }
+ }
+
+ ///
+ /// Struct that stores information about a Diagnostic appearing in a source
+ ///
+ public struct DiagnosticResult
+ {
+ private DiagnosticResultLocation[] locations;
+
+ public DiagnosticResultLocation[] Locations
+ {
+ get
+ {
+ if (this.locations == null)
+ {
+ this.locations = new DiagnosticResultLocation[] { };
+ }
+ return this.locations;
+ }
+
+ set
+ {
+ this.locations = value;
+ }
+ }
+
+ public DiagnosticSeverity Severity { get; set; }
+
+ public string Id { get; set; }
+
+ public string Message { get; set; }
+
+ public string Path
+ {
+ get
+ {
+ return this.Locations.Length > 0 ? this.Locations[0].Path : "";
+ }
+ }
+
+ public int Line
+ {
+ get
+ {
+ return this.Locations.Length > 0 ? this.Locations[0].Line : -1;
+ }
+ }
+
+ public int Column
+ {
+ get
+ {
+ return this.Locations.Length > 0 ? this.Locations[0].Column : -1;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs
new file mode 100644
index 0000000000..b55bf99cf4
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs
@@ -0,0 +1,175 @@
+// 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.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace TestHelper
+{
+ ///
+ /// Class for turning strings into documents and getting the diagnostics on them
+ /// All methods are static
+ ///
+ public abstract partial class DiagnosticVerifier
+ {
+ private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
+ private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location);
+ private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location);
+ private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location);
+
+ internal static string DefaultFilePathPrefix = "Test";
+ internal static string CSharpDefaultFileExt = "cs";
+ internal static string VisualBasicDefaultExt = "vb";
+ internal static string TestProjectName = "TestProject";
+
+ #region Get Diagnostics
+
+ ///
+ /// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document.
+ ///
+ /// Classes in the form of strings
+ /// The language the source classes are in
+ /// The analyzer to be run on the sources
+ /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location
+ private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer)
+ {
+ return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language));
+ }
+
+ ///
+ /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it.
+ /// The returned diagnostics are then ordered by location in the source document.
+ ///
+ /// The analyzer to run on the documents
+ /// The Documents that the analyzer will be run on
+ /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location
+ protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
+ {
+ var projects = new HashSet();
+ foreach (var document in documents)
+ {
+ projects.Add(document.Project);
+ }
+
+ var diagnostics = new List();
+ foreach (var project in projects)
+ {
+ var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer));
+ var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
+ foreach (var diag in diags)
+ {
+ if (diag.Location == Location.None || diag.Location.IsInMetadata)
+ {
+ diagnostics.Add(diag);
+ }
+ else
+ {
+ for (int i = 0; i < documents.Length; i++)
+ {
+ var document = documents[i];
+ var tree = document.GetSyntaxTreeAsync().Result;
+ if (tree == diag.Location.SourceTree)
+ {
+ diagnostics.Add(diag);
+ }
+ }
+ }
+ }
+ }
+
+ var results = SortDiagnostics(diagnostics);
+ diagnostics.Clear();
+ return results;
+ }
+
+ ///
+ /// Sort diagnostics by location in source document
+ ///
+ /// The list of Diagnostics to be sorted
+ /// An IEnumerable containing the Diagnostics in order of Location
+ private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics)
+ {
+ return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
+ }
+
+ #endregion
+
+ #region Set up compilation and documents
+ ///
+ /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it.
+ ///
+ /// Classes in the form of strings
+ /// The language the source code is in
+ /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant
+ private static Document[] GetDocuments(string[] sources, string language)
+ {
+ if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic)
+ {
+ throw new ArgumentException("Unsupported Language");
+ }
+
+ var project = CreateProject(sources, language);
+ var documents = project.Documents.ToArray();
+
+ if (sources.Length != documents.Length)
+ {
+ throw new InvalidOperationException("Amount of sources did not match amount of Documents created");
+ }
+
+ return documents;
+ }
+
+ ///
+ /// Create a Document from a string through creating a project that contains it.
+ ///
+ /// Classes in the form of a string
+ /// The language the source code is in
+ /// A Document created from the source string
+ protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
+ {
+ return CreateProject(new[] { source }, language).Documents.First();
+ }
+
+ ///
+ /// Create a project using the inputted strings as sources.
+ ///
+ /// Classes in the form of strings
+ /// The language the source code is in
+ /// A Project created out of the Documents created from the source strings
+ private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp)
+ {
+ string fileNamePrefix = DefaultFilePathPrefix;
+ string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt;
+
+ var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
+
+ var solution = new AdhocWorkspace()
+ .CurrentSolution
+ .AddProject(projectId, TestProjectName, TestProjectName, language)
+ .AddMetadataReference(projectId, CorlibReference)
+ .AddMetadataReference(projectId, SystemCoreReference)
+ .AddMetadataReference(projectId, CSharpSymbolsReference)
+ .AddMetadataReference(projectId, CodeAnalysisReference);
+
+ int count = 0;
+ foreach (var source in sources)
+ {
+ var newFileName = fileNamePrefix + count + "." + fileExt;
+ var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
+ solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
+ count++;
+ }
+ return solution.GetProject(projectId);
+ }
+ #endregion
+ }
+}
+
diff --git a/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj
new file mode 100644
index 0000000000..4dd32bac1d
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp2.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Verifiers/CodeFixVerifier.cs b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Verifiers/CodeFixVerifier.cs
new file mode 100644
index 0000000000..cb6c9d4329
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Verifiers/CodeFixVerifier.cs
@@ -0,0 +1,133 @@
+// 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.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Formatting;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Xunit;
+
+namespace TestHelper
+{
+ ///
+ /// Superclass of all Unit tests made for diagnostics with codefixes.
+ /// Contains methods used to verify correctness of codefixes
+ ///
+ public abstract partial class CodeFixVerifier : DiagnosticVerifier
+ {
+ ///
+ /// Returns the codefix being tested (C#) - to be implemented in non-abstract class
+ ///
+ /// The CodeFixProvider to be used for CSharp code
+ protected virtual CodeFixProvider GetCSharpCodeFixProvider()
+ {
+ return null;
+ }
+
+ ///
+ /// Returns the codefix being tested (VB) - to be implemented in non-abstract class
+ ///
+ /// The CodeFixProvider to be used for VisualBasic code
+ protected virtual CodeFixProvider GetBasicCodeFixProvider()
+ {
+ return null;
+ }
+
+ ///
+ /// Called to test a C# codefix when applied on the inputted string as a source
+ ///
+ /// A class in the form of a string before the CodeFix was applied to it
+ /// A class in the form of a string after the CodeFix was applied to it
+ /// Index determining which codefix to apply if there are multiple
+ /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied
+ protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
+ {
+ VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
+ }
+
+ ///
+ /// Called to test a VB codefix when applied on the inputted string as a source
+ ///
+ /// A class in the form of a string before the CodeFix was applied to it
+ /// A class in the form of a string after the CodeFix was applied to it
+ /// Index determining which codefix to apply if there are multiple
+ /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied
+ protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
+ {
+ VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
+ }
+
+ ///
+ /// General verifier for codefixes.
+ /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes.
+ /// Then gets the string after the codefix is applied and compares it with the expected result.
+ /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true.
+ ///
+ /// The language the source code is in
+ /// The analyzer to be applied to the source code
+ /// The codefix to be applied to the code wherever the relevant Diagnostic is found
+ /// A class in the form of a string before the CodeFix was applied to it
+ /// A class in the form of a string after the CodeFix was applied to it
+ /// Index determining which codefix to apply if there are multiple
+ /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied
+ private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics)
+ {
+ var document = CreateDocument(oldSource, language);
+ var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
+ var compilerDiagnostics = GetCompilerDiagnostics(document);
+ var attempts = analyzerDiagnostics.Length;
+
+ for (int i = 0; i < attempts; ++i)
+ {
+ var actions = new List();
+ var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None);
+ codeFixProvider.RegisterCodeFixesAsync(context).Wait();
+
+ if (!actions.Any())
+ {
+ break;
+ }
+
+ if (codeFixIndex != null)
+ {
+ document = ApplyFix(document, actions.ElementAt((int)codeFixIndex));
+ break;
+ }
+
+ document = ApplyFix(document, actions.ElementAt(0));
+ analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
+
+ var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
+
+ //check if applying the code fix introduced any new compiler diagnostics
+ if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any())
+ {
+ // Format and get the compiler diagnostics again so that the locations make sense in the output
+ document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace));
+ newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
+
+ Assert.True(false,
+ string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n",
+ string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())),
+ document.GetSyntaxRootAsync().Result.ToFullString()));
+ }
+
+ //check if there are analyzer diagnostics left after the code fix
+ if (!analyzerDiagnostics.Any())
+ {
+ break;
+ }
+ }
+
+ //after applying all of the code fixes, compare the resulting string to the inputted one
+ var actual = GetStringFromDocument(document);
+ Assert.Equal(newSource, actual);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Verifiers/DiagnosticVerifier.cs b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Verifiers/DiagnosticVerifier.cs
new file mode 100644
index 0000000000..f56d4ff93d
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Blazor.Analyzers.Test/Verifiers/DiagnosticVerifier.cs
@@ -0,0 +1,274 @@
+// 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.
+
+// Most of the code in this file comes from the default Roslyn Analyzer project template
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Xunit;
+
+namespace TestHelper
+{
+ ///
+ /// Superclass of all Unit Tests for DiagnosticAnalyzers
+ ///
+ public abstract partial class DiagnosticVerifier
+ {
+ #region To be implemented by Test classes
+ ///
+ /// Get the CSharp analyzer being tested - to be implemented in non-abstract class
+ ///
+ protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
+ {
+ return null;
+ }
+
+ ///
+ /// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
+ ///
+ protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
+ {
+ return null;
+ }
+ #endregion
+
+ #region Verifier wrappers
+
+ ///
+ /// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source
+ /// Note: input a DiagnosticResult for each Diagnostic expected
+ ///
+ /// A class in the form of a string to run the analyzer on
+ /// DiagnosticResults that should appear after the analyzer is run on the source
+ protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected)
+ {
+ VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
+ }
+
+ ///
+ /// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source
+ /// Note: input a DiagnosticResult for each Diagnostic expected
+ ///
+ /// A class in the form of a string to run the analyzer on
+ /// DiagnosticResults that should appear after the analyzer is run on the source
+ protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected)
+ {
+ VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
+ }
+
+ ///
+ /// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
+ /// Note: input a DiagnosticResult for each Diagnostic expected
+ ///
+ /// An array of strings to create source documents from to run the analyzers on
+ /// DiagnosticResults that should appear after the analyzer is run on the sources
+ protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
+ {
+ VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
+ }
+
+ ///
+ /// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source
+ /// Note: input a DiagnosticResult for each Diagnostic expected
+ ///
+ /// An array of strings to create source documents from to run the analyzers on
+ /// DiagnosticResults that should appear after the analyzer is run on the sources
+ protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected)
+ {
+ VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
+ }
+
+ ///
+ /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
+ /// then verifies each of them.
+ ///
+ /// An array of strings to create source documents from to run the analyzers on
+ /// The language of the classes represented by the source strings
+ /// The analyzer to be run on the source code
+ /// DiagnosticResults that should appear after the analyzer is run on the sources
+ private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected)
+ {
+ var diagnostics = GetSortedDiagnostics(sources, language, analyzer);
+ VerifyDiagnosticResults(diagnostics, analyzer, expected);
+ }
+
+ #endregion
+
+ #region Actual comparisons and verifications
+ ///
+ /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results.
+ /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic.
+ ///
+ /// The Diagnostics found by the compiler after running the analyzer on the source code
+ /// The analyzer that was being run on the sources
+ /// Diagnostic Results that should have appeared in the code
+ private static void VerifyDiagnosticResults(IEnumerable actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults)
+ {
+ int expectedCount = expectedResults.Count();
+ int actualCount = actualResults.Count();
+
+ if (expectedCount != actualCount)
+ {
+ string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE.";
+
+ Assert.True(false,
+ string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput));
+ }
+
+ for (int i = 0; i < expectedResults.Length; i++)
+ {
+ var actual = actualResults.ElementAt(i);
+ var expected = expectedResults[i];
+
+ if (expected.Line == -1 && expected.Column == -1)
+ {
+ if (actual.Location != Location.None)
+ {
+ Assert.True(false,
+ string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}",
+ FormatDiagnostics(analyzer, actual)));
+ }
+ }
+ else
+ {
+ VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First());
+ var additionalLocations = actual.AdditionalLocations.ToArray();
+
+ if (additionalLocations.Length != expected.Locations.Length - 1)
+ {
+ Assert.True(false,
+ string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n",
+ expected.Locations.Length - 1, additionalLocations.Length,
+ FormatDiagnostics(analyzer, actual)));
+ }
+
+ for (int j = 0; j < additionalLocations.Length; ++j)
+ {
+ VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
+ }
+ }
+
+ if (actual.Id != expected.Id)
+ {
+ Assert.True(false,
+ string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
+ expected.Id, actual.Id, FormatDiagnostics(analyzer, actual)));
+ }
+
+ if (actual.Severity != expected.Severity)
+ {
+ Assert.True(false,
+ string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
+ expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual)));
+ }
+
+ if (actual.GetMessage() != expected.Message)
+ {
+ Assert.True(false,
+ string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
+ expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual)));
+ }
+ }
+ }
+
+ ///
+ /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult.
+ ///
+ /// The analyzer that was being run on the sources
+ /// The diagnostic that was found in the code
+ /// The Location of the Diagnostic found in the code
+ /// The DiagnosticResultLocation that should have been found
+ private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected)
+ {
+ var actualSpan = actual.GetLineSpan();
+
+ Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")),
+ string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
+ expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic)));
+
+ var actualLinePosition = actualSpan.StartLinePosition;
+
+ // Only check line position if there is an actual line in the real diagnostic
+ if (actualLinePosition.Line > 0)
+ {
+ if (actualLinePosition.Line + 1 != expected.Line)
+ {
+ Assert.True(false,
+ string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
+ expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic)));
+ }
+ }
+
+ // Only check column position if there is an actual column position in the real diagnostic
+ if (actualLinePosition.Character > 0)
+ {
+ if (actualLinePosition.Character + 1 != expected.Column)
+ {
+ Assert.True(false,
+ string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
+ expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic)));
+ }
+ }
+ }
+ #endregion
+
+ #region Formatting Diagnostics
+ ///
+ /// Helper method to format a Diagnostic into an easily readable string
+ ///
+ /// The analyzer that this verifier tests
+ /// The Diagnostics to be formatted
+ /// The Diagnostics formatted as a string
+ private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics)
+ {
+ var builder = new StringBuilder();
+ for (int i = 0; i < diagnostics.Length; ++i)
+ {
+ builder.AppendLine("// " + diagnostics[i].ToString());
+
+ var analyzerType = analyzer.GetType();
+ var rules = analyzer.SupportedDiagnostics;
+
+ foreach (var rule in rules)
+ {
+ if (rule != null && rule.Id == diagnostics[i].Id)
+ {
+ var location = diagnostics[i].Location;
+ if (location == Location.None)
+ {
+ builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id);
+ }
+ else
+ {
+ Assert.True(location.IsInSource,
+ $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
+
+ string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt";
+ var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
+
+ builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
+ resultMethodName,
+ linePosition.Line + 1,
+ linePosition.Character + 1,
+ analyzerType.Name,
+ rule.Id);
+ }
+
+ if (i != diagnostics.Length - 1)
+ {
+ builder.Append(',');
+ }
+
+ builder.AppendLine();
+ break;
+ }
+ }
+ }
+ return builder.ToString();
+ }
+ #endregion
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/BindRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/BindRazorIntegrationTest.cs
index 00f171c1f6..29730499d3 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/BindRazorIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/BindRazorIntegrationTest.cs
@@ -25,10 +25,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public int Value { get; set; }
+ int Value { get; set; }
[Parameter]
- public Action ValueChanged { get; set; }
+ Action ValueChanged { get; set; }
}
}"));
@@ -99,10 +99,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public int Value { get; set; }
+ int Value { get; set; }
[Parameter]
- public Action OnChanged { get; set; }
+ Action OnChanged { get; set; }
}
}"));
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs
index c67bb05357..c72284153b 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentRenderingRazorIntegrationTest.cs
@@ -57,10 +57,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
- [Parameter] public int IntProperty { get; set; }
- [Parameter] public bool BoolProperty { get; set; }
- [Parameter] public string StringProperty { get; set; }
- [Parameter] public SomeType ObjectProperty { get; set; }
+ [Parameter] int IntProperty { get; set; }
+ [Parameter] bool BoolProperty { get; set; }
+ [Parameter] string StringProperty { get; set; }
+ [Parameter] SomeType ObjectProperty { get; set; }
}
}
"));
@@ -132,7 +132,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public string StringProperty { get; set; }
+ string StringProperty { get; set; }
}
}
"));
@@ -206,7 +206,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public Action OnClick { get; set; }
+ Action OnClick { get; set; }
}
}
"));
@@ -254,7 +254,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public Action OnClick { get; set; }
+ Action OnClick { get; set; }
}
}
"));
@@ -301,7 +301,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public bool BoolProperty { get; set; }
+ bool BoolProperty { get; set; }
}
}"));
@@ -331,10 +331,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public string MyAttr { get; set; }
+ string MyAttr { get; set; }
[Parameter]
- public RenderFragment ChildContent { get; set; }
+ RenderFragment ChildContent { get; set; }
}
}
"));
@@ -376,7 +376,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public RenderFragment ChildContent { get; set; }
+ RenderFragment ChildContent { get; set; }
}
}
"));
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationTest.cs
index f5b6c31328..866eed3c2a 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/DesignTimeCodeGenerationTest.cs
@@ -26,10 +26,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
- [Parameter] public int IntProperty { get; set; }
- [Parameter] public bool BoolProperty { get; set; }
- [Parameter] public string StringProperty { get; set; }
- [Parameter] public SomeType ObjectProperty { get; set; }
+ [Parameter] int IntProperty { get; set; }
+ [Parameter] bool BoolProperty { get; set; }
+ [Parameter] string StringProperty { get; set; }
+ [Parameter] SomeType ObjectProperty { get; set; }
}
}
"));
@@ -61,7 +61,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public string StringProperty { get; set; }
+ string StringProperty { get; set; }
}
}
"));
@@ -117,7 +117,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public Action OnClick { get; set; }
+ Action OnClick { get; set; }
}
}
"));
@@ -154,7 +154,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public Action OnClick { get; set; }
+ Action OnClick { get; set; }
}
}
"));
@@ -191,10 +191,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public string MyAttr { get; set; }
+ string MyAttr { get; set; }
[Parameter]
- public RenderFragment ChildContent { get; set; }
+ RenderFragment ChildContent { get; set; }
}
}
"));
@@ -387,10 +387,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public int Value { get; set; }
+ int Value { get; set; }
[Parameter]
- public Action ValueChanged { get; set; }
+ Action ValueChanged { get; set; }
}
}"));
@@ -453,10 +453,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public int Value { get; set; }
+ int Value { get; set; }
[Parameter]
- public Action OnChanged { get; set; }
+ Action OnChanged { get; set; }
}
}"));
// Act
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/DirectiveRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/DirectiveRazorIntegrationTest.cs
index 5b7e7a49c3..e314d07a90 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/DirectiveRazorIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/DirectiveRazorIntegrationTest.cs
@@ -119,10 +119,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
frame => AssertFrame.Text(frame, typeof(MyService2Impl).FullName));
}
- public class TestLayout : ILayoutComponent
+ public class TestLayout : IComponent
{
[Parameter]
- public RenderFragment Body { get; set; }
+ RenderFragment Body { get; set; }
public void Init(RenderHandle renderHandle)
{
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationTest.cs
index d6ddfadfce..6aa7d234a3 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RuntimeCodeGenerationTest.cs
@@ -51,10 +51,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
- [Parameter] public int IntProperty { get; set; }
- [Parameter] public bool BoolProperty { get; set; }
- [Parameter] public string StringProperty { get; set; }
- [Parameter] public SomeType ObjectProperty { get; set; }
+ [Parameter] int IntProperty { get; set; }
+ [Parameter] bool BoolProperty { get; set; }
+ [Parameter] string StringProperty { get; set; }
+ [Parameter] SomeType ObjectProperty { get; set; }
}
}
"));
@@ -86,7 +86,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public string StringProperty { get; set; }
+ string StringProperty { get; set; }
}
}
"));
@@ -143,7 +143,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public Action OnClick { get; set; }
+ Action OnClick { get; set; }
}
}
"));
@@ -180,7 +180,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public Action OnClick { get; set; }
+ Action OnClick { get; set; }
}
}
"));
@@ -217,10 +217,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public string MyAttr { get; set; }
+ string MyAttr { get; set; }
[Parameter]
- public RenderFragment ChildContent { get; set; }
+ RenderFragment ChildContent { get; set; }
}
}
"));
@@ -641,10 +641,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public int Value { get; set; }
+ int Value { get; set; }
[Parameter]
- public Action ValueChanged { get; set; }
+ Action ValueChanged { get; set; }
}
}"));
@@ -707,10 +707,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public int Value { get; set; }
+ int Value { get; set; }
[Parameter]
- public Action OnChanged { get; set; }
+ Action OnChanged { get; set; }
}
}"));
// Act
diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/BindTagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/BindTagHelperDescriptorProviderTest.cs
index 737f190687..64ffa6b41a 100644
--- a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/BindTagHelperDescriptorProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/BindTagHelperDescriptorProviderTest.cs
@@ -28,10 +28,10 @@ namespace Test
public void SetParameters(ParameterCollection parameters) { }
[Parameter]
- public string MyProperty { get; set; }
+ string MyProperty { get; set; }
[Parameter]
- public Action MyPropertyChanged { get; set; }
+ Action MyPropertyChanged { get; set; }
}
}
"));
diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs
index e74476b132..8a9343e92e 100644
--- a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs
@@ -28,7 +28,7 @@ namespace Test
public void SetParameters(ParameterCollection parameters) { }
[Parameter]
- public string MyProperty { get; set; }
+ private string MyProperty { get; set; }
}
}
@@ -138,7 +138,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public string MyProperty { get; set; }
+ string MyProperty { get; set; }
}
}
@@ -179,7 +179,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public bool MyProperty { get; set; }
+ bool MyProperty { get; set; }
}
}
@@ -231,7 +231,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public MyEnum MyProperty { get; set; }
+ MyEnum MyProperty { get; set; }
}
}
@@ -279,7 +279,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
- public Action OnClick { get; set; }
+ Action OnClick { get; set; }
}
}
@@ -326,18 +326,21 @@ namespace Test
public abstract class MyBase : BlazorComponent
{
[Parameter]
- public string Hidden { get; set; }
+ protected string Hidden { get; set; }
}
public class MyComponent : MyBase
{
[Parameter]
- public string NoPublicGetter { private get; set; }
+ string NoSetter { get; }
+
+ [Parameter]
+ static string StaticProperty { get; set; }
public string NoParameterAttribute { get; set; }
// No attribute here, hides base-class property of the same name.
- public new int Hidden { get; set; }
+ protected new int Hidden { get; set; }
public string this[int i]
{
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/LayoutTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/LayoutTest.cs
index c303fbbee3..b3c6c5e024 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/LayoutTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/LayoutTest.cs
@@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Arrange/Act
_layoutDisplayComponent.SetParameters(new Dictionary
{
- { nameof(LayoutDisplay.Page), typeof(ComponentWithLayout) }
+ { LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
});
// Assert
@@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Arrange/Act
_layoutDisplayComponent.SetParameters(new Dictionary
{
- { nameof(LayoutDisplay.Page), typeof(ComponentWithNestedLayout) }
+ { LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
});
// Assert
@@ -114,13 +114,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Arrange
_layoutDisplayComponent.SetParameters(new Dictionary
{
- { nameof(LayoutDisplay.Page), typeof(ComponentWithLayout) }
+ { LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
});
// Act
_layoutDisplayComponent.SetParameters(new Dictionary
{
- { nameof(LayoutDisplay.Page), typeof(DifferentComponentWithLayout) }
+ { LayoutDisplay.NameOfPage, typeof(DifferentComponentWithLayout) }
});
// Assert
@@ -165,13 +165,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Arrange
_layoutDisplayComponent.SetParameters(new Dictionary
{
- { nameof(LayoutDisplay.Page), typeof(ComponentWithLayout) }
+ { LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
});
// Act
_layoutDisplayComponent.SetParameters(new Dictionary
{
- { nameof(LayoutDisplay.Page), typeof(ComponentWithNestedLayout) }
+ { LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
});
// Assert
@@ -218,10 +218,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
});
}
- private class RootLayout : AutoRenderComponent, ILayoutComponent
+ private class RootLayout : AutoRenderComponent
{
[Parameter]
- public RenderFragment Body { get; set; }
+ RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
@@ -232,10 +232,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
[Layout(typeof(RootLayout))]
- private class NestedLayout : AutoRenderComponent, ILayoutComponent
+ private class NestedLayout : AutoRenderComponent
{
[Parameter]
- public RenderFragment Body { get; set; }
+ RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionAssignmentExtensionsTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionAssignmentExtensionsTest.cs
new file mode 100644
index 0000000000..b99c42993b
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Blazor.Test/ParameterCollectionAssignmentExtensionsTest.cs
@@ -0,0 +1,308 @@
+// 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;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Blazor.Components;
+using Microsoft.AspNetCore.Blazor.RenderTree;
+using Microsoft.AspNetCore.Blazor.Test.Helpers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Blazor.Test
+{
+ public class ParameterCollectionAssignmentExtensionsTest
+ {
+ [Fact]
+ public void IncomingParameterMatchesAnnotatedPrivateProperty_SetsValue()
+ {
+ // Arrange
+ var someObject = new object();
+ var parameterCollection = new ParameterCollectionBuilder
+ {
+ { nameof(HasInstanceProperties.IntProp), 123 },
+ { nameof(HasInstanceProperties.StringProp), "Hello" },
+ { HasInstanceProperties.ObjectPropName, someObject },
+ }.Build();
+ var target = new HasInstanceProperties();
+
+ // Act
+ parameterCollection.AssignToProperties(target);
+
+ // Assert
+ Assert.Equal(123, target.IntProp);
+ Assert.Equal("Hello", target.StringProp);
+ Assert.Same(someObject, target.ObjectPropCurrentValue);
+ }
+
+ [Fact]
+ public void IncomingParameterMatchesDeclaredParameterCaseInsensitively_SetsValue()
+ {
+ // Arrange
+ var parameterCollection = new ParameterCollectionBuilder
+ {
+ { nameof(HasInstanceProperties.IntProp).ToLowerInvariant(), 123 }
+ }.Build();
+ var target = new HasInstanceProperties();
+
+ // Act
+ parameterCollection.AssignToProperties(target);
+
+ // Assert
+ Assert.Equal(123, target.IntProp);
+ }
+
+ [Fact]
+ public void IncomingParameterMatchesInheritedDeclaredParameter_SetsValue()
+ {
+ // Arrange
+ var parameterCollection = new ParameterCollectionBuilder
+ {
+ { nameof(HasInheritedProperties.IntProp), 123 },
+ { nameof(HasInheritedProperties.DerivedClassIntProp), 456 },
+ }.Build();
+ var target = new HasInheritedProperties();
+
+ // Act
+ parameterCollection.AssignToProperties(target);
+
+ // Assert
+ Assert.Equal(123, target.IntProp);
+ Assert.Equal(456, target.DerivedClassIntProp);
+ }
+
+ [Fact]
+ public void NoIncomingParameterMatchesDeclaredParameter_LeavesValueUnchanged()
+ {
+ // Arrange
+ var existingObjectValue = new object();
+ var target = new HasInstanceProperties
+ {
+ IntProp = 456,
+ StringProp = "Existing value",
+ ObjectPropCurrentValue = existingObjectValue
+ };
+
+ var parameterCollection = new ParameterCollectionBuilder().Build();
+
+ // Act
+ parameterCollection.AssignToProperties(target);
+
+ // Assert
+ Assert.Equal(456, target.IntProp);
+ Assert.Equal("Existing value", target.StringProp);
+ Assert.Same(existingObjectValue, target.ObjectPropCurrentValue);
+ }
+
+ [Fact]
+ public void IncomingParameterMatchesNoDeclaredParameter_Throws()
+ {
+ // Arrange
+ var target = new HasPropertyWithoutParameterAttribute();
+ var parameterCollection = new ParameterCollectionBuilder
+ {
+ { "AnyOtherKey", 123 },
+ }.Build();
+
+ // Act
+ var ex = Assert.Throws(
+ () => parameterCollection.AssignToProperties(target));
+
+ // Assert
+ Assert.Equal(
+ $"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' does not have a property " +
+ $"matching the name 'AnyOtherKey'.",
+ ex.Message);
+ }
+
+ [Fact]
+ public void IncomingParameterMatchesPropertyNotDeclaredAsParameter_Throws()
+ {
+ // Arrange
+ var target = new HasPropertyWithoutParameterAttribute();
+ var parameterCollection = new ParameterCollectionBuilder
+ {
+ { nameof(HasPropertyWithoutParameterAttribute.IntProp), 123 },
+ }.Build();
+
+ // Act
+ var ex = Assert.Throws(
+ () => parameterCollection.AssignToProperties(target));
+
+ // Assert
+ Assert.Equal(default, target.IntProp);
+ Assert.Equal(
+ $"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' has a property matching the name '{nameof(HasPropertyWithoutParameterAttribute.IntProp)}', " +
+ $"but it does not have [{nameof(ParameterAttribute)}] applied.",
+ ex.Message);
+ }
+
+ [Fact]
+ public void IncomingParameterValueMismatchesDeclaredParameterType_Throws()
+ {
+ // Arrange
+ var someObject = new object();
+ var parameterCollection = new ParameterCollectionBuilder
+ {
+ { nameof(HasInstanceProperties.IntProp), "string value" },
+ }.Build();
+ var target = new HasInstanceProperties();
+
+ // Act
+ var ex = Assert.Throws(
+ () => parameterCollection.AssignToProperties(target));
+
+ // Assert
+ Assert.Equal(
+ $"Unable to set property '{nameof(HasInstanceProperties.IntProp)}' on object of " +
+ $"type '{typeof(HasInstanceProperties).FullName}'. The error was: {ex.InnerException.Message}",
+ ex.Message);
+ }
+
+ [Fact]
+ public void PropertyExplicitSetterException_Throws()
+ {
+ // Arrange
+ var target = new HasPropertyWhoseSetterThrows();
+ var parameterCollection = new ParameterCollectionBuilder
+ {
+ { nameof(HasPropertyWhoseSetterThrows.StringProp), "anything" },
+ }.Build();
+
+ // Act
+ var ex = Assert.Throws(
+ () => parameterCollection.AssignToProperties(target));
+
+ // Assert
+ Assert.Equal(
+ $"Unable to set property '{nameof(HasPropertyWhoseSetterThrows.StringProp)}' on object of " +
+ $"type '{typeof(HasPropertyWhoseSetterThrows).FullName}'. The error was: {ex.InnerException.Message}",
+ ex.Message);
+ }
+
+ [Fact]
+ public void DeclaredParametersVaryOnlyByCase_Throws()
+ {
+ // Arrange
+ var parameterCollection = new ParameterCollectionBuilder().Build();
+ var target = new HasParametersVaryingOnlyByCase();
+
+ // Act
+ var ex = Assert.Throws(() =>
+ parameterCollection.AssignToProperties(target));
+
+ // Assert
+ Assert.Equal(
+ $"The type '{typeof(HasParametersVaryingOnlyByCase).FullName}' declares more than one parameter matching the " +
+ $"name '{nameof(HasParametersVaryingOnlyByCase.MyValue).ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.",
+ ex.Message);
+ }
+
+ [Fact]
+ public void DeclaredParameterClashesWithInheritedParameter_Throws()
+ {
+ // Even when the developer uses 'new' to shadow an inherited property, this is not
+ // an allowed scenario because there would be no way for the consumer to specify
+ // both property values, and it's no good leaving the shadowed one unset because the
+ // base class can legitimately depend on it for correct functioning.
+
+ // Arrange
+ var parameterCollection = new ParameterCollectionBuilder().Build();
+ var target = new HasParameterClashingWithInherited();
+
+ // Act
+ var ex = Assert.Throws(() =>
+ parameterCollection.AssignToProperties(target));
+
+ // Assert
+ Assert.Equal(
+ $"The type '{typeof(HasParameterClashingWithInherited).FullName}' declares more than one parameter matching the " +
+ $"name '{nameof(HasParameterClashingWithInherited.IntProp).ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.",
+ ex.Message);
+ }
+
+
+ class HasInstanceProperties
+ {
+ // "internal" to show we're not requiring public accessors, but also
+ // to keep the assertions simple in the tests
+
+ [Parameter] internal int IntProp { get; set; }
+ [Parameter] internal string StringProp { get; set; }
+
+ // Also a truly private one to show there's nothing special about 'internal'
+ [Parameter] private object ObjectProp { get; set; }
+
+ public static string ObjectPropName => nameof(ObjectProp);
+ public object ObjectPropCurrentValue
+ {
+ get => ObjectProp;
+ set => ObjectProp = value;
+ }
+ }
+
+ class HasPropertyWithoutParameterAttribute
+ {
+ internal int IntProp { get; set; }
+ }
+
+ class HasPropertyWhoseSetterThrows
+ {
+ [Parameter]
+ internal string StringProp
+ {
+ get => string.Empty;
+ set => throw new InvalidOperationException("This setter throws");
+ }
+ }
+
+ class HasInheritedProperties : HasInstanceProperties
+ {
+ [Parameter] internal int DerivedClassIntProp { get; set; }
+ }
+
+ class HasParametersVaryingOnlyByCase
+ {
+ [Parameter] internal object MyValue { get; set; }
+ [Parameter] internal object Myvalue { get; set; }
+ }
+
+ class HasParameterClashingWithInherited : HasInstanceProperties
+ {
+ [Parameter] new int IntProp { get; set; }
+ }
+
+ class ParameterCollectionBuilder : IEnumerable
+ {
+ private readonly List<(string Name, object Value)> _keyValuePairs
+ = new List<(string, object)>();
+
+ public void Add(string name, object value)
+ => _keyValuePairs.Add((name, value));
+
+ public IEnumerator GetEnumerator()
+ => throw new NotImplementedException();
+
+ public ParameterCollection Build()
+ {
+ var builder = new RenderTreeBuilder(new TestRenderer());
+ builder.OpenComponent(0);
+ foreach (var kvp in _keyValuePairs)
+ {
+ builder.AddAttribute(1, kvp.Name, kvp.Value);
+ }
+ builder.CloseComponent();
+ return new ParameterCollection(builder.GetFrames().Array, ownerIndex: 0);
+ }
+ }
+
+ class FakeComponent : IComponent
+ {
+ public void Init(RenderHandle renderHandle)
+ => throw new NotImplementedException();
+
+ public void SetParameters(ParameterCollection parameters)
+ => throw new NotImplementedException();
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs
index a53c433ae6..f0aeb8f6cc 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs
@@ -1504,19 +1504,19 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class FakeComponent : IComponent
{
[Parameter]
- public int IntProperty { get; set; }
+ internal int IntProperty { get; set; }
[Parameter]
- public string StringProperty { get; set; }
+ internal string StringProperty { get; set; }
[Parameter]
- public object ObjectProperty { get; set; }
+ internal object ObjectProperty { get; set; }
[Parameter]
- public string ReadonlyProperty { get; private set; }
+ internal string ReadonlyProperty { get; private set; }
[Parameter]
- private string PrivateProperty { get; set; }
+ string PrivateProperty { get; set; }
public string NonParameterProperty { get; set; }
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
index af7215c3a4..361971ae73 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs
@@ -1110,7 +1110,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class MessageComponent : AutoRenderComponent
{
[Parameter]
- public string Message { get; set; }
+ internal string Message { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
@@ -1121,13 +1121,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class FakeComponent : IComponent
{
[Parameter]
- public int IntProperty { get; set; }
+ internal int IntProperty { get; private set; }
[Parameter]
- public string StringProperty { get; set; }
+ internal string StringProperty { get; private set; }
[Parameter]
- public object ObjectProperty { get; set; }
+ internal object ObjectProperty { get; set; }
public RenderHandle RenderHandle { get; private set; }
@@ -1141,13 +1141,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
{
[Parameter]
- public Action OnTest { get; set; }
+ internal Action OnTest { get; set; }
[Parameter]
- public Action OnClick { get; set; }
+ internal Action OnClick { get; set; }
[Parameter]
- public Action OnClickAction { get; set; }
+ internal Action OnClickAction { get; set; }
public bool SkipElement { get; set; }
private int renderCount = 0;
@@ -1187,10 +1187,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class ConditionalParentComponent : AutoRenderComponent where T : IComponent
{
[Parameter]
- public bool IncludeChild { get; set; }
+ internal bool IncludeChild { get; set; }
[Parameter]
- public IDictionary ChildParameters { get; set; }
+ internal IDictionary ChildParameters { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
@@ -1215,7 +1215,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class ReRendersParentComponent : AutoRenderComponent
{
[Parameter]
- public TestComponent Parent { get; set; }
+ internal TestComponent Parent { get; private set; }
+
private bool _isFirstTime = true;
protected override void BuildRenderTree(RenderTreeBuilder builder)
@@ -1233,7 +1234,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class RendersSelfAfterEventComponent : IComponent, IHandleEvent
{
[Parameter]
- public Action