Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 5100671

Browse files
committed
[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.
1 parent ec9dfe4 commit 5100671

File tree

19 files changed

+527
-122
lines changed

19 files changed

+527
-122
lines changed

src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcBuilderExtensions.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Linq;
56
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.AspNetCore.Mvc.ViewComponents;
8+
using Microsoft.Extensions.DependencyInjection.Extensions;
69

710
namespace Microsoft.Extensions.DependencyInjection
811
{
@@ -16,6 +19,7 @@ public static class MvcViewFeaturesMvcBuilderExtensions
1619
/// </summary>
1720
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
1821
/// <param name="setupAction">The <see cref="MvcViewOptions"/> which need to be configured.</param>
22+
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
1923
public static IMvcBuilder AddViewOptions(
2024
this IMvcBuilder builder,
2125
Action<MvcViewOptions> setupAction)
@@ -33,5 +37,30 @@ public static IMvcBuilder AddViewOptions(
3337
builder.Services.Configure(setupAction);
3438
return builder;
3539
}
40+
41+
/// <summary>
42+
/// Registers discovered view components as services in the <see cref="IServiceCollection"/>.
43+
/// </summary>
44+
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
45+
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
46+
public static IMvcBuilder AddViewComponentsAsServices(this IMvcBuilder builder)
47+
{
48+
if (builder == null)
49+
{
50+
throw new ArgumentNullException(nameof(builder));
51+
}
52+
53+
var feature = new ViewComponentFeature();
54+
builder.PartManager.PopulateFeature(feature);
55+
56+
foreach (var viewComponent in feature.ViewComponents.Select(vc => vc.AsType()))
57+
{
58+
builder.Services.TryAddTransient(viewComponent, viewComponent);
59+
}
60+
61+
builder.Services.Replace(ServiceDescriptor.Singleton<IViewComponentActivator, ServiceBasedViewComponentActivator>());
62+
63+
return builder;
64+
}
3665
}
3766
}

src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
using System;
55
using System.Buffers;
6+
using System.Linq;
67
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.AspNetCore.Mvc.ApplicationParts;
79
using Microsoft.AspNetCore.Mvc.Controllers;
810
using Microsoft.AspNetCore.Mvc.Formatters;
911
using Microsoft.AspNetCore.Mvc.Rendering;
@@ -26,10 +28,19 @@ public static IMvcCoreBuilder AddViews(this IMvcCoreBuilder builder)
2628
}
2729

2830
builder.AddDataAnnotations();
31+
AddViewComponentApplicationPartsProviders(builder.PartManager);
2932
AddViewServices(builder.Services);
3033
return builder;
3134
}
3235

36+
private static void AddViewComponentApplicationPartsProviders(ApplicationPartManager manager)
37+
{
38+
if (!manager.FeatureProviders.OfType<ViewComponentFeatureProvider>().Any())
39+
{
40+
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
41+
}
42+
}
43+
3344
public static IMvcCoreBuilder AddViews(
3445
this IMvcCoreBuilder builder,
3546
Action<MvcViewOptions> setupAction)

src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentDescriptorProvider.cs

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using System.Linq;
77
using System.Reflection;
88
using System.Threading.Tasks;
9-
using Microsoft.AspNetCore.Mvc.Infrastructure;
9+
using Microsoft.AspNetCore.Mvc.ApplicationParts;
1010
using Microsoft.AspNetCore.Mvc.ViewFeatures;
1111

