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

Commit c1f4bfe

Browse files
committed
[Fixes #2648]: Fix registration of MVC services
1 parent 2212bfa commit c1f4bfe

File tree

3 files changed

+167
-74
lines changed

3 files changed

+167
-74
lines changed

src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs

Lines changed: 82 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,79 @@ public static IServiceCollection AddMvc([NotNull] this IServiceCollection servic
3434
{
3535
ConfigureDefaultServices(services);
3636

37+
AddMvcServices(services);
38+
39+
return services;
40+
}
41+
42+
/// <summary>
43+
/// Configures a set of <see cref="MvcOptions"/> for the application.
44+
/// </summary>
45+
/// <param name="services">The services available in the application.</param>
46+
/// <param name="setupAction">The <see cref="MvcOptions"/> which need to be configured.</param>
47+
public static void ConfigureMvc(
48+
[NotNull] this IServiceCollection services,
49+
[NotNull] Action<MvcOptions> setupAction)
50+
{
51+
services.Configure(setupAction);
52+
}
53+
54+
/// <summary>
55+
/// Register the specified <paramref name="controllerTypes"/> as services and as a source for controller
56+
/// discovery.
57+
/// </summary>
58+
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
59+
/// <param name="controllerTypes">A sequence of controller <see cref="Type"/>s to register in the
60+
/// <paramref name="services"/> and used for controller discovery.</param>
61+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
62+
public static IServiceCollection WithControllersAsServices(
63+
[NotNull] this IServiceCollection services,
64+
[NotNull] IEnumerable<Type> controllerTypes)
65+
{
66+
var controllerTypeProvider = new FixedSetControllerTypeProvider();
67+
foreach (var type in controllerTypes)
68+
{
69+
services.TryAdd(ServiceDescriptor.Transient(type, type));
70+
controllerTypeProvider.ControllerTypes.Add(type.GetTypeInfo());
71+
}
72+
73+
services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
74+
services.Replace(ServiceDescriptor.Instance<IControllerTypeProvider>(controllerTypeProvider));
75+
76+
return services;
77+
}
78+
79+
/// <summary>
80+
/// Registers controller types from the specified <paramref name="assemblies"/> as services and as a source
81+
/// for controller discovery.
82+
/// </summary>
83+
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
84+
/// <param name="controllerAssemblies">Assemblies to scan.</param>
85+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
86+
public static IServiceCollection WithControllersAsServices(
87+
[NotNull] this IServiceCollection services,
88+
[NotNull] IEnumerable<Assembly> controllerAssemblies)
89+
{
90+
var assemblyProvider = new FixedSetAssemblyProvider();
91+
foreach (var assembly in controllerAssemblies)
92+
{
93+
assemblyProvider.CandidateAssemblies.Add(assembly);
94+
}
95+
96+
var controllerTypeProvider = new DefaultControllerTypeProvider(assemblyProvider);
97+
var controllerTypes = controllerTypeProvider.ControllerTypes;
98+
99+
return WithControllersAsServices(services, controllerTypes.Select(type => type.AsType()));
100+
}
101+
102+
// To enable unit testing
103+
internal static void AddMvcServices(IServiceCollection services)
104+
{
37105
// Options and core services.
38-
services.TryAdd(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcOptionsSetup>());
39-
services.TryAdd(
40-
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>());
106+
// multiple registration service
107+
services.AddTransient<IConfigureOptions<MvcOptions>, MvcOptionsSetup>();
108+
// multiple registration service
109+
services.AddTransient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>();
41110

42111
services.TryAdd(ServiceDescriptor.Transient<IAssemblyProvider, DefaultAssemblyProvider>());
43112

@@ -62,7 +131,8 @@ public static IServiceCollection AddMvc([NotNull] this IServiceCollection servic
62131

63132
// This provider needs access to the per-request services, but might be used many times for a given
64133
// request.
65-
services.TryAdd(ServiceDescriptor.Transient<IActionConstraintProvider, DefaultActionConstraintProvider>());
134+
// multiple registration service
135+
services.AddTransient<IActionConstraintProvider, DefaultActionConstraintProvider>();
66136

67137
services.TryAdd(ServiceDescriptor
68138
.Singleton<IActionSelectorDecisionTreeProvider, ActionSelectorDecisionTreeProvider>());
@@ -76,18 +146,20 @@ public static IServiceCollection AddMvc([NotNull] this IServiceCollection servic
76146
return new DefaultObjectValidator(options.ValidationExcludeFilters, modelMetadataProvider);
77147
}));
78148

79-
services.TryAdd(ServiceDescriptor
80-
.Transient<IActionDescriptorProvider, ControllerActionDescriptorProvider>());
149+
// multiple registration service
150+
services.AddTransient<IActionDescriptorProvider, ControllerActionDescriptorProvider>();
81151

82-
services.TryAdd(ServiceDescriptor.Transient<IActionInvokerProvider, ControllerActionInvokerProvider>());
152+
// multiple registration service
153+
services.AddTransient<IActionInvokerProvider, ControllerActionInvokerProvider>();
83154

84155
services.TryAdd(ServiceDescriptor
85156
.Singleton<IActionDescriptorsCollectionProvider, DefaultActionDescriptorsCollectionProvider>());
86157

87158
// The IGlobalFilterProvider is used to build the action descriptors (likely once) and so should
88159
// remain transient to avoid keeping it in memory.
89160
services.TryAdd(ServiceDescriptor.Transient<IGlobalFilterProvider, DefaultGlobalFilterProvider>());
90-
services.TryAdd(ServiceDescriptor.Transient<IFilterProvider, DefaultFilterProvider>());
161+
// multiple registration service
162+
services.AddTransient<IFilterProvider, DefaultFilterProvider>();
91163

92164
services.TryAdd(ServiceDescriptor.Transient<FormatFilter, FormatFilter>());
93165
services.TryAdd(ServiceDescriptor.Transient<CorsAuthorizationFilter, CorsAuthorizationFilter>());
@@ -184,74 +256,13 @@ public static IServiceCollection AddMvc([NotNull] this IServiceCollection servic
184256
// Api Description
185257
services.TryAdd(ServiceDescriptor
186258
.Singleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>());
187-
services.TryAdd(ServiceDescriptor.Transient<IApiDescriptionProvider, DefaultApiDescriptionProvider>());
259+
// multiple registration service
260+
services.AddTransient<IApiDescriptionProvider, DefaultApiDescriptionProvider>();
188261

189262
// Temp Data
190263
services.TryAdd(ServiceDescriptor.Scoped<ITempDataDictionary, TempDataDictionary>());
191264
// This does caching so it should stay singleton
192265
services.TryAdd(ServiceDescriptor.Singleton<ITempDataProvider, SessionStateTempDataProvider>());
193-
194-
return services;
195-
}
196-
197-
/// <summary>
198-
/// Configures a set of <see cref="MvcOptions"/> for the application.
199-
/// </summary>
200-
/// <param name="services">The services available in the application.</param>
201-
/// <param name="setupAction">The <see cref="MvcOptions"/> which need to be configured.</param>
202-
public static void ConfigureMvc(
203-
[NotNull] this IServiceCollection services,
204-
[NotNull] Action<MvcOptions> setupAction)
205-
{
206-
services.Configure(setupAction);
207-
}
208-
209-
/// <summary>
210-
/// Register the specified <paramref name="controllerTypes"/> as services and as a source for controller
211-
/// discovery.
212-
/// </summary>
213-
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
214-
/// <param name="controllerTypes">A sequence of controller <see cref="Type"/>s to register in the
215-
/// <paramref name="services"/> and used for controller discovery.</param>
216-
/// <returns>The <see cref="IServiceCollection"/>.</returns>
217-
public static IServiceCollection WithControllersAsServices(
218-
[NotNull] this IServiceCollection services,
219-
[NotNull] IEnumerable<Type> controllerTypes)
220-
{
221-
var controllerTypeProvider = new FixedSetControllerTypeProvider();
222-
foreach (var type in controllerTypes)
223-
{
224-
services.TryAdd(ServiceDescriptor.Transient(type, type));
225-
controllerTypeProvider.ControllerTypes.Add(type.GetTypeInfo());
226-
}
227-
228-
services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
229-
services.Replace(ServiceDescriptor.Instance<IControllerTypeProvider>(controllerTypeProvider));
230-
231-
return services;
232-
}
233-
234-
/// <summary>
235-
/// Registers controller types from the specified <paramref name="assemblies"/> as services and as a source
236-
/// for controller discovery.
237-
/// </summary>
238-
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
239-
/// <param name="controllerAssemblies">Assemblies to scan.</param>
240-
/// <returns>The <see cref="IServiceCollection"/>.</returns>
241-
public static IServiceCollection WithControllersAsServices(
242-
[NotNull] this IServiceCollection services,
243-
[NotNull] IEnumerable<Assembly> controllerAssemblies)
244-
{
245-
var assemblyProvider = new FixedSetAssemblyProvider();
246-
foreach (var assembly in controllerAssemblies)
247-
{
248-
assemblyProvider.CandidateAssemblies.Add(assembly);
249-
}
250-
251-
var controllerTypeProvider = new DefaultControllerTypeProvider(assemblyProvider);
252-
var controllerTypes = controllerTypeProvider.ControllerTypes;
253-
254-
return WithControllersAsServices(services, controllerTypes.Select(type => type.AsType()));
255266
}
256267

257268
private static void ConfigureDefaultServices(IServiceCollection services)

src/Microsoft.AspNet.Mvc/Properties/AssemblyInfo.cs

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

44
using System.Reflection;
5+
using System.Runtime.CompilerServices;
56

6-
[assembly: AssemblyMetadata("Serviceable", "True")]
7+
[assembly: AssemblyMetadata("Serviceable", "True")]
8+
[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Test")]

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

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Reflection;
8+
using Microsoft.AspNet.Mvc.ActionConstraints;
9+
using Microsoft.AspNet.Mvc.ApiExplorer;
10+
using Microsoft.AspNet.Mvc.Core;
811
using Microsoft.AspNet.Mvc.MvcServiceCollectionExtensionsTestControllers;
9-
using Microsoft.Framework.Configuration;
12+
using Microsoft.AspNet.Mvc.Razor;
1013
using Microsoft.Framework.DependencyInjection;
14+
using Microsoft.Framework.OptionsModel;
1115
using Xunit;
1216

1317
namespace Microsoft.AspNet.Mvc
@@ -79,6 +83,82 @@ public void WithControllersAsServices_ScansControllersFromSpecifiedAssemblies()
7983
Assert.Equal(ServiceLifetime.Singleton, services[3].Lifetime);
8084
}
8185

86+
// Some MVC services can be registered multiple times, for example, 'IConfigureOptions<MvcOptions>' can
87+
// be registered by calling 'ConfigureMvc(...)' before the call to 'AddMvc()' in which case the options
88+
// configiuration is run in the order they were registered.
89+
// For these kind of multi registration service types, we want to make sure that MVC appends its services
90+
// to the list i.e it does a 'Add' rather than 'TryAdd' on the ServiceCollection.
91+
[Fact]
92+
public void MultiRegistrationServiceTypes_AreRegistered_MultipleTimes()
93+
{
94+
// Arrange
95+
var services = new ServiceCollection();
96+
var multiRegistrationServiceTypes = MutliRegistrationServiceTypes;
97+
98+
// Act
99+
MvcServiceCollectionExtensions.AddMvcServices(services);
100+
MvcServiceCollectionExtensions.AddMvcServices(services);
101+
102+
// Assert
103+
foreach (var serviceType in multiRegistrationServiceTypes)
104+
{
105+
AssertServiceCountEquals(services, serviceType, 2);
106+
}
107+
}
108+
109+
[Fact]
110+
public void SingleRegistrationServiceTypes_AreNotRegistered_MultipleTimes()
111+
{
112+
// Arrange
113+
var services = new ServiceCollection();
114+
var multiRegistrationServiceTypes = MutliRegistrationServiceTypes;
115+
116+
// Act
117+
MvcServiceCollectionExtensions.AddMvcServices(services);
118+
MvcServiceCollectionExtensions.AddMvcServices(services);
119+
120+
// Assert
121+
var singleRegistrationServiceTypes = services
122+
.Where(serviceDescriptor => !multiRegistrationServiceTypes.Contains(serviceDescriptor.ServiceType))
123+
.Select(serviceDescriptor => serviceDescriptor.ServiceType);
124+
125+
foreach (var singleRegistrationType in singleRegistrationServiceTypes)
126+
{
127+
AssertServiceCountEquals(services, singleRegistrationType, 1);
128+
}
129+
}
130+
131+
private IEnumerable<Type> MutliRegistrationServiceTypes
132+
{
133+
get
134+
{
135+
return new[]
136+
{
137+
typeof(IConfigureOptions<MvcOptions>),
138+
typeof(IConfigureOptions<RazorViewEngineOptions>),
139+
typeof(IActionConstraintProvider),
140+
typeof(IActionDescriptorProvider),
141+
typeof(IActionInvokerProvider),
142+
typeof(IFilterProvider),
143+
typeof(IApiDescriptionProvider)
144+
};
145+
}
146+
}
147+
148+
private void AssertServiceCountEquals(
149+
IServiceCollection services,
150+
Type serviceType,
151+
int expectedServiceRegistrationCount)
152+
{
153+
var serviceDescriptors = services.Where(serviceDescriptor => serviceDescriptor.ServiceType == serviceType);
154+
var actual = serviceDescriptors.Count();
155+
156+
Assert.True(
157+
(expectedServiceRegistrationCount == actual),
158+
$"Expected service type '{serviceType}' to be registered {expectedServiceRegistrationCount}" +
159+
$" time(s) but was actually registered {actual} time(s).");
160+
}
161+
82162
private class CustomActivator : IControllerActivator
83163
{
84164
public object Create(ActionContext context, Type controllerType)
@@ -110,4 +190,4 @@ public class TypeBController
110190
{
111191

112192
}
113-
}
193+
}

0 commit comments

Comments
 (0)