A new pattern for adding multi-registration services

This is some cleanup of how we add multi-registration services to avoid
duplication when calling AddMvcServices multiple times. Also improved
tests to be more clear, and to verify all of our special cases
explicitly.
This commit is contained in:
Ryan Nowak 2015-06-09 14:58:58 -07:00
parent 1f9a451e2f
commit 0861612779
2 changed files with 249 additions and 54 deletions

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc;
@ -163,13 +164,23 @@ namespace Microsoft.Framework.DependencyInjection
internal static void AddMvcServices(IServiceCollection services)
{
// Options - all of these are multi-registration
services.AddTransient<IConfigureOptions<MvcOptions>, MvcOptionsSetup>();
services.AddTransient<IConfigureOptions<MvcOptions>, JsonMvcOptionsSetup>();
services.AddTransient<IConfigureOptions<MvcFormatterMappingOptions>, JsonMvcFormatterMappingOptionsSetup>();
services.AddTransient<IConfigureOptions<MvcViewOptions>, MvcViewOptionsSetup>();
services.AddTransient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>();
services.TryAdd(ServiceDescriptor.Transient<IAssemblyProvider, DefaultAssemblyProvider>());
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcOptionsSetup>());
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, JsonMvcOptionsSetup>());
TryAddMultiRegistrationService(
services,
ServiceDescriptor
.Transient<IConfigureOptions<MvcFormatterMappingOptions>, JsonMvcFormatterMappingOptionsSetup>());
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IConfigureOptions<MvcViewOptions>, MvcViewOptionsSetup>());
TryAddMultiRegistrationService(
services,
ServiceDescriptor
.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>());
services.TryAdd(ServiceDescriptor.Transient<MvcMarkerService, MvcMarkerService>());
services.TryAdd((ServiceDescriptor.Singleton<ITypeActivatorCache, DefaultTypeActivatorCache>()));
@ -178,7 +189,17 @@ namespace Microsoft.Framework.DependencyInjection
// Core action discovery, filters and action execution.
// This are consumed only when creating action descriptors, then they can be de-allocated
services.TryAdd(ServiceDescriptor.Transient<IControllerTypeProvider, DefaultControllerTypeProvider>());;
services.TryAdd(ServiceDescriptor.Transient<IAssemblyProvider, DefaultAssemblyProvider>());
services.TryAdd(ServiceDescriptor.Transient<IControllerTypeProvider, DefaultControllerTypeProvider>()); ;
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IApplicationModelProvider, DefaultApplicationModelProvider>());
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IApplicationModelProvider, CorsApplicationModelProvider>());
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IApplicationModelProvider, AuthorizationApplicationModelProvider>());
// This has a cache, so it needs to be a singleton
services.TryAdd(ServiceDescriptor.Singleton<IControllerFactory, DefaultControllerFactory>());
@ -190,8 +211,9 @@ namespace Microsoft.Framework.DependencyInjection
// This provider needs access to the per-request services, but might be used many times for a given
// request.
// multiple registration service
services.AddTransient<IActionConstraintProvider, DefaultActionConstraintProvider>();
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IActionConstraintProvider, DefaultActionConstraintProvider>());
services.TryAdd(ServiceDescriptor
.Singleton<IActionSelectorDecisionTreeProvider, ActionSelectorDecisionTreeProvider>());
@ -205,27 +227,28 @@ namespace Microsoft.Framework.DependencyInjection
return new DefaultObjectValidator(options.ValidationExcludeFilters, modelMetadataProvider);
}));
// multiple registration service
services.AddTransient<IActionDescriptorProvider, ControllerActionDescriptorProvider>();
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IActionDescriptorProvider, ControllerActionDescriptorProvider>());
// multiple registration service
services.AddTransient<IActionInvokerProvider, ControllerActionInvokerProvider>();
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IActionInvokerProvider, ControllerActionInvokerProvider>());
services.TryAdd(ServiceDescriptor
.Singleton<IActionDescriptorsCollectionProvider, DefaultActionDescriptorsCollectionProvider>());
// multiple registration service
services.AddTransient<IFilterProvider, DefaultFilterProvider>();
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IFilterProvider, DefaultFilterProvider>());
// multiple registration service
services.AddTransient<IApplicationModelProvider, DefaultApplicationModelProvider>();
// multiple registration services
services.AddTransient<IApplicationModelProvider, CorsApplicationModelProvider>();
services.AddTransient<IApplicationModelProvider, AuthorizationApplicationModelProvider>();
services.AddTransient<IControllerPropertyActivator, DefaultControllerPropertyActivator>();
services.AddTransient<IControllerPropertyActivator, ViewDataDictionaryControllerPropertyActivator>();
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IControllerPropertyActivator, DefaultControllerPropertyActivator>());
TryAddMultiRegistrationService(
services,
ServiceDescriptor
.Transient<IControllerPropertyActivator, ViewDataDictionaryControllerPropertyActivator>());
services.TryAdd(ServiceDescriptor.Transient<FormatFilter, FormatFilter>());
services.TryAdd(ServiceDescriptor.Transient<CorsAuthorizationFilter, CorsAuthorizationFilter>());
@ -322,8 +345,9 @@ namespace Microsoft.Framework.DependencyInjection
// Api Description
services.TryAdd(ServiceDescriptor
.Singleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>());
// multiple registration service
services.AddTransient<IApiDescriptionProvider, DefaultApiDescriptionProvider>();
TryAddMultiRegistrationService(
services,
ServiceDescriptor.Transient<IApiDescriptionProvider, DefaultApiDescriptionProvider>());
// Temp Data
services.TryAdd(ServiceDescriptor.Scoped<ITempDataDictionary, TempDataDictionary>());
@ -359,6 +383,27 @@ namespace Microsoft.Framework.DependencyInjection
return services;
}
// Adds a service if the service type and implementation type hasn't been added yet. This is needed for
// services like IConfigureOptions<MvcOptions> or IApplicationModelProvider where you need the ability
// to register multiple implementation types for the same service type.
private static bool TryAddMultiRegistrationService(IServiceCollection services, ServiceDescriptor descriptor)
{
// This can't work when registering a factory or instance, you have to register a type.
// Additionally, if any existing registrations use a factory or instance, we can't check those, but we don't
// assert that because it might be added by user-code.
Debug.Assert(descriptor.ImplementationType != null);
if (services.Any(d =>
d.ServiceType == descriptor.ServiceType &&
d.ImplementationType == descriptor.ImplementationType))
{
return false;
}
services.Add(descriptor);
return true;
}
private static void ConfigureDefaultServices(IServiceCollection services)
{
services.AddOptions();

View File

@ -9,10 +9,12 @@ using Microsoft.AspNet.Mvc.ActionConstraints;
using Microsoft.AspNet.Mvc.ApiExplorer;
using Microsoft.AspNet.Mvc.ApplicationModels;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.MvcServiceCollectionExtensionsTestControllers;
using Microsoft.AspNet.Mvc.Razor;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc
@ -86,24 +88,35 @@ namespace Microsoft.AspNet.Mvc
// Some MVC services can be registered multiple times, for example, 'IConfigureOptions<MvcOptions>' can
// be registered by calling 'ConfigureMvc(...)' before the call to 'AddMvc()' in which case the options
// configiuration is run in the order they were registered.
// For these kind of multi registration service types, we want to make sure that MVC appends its services
// to the list i.e it does a 'Add' rather than 'TryAdd' on the ServiceCollection.
// configuration is run in the order they were registered.
//
// For these kind of multi registration service types, we want to make sure that MVC will still add its
// services if the implementation type is different.
[Fact]
public void MultiRegistrationServiceTypes_AreRegistered_MultipleTimes()
{
// Arrange
var services = new ServiceCollection();
var multiRegistrationServiceTypes = MutliRegistrationServiceTypes;
// Register a mock implementation of each service, AddMvcServices should add another implemenetation.
foreach (var serviceType in MutliRegistrationServiceTypes)
{
var mockType = typeof(Mock<>).MakeGenericType(serviceType.Key);
services.Add(ServiceDescriptor.Transient(serviceType.Key, mockType));
}
// Act
MvcServiceCollectionExtensions.AddMvcServices(services);
MvcServiceCollectionExtensions.AddMvcServices(services);
// Assert
foreach (var serviceType in multiRegistrationServiceTypes)
foreach (var serviceType in MutliRegistrationServiceTypes)
{
AssertServiceCountEquals(services, serviceType.Key, serviceType.Value);
AssertServiceCountEquals(services, serviceType.Key, serviceType.Value.Length + 1);
foreach (var implementationType in serviceType.Value)
{
AssertContainsSingle(services, serviceType.Key, implementationType);
}
}
}
@ -112,40 +125,152 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var services = new ServiceCollection();
var multiRegistrationServiceTypes = MutliRegistrationServiceTypes;
// Register a mock implementation of each service, AddMvcServices should not replace it.
foreach (var serviceType in SingleRegistrationServiceTypes)
{
var mockType = typeof(Mock<>).MakeGenericType(serviceType);
services.Add(ServiceDescriptor.Transient(serviceType, mockType));
}
// Act
MvcServiceCollectionExtensions.AddMvcServices(services);
// Assert
foreach (var singleRegistrationType in SingleRegistrationServiceTypes)
{
AssertServiceCountEquals(services, singleRegistrationType, 1);
}
}
[Fact]
public void AddMvcServicesTwice_DoesNotAddDuplicates()
{
// Arrange
var services = new ServiceCollection();
// Act
MvcServiceCollectionExtensions.AddMvcServices(services);
MvcServiceCollectionExtensions.AddMvcServices(services);
// Assert
var singleRegistrationServiceTypes = services
.Where(serviceDescriptor => !multiRegistrationServiceTypes.Keys.Contains(serviceDescriptor.ServiceType))
.Select(serviceDescriptor => serviceDescriptor.ServiceType);
foreach (var singleRegistrationType in singleRegistrationServiceTypes)
var singleRegistrationServiceTypes = SingleRegistrationServiceTypes;
foreach (var service in services)
{
AssertServiceCountEquals(services, singleRegistrationType, 1);
if (singleRegistrationServiceTypes.Contains(service.ServiceType))
{
// 'single-registration' services should only have one implementation registered.
AssertServiceCountEquals(services, service.ServiceType, 1);
}
else
{
// 'multi-registration' services should only have one *instance* of each implementation registered.
AssertContainsSingle(services, service.ServiceType, service.ImplementationType);
}
}
}
private Dictionary<Type, int> MutliRegistrationServiceTypes
private IEnumerable<Type> SingleRegistrationServiceTypes
{
get
{
return new Dictionary<Type, int>()
var services = new ServiceCollection();
MvcServiceCollectionExtensions.AddMvcServices(services);
var multiRegistrationServiceTypes = MutliRegistrationServiceTypes;
return services
.Where(sd => !multiRegistrationServiceTypes.Keys.Contains(sd.ServiceType))
.Select(sd => sd.ServiceType);
}
}
private Dictionary<Type, Type[]> MutliRegistrationServiceTypes
{
get
{
return new Dictionary<Type, Type[]>()
{
{ typeof(IConfigureOptions<MvcOptions>), 4 },
{ typeof(IConfigureOptions<MvcFormatterMappingOptions>), 2 },
{ typeof(IConfigureOptions<MvcViewOptions>), 2 },
{ typeof(IConfigureOptions<RazorViewEngineOptions>), 2 },
{ typeof(IActionConstraintProvider), 2 },
{ typeof(IActionDescriptorProvider), 2 },
{ typeof(IActionInvokerProvider), 2 },
{ typeof(IControllerPropertyActivator), 4 },
{ typeof(IFilterProvider), 2 },
{ typeof(IApiDescriptionProvider), 2 },
{ typeof(IApplicationModelProvider), 6 },
{
typeof(IConfigureOptions<MvcOptions>),
new Type[]
{
typeof(MvcOptionsSetup),
typeof(JsonMvcOptionsSetup),
}
},
{
typeof(IConfigureOptions<MvcFormatterMappingOptions>),
new Type[]
{
typeof(JsonMvcFormatterMappingOptionsSetup),
}
},
{
typeof(IConfigureOptions<MvcViewOptions>),
new Type[]
{
typeof(MvcViewOptionsSetup),
}
},
{
typeof(IConfigureOptions<RazorViewEngineOptions>),
new Type[]
{
typeof(RazorViewEngineOptionsSetup),
}
},
{
typeof(IActionConstraintProvider),
new Type[]
{
typeof(DefaultActionConstraintProvider),
}
},
{
typeof(IActionDescriptorProvider),
new Type[]
{
typeof(ControllerActionDescriptorProvider),
}
},
{
typeof(IActionInvokerProvider),
new Type[]
{
typeof(ControllerActionInvokerProvider),
}
},
{
typeof(IFilterProvider),
new Type[]
{
typeof(DefaultFilterProvider),
}
},
{
typeof(IApiDescriptionProvider),
new Type[]
{
typeof(DefaultApiDescriptionProvider),
}
},
{
typeof(IControllerPropertyActivator),
new Type[]
{
typeof(DefaultControllerPropertyActivator),
typeof(ViewDataDictionaryControllerPropertyActivator),
}
},
{
typeof(IApplicationModelProvider),
new Type[]
{
typeof(DefaultApplicationModelProvider),
typeof(CorsApplicationModelProvider),
typeof(AuthorizationApplicationModelProvider),
}
},
};
}
}
@ -164,6 +289,31 @@ namespace Microsoft.AspNet.Mvc
$" time(s) but was actually registered {actual} time(s).");
}
private void AssertContainsSingle(
IServiceCollection services,
Type serviceType,
Type implementationType)
{
var matches = services
.Where(sd =>
sd.ServiceType == serviceType &&
sd.ImplementationType == implementationType)
.ToArray();
if (matches.Length == 0)
{
Assert.True(
false,
$"Could not find an instance of {implementationType} registered as {serviceType}");
}
else if (matches.Length > 1)
{
Assert.True(
false,
$"Found multiple instances of {implementationType} registered as {serviceType}");
}
}
private class CustomActivator : IControllerActivator
{
public object Create(ActionContext context, Type controllerType)