From 574ecbb3ebfbb53697be3b1ad69a0a0232a548db Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 24 Mar 2016 22:33:42 -0700 Subject: [PATCH] [Fixes #4087] Add support for AddTagHelpersAsServices() * Added TagHelperFeature and TagHelperFeatureProvider to perform tag helper discovery. * Changed tag helper discovery to use application parts when using tag helpers as services. * Added FeatureTagHelperTypeResolver to resolve tag helper type definitions from the list of application parts. * Added AddTagHelpersAsServices extension method on IMvcBuilder and IMvcCoreBuilder that performs tag helper discovery through the ApplicationPartManager and registers those tag helpers as services in the service collection. Assemblies should be added to the ApplicationPartManager in order to discover tag helpers in them in them. The @addTagHelper directive is still required on Razor pages to indicate what tag helpers to use. --- .../MvcRazorHost.cs | 4 +- .../MvcRazorMvcBuilderExtensions.cs | 22 +++- .../MvcRazorMvcCoreBuilderExtensions.cs | 39 ++++++- .../ServiceBasedTagHelperActivator.cs | 28 +++++ .../Internal/TagHelpersAsServices.cs | 40 +++++++ .../FeatureTagHelperTypeResolver.cs | 94 ++++++++++++++++ .../TagHelpers/TagHelperFeature.cs | 24 +++++ .../TagHelpers/TagHelperFeatureProvider.cs | 36 +++++++ .../MvcServiceCollectionExtensions.cs | 22 ++++ .../TagHelpersFromServicesTest.cs | 34 ++++++ .../Directives/ChunkInheritanceUtilityTest.cs | 7 +- .../InjectChunkVisitorTest.cs | 3 +- .../MvcRazorHostTest.cs | 7 +- .../MvcRazorMvcBuilderExtensionsTest.cs | 101 ++++++++++++++++++ .../MvcRazorMvcCoreBuilderExtensionsTest.cs | 101 ++++++++++++++++++ .../FeatureTagHelperTypeResolverTest.cs | 99 +++++++++++++++++ .../TagHelperFeatureProviderTest.cs | 87 +++++++++++++++ .../TestApplicationPart.cs | 44 ++++++++ .../project.json | 1 + .../MvcServiceCollectionExtensionsTest.cs | 60 ++++++++++- .../AnotherController.cs | 6 ++ .../ControllersFromServicesWebSite/Startup.cs | 7 +- .../TagHelpers/InServicesTagHelper.cs | 28 +++++ .../Views/Another/InServicesTagHelper.cshtml | 2 + 24 files changed, 879 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/ServiceBasedTagHelperActivator.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/Internal/TagHelpersAsServices.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/FeatureTagHelperTypeResolver.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperFeature.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperFeatureProvider.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/TagHelpersFromServicesTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcBuilderExtensionsTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcCoreBuilderExtensionsTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/FeatureTagHelperTypeResolverTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/TagHelperFeatureProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/TestApplicationPart.cs create mode 100644 test/WebSites/ControllersFromServicesWebSite/TagHelpers/InServicesTagHelper.cs create mode 100644 test/WebSites/ControllersFromServicesWebSite/Views/Another/InServicesTagHelper.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs index f5df3ba0e6..43f0ec045f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs @@ -143,9 +143,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// Initializes a new instance of using the specified . /// /// An rooted at the application base path. - public MvcRazorHost(IChunkTreeCache chunkTreeCache) + /// The used to resolve tag helpers on razor views. + public MvcRazorHost(IChunkTreeCache chunkTreeCache, ITagHelperDescriptorResolver resolver) : this(chunkTreeCache, new RazorPathNormalizer()) { + TagHelperDescriptorResolver = resolver; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcBuilderExtensions.cs index d1cadf0dc5..0b9236b799 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcBuilderExtensions.cs @@ -2,13 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; -using System.Reflection; using Microsoft.AspNetCore.Mvc.Razor; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.TagHelpers; -using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection { @@ -41,6 +38,23 @@ namespace Microsoft.Extensions.DependencyInjection return builder; } + /// + /// Registers tag helpers as services and replaces the existing + /// with an . + /// + /// The instance this method extends. + /// The instance this method extends. + public static IMvcBuilder AddTagHelpersAsServices(this IMvcBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + TagHelpersAsServices.AddTagHelpersAsServices(builder.PartManager, builder.Services); + return builder; + } + /// /// Adds an initialization callback for a given . /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index abcc7c05da..484229ac0f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -2,17 +2,20 @@ // 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.Mvc; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Directives; using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using Microsoft.Extensions.PlatformAbstractions; namespace Microsoft.Extensions.DependencyInjection { @@ -26,6 +29,7 @@ namespace Microsoft.Extensions.DependencyInjection } builder.AddViews(); + AddRazorViewEngineFeatureProviders(builder); AddRazorViewEngineServices(builder.Services); return builder; } @@ -45,6 +49,8 @@ namespace Microsoft.Extensions.DependencyInjection } builder.AddViews(); + + AddRazorViewEngineFeatureProviders(builder); AddRazorViewEngineServices(builder.Services); if (setupAction != null) @@ -55,6 +61,31 @@ namespace Microsoft.Extensions.DependencyInjection return builder; } + private static void AddRazorViewEngineFeatureProviders(IMvcCoreBuilder builder) + { + if (!builder.PartManager.FeatureProviders.OfType().Any()) + { + builder.PartManager.FeatureProviders.Add(new TagHelperFeatureProvider()); + } + } + + /// + /// Registers discovered tag helpers as services and changes the existing + /// for an . + /// + /// The instance this method extends. + /// The instance this method extends. + public static IMvcCoreBuilder AddTagHelpersAsServices(this IMvcCoreBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + TagHelpersAsServices.AddTagHelpersAsServices(builder.PartManager, builder.Services); + return builder; + } + /// /// Adds an initialization callback for a given . /// @@ -116,6 +147,10 @@ namespace Microsoft.Extensions.DependencyInjection return new DefaultChunkTreeCache(accessor.FileProvider); })); + services.TryAddSingleton(); + services.TryAddSingleton(s => new TagHelperDescriptorFactory(designTime: false)); + services.TryAddSingleton(); + // Caches compilation artifacts across the lifetime of the application. services.TryAddSingleton(); @@ -123,7 +158,7 @@ namespace Microsoft.Extensions.DependencyInjection // creating the singleton RazorViewEngine instance. services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); + services.TryAddTransient(); // This caches Razor page activation details that are valid for the lifetime of the application. services.TryAddSingleton(); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ServiceBasedTagHelperActivator.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ServiceBasedTagHelperActivator.cs new file mode 100644 index 0000000000..d5ba1e6409 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ServiceBasedTagHelperActivator.cs @@ -0,0 +1,28 @@ +// 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 Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + /// + /// A that retrieves tag helpers as services from the request's + /// . + /// + public class ServiceBasedTagHelperActivator : ITagHelperActivator + { + /// + public TTagHelper Create(ViewContext context) where TTagHelper : ITagHelper + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.HttpContext.RequestServices.GetRequiredService(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/TagHelpersAsServices.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/TagHelpersAsServices.cs new file mode 100644 index 0000000000..377d5106b5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/TagHelpersAsServices.cs @@ -0,0 +1,40 @@ +// 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.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public static class TagHelpersAsServices + { + public static void AddTagHelpersAsServices(ApplicationPartManager manager, IServiceCollection services) + { + if (manager == null) + { + throw new ArgumentNullException(nameof(manager)); + } + + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + var feature = new TagHelperFeature(); + manager.PopulateFeature(feature); + + foreach (var type in feature.TagHelpers.Select(t => t.AsType())) + { + services.TryAddTransient(type, type); + } + + services.Replace(ServiceDescriptor.Transient()); + services.Replace(ServiceDescriptor.Transient()); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/FeatureTagHelperTypeResolver.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/FeatureTagHelperTypeResolver.cs new file mode 100644 index 0000000000..db55f0a496 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/FeatureTagHelperTypeResolver.cs @@ -0,0 +1,94 @@ +// 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; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; +using System.Reflection; + +namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers +{ + /// + /// Resolves tag helper types from the + /// of the application. + /// + public class FeatureTagHelperTypeResolver : TagHelperTypeResolver + { + private readonly TagHelperFeature _feature; + + /// + /// Initializes a new instance. + /// + /// The of the application. + public FeatureTagHelperTypeResolver(ApplicationPartManager manager) + { + if (manager == null) + { + throw new ArgumentNullException(nameof(manager)); + } + + _feature = new TagHelperFeature(); + manager.PopulateFeature(_feature); + } + + /// + protected override IEnumerable GetExportedTypes(AssemblyName assemblyName) + { + if (assemblyName == null) + { + throw new ArgumentNullException(nameof(assemblyName)); + } + + var results = new List(); + for (var i = 0; i < _feature.TagHelpers.Count; i++) + { + var tagHelperAssemblyName = _feature.TagHelpers[i].Assembly.GetName(); + + if (AssemblyNameComparer.OrdinalIgnoreCase.Equals(tagHelperAssemblyName, assemblyName)) + { + results.Add(_feature.TagHelpers[i]); + } + } + + return results; + } + + /// + protected sealed override bool IsTagHelper(TypeInfo typeInfo) + { + // Return true always as we have already decided what types are tag helpers when GetExportedTypes + // gets called. + return true; + } + + private class AssemblyNameComparer : IEqualityComparer + { + public static readonly IEqualityComparer OrdinalIgnoreCase = new AssemblyNameComparer(); + + private AssemblyNameComparer() + { + } + + public bool Equals(AssemblyName x, AssemblyName y) + { + // Ignore case because that's what Assembly.Load does. + return string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase) && + string.Equals(x.CultureName ?? string.Empty, y.CultureName ?? string.Empty, StringComparison.Ordinal); + } + + public int GetHashCode(AssemblyName obj) + { + var hashCode = 0; + if (obj.Name != null) + { + hashCode ^= obj.Name.GetHashCode(); + } + + hashCode ^= (obj.CultureName ?? string.Empty).GetHashCode(); + return hashCode; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperFeature.cs new file mode 100644 index 0000000000..1f6561bd4d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperFeature.cs @@ -0,0 +1,24 @@ +// 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.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers +{ + /// + /// The list of tag helper types in an MVC application. The can be populated + /// using the that is available during startup at + /// and or at a later stage by requiring the + /// as a dependency in a component. + /// + public class TagHelperFeature + { + /// + /// Gets the list of tag helper types in an MVC application. + /// + public IList TagHelpers { get; } = new List(); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperFeatureProvider.cs new file mode 100644 index 0000000000..32bd368d78 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperFeatureProvider.cs @@ -0,0 +1,36 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; + +namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers +{ + /// + /// Discovers tag helpers from a list of instances. + /// + public class TagHelperFeatureProvider : IApplicationFeatureProvider + { + /// + public void PopulateFeature(IEnumerable parts, TagHelperFeature feature) + { + foreach (var type in parts.OfType()) + { + ProcessPart(type, feature); + } + } + + private static void ProcessPart(IApplicationPartTypeProvider part, TagHelperFeature feature) + { + foreach (var type in part.Types) + { + if (TagHelperConventions.IsTagHelper(type) && !feature.TagHelpers.Contains(type)) + { + feature.TagHelpers.Add(type); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs index 355478f12a..74e16726af 100644 --- a/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs @@ -2,8 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; +using System.Reflection; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; +using Microsoft.AspNetCore.Mvc.TagHelpers; namespace Microsoft.Extensions.DependencyInjection { @@ -29,6 +34,8 @@ namespace Microsoft.Extensions.DependencyInjection builder.AddApiExplorer(); builder.AddAuthorization(); + AddDefaultFrameworkParts(builder.PartManager); + // Order added affects options setup order // Default framework order @@ -48,6 +55,21 @@ namespace Microsoft.Extensions.DependencyInjection return new MvcBuilder(builder.Services, builder.PartManager); } + private static void AddDefaultFrameworkParts(ApplicationPartManager partManager) + { + var mvcTagHelpersAssembly = typeof(InputTagHelper).GetTypeInfo().Assembly; + if(!partManager.ApplicationParts.OfType().Any(p => p.Assembly == mvcTagHelpersAssembly)) + { + partManager.ApplicationParts.Add(new AssemblyPart(mvcTagHelpersAssembly)); + } + + var mvcRazorAssembly = typeof(UrlResolutionTagHelper).GetTypeInfo().Assembly; + if(!partManager.ApplicationParts.OfType().Any(p => p.Assembly == mvcRazorAssembly)) + { + partManager.ApplicationParts.Add(new AssemblyPart(mvcRazorAssembly)); + } + } + /// /// Adds MVC services to the specified . /// diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TagHelpersFromServicesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TagHelpersFromServicesTest.cs new file mode 100644 index 0000000000..2f083e06df --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TagHelpersFromServicesTest.cs @@ -0,0 +1,34 @@ +// 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.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class TagHelpersFromServicesTest : IClassFixture> + { + public TagHelpersFromServicesTest(MvcTestFixture fixture) + { + Client = fixture.Client; + } + + public HttpClient Client { get; } + + [Fact] + public async Task TagHelpersWithConstructorInjectionAreCreatedAndActivated() + { + // Arrange + var expected = "3"; + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/another/inservicestaghelper"); + + // Act + var response = await Client.SendAsync(request); + var responseText = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(expected, responseText.Trim()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs index 68c315bacd..1ebce27f20 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Razor.Chunks; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor.Directives @@ -31,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Directives new UsingChunk { Namespace = "AppNamespace.Model" }, }; var cache = new DefaultChunkTreeCache(fileProvider); - using (var host = new MvcRazorHost(cache)) + using (var host = new MvcRazorHost(cache, new TagHelperDescriptorResolver(designTime: false))) { var utility = new ChunkInheritanceUtility(host, cache, defaultChunks); @@ -119,7 +120,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Directives fileProvider.AddFile(@"/Views/_Layout.cshtml", string.Empty); fileProvider.AddFile(@"/Views/home/_not-viewimports.cshtml", string.Empty); var cache = new DefaultChunkTreeCache(fileProvider); - using (var host = new MvcRazorHost(cache)) + using (var host = new MvcRazorHost(cache, new TagHelperDescriptorResolver(designTime: false))) { var defaultChunks = new Chunk[] { @@ -144,7 +145,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Directives fileProvider.AddFile(@"/Views/_ViewImports.cshtml", "@inject DifferentHelper Html"); var cache = new DefaultChunkTreeCache(fileProvider); - using (var host = new MvcRazorHost(cache)) + using (var host = new MvcRazorHost(cache, new TagHelperDescriptorResolver(designTime: false))) { var defaultChunks = new Chunk[] { diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs index c966a05f0c..366b2b6fe8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Razor.Chunks; using Microsoft.AspNetCore.Razor.Chunks.Generators; using Microsoft.AspNetCore.Razor.CodeGenerators; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor @@ -149,7 +150,7 @@ MyType1 var chunkTreeCache = new DefaultChunkTreeCache(new TestFileProvider()); return new CodeGeneratorContext( new ChunkGeneratorContext( - new MvcRazorHost(chunkTreeCache), + new MvcRazorHost(chunkTreeCache, new TagHelperDescriptorResolver(designTime: false)), "MyClass", "MyNamespace", string.Empty, diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcRazorHostTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcRazorHostTest.cs index faa0398b60..eafb1d0d27 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcRazorHostTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/MvcRazorHostTest.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Razor.Chunks.Generators; using Microsoft.AspNetCore.Razor.CodeGenerators; using Microsoft.AspNetCore.Razor.CodeGenerators.Visitors; using Microsoft.AspNetCore.Razor.Parser; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; using Microsoft.AspNetCore.Testing; using Xunit; @@ -112,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor { // Arrange var fileProvider = new TestFileProvider(); - using (var host = new MvcRazorHost(new DefaultChunkTreeCache(fileProvider))) + using (var host = new MvcRazorHost(new DefaultChunkTreeCache(fileProvider), new TagHelperDescriptorResolver(designTime: false))) { // Act var instrumented = host.EnableInstrumentation; @@ -594,7 +595,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor private class MvcRazorHostWithNormalizedNewLine : MvcRazorHost { public MvcRazorHostWithNormalizedNewLine(IChunkTreeCache codeTreeCache) - : base(codeTreeCache) + : base(codeTreeCache, new TagHelperDescriptorResolver(designTime: false)) { } public override CodeGenerator DecorateCodeGenerator( @@ -646,7 +647,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor private class TestMvcRazorHost : MvcRazorHost { public TestMvcRazorHost(IChunkTreeCache ChunkTreeCache) - : base(ChunkTreeCache) + : base(ChunkTreeCache, new TagHelperDescriptorResolver(designTime: false)) { } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcBuilderExtensionsTest.cs new file mode 100644 index 0000000000..aab8b5a028 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcBuilderExtensionsTest.cs @@ -0,0 +1,101 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Test.DependencyInjection +{ + public class MvcRazorMvcBuilderExtensionsTest + { + [Fact] + public void AddTagHelpersAsServices_ReplacesTagHelperActivatorAndTagHelperTypeResolver() + { + // Arrange + var services = new ServiceCollection(); + var builder = services + .AddMvc() + .ConfigureApplicationPartManager(manager => + { + manager.ApplicationParts.Add(new TestApplicationPart()); + manager.FeatureProviders.Add(new TagHelperFeatureProvider()); + }); + + // Act + builder.AddTagHelpersAsServices(); + + // Assert + var activatorDescriptor = Assert.Single(services.ToList(), d => d.ServiceType == typeof(ITagHelperActivator)); + Assert.Equal(typeof(ServiceBasedTagHelperActivator), activatorDescriptor.ImplementationType); + + var resolverDescriptor = Assert.Single(services.ToList(), d => d.ServiceType == typeof(ITagHelperTypeResolver)); + Assert.Equal(typeof(FeatureTagHelperTypeResolver), resolverDescriptor.ImplementationType); + } + + [Fact] + public void AddTagHelpersAsServices_RegistersDiscoveredTagHelpers() + { + // Arrange + var services = new ServiceCollection(); + + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new TestApplicationPart( + typeof(TestTagHelperOne), + typeof(TestTagHelperTwo))); + + manager.FeatureProviders.Add(new TestFeatureProvider()); + + var builder = new MvcBuilder(services, manager); + + // Act + builder.AddTagHelpersAsServices(); + + // Assert + var collection = services.ToList(); + Assert.Equal(4, collection.Count); + + var tagHelperOne = Assert.Single(collection,t => t.ServiceType == typeof(TestTagHelperOne)); + Assert.Equal(typeof(TestTagHelperOne), tagHelperOne.ImplementationType); + Assert.Equal(ServiceLifetime.Transient, tagHelperOne.Lifetime); + + var tagHelperTwo = Assert.Single(collection, t => t.ServiceType == typeof(TestTagHelperTwo)); + Assert.Equal(typeof(TestTagHelperTwo), tagHelperTwo.ImplementationType); + Assert.Equal(ServiceLifetime.Transient, tagHelperTwo.Lifetime); + + var activator = Assert.Single(collection, t => t.ServiceType == typeof(ITagHelperActivator)); + Assert.Equal(typeof(ServiceBasedTagHelperActivator), activator.ImplementationType); + Assert.Equal(ServiceLifetime.Transient, activator.Lifetime); + + var typeResolver = Assert.Single(collection, t => t.ServiceType == typeof(ITagHelperTypeResolver)); + Assert.Equal(typeof(FeatureTagHelperTypeResolver), typeResolver.ImplementationType); + Assert.Equal(ServiceLifetime.Transient, typeResolver.Lifetime); + } + + private class TestTagHelperOne : TagHelper + { + } + + private class TestTagHelperTwo : TagHelper + { + } + + private class TestFeatureProvider : IApplicationFeatureProvider + { + public void PopulateFeature(IEnumerable parts, TagHelperFeature feature) + { + foreach (var type in parts.OfType().SelectMany(tp => tp.Types)) + { + feature.TagHelpers.Add(type); + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcCoreBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcCoreBuilderExtensionsTest.cs new file mode 100644 index 0000000000..fc134364ed --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcCoreBuilderExtensionsTest.cs @@ -0,0 +1,101 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; +using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Test.DependencyInjection +{ + public class MvcRazorMvcCoreBuilderExtensionsTest + { + [Fact] + public void AddTagHelpersAsServices_ReplacesTagHelperActivatorAndTagHelperTypeResolver() + { + // Arrange + var services = new ServiceCollection(); + var builder = services + .AddMvcCore() + .ConfigureApplicationPartManager(manager => + { + manager.ApplicationParts.Add(new TestApplicationPart()); + manager.FeatureProviders.Add(new TagHelperFeatureProvider()); + }); + + // Act + builder.AddTagHelpersAsServices(); + + // Assert + var activatorDescriptor = Assert.Single(services.ToList(), d => d.ServiceType == typeof(ITagHelperActivator)); + Assert.Equal(typeof(ServiceBasedTagHelperActivator), activatorDescriptor.ImplementationType); + + var resolverDescriptor = Assert.Single(services.ToList(), d => d.ServiceType == typeof(ITagHelperTypeResolver)); + Assert.Equal(typeof(FeatureTagHelperTypeResolver), resolverDescriptor.ImplementationType); + } + + [Fact] + public void AddTagHelpersAsServices_RegistersDiscoveredTagHelpers() + { + // Arrange + var services = new ServiceCollection(); + + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new TestApplicationPart( + typeof(TestTagHelperOne), + typeof(TestTagHelperTwo))); + + manager.FeatureProviders.Add(new TestFeatureProvider()); + + var builder = new MvcCoreBuilder(services, manager); + + // Act + builder.AddTagHelpersAsServices(); + + // Assert + var collection = services.ToList(); + Assert.Equal(4, collection.Count); + + var tagHelperOne = Assert.Single(collection, t => t.ServiceType == typeof(TestTagHelperOne)); + Assert.Equal(typeof(TestTagHelperOne), tagHelperOne.ImplementationType); + Assert.Equal(ServiceLifetime.Transient, tagHelperOne.Lifetime); + + var tagHelperTwo = Assert.Single(collection, t => t.ServiceType == typeof(TestTagHelperTwo)); + Assert.Equal(typeof(TestTagHelperTwo), tagHelperTwo.ImplementationType); + Assert.Equal(ServiceLifetime.Transient, tagHelperTwo.Lifetime); + + var activator = Assert.Single(collection, t => t.ServiceType == typeof(ITagHelperActivator)); + Assert.Equal(typeof(ServiceBasedTagHelperActivator), activator.ImplementationType); + Assert.Equal(ServiceLifetime.Transient, activator.Lifetime); + + var typeResolver = Assert.Single(collection, t => t.ServiceType == typeof(ITagHelperTypeResolver)); + Assert.Equal(typeof(FeatureTagHelperTypeResolver), typeResolver.ImplementationType); + Assert.Equal(ServiceLifetime.Transient, typeResolver.Lifetime); + } + + private class TestTagHelperOne : TagHelper + { + } + + private class TestTagHelperTwo : TagHelper + { + } + + private class TestFeatureProvider : IApplicationFeatureProvider + { + public void PopulateFeature(IEnumerable parts, TagHelperFeature feature) + { + foreach (var type in parts.OfType().SelectMany(tp => tp.Types)) + { + feature.TagHelpers.Add(type); + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/FeatureTagHelperTypeResolverTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/FeatureTagHelperTypeResolverTest.cs new file mode 100644 index 0000000000..48688a383c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/FeatureTagHelperTypeResolverTest.cs @@ -0,0 +1,99 @@ +// 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; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers +{ + public class FeatureTagHelperTypeResolverTest + { + [Fact] + public void Resolve_ReturnsTagHelpers_FromApplicationParts() + { + // Arrange + var manager = new ApplicationPartManager(); + var types = new[] { typeof(TestTagHelper) }; + manager.ApplicationParts.Add(new TestApplicationPart(types)); + manager.FeatureProviders.Add(new TestFeatureProvider()); + + var resolver = new FeatureTagHelperTypeResolver(manager); + + var assemblyName = typeof(FeatureTagHelperTypeResolverTest).GetTypeInfo().Assembly.GetName().Name; + + // Act + var result = resolver.Resolve(assemblyName, SourceLocation.Undefined, new ErrorSink()); + + // Assert + var type = Assert.Single(result); + Assert.Equal(typeof(TestTagHelper), type); + } + + [Fact] + public void Resolve_ReturnsTagHelpers_FilteredByAssembly() + { + // Arrange + var manager = new ApplicationPartManager(); + var types = new[] { typeof(TestTagHelper) }; + manager.ApplicationParts.Add(new AssemblyPart(typeof(InputTagHelper).GetTypeInfo().Assembly)); + manager.ApplicationParts.Add(new TestApplicationPart(types)); + manager.FeatureProviders.Add(new TestFeatureProvider()); + + var resolver = new FeatureTagHelperTypeResolver(manager); + + var assemblyName = typeof(FeatureTagHelperTypeResolverTest).GetTypeInfo().Assembly.GetName().Name; + + // Act + var result = resolver.Resolve(assemblyName, SourceLocation.Undefined, new ErrorSink()); + + // Assert + var type = Assert.Single(result); + Assert.Equal(typeof(TestTagHelper), type); + } + + [Fact] + public void Resolve_ReturnsEmptyTypesList_IfAssemblyLoadFails() + { + // Arrange + var manager = new ApplicationPartManager(); + var types = new[] { typeof(TestTagHelper) }; + manager.ApplicationParts.Add(new AssemblyPart(typeof(InputTagHelper).GetTypeInfo().Assembly)); + manager.ApplicationParts.Add(new TestApplicationPart(types)); + manager.FeatureProviders.Add(new TestFeatureProvider()); + + var resolver = new FeatureTagHelperTypeResolver(manager); + + // Act + var result = resolver.Resolve("UnknownAssembly", SourceLocation.Undefined, new ErrorSink()); + + // Assert + Assert.Empty(result); + } + + private class TestFeatureProvider : IApplicationFeatureProvider + { + public void PopulateFeature(IEnumerable parts, TagHelperFeature feature) + { + foreach (var type in parts.OfType().SelectMany(tp => tp.Types)) + { + feature.TagHelpers.Add(type); + } + } + } + + private class TestTagHelper : TagHelper + { + } + + private class NotInPartsTagHelper : TagHelper + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/TagHelperFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/TagHelperFeatureProviderTest.cs new file mode 100644 index 0000000000..5753a179c4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/TagHelperFeatureProviderTest.cs @@ -0,0 +1,87 @@ +// 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.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.TagHelpers.TagHelperFeatureProviderTests; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers +{ + public class TagHelperFeatureProviderTest + { + [Fact] + public void Populate_IncludesTagHelpers() + { + // Arrange + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new TestApplicationPart(typeof(DiscoveryTagHelper))); + manager.FeatureProviders.Add(new TagHelperFeatureProvider()); + + var feature = new TagHelperFeature(); + + // Act + manager.PopulateFeature(feature); + + // Assert + var tagHelperType = Assert.Single(feature.TagHelpers, th => th == typeof(DiscoveryTagHelper).GetTypeInfo()); + } + + [Fact] + public void Populate_DoesNotIncludeDuplicateTagHelpers() + { + // Arrange + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new TestApplicationPart(typeof(DiscoveryTagHelper))); + manager.ApplicationParts.Add(new TestApplicationPart(typeof(DiscoveryTagHelper))); + manager.FeatureProviders.Add(new TagHelperFeatureProvider()); + + var feature = new TagHelperFeature(); + + // Act + manager.PopulateFeature(feature); + + // Assert + var tagHelperType = Assert.Single(feature.TagHelpers, th => th == typeof(DiscoveryTagHelper).GetTypeInfo()); + } + + [Fact] + public void Populate_OnlyRunsOnPartsThatExportTypes() + { + // Arrange + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new TestApplicationPart(typeof(DiscoveryTagHelper))); + manager.ApplicationParts.Add(new NonApplicationTypeProviderPart()); + manager.FeatureProviders.Add(new TagHelperFeatureProvider()); + + var feature = new TagHelperFeature(); + + // Act + manager.PopulateFeature(feature); + + // Assert + var tagHelperType = Assert.Single(feature.TagHelpers, th => th == typeof(DiscoveryTagHelper).GetTypeInfo()); + } + + private class NonApplicationTypeProviderPart : ApplicationPart + { + public override string Name => nameof(NonApplicationTypeProviderPart); + + public IEnumerable Types => new[] { typeof(AnotherTagHelper).GetTypeInfo() }; + } + } +} + +// These types need to be public for the test to work. +namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers.TagHelperFeatureProviderTests +{ + public class DiscoveryTagHelper : TagHelper + { + } + + public class AnotherTagHelper : TagHelper + { + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/TestApplicationPart.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/TestApplicationPart.cs new file mode 100644 index 0000000000..5f67622424 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/TestApplicationPart.cs @@ -0,0 +1,44 @@ +// 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; + +using Microsoft.AspNetCore.Mvc.ApplicationParts; + +namespace Microsoft.AspNetCore.Mvc +{ + public class TestApplicationPart : ApplicationPart, IApplicationPartTypeProvider + { + public TestApplicationPart() + { + Types = Enumerable.Empty(); + } + + public TestApplicationPart(params TypeInfo[] types) + { + Types = types; + } + + public TestApplicationPart(IEnumerable types) + { + Types = types; + } + + public TestApplicationPart(IEnumerable types) + :this(types.Select(t => t.GetTypeInfo())) + { + } + + public TestApplicationPart(params Type[] types) + : this(types.Select(t => t.GetTypeInfo())) + { + } + + public override string Name => "Test part"; + + public IEnumerable Types { get; } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json b/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json index 0800c8f276..b50314bcd4 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json @@ -12,6 +12,7 @@ ], "dependencies": { "Microsoft.AspNetCore.Http": "1.0.0-*", + "Microsoft.AspNetCore.Mvc": "1.0.0-*", "Microsoft.AspNetCore.Mvc.DataAnnotations": "1.0.0-*", "Microsoft.AspNetCore.Mvc.Formatters.Xml": "1.0.0-*", "Microsoft.AspNetCore.Mvc.Razor": "1.0.0-*", diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index 32b3d70341..f0b0ba4906 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -19,6 +19,8 @@ using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; +using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; @@ -124,6 +126,61 @@ namespace Microsoft.AspNetCore.Mvc } } + [Fact] + public void AddMvc_AddsAssemblyPartsForFrameworkTagHelpers() + { + // Arrange + var mvcRazorAssembly = typeof(UrlResolutionTagHelper).GetTypeInfo().Assembly; + var mvcTagHelpersAssembly = typeof(InputTagHelper).GetTypeInfo().Assembly; + var services = new ServiceCollection(); + var providers = new IApplicationFeatureProvider[] + { + new ControllerFeatureProvider(), + new ViewComponentFeatureProvider() + }; + + // Act + services.AddMvc(); + + // Assert + var descriptor = Assert.Single(services, d => d.ServiceType == typeof(ApplicationPartManager)); + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + Assert.NotNull(descriptor.ImplementationInstance); + var manager = Assert.IsType(descriptor.ImplementationInstance); + + Assert.Equal(2, manager.ApplicationParts.Count); + Assert.Single(manager.ApplicationParts.OfType(), p => p.Assembly == mvcRazorAssembly); + Assert.Single(manager.ApplicationParts.OfType(), p => p.Assembly == mvcTagHelpersAssembly); + } + + [Fact] + public void AddMvcTwice_DoesNotAddDuplicateFramewokrParts() + { + // Arrange + var mvcRazorAssembly = typeof(UrlResolutionTagHelper).GetTypeInfo().Assembly; + var mvcTagHelpersAssembly = typeof(InputTagHelper).GetTypeInfo().Assembly; + var services = new ServiceCollection(); + var providers = new IApplicationFeatureProvider[] + { + new ControllerFeatureProvider(), + new ViewComponentFeatureProvider() + }; + + // Act + services.AddMvc(); + services.AddMvc(); + + // Assert + var descriptor = Assert.Single(services, d => d.ServiceType == typeof(ApplicationPartManager)); + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + Assert.NotNull(descriptor.ImplementationInstance); + var manager = Assert.IsType(descriptor.ImplementationInstance); + + Assert.Equal(2, manager.ApplicationParts.Count); + Assert.Single(manager.ApplicationParts.OfType(), p => p.Assembly == mvcRazorAssembly); + Assert.Single(manager.ApplicationParts.OfType(), p => p.Assembly == mvcTagHelpersAssembly); + } + [Fact] public void AddMvcTwice_DoesNotAddApplicationFeatureProvidersTwice() { @@ -145,9 +202,10 @@ namespace Microsoft.AspNetCore.Mvc Assert.NotNull(descriptor.ImplementationInstance); var manager = Assert.IsType(descriptor.ImplementationInstance); - Assert.Equal(2, manager.FeatureProviders.Count); + Assert.Equal(3, manager.FeatureProviders.Count); Assert.IsType(manager.FeatureProviders[0]); Assert.IsType(manager.FeatureProviders[1]); + Assert.IsType(manager.FeatureProviders[2]); } [Fact] diff --git a/test/WebSites/ControllersFromServicesWebSite/AnotherController.cs b/test/WebSites/ControllersFromServicesWebSite/AnotherController.cs index 05d7f60732..2f2432b366 100644 --- a/test/WebSites/ControllersFromServicesWebSite/AnotherController.cs +++ b/test/WebSites/ControllersFromServicesWebSite/AnotherController.cs @@ -19,5 +19,11 @@ namespace ControllersFromServicesWebSite { return ViewComponent("ComponentFromServices"); } + + [HttpGet("InServicesTagHelper")] + public IActionResult InServicesTagHelper() + { + return View(); + } } } diff --git a/test/WebSites/ControllersFromServicesWebSite/Startup.cs b/test/WebSites/ControllersFromServicesWebSite/Startup.cs index 74e5693442..85c805a688 100644 --- a/test/WebSites/ControllersFromServicesWebSite/Startup.cs +++ b/test/WebSites/ControllersFromServicesWebSite/Startup.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using ControllersFromServicesClassLibrary; using ControllersFromServicesWebSite.Components; +using ControllersFromServicesWebSite.TagHelpers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -25,9 +26,11 @@ namespace ControllersFromServicesWebSite .AddApplicationPart(typeof(TimeScheduleController).GetTypeInfo().Assembly) .ConfigureApplicationPartManager(manager => manager.ApplicationParts.Add(new TypesPart( typeof(AnotherController), - typeof(ComponentFromServicesViewComponent)))) + typeof(ComponentFromServicesViewComponent), + typeof(InServicesTagHelper)))) .AddControllersAsServices() - .AddViewComponentsAsServices(); + .AddViewComponentsAsServices() + .AddTagHelpersAsServices(); services.AddTransient(); services.AddTransient(); diff --git a/test/WebSites/ControllersFromServicesWebSite/TagHelpers/InServicesTagHelper.cs b/test/WebSites/ControllersFromServicesWebSite/TagHelpers/InServicesTagHelper.cs new file mode 100644 index 0000000000..e6d2ec6ac3 --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/TagHelpers/InServicesTagHelper.cs @@ -0,0 +1,28 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace ControllersFromServicesWebSite.TagHelpers +{ + [HtmlTargetElement("InServices")] + public class InServicesTagHelper : TagHelper + { + private ValueService _value; + + public InServicesTagHelper(ValueService value) + { + _value = value; + } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = null; + output.Content.SetContent(_value.Value.ToString()); + } + } +} diff --git a/test/WebSites/ControllersFromServicesWebSite/Views/Another/InServicesTagHelper.cshtml b/test/WebSites/ControllersFromServicesWebSite/Views/Another/InServicesTagHelper.cshtml new file mode 100644 index 0000000000..562c3573d0 --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/Views/Another/InServicesTagHelper.cshtml @@ -0,0 +1,2 @@ +@addTagHelper *, ControllersFromServicesWebSite + \ No newline at end of file