[Fixes #4087] Add AddViewComponentsAsServices() and ServiceBasedViewComponentActivator

* Added ViewComponentFeture and ViewComponentFeatureProvider to perform view component discovery.
* Changed view component discovery to use application parts.
* Changed ViewComponentDescriptorProvider to make use of Application parts.
* Added AddViewComponentsAsServices method on IMvcBuilder that performs view component
  discovery through the ApplicationPartManager and registers those view components as
  services in the service collection. Assemblies should be added to the ApplicationPartManager
  in order to discover view components in them in them.
This commit is contained in:
jacalvar 2016-03-24 13:05:36 -07:00
parent 87e89befc8
commit 5246125cb7
19 changed files with 530 additions and 123 deletions

View File

@ -2,7 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
{ {
@ -16,6 +19,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary> /// </summary>
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param> /// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
/// <param name="setupAction">The <see cref="MvcViewOptions"/> which need to be configured.</param> /// <param name="setupAction">The <see cref="MvcViewOptions"/> which need to be configured.</param>
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
public static IMvcBuilder AddViewOptions( public static IMvcBuilder AddViewOptions(
this IMvcBuilder builder, this IMvcBuilder builder,
Action<MvcViewOptions> setupAction) Action<MvcViewOptions> setupAction)
@ -33,5 +37,30 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.Configure(setupAction); builder.Services.Configure(setupAction);
return builder; return builder;
} }
/// <summary>
/// Registers discovered view components as services in the <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
public static IMvcBuilder AddViewComponentsAsServices(this IMvcBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
var feature = new ViewComponentFeature();
builder.PartManager.PopulateFeature(feature);
foreach (var viewComponent in feature.ViewComponents.Select(vc => vc.AsType()))
{
builder.Services.TryAddTransient(viewComponent, viewComponent);
}
builder.Services.Replace(ServiceDescriptor.Singleton<IViewComponentActivator, ServiceBasedViewComponentActivator>());
return builder;
}
} }
} }

View File