1212
namespace Microsoft.AspNetCore.Mvc.ViewComponents
@@ -18,64 +18,47 @@ public class DefaultViewComponentDescriptorProvider : IViewComponentDescriptorPr
1818
{
1919
private const string AsyncMethodName = "InvokeAsync";
2020
private const string SyncMethodName = "Invoke";
21-
private readonly IAssemblyProvider _assemblyProvider;
21+
private readonly ApplicationPartManager _partManager;
2222

2323
/// <summary>
2424
/// Creates a new <see cref="DefaultViewComponentDescriptorProvider"/>.
2525
/// </summary>
26-
/// <param name="assemblyProvider">The <see cref="IAssemblyProvider"/>.</param>
27-
public DefaultViewComponentDescriptorProvider(IAssemblyProvider assemblyProvider)
26+
/// <param name="partManager">The <see cref="ApplicationPartManager"/>.</param>
27+
public DefaultViewComponentDescriptorProvider(ApplicationPartManager partManager)
2828
{
29-
_assemblyProvider = assemblyProvider;
29+
if (partManager == null)
30+
{
31+
throw new ArgumentNullException(nameof(partManager));
32+
}
33+
34+
_partManager = partManager;
3035
}
3136

3237
/// <inheritdoc />
3338
public virtual IEnumerable<ViewComponentDescriptor> GetViewComponents()
3439
{
35-
var types = GetCandidateTypes();
36-
37-
return types
38-
.Where(IsViewComponentType)
39-
.Select(CreateDescriptor);
40+
return GetCandidateTypes().Select(CreateDescriptor);
4041
}
4142

4243
/// <summary>
43-
/// Gets the candidate <see cref="TypeInfo"/> instances. The results of this will be provided to
44-
/// <see cref="IsViewComponentType"/> for filtering.
44+
/// Gets the candidate <see cref="TypeInfo"/> instances provided by the <see cref="ApplicationPartManager"/>.
4545
/// </summary>
4646
/// <returns>A list of <see cref="TypeInfo"/> instances.</returns>
4747
protected virtual IEnumerable<TypeInfo> GetCandidateTypes()
4848
{
49-
var assemblies = _assemblyProvider.CandidateAssemblies;
50-
return assemblies.SelectMany(a => a.ExportedTypes).Select(t => t.GetTypeInfo());
51-
}
52-
53-
/// <summary>
54-
/// Determines whether or not the given <see cref="TypeInfo"/> is a view component class.
55-
/// </summary>
56-
/// <param name="typeInfo">The <see cref="TypeInfo"/>.</param>
57-
/// <returns>
58-
/// <c>true</c> if <paramref name="typeInfo"/>represents a view component class, otherwise <c>false</c>.
59-
/// </returns>
60-
protected virtual bool IsViewComponentType(TypeInfo typeInfo)
61-
{
62-
if (typeInfo == null)
63-
{
64-
throw new ArgumentNullException(nameof(typeInfo));
65-
}
66-
67-
return ViewComponentConventions.IsComponent(typeInfo);
49+
var feature = new ViewComponentFeature();
50+
_partManager.PopulateFeature(feature);
51+
return feature.ViewComponents;
6852
}
6953

7054
private static ViewComponentDescriptor CreateDescriptor(TypeInfo typeInfo)
7155
{
72-
var type = typeInfo.AsType();
7356
var candidate = new ViewComponentDescriptor
7457
{
7558
FullName = ViewComponentConventions.GetComponentFullName(typeInfo),
7659
ShortName = ViewComponentConventions.GetComponentName(typeInfo),
7760
TypeInfo = typeInfo,
78-
MethodInfo = FindMethod(type)
61+
MethodInfo = FindMethod(typeInfo.AsType())
7962
};
8063

8164
return candidate;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace Microsoft.AspNetCore.Mvc.ViewComponents
8+
{
9+
/// <summary>
10+
/// A <see cref="IViewComponentActivator"/> that retrieves view components as services from the request's
11+
/// <see cref="IServiceProvider"/>.
12+
/// </summary>
13+
public class ServiceBasedViewComponentActivator : IViewComponentActivator
14+
{
15+
/// <inheritdoc />
16+
public object Create(ViewComponentContext context)
17+
{
18+
if (context == null)
19+
{
20+
throw new ArgumentNullException(nameof(context));
21+
}
22+
23+
var viewComponentType = context.ViewComponentDescriptor.TypeInfo.AsType();
24+
25+
return context.ViewContext.HttpContext.RequestServices.GetRequiredService(viewComponentType);
26+
}
27+
28+
/// <inheritdoc />
29+
public virtual void Release(ViewComponentContext context, object viewComponent)
30+
{
31+
}
32+
}
33+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Reflection;
6+
using Microsoft.AspNetCore.Mvc.ApplicationParts;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace Microsoft.AspNetCore.Mvc.ViewComponents
10+
{
11+
/// <summary>
12+
/// The list of view component types in an MVC application.The <see cref="ViewComponentFeature"/> can be populated
13+
/// using the <see cref="ApplicationPartManager"/> that is available during startup at <see cref="IMvcBuilder.PartManager"/>
14+
/// and <see cref="IMvcCoreBuilder.PartManager"/> or at a later stage by requiring the <see cref="ApplicationPartManager"/>
15+
/// as a dependency in a component.
16+
/// </summary>
17+
public class ViewComponentFeature
18+
{
19+
/// <summary>
20+
/// Gets the list of view component types in an MVC application.
21+
/// </summary>
22+
public IList<TypeInfo> ViewComponents { get; } = new List<TypeInfo>();
23+
}
24+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Microsoft.AspNetCore.Mvc.ApplicationParts;
8+
9+
namespace Microsoft.AspNetCore.Mvc.ViewComponents
10+
{
11+
/// <summary>
12+
/// Discovers view components from a list of <see cref="ApplicationPart"/> instances.
13+
/// </summary>
14+
public class ViewComponentFeatureProvider : IApplicationFeatureProvider<ViewComponentFeature>
15+
{
16+
/// <inheritdoc />
17+
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewComponentFeature feature)
18+
{
19+
if (parts == null)
20+
{
21+
throw new ArgumentNullException(nameof(parts));
22+
}
23+
24+
if (feature == null)
25+
{
26+
throw new ArgumentNullException(nameof(feature));
27+
}
28+
29+
foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(p => p.Types))
30+
{
31+
if (ViewComponentConventions.IsComponent(type) && ! feature.ViewComponents.Contains(type))
32+
{
33+
feature.ViewComponents.Add(type);
34+
}
35+
}
36+
}
37+
}
38+
}

test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/ControllerFeatureProviderTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ public void AncestorTypeDoesNotHaveControllerAttribute_IsNotController()
280280
}
281281

282282
[Fact]
283-
public void GetFeature_OnlyRunsOnParts_ThatImplementIExportTypes()
283+
public void GetFeature_OnlyRunsOnParts_ThatImplementIApplicationPartTypeProvider()
284284
{
285285
// Arrange
286286
var otherPart = new Mock<ApplicationPart>();

test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Mvc.ApplicationParts;
1111
using Microsoft.AspNetCore.Mvc.Controllers;
1212
using Microsoft.AspNetCore.Mvc.Infrastructure;
13+
using Microsoft.AspNetCore.Mvc.ViewComponents;
1314
using Microsoft.AspNetCore.TestHost;
1415
using Microsoft.AspNetCore.Testing;
1516
using Microsoft.Extensions.DependencyInjection;
@@ -72,7 +73,10 @@ protected virtual void InitializeServices(IServiceCollection services)
7273

7374
var manager = new ApplicationPartManager();
7475
manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
76+
7577
manager.FeatureProviders.Add(new ControllerFeatureProvider());
78+
manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
79+
7680
services.AddSingleton(manager);
7781
}
7882

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Net.Http;
6+
using System.Threading.Tasks;
7+
using Xunit;
8+
9+
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
10+
{
11+
public class ViewComponentFromServicesTest : IClassFixture<MvcTestFixture<ControllersFromServicesWebSite.Startup>>
12+
{
13+
public ViewComponentFromServicesTest(MvcTestFixture<ControllersFromServicesWebSite.Startup> fixture)
14+
{
15+
Client = fixture.Client;
16+
}
17+
18+
public HttpClient Client { get; }
19+
20+
[Fact]
21+
public async Task ViewComponentsWithConstructorInjectionAreCreatedAndActivated()
22+
{
23+
// Arrange
24+
var expected = "Value = 3";
25+
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/another/InServicesViewComponent");
26+
27+
// Act
28+
var response = await Client.SendAsync(request);
29+
var responseText = await response.Content.ReadAsStringAsync();
30+
31+
// Assert
32+
Assert.Equal(expected, responseText);
33+
}
34+
}
35+
}

test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Mvc.ActionConstraints;
1111
using Microsoft.AspNetCore.Mvc.ApiExplorer;
1212
using Microsoft.AspNetCore.Mvc.ApplicationModels;
13+
using Microsoft.AspNetCore.Mvc.ApplicationParts;
1314
using Microsoft.AspNetCore.Mvc.Controllers;
1415
using Microsoft.AspNetCore.Mvc.Cors.Internal;
1516
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
@@ -18,6 +19,7 @@
1819
using Microsoft.AspNetCore.Mvc.Internal;
1920
using Microsoft.AspNetCore.Mvc.Razor;
2021
using Microsoft.AspNetCore.Mvc.Razor.Internal;
22+
using Microsoft.AspNetCore.Mvc.ViewComponents;
2123
using Microsoft.AspNetCore.Mvc.ViewFeatures;
2224
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
2325
using Microsoft.AspNetCore.Routing;
@@ -122,6 +124,48 @@ public void AddMvcServicesTwice_DoesNotAddDuplicates()
122124
}
123125
}
124126

