[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.
This commit is contained in:
jacalvar 2016-03-24 22:33:42 -07:00
parent 384fd1f218
commit 574ecbb3eb
24 changed files with 879 additions and 17 deletions

View File

@ -143,9 +143,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// Initializes a new instance of <see cref="MvcRazorHost"/> using the specified <paramref name="chunkTreeCache"/>.
/// </summary>
/// <param name="chunkTreeCache">An <see cref="IChunkTreeCache"/> rooted at the application base path.</param>
public MvcRazorHost(IChunkTreeCache chunkTreeCache)
/// <param name="resolver">The <see cref="ITagHelperDescriptorResolver"/> used to resolve tag helpers on razor views.</param>
public MvcRazorHost(IChunkTreeCache chunkTreeCache, ITagHelperDescriptorResolver resolver)
: this(chunkTreeCache, new RazorPathNormalizer())
{
TagHelperDescriptorResolver = resolver;
}
/// <inheritdoc />

View File

@ -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;
}
/// <summary>
/// Registers tag helpers as services and replaces the existing <see cref="ITagHelperActivator"/>
/// with an <see cref="ServiceBasedTagHelperActivator"/>.
/// </summary>
/// <param name="builder">The <see cref="IMvcBuilder"/> instance this method extends.</param>
/// <returns>The <see cref="IMvcBuilder"/> instance this method extends.</returns>
public static IMvcBuilder AddTagHelpersAsServices(this IMvcBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
TagHelpersAsServices.AddTagHelpersAsServices(builder.PartManager, builder.Services);
return builder;
}
/// <summary>
/// Adds an initialization callback for a given <typeparamref name="TTagHelper"/>.
/// </summary>

View File

@ -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<TagHelperFeatureProvider>().Any())
{
builder.PartManager.FeatureProviders.Add(new TagHelperFeatureProvider());
}
}
/// <summary>
/// Registers discovered tag helpers as services and changes the existing <see cref="ITagHelperActivator"/>
/// for an <see cref="ServiceBasedTagHelperActivator"/>.
/// </summary>
/// <param name="builder">The <see cref="IMvcCoreBuilder"/> instance this method extends.</param>
/// <returns>The <see cref="IMvcCoreBuilder"/> instance this method extends.</returns>
public static IMvcCoreBuilder AddTagHelpersAsServices(this IMvcCoreBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
TagHelpersAsServices.AddTagHelpersAsServices(builder.PartManager, builder.Services);
return builder;
}
/// <summary>
/// Adds an initialization callback for a given <typeparamref name="TTagHelper"/>.
/// </summary>
@ -116,6 +147,10 @@ namespace Microsoft.Extensions.DependencyInjection
return new DefaultChunkTreeCache(accessor.FileProvider);
}));
services.TryAddSingleton<ITagHelperTypeResolver, TagHelperTypeResolver>();
services.TryAddSingleton<ITagHelperDescriptorFactory>(s => new TagHelperDescriptorFactory(designTime: false));
services.TryAddSingleton<ITagHelperDescriptorResolver, TagHelperDescriptorResolver>();
// Caches compilation artifacts across the lifetime of the application.
services.TryAddSingleton<ICompilerCacheProvider, DefaultCompilerCacheProvider>();
@ -123,7 +158,7 @@ namespace Microsoft.Extensions.DependencyInjection
// creating the singleton RazorViewEngine instance.
services.TryAddTransient<IRazorPageFactoryProvider, DefaultRazorPageFactoryProvider>();
services.TryAddTransient<IRazorCompilationService, RazorCompilationService>();
services.TryAddTransient<IMvcRazorHost, MvcRazorHost>();
services.TryAddTransient<IMvcRazorHost,MvcRazorHost>();
// This caches Razor page activation details that are valid for the lifetime of the application.
services.TryAddSingleton<IRazorPageActivator, RazorPageActivator>();

View File

@ -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
{
/// <summary>
/// A <see cref="ITagHelperActivator"/> that retrieves tag helpers as services from the request's
/// <see cref="IServiceProvider"/>.
/// </summary>
public class ServiceBasedTagHelperActivator : ITagHelperActivator
{
/// <inheritdoc />
public TTagHelper Create<TTagHelper>(ViewContext context) where TTagHelper : ITagHelper
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return context.HttpContext.RequestServices.GetRequiredService<TTagHelper>();
}
}
}

View File

@ -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<ITagHelperActivator, ServiceBasedTagHelperActivator>());
services.Replace(ServiceDescriptor.Transient<ITagHelperTypeResolver, FeatureTagHelperTypeResolver>());
}
}
}

View File