@ -3,7 +3,9 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Linq;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
@ -26,10 +28,19 @@ namespace Microsoft.Extensions.DependencyInjection
} }
builder.AddDataAnnotations(); builder.AddDataAnnotations();
AddViewComponentApplicationPartsProviders(builder.PartManager);
AddViewServices(builder.Services); AddViewServices(builder.Services);
return builder; return builder;
} }
private static void AddViewComponentApplicationPartsProviders(ApplicationPartManager manager)
{
if (!manager.FeatureProviders.OfType<ViewComponentFeatureProvider>().Any())
{
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
}
}
public static IMvcCoreBuilder AddViews( public static IMvcCoreBuilder AddViews(
this IMvcCoreBuilder builder, this IMvcCoreBuilder builder,
Action<MvcViewOptions> setupAction) Action<MvcViewOptions> setupAction)

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Microsoft.AspNetCore.Mvc.ViewComponents namespace Microsoft.AspNetCore.Mvc.ViewComponents
@ -18,64 +18,47 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
{ {
private const string AsyncMethodName = "InvokeAsync"; private const string AsyncMethodName = "InvokeAsync";
private const string SyncMethodName = "Invoke"; private const string SyncMethodName = "Invoke";
private readonly IAssemblyProvider _assemblyProvider; private readonly ApplicationPartManager _partManager;
/// <summary> /// <summary>
/// Creates a new <see cref="DefaultViewComponentDescriptorProvider"/>. /// Creates a new <see cref="DefaultViewComponentDescriptorProvider"/>.
/// </summary> /// </summary>
/// <param name="assemblyProvider">The <see cref="IAssemblyProvider"/>.</param> /// <param name="partManager">The <see cref="ApplicationPartManager"/>.</param>
public DefaultViewComponentDescriptorProvider(IAssemblyProvider assemblyProvider) public DefaultViewComponentDescriptorProvider(ApplicationPartManager partManager)
{ {
_assemblyProvider = assemblyProvider; if (partManager == null)
{
throw new ArgumentNullException(nameof(partManager));
}
_partManager = partManager;
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual IEnumerable<ViewComponentDescriptor> GetViewComponents() public virtual IEnumerable<ViewComponentDescriptor> GetViewComponents()
{ {
var types = GetCandidateTypes(); return GetCandidateTypes().Select(CreateDescriptor);
return types
.Where(IsViewComponentType)
.Select(CreateDescriptor);
} }
/// <summary> /// <summary>
/// Gets the candidate <see cref="TypeInfo"/> instances. The results of this will be provided to /// Gets the candidate <see cref="TypeInfo"/> instances provided by the <see cref="ApplicationPartManager"/>.
/// <see cref="IsViewComponentType"/> for filtering.
/// </summary> /// </summary>
/// <returns>A list of <see cref="TypeInfo"/> instances.</returns> /// <returns>A list of <see cref="TypeInfo"/> instances.</returns>
protected virtual IEnumerable<TypeInfo> GetCandidateTypes() protected virtual IEnumerable<TypeInfo> GetCandidateTypes()
{ {
var assemblies = _assemblyProvider.CandidateAssemblies; var feature = new ViewComponentFeature();
return assemblies.SelectMany(a => a.ExportedTypes).Select(t => t.GetTypeInfo()); _partManager.PopulateFeature(feature);
} return feature.ViewComponents;
/// <summary>
/// Determines whether or not the given <see cref="TypeInfo"/> is a view component class.
/// </summary>
/// <param name="typeInfo">The <see cref="TypeInfo"/>.</param>
/// <returns>
/// <c>true</c> if <paramref name="typeInfo"/>represents a view component class, otherwise <c>false</c>.
/// </returns>
protected virtual bool IsViewComponentType(TypeInfo typeInfo)
{
if (typeInfo == null)
{
throw new ArgumentNullException(nameof(typeInfo));
}
return ViewComponentConventions.IsComponent(typeInfo);
} }
private static ViewComponentDescriptor CreateDescriptor(TypeInfo typeInfo) private static ViewComponentDescriptor CreateDescriptor(TypeInfo typeInfo)
{ {
var type = typeInfo.AsType();
var candidate = new ViewComponentDescriptor var candidate = new ViewComponentDescriptor
{ {
FullName = ViewComponentConventions.GetComponentFullName(typeInfo), FullName = ViewComponentConventions.GetComponentFullName(typeInfo),
ShortName = ViewComponentConventions.GetComponentName(typeInfo), ShortName = ViewComponentConventions.GetComponentName(typeInfo),
TypeInfo = typeInfo, TypeInfo = typeInfo,
MethodInfo = FindMethod(type) MethodInfo = FindMethod(typeInfo.AsType())
}; };
return candidate; return candidate;

View File

@ -0,0 +1,33 @@
// 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.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ViewComponents
{
/// <summary>
/// A <see cref="IViewComponentActivator"/> that retrieves view components as services from the request's
/// <see cref="IServiceProvider"/>.
/// </summary>
public class ServiceBasedViewComponentActivator : IViewComponentActivator
{
/// <inheritdoc />
public object Create(ViewComponentContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var viewComponentType = context.ViewComponentDescriptor.TypeInfo.AsType();
return context.ViewContext.HttpContext.RequestServices.GetRequiredService(viewComponentType);
}
/// <inheritdoc />
public virtual void Release(ViewComponentContext context, object viewComponent)
{
}
}
}

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.ViewComponents
{
/// <summary>
/// The list of view component types in an MVC application.The <see cref="ViewComponentFeature"/> 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 ViewComponentFeature
{
/// <summary>
/// Gets the list of view component types in an MVC application.
/// </summary>
public IList<TypeInfo> ViewComponents { get; } = new List<TypeInfo>();
}
}

View File

@ -0,0 +1,38 @@
// 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 Microsoft.AspNetCore.Mvc.ApplicationParts;
namespace Microsoft.AspNetCore.Mvc.ViewComponents
{
/// <summary>
/// Discovers view components from a list of <see cref="ApplicationPart"/> instances.
/// </summary>
public class ViewComponentFeatureProvider : IApplicationFeatureProvider<ViewComponentFeature>
{
/// <inheritdoc />
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewComponentFeature feature)
{
if (parts == null)
{
throw new ArgumentNullException(nameof(parts));
}
if (feature == null)
{
throw new ArgumentNullException(nameof(feature));
}
foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(p => p.Types))
{
if (ViewComponentConventions.IsComponent(type) && ! feature.ViewComponents.Contains(type))
{
feature.ViewComponents.Add(type);
}
}
}
}
}

View File

@ -280,7 +280,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
} }
[Fact] [Fact]
public void GetFeature_OnlyRunsOnParts_ThatImplementIExportTypes() public void GetFeature_OnlyRunsOnParts_ThatImplementIApplicationPartTypeProvider()
{ {
// Arrange // Arrange
var otherPart = new Mock<ApplicationPart>(); var otherPart = new Mock<ApplicationPart>();

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -72,7 +73,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var manager = new ApplicationPartManager(); var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new AssemblyPart(startupAssembly)); manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
manager.FeatureProviders.Add(new ControllerFeatureProvider()); manager.FeatureProviders.Add(new ControllerFeatureProvider());
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
services.AddSingleton(manager); services.AddSingleton(manager);
} }

View File

@ -0,0 +1,35 @@
// 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.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class ViewComponentFromServicesTest : IClassFixture<MvcTestFixture<ControllersFromServicesWebSite.Startup>>
{
public ViewComponentFromServicesTest(MvcTestFixture<ControllersFromServicesWebSite.Startup> fixture)
{
Client = fixture.Client;
}
public HttpClient Client { get; }
[Fact]
public async Task ViewComponentsWithConstructorInjectionAreCreatedAndActivated()
{
// Arrange
var expected = "Value = 3";
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/another/InServicesViewComponent");
// Act
var response = await Client.SendAsync(request);
var responseText = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(expected, responseText);
}
}
}

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Cors.Internal; using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
@ -18,6 +19,7 @@ using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
@ -122,6 +124,48 @@ namespace Microsoft.AspNetCore.Mvc
} }
} }
[Fact]
public void AddMvcTwice_DoesNotAddApplicationFeatureProvidersTwice()
{
// Arrange
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.FeatureProviders.Count);
Assert.IsType<ControllerFeatureProvider>(manager.FeatureProviders[0]);
Assert.IsType<ViewComponentFeatureProvider>(manager.FeatureProviders[1]);
}
[Fact]
public void AddMvcCore_ReusesExistingApplicationPartManagerInstance_IfFoundOnServiceCollection()
{
// Arrange
var services = new ServiceCollection();
var manager = new ApplicationPartManager();
services.AddSingleton(manager);
// Act
services.AddMvc();
// Assert
var descriptor = Assert.Single(services, d => d.ServiceType == typeof(ApplicationPartManager));
Assert.Same(manager, descriptor.ImplementationInstance);
}
private IEnumerable<Type> SingleRegistrationServiceTypes private IEnumerable<Type> SingleRegistrationServiceTypes
{ {
get get

View File

@ -0,0 +1,95 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Xunit;
namespace Microsoft.Extensions.DependencyInjection
{
public class MvcViewFeaturesMvcBuilderExtensionsTest
{
[Fact]
public void AddViewComponentsAsServices_ReplacesViewComponentActivator()
{
// Arrange
var services = new ServiceCollection();
var builder = services
.AddMvc()
.ConfigureApplicationPartManager(manager =>
{
manager.ApplicationParts.Add(new TestApplicationPart());
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
});
// Act
builder.AddViewComponentsAsServices();
// Assert
var descriptor = Assert.Single(services.ToList(), d => d.ServiceType == typeof(IViewComponentActivator));
Assert.Equal(typeof(ServiceBasedViewComponentActivator), descriptor.ImplementationType);
}
[Fact]
public void AddViewComponentsAsServices_RegistersDiscoveredViewComponents()
{
// Arrange
var services = new ServiceCollection();
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new TestApplicationPart(
typeof(ConventionsViewComponent),
typeof(AttributeViewComponent)));
manager.FeatureProviders.Add(new TestProvider());
var builder = new MvcBuilder(services, manager);
// Act
builder.AddViewComponentsAsServices();
// Assert
var collection = services.ToList();
Assert.Equal(3, collection.Count);
Assert.Equal(typeof(ConventionsViewComponent), collection[0].ServiceType);
Assert.Equal(typeof(ConventionsViewComponent), collection[0].ImplementationType);
Assert.Equal(ServiceLifetime.Transient, collection[0].Lifetime);
Assert.Equal(typeof(AttributeViewComponent), collection[1].ServiceType);
Assert.Equal(typeof(AttributeViewComponent), collection[1].ImplementationType);
Assert.Equal(ServiceLifetime.Transient, collection[1].Lifetime);
Assert.Equal(typeof(IViewComponentActivator), collection[2].ServiceType);
Assert.Equal(typeof(ServiceBasedViewComponentActivator), collection[2].ImplementationType);
Assert.Equal(ServiceLifetime.Singleton, collection[2].Lifetime);
}
public class ConventionsViewComponent
{
public string Invoke() => "Hello world";
}
[ViewComponent(Name = "AttributesAreGreat")]
public class AttributeViewComponent
{
public Task<string> InvokeAsync() => Task.FromResult("Hello world");
}
private class TestProvider : IApplicationFeatureProvider<ViewComponentFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewComponentFeature feature)
{
foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(p => p.Types))
{
feature.ViewComponents.Add(type);
}
}
}
}
}

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