127+
[Fact]
128+
public void AddMvcTwice_DoesNotAddApplicationFeatureProvidersTwice()
129+
{
130+
// Arrange
131+
var services = new ServiceCollection();
132+
var providers = new IApplicationFeatureProvider[]
133+
{
134+
new ControllerFeatureProvider(),
135+
new ViewComponentFeatureProvider()
136+
};
137+
138+
// Act
139+
services.AddMvc();
140+
services.AddMvc();
141+
142+
// Assert
143+
var descriptor = Assert.Single(services, d => d.ServiceType == typeof(ApplicationPartManager));
144+
Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime);
145+
Assert.NotNull(descriptor.ImplementationInstance);
146+
var manager = Assert.IsType<ApplicationPartManager>(descriptor.ImplementationInstance);
147+
148+
Assert.Equal(2, manager.FeatureProviders.Count);
149+
Assert.IsType<ControllerFeatureProvider>(manager.FeatureProviders[0]);
150+
Assert.IsType<ViewComponentFeatureProvider>(manager.FeatureProviders[1]);
151+
}
152+
153+
[Fact]
154+
public void AddMvcCore_ReusesExistingApplicationPartManagerInstance_IfFoundOnServiceCollection()
155+
{
156+
// Arrange
157+
var services = new ServiceCollection();
158+
var manager = new ApplicationPartManager();
159+
services.AddSingleton(manager);
160+
161+
// Act
162+
services.AddMvc();
163+
164+
// Assert
165+
var descriptor = Assert.Single(services, d => d.ServiceType == typeof(ApplicationPartManager));
166+
Assert.Same(manager, descriptor.ImplementationInstance);
167+
}
168+
125169
private IEnumerable<Type> SingleRegistrationServiceTypes
126170
{
127171
get

0 commit comments

Comments
 (0)