From daf6a404f97e1b86e95a5691a2706ca0abda787f Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 28 Feb 2018 20:16:15 -0800 Subject: [PATCH] Implement basic Component discovery This is the basics of component discovery along with some tests. The next set of changes will integrate it into the compilation process. --- Blazor.sln | 7 + .../BlazorApi.cs | 12 + .../ComponentDocumentClassifierPass.cs | 4 +- .../ComponentTagHelperDescriptorProvider.cs | 247 +++++++++++++++ .../Properties/AssemblyInfo.cs | 5 +- ...omponentTagHelperDescriptorProviderTest.cs | 295 ++++++++++++++++++ ...etCore.Blazor.Razor.Extensions.Test.csproj | 32 ++ .../xunit.runner.json | 3 + 8 files changed, 603 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs create mode 100644 test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj create mode 100644 test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/xunit.runner.json diff --git a/Blazor.sln b/Blazor.sln index 3712b7b057..0e1fbd7afe 100644 --- a/Blazor.sln +++ b/Blazor.sln @@ -87,6 +87,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorHosted.CSharp.Server" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorHosted.CSharp.Shared", "src\Microsoft.AspNetCore.Blazor.Templates\content\BlazorHosted.CSharp\BlazorHosted.CSharp.Shared\BlazorHosted.CSharp.Shared.csproj", "{F3E02B21-1127-431A-B832-0E53CB72097B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Blazor.Razor.Extensions.Test", "test\Microsoft.AspNetCore.Blazor.Razor.Extensions.Test\Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj", "{FF25111E-5A3E-48A3-96D8-08A2C5A2A91C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -209,6 +211,10 @@ Global {F3E02B21-1127-431A-B832-0E53CB72097B}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3E02B21-1127-431A-B832-0E53CB72097B}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3E02B21-1127-431A-B832-0E53CB72097B}.Release|Any CPU.Build.0 = Release|Any CPU + {FF25111E-5A3E-48A3-96D8-08A2C5A2A91C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF25111E-5A3E-48A3-96D8-08A2C5A2A91C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF25111E-5A3E-48A3-96D8-08A2C5A2A91C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF25111E-5A3E-48A3-96D8-08A2C5A2A91C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -249,6 +255,7 @@ Global {7549444A-9C81-44DE-AD0D-2C55501EAAC7} = {73DA1DFD-79F0-4BA2-B0B6-4F3A21D2C3F8} {78ED9932-0912-4F36-8F82-33DE850E7A33} = {73DA1DFD-79F0-4BA2-B0B6-4F3A21D2C3F8} {F3E02B21-1127-431A-B832-0E53CB72097B} = {73DA1DFD-79F0-4BA2-B0B6-4F3A21D2C3F8} + {FF25111E-5A3E-48A3-96D8-08A2C5A2A91C} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs index 5e8da5d613..ed5d666141 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorApi.cs @@ -19,6 +19,18 @@ namespace Microsoft.AspNetCore.Blazor.Razor public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute"; } + public static class IComponent + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.IComponent"; + + public static readonly string MetadataName = FullTypeName; + } + + public static class IDictionary + { + public static readonly string MetadataName = "System.Collection.IDictionary`2"; + } + public static class RenderFragment { public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.RenderFragment"; diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs index 3307af5d87..6ad5c0273d 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentClassifierPass.cs @@ -12,7 +12,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor { internal class ComponentDocumentClassifierPass : DocumentClassifierPassBase, IRazorDocumentClassifierPass { - protected override string DocumentKind => "Blazor.Component-0.1"; + public static readonly string ComponentDocumentKind = "Blazor.Component-0.1"; + + protected override string DocumentKind => ComponentDocumentKind; protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs new file mode 100644 index 0000000000..21cb10a6eb --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentTagHelperDescriptorProvider.cs @@ -0,0 +1,247 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + internal class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider + { + public readonly static string ComponentTagHelperKind = ComponentDocumentClassifierPass.ComponentDocumentKind; + + private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat = + SymbolDisplayFormat.FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) + .WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes)); + + public bool IncludeDocumentation { get; set; } + + public int Order { get; set; } + + public void Execute(TagHelperDescriptorProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var compilation = context.GetCompilation(); + if (compilation == null) + { + // No compilation, nothing to do. + return; + } + + var componentSymbol = compilation.GetTypeByMetadataName(BlazorApi.IComponent.MetadataName); + if (componentSymbol == null) + { + // No definition for IComponent, nothing to do. + return; + } + + var types = new List(); + var visitor = new ComponentTypeVisitor(componentSymbol, types); + + // Visit the primary output of this compilation, as well as all references. + visitor.Visit(compilation.Assembly); + foreach (var reference in compilation.References) + { + // We ignore .netmodules here - there really isn't a case where they are used by user code + // even though the Roslyn APIs all support them. + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) + { + visitor.Visit(assembly); + } + } + + for (var i = 0; i < types.Count; i++) + { + var type = types[i]; + context.Results.Add(CreateDescriptor(type)); + } + } + + private TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + var typeName = type.ToDisplayString(FullNameTypeDisplayFormat); + var assemblyName = type.ContainingAssembly.Identity.Name; + + var builder = TagHelperDescriptorBuilder.Create(ComponentTagHelperKind, typeName, assemblyName); + builder.SetTypeName(typeName); + + // This opts out this 'component' tag helper for any processing that's specific to the default + // Razor ITagHelper runtime. + builder.Metadata[TagHelperMetadata.Runtime.Name] = "Blazor.IComponent"; + + var xml = type.GetDocumentationCommentXml(); + if (!string.IsNullOrEmpty(xml)) + { + builder.Documentation = xml; + } + + // 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 GetVisibleProperties(type)) + { + if (property.kind == PropertyKind.Ignored) + { + continue; + } + + builder.BindAttribute(pb => + { + pb.Name = property.property.Name; + pb.TypeName = property.property.Type.ToDisplayString(FullNameTypeDisplayFormat); + pb.SetPropertyName(property.property.Name); + + if (property.kind == PropertyKind.Enum) + { + pb.IsEnum = true; + } + + xml = property.property.GetDocumentationCommentXml(); + if (!string.IsNullOrEmpty(xml)) + { + pb.Documentation = xml; + } + }); + } + + var descriptor = builder.Build(); + + return descriptor; + } + + // Does a walk up the inheritance chain to determine the set of 'visible' properties 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)> GetVisibleProperties(INamedTypeSymbol type) + { + var properties = new Dictionary(StringComparer.Ordinal); + do + { + var members = type.GetMembers(); + for (var i = 0; i < members.Length; i++) + { + var property = members[i] as IPropertySymbol; + if (property == null) + { + // Not a property + continue; + } + + var kind = PropertyKind.Default; + if (properties.ContainsKey(property.Name)) + { + // Not visible + kind = PropertyKind.Ignored; + } + + if (property.Parameters.Length != 0) + { + // Indexer + kind = PropertyKind.Ignored; + } + + if (property.GetMethod?.DeclaredAccessibility != Accessibility.Public) + { + // Non-public getter or no getter + kind = PropertyKind.Ignored; + } + + if (property.SetMethod?.DeclaredAccessibility != Accessibility.Public) + { + // Non-public setter or no setter + kind = PropertyKind.Ignored; + } + + if (kind == PropertyKind.Default && property.Type.TypeKind == TypeKind.Enum) + { + kind = PropertyKind.Enum; + } + + properties.Add(property.Name, (property, kind)); + } + + type = type.BaseType; + } + while (type != null); + + return properties.Values; + } + + private enum PropertyKind + { + Ignored, + Default, + Enum, + } + + private class ComponentTypeVisitor : SymbolVisitor + { + private INamedTypeSymbol _interface; + private List _results; + + public ComponentTypeVisitor(INamedTypeSymbol @interface, List results) + { + _interface = @interface; + _results = results; + } + + public override void VisitNamedType(INamedTypeSymbol symbol) + { + if (IsComponent(symbol)) + { + _results.Add(symbol); + } + } + + public override void VisitNamespace(INamespaceSymbol symbol) + { + foreach (var member in symbol.GetMembers()) + { + Visit(member); + } + } + + public override void VisitAssembly(IAssemblySymbol symbol) + { + // This as a simple yet high-value optimization that excludes the vast majority of + // assemblies that (by definition) can't contain a component. + if (symbol.Name != null && !symbol.Name.StartsWith("System.", StringComparison.Ordinal)) + { + Visit(symbol.GlobalNamespace); + } + } + + internal bool IsComponent(INamedTypeSymbol symbol) + { + if (_interface == null) + { + return false; + } + + return + symbol.DeclaredAccessibility == Accessibility.Public && + !symbol.IsAbstract && + !symbol.IsGenericType && + symbol.AllInterfaces.Contains(_interface); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Properties/AssemblyInfo.cs index 4b642e6ea5..843209b4d1 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Properties/AssemblyInfo.cs @@ -1,7 +1,10 @@ // 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.Runtime.CompilerServices; using Microsoft.AspNetCore.Blazor.Razor; using Microsoft.AspNetCore.Razor.Language; -[assembly: ProvideRazorExtensionInitializer("Blazor-0.1", typeof(BlazorExtensionInitializer))] \ No newline at end of file +[assembly: ProvideRazorExtensionInitializer("Blazor-0.1", typeof(BlazorExtensionInitializer))] + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Razor.Extensions.Test")] \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs new file mode 100644 index 0000000000..97186ebdbc --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentTagHelperDescriptorProviderTest.cs @@ -0,0 +1,295 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.Extensions.DependencyModel; +using Xunit; + +namespace Microsoft.AspNetCore.Blazor.Razor.Extensions +{ + public class ComponentTagHelperDescriptorProviderTest + { + static ComponentTagHelperDescriptorProviderTest() + { + var dependencyContext = DependencyContext.Load(typeof(ComponentTagHelperDescriptorProviderTest).Assembly); + + var metadataReferences = dependencyContext.CompileLibraries + .SelectMany(l => l.ResolveReferencePaths()) + .Select(assemblyPath => MetadataReference.CreateFromFile(assemblyPath)) + .ToArray(); + + BaseCompilation = CSharpCompilation.Create( + "TestAssembly", + Array.Empty(), + metadataReferences, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + private static Compilation BaseCompilation { get; } + + [Fact] + public void Excecute_FindsIComponentType_CreatesDescriptor() + { + // Arrange + + var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@" +using Microsoft.AspNetCore.Blazor.Components; + +namespace Test +{ + public class MyComponent : IComponent + { + public void Init(RenderHandle renderHandle) { } + + public void SetParameters(ParameterCollection parameters) { } + + public string MyProperty { get; set; } + } +} + +")); + + Assert.Empty(compilation.GetDiagnostics()); + + var context = TagHelperDescriptorProviderContext.Create(); + context.SetCompilation(compilation); + + var provider = new ComponentTagHelperDescriptorProvider(); + + // Act + provider.Execute(context); + + // Assert + var components = ExcludeBuiltInComponents(context); + var component = Assert.Single(components); + + // These are features Components don't use. Verifying them once here and + // then ignoring them. + Assert.Empty(component.AllowedChildTags); + Assert.Null(component.TagOutputHint); + + // These are features that are invariants of all Components. Verifying them once + // here and then ignoring them. + Assert.Empty(component.Diagnostics); + Assert.False(component.HasErrors); + Assert.Equal(ComponentTagHelperDescriptorProvider.ComponentTagHelperKind, component.Kind); + Assert.Equal("Blazor.Component-0.1", component.Kind); + Assert.False(component.IsDefaultKind()); + Assert.False(component.KindUsesDefaultTagHelperRuntime()); + + // No documentation in this test + Assert.Null(component.Documentation); + + // These are all trivally derived from the assembly/namespace/type name + Assert.Equal("TestAssembly", component.AssemblyName); + Assert.Equal("Test.MyComponent", component.Name); + Assert.Equal("Test.MyComponent", component.DisplayName); + Assert.Equal("Test.MyComponent", component.GetTypeName()); + + // Our use of matching rules is also very simple, and derived from the name. Veriying + // it once in detail here and then ignoring it. + var rule = Assert.Single(component.TagMatchingRules); + Assert.Empty(rule.Attributes); + Assert.Empty(rule.Diagnostics); + Assert.False(rule.HasErrors); + Assert.Null(rule.ParentTag); + Assert.Equal("MyComponent", rule.TagName); + Assert.Equal(TagStructure.Unspecified, rule.TagStructure); + + // Our use of metadata is also (for now) an invariant for all Components - other than the type name + // which is trivial. Verifying it once in detail and then ignoring it. + Assert.Collection( + component.Metadata.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal(TagHelperMetadata.Common.TypeName, kvp.Key); Assert.Equal("Test.MyComponent", kvp.Value); }, + kvp => { Assert.Equal(TagHelperMetadata.Runtime.Name, kvp.Key); Assert.Equal("Blazor.IComponent", kvp.Value); }); + + // Our use of bound attributes is what tests will focus on. As you might expect right now, this test + // is going to cover a lot of trivial stuff that will be true for all components/component-properties. + var attribute = Assert.Single(component.BoundAttributes); + + // Invariants + Assert.Empty(attribute.Diagnostics); + Assert.False(attribute.HasErrors); + Assert.Equal("Blazor.Component-0.1", attribute.Kind); + Assert.False(attribute.IsDefaultKind()); + + // Related to dictionaries/indexers, not supported currently, not sure if we ever will + Assert.False(attribute.HasIndexer); + Assert.Null(attribute.IndexerNamePrefix); + Assert.Null(attribute.IndexerTypeName); + Assert.False(attribute.IsIndexerBooleanProperty); + Assert.False(attribute.IsIndexerStringProperty); + + // No documentation in this test + Assert.Null(attribute.Documentation); + + // Names are trivially derived from the property name + Assert.Equal("MyProperty", attribute.Name); + Assert.Equal("MyProperty", attribute.GetPropertyName()); + Assert.Equal("string Test.MyComponent.MyProperty", attribute.DisplayName); + + // Defined from the property type + Assert.Equal("System.String", attribute.TypeName); + Assert.True(attribute.IsStringProperty); + Assert.False(attribute.IsBooleanProperty); + Assert.False(attribute.IsEnum); + + // Our use of metadata is also (for now) an invariant for all Component properties - other than the type name + // which is trivial. Verifying it once in detail and then ignoring it. + Assert.Collection( + attribute.Metadata.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal(TagHelperMetadata.Common.PropertyName, kvp.Key); Assert.Equal("MyProperty", kvp.Value); }); + } + + [Fact] + public void Excecute_FindsBlazorComponentType_CreatesDescriptor() + { + // Arrange + + var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@" +using Microsoft.AspNetCore.Blazor.Components; + +namespace Test +{ + public class MyComponent : BlazorComponent + { + public string MyProperty { get; set; } + } +} + +")); + + Assert.Empty(compilation.GetDiagnostics()); + + var context = TagHelperDescriptorProviderContext.Create(); + context.SetCompilation(compilation); + + var provider = new ComponentTagHelperDescriptorProvider(); + + // Act + provider.Execute(context); + + // Assert + var components = ExcludeBuiltInComponents(context); + var component = Assert.Single(components); + + Assert.Equal("TestAssembly", component.AssemblyName); + Assert.Equal("Test.MyComponent", component.Name); + + var attribute = Assert.Single(component.BoundAttributes); + Assert.Equal("MyProperty", attribute.Name); + Assert.Equal("System.String", attribute.TypeName); + } + + [Fact] // bool properties support minimized attributes + public void Excecute_BoolProperty_CreatesDescriptor() + { + // Arrange + + var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@" +using Microsoft.AspNetCore.Blazor.Components; + +namespace Test +{ + public class MyComponent : BlazorComponent + { + public bool MyProperty { get; set; } + } +} + +")); + + Assert.Empty(compilation.GetDiagnostics()); + + var context = TagHelperDescriptorProviderContext.Create(); + context.SetCompilation(compilation); + + var provider = new ComponentTagHelperDescriptorProvider(); + + // Act + provider.Execute(context); + + // Assert + var components = ExcludeBuiltInComponents(context); + var component = Assert.Single(components); + + Assert.Equal("TestAssembly", component.AssemblyName); + Assert.Equal("Test.MyComponent", component.Name); + + var attribute = Assert.Single(component.BoundAttributes); + Assert.Equal("MyProperty", attribute.Name); + Assert.Equal("System.Boolean", attribute.TypeName); + + Assert.False(attribute.HasIndexer); + Assert.True(attribute.IsBooleanProperty); + Assert.False(attribute.IsEnum); + Assert.False(attribute.IsStringProperty); + } + + [Fact] // enum properties have some special intellisense behavior + public void Excecute_EnumProperty_CreatesDescriptor() + { + // Arrange + + var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@" +using Microsoft.AspNetCore.Blazor.Components; + +namespace Test +{ + public enum MyEnum + { + One, + Two + } + + public class MyComponent : BlazorComponent + { + public MyEnum MyProperty { get; set; } + } +} + +")); + + Assert.Empty(compilation.GetDiagnostics()); + + var context = TagHelperDescriptorProviderContext.Create(); + context.SetCompilation(compilation); + + var provider = new ComponentTagHelperDescriptorProvider(); + + // Act + provider.Execute(context); + + // Assert + var components = ExcludeBuiltInComponents(context); + var component = Assert.Single(components); + + Assert.Equal("TestAssembly", component.AssemblyName); + Assert.Equal("Test.MyComponent", component.Name); + + var attribute = Assert.Single(component.BoundAttributes); + Assert.Equal("MyProperty", attribute.Name); + Assert.Equal("Test.MyEnum", attribute.TypeName); + + Assert.False(attribute.HasIndexer); + Assert.False(attribute.IsBooleanProperty); + Assert.True(attribute.IsEnum); + Assert.False(attribute.IsStringProperty); + } + + // For simplicity in testing, exlude the built-in components. We'll add more and we + // don't want to update the tests when that happens. + private TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescriptorProviderContext context) + { + return context.Results + .Where(c => c.AssemblyName == "TestAssembly") + .OrderBy(c => c.Name) + .ToArray(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj new file mode 100644 index 0000000000..68cb511038 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp2.0 + + false + + + true + + + + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/xunit.runner.json b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/xunit.runner.json new file mode 100644 index 0000000000..6ce741b6c0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "shadowCopy": false +} \ No newline at end of file