@ -6,47 +6,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewComponents namespace Microsoft.AspNetCore.Mvc.ViewComponents
{ {
public class DefaultViewComponentDescriptorProviderTest public class DefaultViewComponentDescriptorProviderTest
{ {
[Fact]
public void GetDescriptor_DefaultConventions()
{
// Arrange
var provider = CreateProvider(typeof(ConventionsViewComponent));
// Act
var descriptors = provider.GetViewComponents();
// Assert
var descriptor = Assert.Single(descriptors);
Assert.Same(typeof(ConventionsViewComponent).GetTypeInfo(), descriptor.TypeInfo);
Assert.Equal("Microsoft.AspNetCore.Mvc.ViewComponents.Conventions", descriptor.FullName);
Assert.Equal("Conventions", descriptor.ShortName);
Assert.Same(typeof(ConventionsViewComponent).GetMethod("Invoke"), descriptor.MethodInfo);
}
[Fact]
public void GetDescriptor_WithAttribute()
{
// Arrange
var provider = CreateProvider(typeof(AttributeViewComponent));
// Act
var descriptors = provider.GetViewComponents();
// Assert
var descriptor = Assert.Single(descriptors);
Assert.Equal(typeof(AttributeViewComponent).GetTypeInfo(), descriptor.TypeInfo);
Assert.Equal("AttributesAreGreat", descriptor.FullName);
Assert.Equal("AttributesAreGreat", descriptor.ShortName);
Assert.Same(typeof(AttributeViewComponent).GetMethod("InvokeAsync"), descriptor.MethodInfo);
}
[Theory] [Theory]
[InlineData(typeof(NoMethodsViewComponent))] [InlineData(typeof(NoMethodsViewComponent))]
[InlineData(typeof(NonPublicInvokeAsyncViewComponent))] [InlineData(typeof(NonPublicInvokeAsyncViewComponent))]
@ -120,17 +86,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
Assert.Equal(expected, ex.Message); Assert.Equal(expected, ex.Message);
} }
private class ConventionsViewComponent
{
public string Invoke() => "Hello world";
}
[ViewComponent(Name = "AttributesAreGreat")]
private class AttributeViewComponent
{
public Task<string> InvokeAsync() => Task.FromResult("Hello world");
}
private class MultipleInvokeViewComponent private class MultipleInvokeViewComponent
{ {
public IViewComponentResult Invoke() => null; public IViewComponentResult Invoke() => null;
@ -211,34 +166,27 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
private class FilteredViewComponentDescriptorProvider : DefaultViewComponentDescriptorProvider private class FilteredViewComponentDescriptorProvider : DefaultViewComponentDescriptorProvider
{ {
public FilteredViewComponentDescriptorProvider(params Type[] allowedTypes) public FilteredViewComponentDescriptorProvider(params Type[] allowedTypes)
: base(GetAssemblyProvider()) : base(GetApplicationPartManager(allowedTypes.Select(t => t.GetTypeInfo())))
{ {
AllowedTypes = allowedTypes;
} }
public Type[] AllowedTypes { get; } private static ApplicationPartManager GetApplicationPartManager(IEnumerable<TypeInfo> types)
protected override bool IsViewComponentType(TypeInfo typeInfo)
{ {
return AllowedTypes.Contains(typeInfo.AsType()); var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new TestApplicationPart(types));
manager.FeatureProviders.Add(new TestFeatureProvider());
return manager;
} }
// Need to override this since the default provider does not support private classes. private class TestFeatureProvider : IApplicationFeatureProvider<ViewComponentFeature>
protected override IEnumerable<TypeInfo> GetCandidateTypes()
{ {
return public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewComponentFeature feature)
GetAssemblyProvider() {
.CandidateAssemblies foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(p => p.Types))
.SelectMany(a => a.DefinedTypes); {
} feature.ViewComponents.Add(type);
}
private static IAssemblyProvider GetAssemblyProvider() }
{
var assemblyProvider = new StaticAssemblyProvider();
assemblyProvider.CandidateAssemblies.Add(
typeof(FilteredViewComponentDescriptorProvider).GetTypeInfo().Assembly);
return assemblyProvider;
} }
} }
} }

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewComponents namespace Microsoft.AspNetCore.Mvc.ViewComponents
@ -188,39 +188,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
public string Invoke() => "Hello"; public string Invoke() => "Hello";
} }
} }
// This will only consider types nested inside this class as ViewComponent classes // This will only consider types nested inside this class as ViewComponent classes
private class FilteredViewComponentDescriptorProvider : DefaultViewComponentDescriptorProvider private class FilteredViewComponentDescriptorProvider : DefaultViewComponentDescriptorProvider
{ {
public FilteredViewComponentDescriptorProvider() public FilteredViewComponentDescriptorProvider()
: base(GetAssemblyProvider()) : this(typeof(ViewComponentContainer).GetNestedTypes(bindingAttr: BindingFlags.Public))
{ {
AllowedTypes = typeof(ViewComponentContainer).GetNestedTypes(bindingAttr: BindingFlags.Public);
} }
public Type[] AllowedTypes { get; } public FilteredViewComponentDescriptorProvider(params Type[] allowedTypes)
: base(GetApplicationPartManager(allowedTypes.Select(t => t.GetTypeInfo())))
protected override bool IsViewComponentType(TypeInfo typeInfo)
{ {
return AllowedTypes.Contains(typeInfo.AsType());
} }
// Need to override this since the default provider does not support private classes. private static ApplicationPartManager GetApplicationPartManager(IEnumerable<TypeInfo> types)
protected override IEnumerable<TypeInfo> GetCandidateTypes()
{ {
return var manager = new ApplicationPartManager();
GetAssemblyProvider() manager.ApplicationParts.Add(new TestApplicationPart(types));
.CandidateAssemblies manager.FeatureProviders.Add(new TestFeatureProvider());
.SelectMany(a => a.DefinedTypes); return manager;
} }
private static IAssemblyProvider GetAssemblyProvider() private class TestFeatureProvider : IApplicationFeatureProvider<ViewComponentFeature>
{ {
var assemblyProvider = new StaticAssemblyProvider(); public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewComponentFeature feature)
assemblyProvider.CandidateAssemblies.Add( {
typeof(ViewComponentContainer).GetTypeInfo().Assembly); foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(p => p.Types))
{
return assemblyProvider; feature.ViewComponents.Add(type);
}
}
} }
} }
} }

