diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcBuilderExtensions.cs index 237ff3cf29..3ba1879aa3 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcBuilderExtensions.cs @@ -2,7 +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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection { @@ -16,6 +19,7 @@ public static class MvcViewFeaturesMvcBuilderExtensions /// /// The . /// The which need to be configured. + /// The . public static IMvcBuilder AddViewOptions( this IMvcBuilder builder, Action setupAction) @@ -33,5 +37,30 @@ public static IMvcBuilder AddViewOptions( builder.Services.Configure(setupAction); return builder; } + + /// + /// Registers discovered view components as services in the . + /// + /// The . + /// The . + 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()); + + return builder; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index 178b9f6265..05c913720f 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -3,7 +3,9 @@ using System; using System.Buffers; +using System.Linq; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Rendering; @@ -26,10 +28,19 @@ public static IMvcCoreBuilder AddViews(this IMvcCoreBuilder builder) } builder.AddDataAnnotations(); + AddViewComponentApplicationPartsProviders(builder.PartManager); AddViewServices(builder.Services); return builder; } + private static void AddViewComponentApplicationPartsProviders(ApplicationPartManager manager) + { + if (!manager.FeatureProviders.OfType().Any()) + { + manager.FeatureProviders.Add(new ViewComponentFeatureProvider()); + } + } + public static IMvcCoreBuilder AddViews( this IMvcCoreBuilder builder, Action setupAction) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentDescriptorProvider.cs index ab54680b1a..8c3ee66855 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentDescriptorProvider.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Microsoft.AspNetCore.Mvc.ViewComponents @@ -18,64 +18,47 @@ public class DefaultViewComponentDescriptorProvider : IViewComponentDescriptorPr { private const string AsyncMethodName = "InvokeAsync"; private const string SyncMethodName = "Invoke"; - private readonly IAssemblyProvider _assemblyProvider; + private readonly ApplicationPartManager _partManager; /// /// Creates a new . /// - /// The . - public DefaultViewComponentDescriptorProvider(IAssemblyProvider assemblyProvider) + /// The . + public DefaultViewComponentDescriptorProvider(ApplicationPartManager partManager) { - _assemblyProvider = assemblyProvider; + if (partManager == null) + { + throw new ArgumentNullException(nameof(partManager)); + } + + _partManager = partManager; } /// public virtual IEnumerable GetViewComponents() { - var types = GetCandidateTypes(); - - return types - .Where(IsViewComponentType) - .Select(CreateDescriptor); + return GetCandidateTypes().Select(CreateDescriptor); } /// - /// Gets the candidate instances. The results of this will be provided to - /// for filtering. + /// Gets the candidate instances provided by the . /// /// A list of instances. protected virtual IEnumerable GetCandidateTypes() { - var assemblies = _assemblyProvider.CandidateAssemblies; - return assemblies.SelectMany(a => a.ExportedTypes).Select(t => t.GetTypeInfo()); - } - - /// - /// Determines whether or not the given is a view component class. - /// - /// The . - /// - /// true if represents a view component class, otherwise false. - /// - protected virtual bool IsViewComponentType(TypeInfo typeInfo) - { - if (typeInfo == null) - { - throw new ArgumentNullException(nameof(typeInfo)); - } - - return ViewComponentConventions.IsComponent(typeInfo); + var feature = new ViewComponentFeature(); + _partManager.PopulateFeature(feature); + return feature.ViewComponents; } private static ViewComponentDescriptor CreateDescriptor(TypeInfo typeInfo) { - var type = typeInfo.AsType(); var candidate = new ViewComponentDescriptor { FullName = ViewComponentConventions.GetComponentFullName(typeInfo), ShortName = ViewComponentConventions.GetComponentName(typeInfo), TypeInfo = typeInfo, - MethodInfo = FindMethod(type) + MethodInfo = FindMethod(typeInfo.AsType()) }; return candidate; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ServiceBasedViewComponentActivator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ServiceBasedViewComponentActivator.cs new file mode 100644 index 0000000000..deb26a9e44 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ServiceBasedViewComponentActivator.cs @@ -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 +{ + /// + /// A that retrieves view components as services from the request's + /// . + /// + public class ServiceBasedViewComponentActivator : IViewComponentActivator + { + /// + 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); + } + + /// + public virtual void Release(ViewComponentContext context, object viewComponent) + { + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentFeature.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentFeature.cs new file mode 100644 index 0000000000..6abb947f21 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentFeature.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.ViewComponents +{ + /// + /// The list of view component 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 ViewComponentFeature + { + /// + /// Gets the list of view component types in an MVC application. + /// + public IList ViewComponents { get; } = new List(); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentFeatureProvider.cs new file mode 100644 index 0000000000..eb953fb5af --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentFeatureProvider.cs @@ -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 +{ + /// + /// Discovers view components from a list of instances. + /// + public class ViewComponentFeatureProvider : IApplicationFeatureProvider + { + /// + public void PopulateFeature(IEnumerable 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().SelectMany(p => p.Types)) + { + if (ViewComponentConventions.IsComponent(type) && ! feature.ViewComponents.Contains(type)) + { + feature.ViewComponents.Add(type); + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/ControllerFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/ControllerFeatureProviderTest.cs index 69bd65a80e..d4579aad0b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/ControllerFeatureProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/ControllerFeatureProviderTest.cs @@ -280,7 +280,7 @@ public void AncestorTypeDoesNotHaveControllerAttribute_IsNotController() } [Fact] - public void GetFeature_OnlyRunsOnParts_ThatImplementIExportTypes() + public void GetFeature_OnlyRunsOnParts_ThatImplementIApplicationPartTypeProvider() { // Arrange var otherPart = new Mock(); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs index 33a74a5712..2d48dda86c 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; @@ -72,7 +73,10 @@ protected virtual void InitializeServices(IServiceCollection services) var manager = new ApplicationPartManager(); manager.ApplicationParts.Add(new AssemblyPart(startupAssembly)); + manager.FeatureProviders.Add(new ControllerFeatureProvider()); + manager.FeatureProviders.Add(new ViewComponentFeatureProvider()); + services.AddSingleton(manager); } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewComponentFromServicesTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewComponentFromServicesTests.cs new file mode 100644 index 0000000000..446f159a02 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewComponentFromServicesTests.cs @@ -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> + { + public ViewComponentFromServicesTest(MvcTestFixture 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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index ff4492cc08..32b3d70341 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Cors.Internal; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; @@ -18,6 +19,7 @@ using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Routing; @@ -122,6 +124,48 @@ public void AddMvcServicesTwice_DoesNotAddDuplicates() } } + [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(descriptor.ImplementationInstance); + + Assert.Equal(2, manager.FeatureProviders.Count); + Assert.IsType(manager.FeatureProviders[0]); + Assert.IsType(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 SingleRegistrationServiceTypes { get diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/DependencyInjection/MvcViewFeaturesMvcBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/DependencyInjection/MvcViewFeaturesMvcBuilderExtensionsTest.cs new file mode 100644 index 0000000000..37a887ce36 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/DependencyInjection/MvcViewFeaturesMvcBuilderExtensionsTest.cs @@ -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 InvokeAsync() => Task.FromResult("Hello world"); + } + + private class TestProvider : IApplicationFeatureProvider + { + public void PopulateFeature(IEnumerable parts, ViewComponentFeature feature) + { + foreach (var type in parts.OfType().SelectMany(p => p.Types)) + { + feature.ViewComponents.Add(type); + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/TestApplicationPart.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/TestApplicationPart.cs new file mode 100644 index 0000000000..5f67622424 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.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.ViewFeatures.Test/ViewComponents/DefaultViewComponentDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentDescriptorProviderTest.cs index 273de6c2ce..370d027750 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentDescriptorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentDescriptorProviderTest.cs @@ -6,47 +6,13 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewComponents { 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] [InlineData(typeof(NoMethodsViewComponent))] [InlineData(typeof(NonPublicInvokeAsyncViewComponent))] @@ -120,17 +86,6 @@ public void GetViewComponents_ThrowsIfInvokeIsVoidReturning() Assert.Equal(expected, ex.Message); } - private class ConventionsViewComponent - { - public string Invoke() => "Hello world"; - } - - [ViewComponent(Name = "AttributesAreGreat")] - private class AttributeViewComponent - { - public Task InvokeAsync() => Task.FromResult("Hello world"); - } - private class MultipleInvokeViewComponent { public IViewComponentResult Invoke() => null; @@ -211,34 +166,27 @@ private DefaultViewComponentDescriptorProvider CreateProvider(Type componentType private class FilteredViewComponentDescriptorProvider : DefaultViewComponentDescriptorProvider { public FilteredViewComponentDescriptorProvider(params Type[] allowedTypes) - : base(GetAssemblyProvider()) - { - AllowedTypes = allowedTypes; - } - - public Type[] AllowedTypes { get; } - - protected override bool IsViewComponentType(TypeInfo typeInfo) + : base(GetApplicationPartManager(allowedTypes.Select(t => t.GetTypeInfo()))) { - return AllowedTypes.Contains(typeInfo.AsType()); } - // Need to override this since the default provider does not support private classes. - protected override IEnumerable GetCandidateTypes() + private static ApplicationPartManager GetApplicationPartManager(IEnumerable types) { - return - GetAssemblyProvider() - .CandidateAssemblies - .SelectMany(a => a.DefinedTypes); + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new TestApplicationPart(types)); + manager.FeatureProviders.Add(new TestFeatureProvider()); + return manager; } - private static IAssemblyProvider GetAssemblyProvider() + private class TestFeatureProvider : IApplicationFeatureProvider { - var assemblyProvider = new StaticAssemblyProvider(); - assemblyProvider.CandidateAssemblies.Add( - typeof(FilteredViewComponentDescriptorProvider).GetTypeInfo().Assembly); - - return assemblyProvider; + public void PopulateFeature(IEnumerable parts, ViewComponentFeature feature) + { + foreach (var type in parts.OfType().SelectMany(p => p.Types)) + { + feature.ViewComponents.Add(type); + } + } } } } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentSelectorTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentSelectorTest.cs index ce84da9c9b..5a36085676 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentSelectorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentSelectorTest.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewComponents @@ -188,39 +188,36 @@ public class FullNameInAttribute public string Invoke() => "Hello"; } } - // This will only consider types nested inside this class as ViewComponent classes private class FilteredViewComponentDescriptorProvider : DefaultViewComponentDescriptorProvider { public FilteredViewComponentDescriptorProvider() - : base(GetAssemblyProvider()) + : this(typeof(ViewComponentContainer).GetNestedTypes(bindingAttr: BindingFlags.Public)) { - AllowedTypes = typeof(ViewComponentContainer).GetNestedTypes(bindingAttr: BindingFlags.Public); } - public Type[] AllowedTypes { get; } - - protected override bool IsViewComponentType(TypeInfo typeInfo) + public FilteredViewComponentDescriptorProvider(params Type[] allowedTypes) + : base(GetApplicationPartManager(allowedTypes.Select(t => t.GetTypeInfo()))) { - return AllowedTypes.Contains(typeInfo.AsType()); } - // Need to override this since the default provider does not support private classes. - protected override IEnumerable GetCandidateTypes() + private static ApplicationPartManager GetApplicationPartManager(IEnumerable types) { - return - GetAssemblyProvider() - .CandidateAssemblies - .SelectMany(a => a.DefinedTypes); + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new TestApplicationPart(types)); + manager.FeatureProviders.Add(new TestFeatureProvider()); + return manager; } - private static IAssemblyProvider GetAssemblyProvider() + private class TestFeatureProvider : IApplicationFeatureProvider { - var assemblyProvider = new StaticAssemblyProvider(); - assemblyProvider.CandidateAssemblies.Add( - typeof(ViewComponentContainer).GetTypeInfo().Assembly); - - return assemblyProvider; + public void PopulateFeature(IEnumerable parts, ViewComponentFeature feature) + { + foreach (var type in parts.OfType().SelectMany(p => p.Types)) + { + feature.ViewComponents.Add(type); + } + } } } } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/ViewComponentFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/ViewComponentFeatureProviderTest.cs new file mode 100644 index 0000000000..6a075bf5d9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/ViewComponentFeatureProviderTest.cs @@ -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 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 InvokeAsync() => Task.FromResult("Hello world"); + } +} diff --git a/test/WebSites/ControllersFromServicesWebSite/AnotherController.cs b/test/WebSites/ControllersFromServicesWebSite/AnotherController.cs index 9277327a06..05d7f60732 100644 --- a/test/WebSites/ControllersFromServicesWebSite/AnotherController.cs +++ b/test/WebSites/ControllersFromServicesWebSite/AnotherController.cs @@ -13,5 +13,11 @@ public int Get() { return 1; } + + [HttpGet("InServicesViewComponent")] + public IActionResult ViewComponentAction() + { + return ViewComponent("ComponentFromServices"); + } } } diff --git a/test/WebSites/ControllersFromServicesWebSite/Components/ComponentFromServicesViewComponent.cs b/test/WebSites/ControllersFromServicesWebSite/Components/ComponentFromServicesViewComponent.cs new file mode 100644 index 0000000000..54901d4fbb --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/Components/ComponentFromServicesViewComponent.cs @@ -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}"; + } + } +} diff --git a/test/WebSites/ControllersFromServicesWebSite/Startup.cs b/test/WebSites/ControllersFromServicesWebSite/Startup.cs index 7339ad8400..74e5693442 100644 --- a/test/WebSites/ControllersFromServicesWebSite/Startup.cs +++ b/test/WebSites/ControllersFromServicesWebSite/Startup.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using ControllersFromServicesClassLibrary; +using ControllersFromServicesWebSite.Components; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -22,10 +23,14 @@ public void ConfigureServices(IServiceCollection services) .AddMvc() .ConfigureApplicationPartManager(manager => manager.ApplicationParts.Clear()) .AddApplicationPart(typeof(TimeScheduleController).GetTypeInfo().Assembly) - .ConfigureApplicationPartManager(manager => manager.ApplicationParts.Add(new TypesPart(typeof(AnotherController)))) - .AddControllersAsServices(); + .ConfigureApplicationPartManager(manager => manager.ApplicationParts.Add(new TypesPart( + typeof(AnotherController), + typeof(ComponentFromServicesViewComponent)))) + .AddControllersAsServices() + .AddViewComponentsAsServices(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); } diff --git a/test/WebSites/ControllersFromServicesWebSite/ValueService.cs b/test/WebSites/ControllersFromServicesWebSite/ValueService.cs new file mode 100644 index 0000000000..f08089282b --- /dev/null +++ b/test/WebSites/ControllersFromServicesWebSite/ValueService.cs @@ -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; + } +}