@ -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
{
/// <summary>
/// Resolves tag helper types from the <see cref="ApplicationPartManager.ApplicationParts"/>
/// of the application.
/// </summary>
public class FeatureTagHelperTypeResolver : TagHelperTypeResolver
{
private readonly TagHelperFeature _feature;
/// <summary>
/// Initializes a new <see cref="FeatureTagHelperTypeResolver"/> instance.
/// </summary>
/// <param name="manager">The <see cref="ApplicationPartManager"/> of the application.</param>
public FeatureTagHelperTypeResolver(ApplicationPartManager manager)
{
if (manager == null)
{
throw new ArgumentNullException(nameof(manager));
}
_feature = new TagHelperFeature();
manager.PopulateFeature(_feature);
}
/// <inheritdoc />
protected override IEnumerable<TypeInfo> GetExportedTypes(AssemblyName assemblyName)
{
if (assemblyName == null)
{
throw new ArgumentNullException(nameof(assemblyName));
}
var results = new List<TypeInfo>();
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;
}
/// <inheritdoc />
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<AssemblyName>
{
public static readonly IEqualityComparer<AssemblyName> 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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// The list of tag helper types in an MVC application. The <see cref="TagHelperFeature"/> can be populated
/// using the <see cref="ApplicationPartManager"/> that is available during startup at <see cref="IMvcBuilder.PartManager"/>
/// and <see cref="IMvcCoreBuilder.PartManager"/> or at a later stage by requiring the <see cref="ApplicationPartManager"/>
/// as a dependency in a component.
/// </summary>
public class TagHelperFeature
{
/// <summary>
/// Gets the list of tag helper types in an MVC application.
/// </summary>
public IList<TypeInfo> TagHelpers { get; } = new List<TypeInfo>();
}
}

View File

@ -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
{
/// <summary>
/// Discovers tag helpers from a list of <see cref="ApplicationPart"/> instances.
/// </summary>
public class TagHelperFeatureProvider : IApplicationFeatureProvider<TagHelperFeature>
{
/// <inheritdoc />
public void PopulateFeature(IEnumerable<ApplicationPart> parts, TagHelperFeature feature)
{
foreach (var type in parts.OfType<IApplicationPartTypeProvider>())
{
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);
}
}
}
}
}

View File

@ -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<AssemblyPart>().Any(p => p.Assembly == mvcTagHelpersAssembly))
{
partManager.ApplicationParts.Add(new AssemblyPart(mvcTagHelpersAssembly));
}
var mvcRazorAssembly = typeof(UrlResolutionTagHelper).GetTypeInfo().Assembly;
if(!partManager.ApplicationParts.OfType<AssemblyPart>().Any(p => p.Assembly == mvcRazorAssembly))
{
partManager.ApplicationParts.Add(new AssemblyPart(mvcRazorAssembly));
}
}
/// <summary>
/// Adds MVC services to the specified <see cref="IServiceCollection" />.
/// </summary>

View File

@ -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<MvcTestFixture<ControllersFromServicesWebSite.Startup>>
{
public TagHelpersFromServicesTest(MvcTestFixture<ControllersFromServicesWebSite.Startup> 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());
}
}
}

View File

@ -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<TModel> 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[]
{

View File

@ -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,

View File

@ -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))
{
}

View File

@ -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<TagHelperFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, TagHelperFeature feature)
{
foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(tp => tp.Types))
{
feature.TagHelpers.Add(type);
}
}
}
}
}

View File

@ -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<TagHelperFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, TagHelperFeature feature)
{
foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(tp => tp.Types))
{
feature.TagHelpers.Add(type);
}
}
}
}
}

View File

@ -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<TagHelperFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, TagHelperFeature feature)
{
foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(tp => tp.Types))
{
feature.TagHelpers.Add(type);
}
}
}
private class TestTagHelper : TagHelper
{
}
private class NotInPartsTagHelper : TagHelper
{
}
}
}

View File

@ -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<TypeInfo> 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
{
}
}

View File

@ -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<TypeInfo>();
}
public TestApplicationPart(params TypeInfo[] types)
{
Types = types;
}
public TestApplicationPart(IEnumerable<TypeInfo> types)
{
Types = types;
}
public TestApplicationPart(IEnumerable<Type> 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<TypeInfo> Types { get; }
}
}

View File

@ -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-*",

View File

@ -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<ApplicationPartManager>(descriptor.ImplementationInstance);
Assert.Equal(2, manager.ApplicationParts.Count);
Assert.Single(manager.ApplicationParts.OfType<AssemblyPart>(), p => p.Assembly == mvcRazorAssembly);
Assert.Single(manager.ApplicationParts.OfType<AssemblyPart>(), 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<ApplicationPartManager>(descriptor.ImplementationInstance);
Assert.Equal(2, manager.ApplicationParts.Count);
Assert.Single(manager.ApplicationParts.OfType<AssemblyPart>(), p => p.Assembly == mvcRazorAssembly);
Assert.Single(manager.ApplicationParts.OfType<AssemblyPart>(), 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<ApplicationPartManager>(descriptor.ImplementationInstance);
Assert.Equal(2, manager.FeatureProviders.Count);
Assert.Equal(3, manager.FeatureProviders.Count);
Assert.IsType<ControllerFeatureProvider>(manager.FeatureProviders[0]);
Assert.IsType<ViewComponentFeatureProvider>(manager.FeatureProviders[1]);
Assert.IsType<TagHelperFeatureProvider>(manager.FeatureProviders[2]);
}
[Fact]

View File

@ -19,5 +19,11 @@ namespace ControllersFromServicesWebSite
{
return ViewComponent("ComponentFromServices");
}
[HttpGet("InServicesTagHelper")]
public IActionResult InServicesTagHelper()
{
return View();
}
}
}

View File

@ -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<QueryValueService>();
services.AddTransient<ValueService>();

View File

@ -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());
}
}
}

View File

@ -0,0 +1,2 @@
@addTagHelper *, ControllersFromServicesWebSite
<inservices></inservices>