View File

@ -0,0 +1,79 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures.ViewComponents.ViewComponentsFeatureTest;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.ViewComponents
{
public class ViewComponentFeatureProviderTest
{
[Fact]
public void GetDescriptor_DefaultConventions()
{
// Arrange
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new TestPart(typeof(ConventionsViewComponent)));
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
var feature = new ViewComponentFeature();
// Act
manager.PopulateFeature(feature);
// Assert
Assert.Equal(new[] { typeof(ConventionsViewComponent).GetTypeInfo() }, feature.ViewComponents.ToArray());
}
[Fact]
public void GetDescriptor_WithAttribute()
{
// Arrange
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new TestPart(typeof(AttributeViewComponent)));
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
var feature = new ViewComponentFeature();
// Act
manager.PopulateFeature(feature);
// Assert
Assert.Equal(new[] { typeof(AttributeViewComponent).GetTypeInfo() }, feature.ViewComponents.ToArray());
}
private class TestPart : ApplicationPart, IApplicationPartTypeProvider
{
public TestPart(params Type[] types)
{
Types = types.Select(t => t.GetTypeInfo());
}
public override string Name => "Test";
public IEnumerable<TypeInfo> Types { get; }
}
}
}
// These tests need to be public for the test to be valid
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.ViewComponents.ViewComponentsFeatureTest
{
public class ConventionsViewComponent
{
public string Invoke() => "Hello world";
}
[ViewComponent(Name = "AttributesAreGreat")]
public class AttributeViewComponent
{
public Task<string> InvokeAsync() => Task.FromResult("Hello world");
}
}

