From 5246125cb71851b70786fafd88996eb500bab455 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Thu, 24 Mar 2016 13:05:36 -0700 Subject: [PATCH] [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. --- .../MvcViewFeaturesMvcBuilderExtensions.cs | 29 ++++++ ...MvcViewFeaturesMvcCoreBuilderExtensions.cs | 11 +++ .../DefaultViewComponentDescriptorProvider.cs | 49 ++++------ .../ServiceBasedViewComponentActivator.cs | 33 +++++++ .../ViewComponents/ViewComponentFeature.cs | 24 +++++ .../ViewComponentFeatureProvider.cs | 38 ++++++++ .../ControllerFeatureProviderTest.cs | 2 +- .../MvcTestFixture.cs | 4 + .../ViewComponentFromServicesTests.cs | 35 +++++++ .../MvcServiceCollectionExtensionsTest.cs | 44 +++++++++ ...MvcViewFeaturesMvcBuilderExtensionsTest.cs | 95 +++++++++++++++++++ .../TestApplicationPart.cs | 44 +++++++++ ...aultViewComponentDescriptorProviderTest.cs | 82 +++------------- .../DefaultViewComponentSelectorTest.cs | 37 ++++---- .../ViewComponentFeatureProviderTest.cs | 79 +++++++++++++++ .../AnotherController.cs | 6 ++ .../ComponentFromServicesViewComponent.cs | 22 +++++ .../ControllersFromServicesWebSite/Startup.cs | 9 +- .../ValueService.cs | 10 ++ 19 files changed, 530 insertions(+), 123 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ServiceBasedViewComponentActivator.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentFeature.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentFeatureProvider.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewComponentFromServicesTests.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/DependencyInjection/MvcViewFeaturesMvcBuilderExtensionsTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/TestApplicationPart.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/ViewComponentFeatureProviderTest.cs create mode 100644 test/WebSites/ControllersFromServicesWebSite/Components/ComponentFromServicesViewComponent.cs create mode 100644 test/WebSites/ControllersFromServicesWebSite/ValueService.cs 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; + } +}