View File

@ -13,5 +13,11 @@ namespace ControllersFromServicesWebSite
{ {
return 1; return 1;
} }
[HttpGet("InServicesViewComponent")]
public IActionResult ViewComponentAction()
{
return ViewComponent("ComponentFromServices");
}
} }
} }

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc;
namespace ControllersFromServicesWebSite.Components
{
public class ComponentFromServicesViewComponent : ViewComponent
{
private readonly ValueService _value;
public ComponentFromServicesViewComponent(ValueService value)
{
_value = value;
}
public string Invoke()
{
return $"Value = {_value.Value}";
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using ControllersFromServicesClassLibrary; using ControllersFromServicesClassLibrary;
using ControllersFromServicesWebSite.Components;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -22,10 +23,14 @@ namespace ControllersFromServicesWebSite
.AddMvc() .AddMvc()
.ConfigureApplicationPartManager(manager => manager.ApplicationParts.Clear()) .ConfigureApplicationPartManager(manager => manager.ApplicationParts.Clear())
.AddApplicationPart(typeof(TimeScheduleController).GetTypeInfo().Assembly) .AddApplicationPart(typeof(TimeScheduleController).GetTypeInfo().Assembly)
.ConfigureApplicationPartManager(manager => manager.ApplicationParts.Add(new TypesPart(typeof(AnotherController)))) .ConfigureApplicationPartManager(manager => manager.ApplicationParts.Add(new TypesPart(
.AddControllersAsServices(); typeof(AnotherController),
typeof(ComponentFromServicesViewComponent))))
.AddControllersAsServices()
.AddViewComponentsAsServices();
services.AddTransient<QueryValueService>(); services.AddTransient<QueryValueService>();
services.AddTransient<ValueService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
} }

View File

@ -0,0 +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.
namespace ControllersFromServicesWebSite
{
public class ValueService
{
public int Value { get; } = 3;
}
}