|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Abstractions;
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;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Metrics;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Moq;
namespace Microsoft.AspNetCore.Mvc;
public class MvcServiceCollectionExtensionsTest
{
// 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
// 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 AddMvc_MultiRegistrationServiceTypes_AreRegistered_MultipleTimes()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
RegisterMockMultiRegistrationServices(services);
// Act
services.AddMvc();
// Assert
VerifyMultiRegistrationServices(services);
}
private void RegisterMockMultiRegistrationServices(IServiceCollection services)
{
// Register a mock implementation of each service, AddMvcServices should add another implementation.
foreach (var serviceType in MultiRegistrationServiceTypes)
{
var mockType = typeof(Mock<>).MakeGenericType(serviceType.Key);
services.Add(ServiceDescriptor.Transient(serviceType.Key, mockType));
}
}
private void VerifyMultiRegistrationServices(IServiceCollection services)
{
foreach (var serviceType in MultiRegistrationServiceTypes)
{
AssertServiceCountEquals(services, serviceType.Key, serviceType.Value.Length + 1);
foreach (var implementationType in serviceType.Value)
{
AssertContainsSingle(services, serviceType.Key, implementationType);
}
}
}
[Fact]
public void AddMvc_SingleRegistrationServiceTypes_AreNotRegistered_MultipleTimes()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
RegisterMockSingleRegistrationServices(services);
// Act
services.AddMvc();
// Assert
VerifySingleRegistrationServices(services);
}
[Fact]
public void AddControllers_AddRazorPages_SingleRegistrationServiceTypes_AreNotRegistered_MultipleTimes()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
RegisterMockSingleRegistrationServices(services);
// Act
services.AddControllers();
services.AddRazorPages();
// Assert
VerifySingleRegistrationServices(services);
}
[Fact]
public void AddControllersWithViews_SingleRegistrationServiceTypes_AreNotRegistered_MultipleTimes()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
RegisterMockSingleRegistrationServices(services);
// Act
services.AddControllers();
// Assert
VerifySingleRegistrationServices(services);
}
[Fact]
public void AddRazorPages_SingleRegistrationServiceTypes_AreNotRegistered_MultipleTimes()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
RegisterMockSingleRegistrationServices(services);
// Act
services.AddRazorPages();
// Assert
VerifySingleRegistrationServices(services);
}
private void RegisterMockSingleRegistrationServices(IServiceCollection services)
{
// 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));
}
}
private void VerifySingleRegistrationServices(IServiceCollection services)
{
foreach (var singleRegistrationType in SingleRegistrationServiceTypes)
{
AssertServiceCountEquals(services, singleRegistrationType, 1);
}
}
[Fact]
public void AddMvc_Twice_DoesNotAddDuplicates()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().AddInMemoryCollection().Build());
services.AddLogging();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
// Act
services.AddMvc();
services.AddMvc();
// Assert
VerifyAllServices(services);
}
[Fact]
public void AddControllersAddRazorPages_Twice_DoesNotAddDuplicates()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().AddInMemoryCollection().Build());
services.AddLogging();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
// Act
services.AddControllers();
services.AddRazorPages();
services.AddControllers();
services.AddRazorPages();
// Assert
VerifyAllServices(services);
}
[Fact]
public void AddControllersWithViews_Twice_DoesNotAddDuplicates()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().AddInMemoryCollection().Build());
services.AddLogging();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
// Act
services.AddControllersWithViews();
services.AddControllersWithViews();
// Assert
VerifyAllServices(services);
}
[Fact]
public void AddRazorPages_Twice_DoesNotAddDuplicates()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().AddInMemoryCollection().Build());
services.AddLogging();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
// Act
services.AddRazorPages();
services.AddRazorPages();
// Assert
VerifyAllServices(services);
}
[Fact]
public void AddControllersWithViews_AddsDocumentedServices()
{
// Arrange
var services = new ServiceCollection();
services.AddControllersWithViews();
// Assert
// Adds controllers
Assert.Contains(services, s => s.ServiceType == typeof(IActionInvokerProvider) && s.ImplementationType == typeof(ControllerActionInvokerProvider));
// Adds ApiExplorer
Assert.Contains(services, s => s.ServiceType == typeof(IApiDescriptionGroupCollectionProvider));
// Adds CORS
Assert.Contains(services, s => s.ServiceType == typeof(CorsAuthorizationFilter));
// Adds DataAnnotations
Assert.Contains(services, s => s.ServiceType == typeof(IConfigureOptions<MvcOptions>) && s.ImplementationType == typeof(MvcDataAnnotationsMvcOptionsSetup));
// Adds FormatterMappings
Assert.Contains(services, s => s.ServiceType == typeof(FormatFilter));
// Adds Views
Assert.Contains(services, s => s.ServiceType == typeof(IHtmlHelper));
// Adds Razor
Assert.Contains(services, s => s.ServiceType == typeof(IRazorViewEngine));
// Adds CacheTagHelper
Assert.Contains(services, s => s.ServiceType == typeof(CacheTagHelperMemoryCacheFactory));
// No Razor Pages
Assert.DoesNotContain(services, s => s.ServiceType == typeof(IActionInvokerProvider) && s.ImplementationType == typeof(PageActionInvokerProvider));
}
private void VerifyAllServices(IServiceCollection services)
{
var singleRegistrationServiceTypes = SingleRegistrationServiceTypes;
var serviceProvider = services.BuildServiceProvider();
foreach (var service in services)
{
if (singleRegistrationServiceTypes.Contains(service.ServiceType))
{
// 'single-registration' services should only have one implementation registered.
AssertServiceCountEquals(services, service.ServiceType, 1);
}
else
{
var implementationType = service switch
{
{ ImplementationType: { } type } => type,
{ ImplementationInstance: { } instance } => instance.GetType(),
{ ImplementationFactory: { } factory } => factory(serviceProvider).GetType(),
};
if (implementationType != null && !implementationType.Assembly.FullName.Contains("Mvc"))
{
// Ignore types that don't come from MVC
}
else
{
// 'multi-registration' services should only have one *instance* of each implementation registered.
AssertContainsSingle(services, service.ServiceType, service.ImplementationType);
}
}
}
}
[Fact]
public void AddMvc_AddsAssemblyPartsForFrameworkTagHelpers()
{
// Arrange
var mvcRazorAssembly = typeof(UrlResolutionTagHelper).Assembly;
var mvcTagHelpersAssembly = typeof(InputTagHelper).Assembly;
var services = new ServiceCollection();
var providers = new IApplicationFeatureProvider[]
{
new ControllerFeatureProvider(),
new ViewComponentFeatureProvider()
};
// Act
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<ApplicationPartManager>(descriptor.ImplementationInstance);
Assert.Equal(2, manager.ApplicationParts.Count);
Assert.Single(manager.ApplicationParts.OfType<AssemblyPart>(), p => p.Assembly == mvcRazorAssembly);
Assert.Single(manager.ApplicationParts.OfType<AssemblyPart>(), p => p.Assembly == mvcTagHelpersAssembly);
}
[Fact]
public void AddMvcTwice_DoesNotAddDuplicateFrameworkParts()
{
// Arrange
var mvcRazorAssembly = typeof(UrlResolutionTagHelper).Assembly;
var mvcTagHelpersAssembly = typeof(InputTagHelper).Assembly;
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<ApplicationPartManager>(descriptor.ImplementationInstance);
Assert.Equal(2, manager.ApplicationParts.Count);
Assert.Single(manager.ApplicationParts.OfType<AssemblyPart>(), p => p.Assembly == mvcRazorAssembly);
Assert.Single(manager.ApplicationParts.OfType<AssemblyPart>(), p => p.Assembly == mvcTagHelpersAssembly);
}
[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<ApplicationPartManager>(descriptor.ImplementationInstance);
Assert.Collection(manager.FeatureProviders,
feature => Assert.IsType<ControllerFeatureProvider>(feature),
feature => Assert.IsType<ViewComponentFeatureProvider>(feature),
feature => Assert.IsType<TagHelperFeatureProvider>(feature),
feature => Assert.IsType<RazorCompiledItemFeatureProvider>(feature));
}
[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);
}
[Fact]
public void AddMvc_NoScopedServiceIsReferredToByASingleton()
{
// Arrange
var services = new ServiceCollection();
var hostEnvironment = GetHostingEnvironment();
services.AddSingleton<IWebHostEnvironment>(hostEnvironment);
services.AddSingleton<IHostEnvironment>(hostEnvironment);
var diagnosticListener = new DiagnosticListener("Microsoft.AspNet");
services.AddSingleton<DiagnosticSource>(diagnosticListener);
services.AddSingleton<DiagnosticListener>(diagnosticListener);
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
services.AddMetrics();
services.AddLogging();
services.AddOptions();
services.AddMvc();
var root = services.BuildServiceProvider(validateScopes: true);
var scopeFactory = root.GetRequiredService<IServiceScopeFactory>();
// Act & Assert
using (var scope = scopeFactory.CreateScope())
{
foreach (var serviceType in services.Select(d => d.ServiceType).Where(t => !t.IsGenericTypeDefinition).Distinct())
{
// This will throw if something is invalid.
scope.ServiceProvider.GetService(typeof(IEnumerable<>).MakeGenericType(serviceType));
}
}
}
[Fact]
public void AddMvc_RegistersExpectedTempDataProvider()
{
// Arrange
var services = new ServiceCollection();
// Act
services.AddMvc();
// Assert
var descriptor = Assert.Single(services, item => item.ServiceType == typeof(ITempDataProvider));
Assert.Equal(typeof(CookieTempDataProvider), descriptor.ImplementationType);
}
[Fact]
public void AddMvc_DoesNotRegisterCookieTempDataOptionsConfiguration()
{
// Arrange
var services = new ServiceCollection();
// Act
var builder = services.AddMvc();
// Assert
Assert.DoesNotContain(
services,
item => item.ServiceType == typeof(IConfigureOptions<CookieTempDataProviderOptions>));
}
private IEnumerable<Type> SingleRegistrationServiceTypes
{
get
{
var services = new ServiceCollection();
services.AddSingleton<IWebHostEnvironment>(GetHostingEnvironment());
services.AddMvc();
var multiRegistrationServiceTypes = MultiRegistrationServiceTypes;
return services
.Where(sd => !multiRegistrationServiceTypes.ContainsKey(sd.ServiceType))
.Where(sd => sd.ServiceType.Assembly.FullName.Contains("Mvc"))
.Select(sd => sd.ServiceType);
}
}
private Dictionary<Type, Type[]> MultiRegistrationServiceTypes
{
get
{
return new Dictionary<Type, Type[]>()
{
{
typeof(IConfigureOptions<MvcOptions>),
new Type[]
{
typeof(MvcCoreMvcOptionsSetup),
typeof(MvcDataAnnotationsMvcOptionsSetup),
typeof(TempDataMvcOptionsSetup),
}
},
{
typeof(IConfigureOptions<RouteOptions>),
new Type[]
{
typeof(MvcCoreRouteOptionsSetup),
typeof(MvcCoreRouteOptionsSetup),
typeof(RegexInlineRouteConstraintSetup),
}
},
{
typeof(IConfigureOptions<ApiBehaviorOptions>),
new Type[]
{
typeof(ApiBehaviorOptionsSetup),
}
},
{
typeof(IConfigureOptions<MvcViewOptions>),
new Type[]
{
typeof(MvcViewOptionsSetup),
typeof(MvcRazorMvcViewOptionsSetup),
}
},
{
typeof(IConfigureOptions<RazorViewEngineOptions>),
new[]
{
typeof(RazorViewEngineOptionsSetup),
typeof(RazorPagesRazorViewEngineOptionsSetup),
}
},
{
typeof(IPostConfigureOptions<MvcOptions>),
new[]
{
typeof(MvcCoreMvcOptionsSetup),
}
},
{
typeof(IActionConstraintProvider),
new Type[]
{
typeof(DefaultActionConstraintProvider),
}
},
{
typeof(IActionDescriptorProvider),
new Type[]
{
typeof(ControllerActionDescriptorProvider),
typeof(CompiledPageActionDescriptorProvider),
}
},
{
typeof(IActionInvokerProvider),
new Type[]
{
typeof(ControllerActionInvokerProvider),
typeof(PageActionInvokerProvider),
}
},
{
typeof(IRequestDelegateFactory),
new Type[]
{
typeof(PageRequestDelegateFactory),
typeof(ControllerRequestDelegateFactory)
}
},
{
typeof(IFilterProvider),
new Type[]
{
typeof(DefaultFilterProvider),
}
},
{
typeof(IControllerPropertyActivator),
new Type[]
{
typeof(DefaultControllerPropertyActivator),
typeof(ViewDataDictionaryControllerPropertyActivator),
}
},
{
typeof(IApplicationModelProvider),
new Type[]
{
typeof(DefaultApplicationModelProvider),
typeof(CorsApplicationModelProvider),
typeof(AuthorizationApplicationModelProvider),
typeof(TempDataApplicationModelProvider),
typeof(ViewDataAttributeApplicationModelProvider),
typeof(ApiBehaviorApplicationModelProvider),
typeof(AntiforgeryApplicationModelProvider)
}
},
{
typeof(IApiDescriptionProvider),
new Type[]
{
typeof(DefaultApiDescriptionProvider),
}
},
{
typeof(IPageRouteModelProvider),
new[]
{
typeof(CompiledPageRouteModelProvider),
}
},
{
typeof(IPageApplicationModelProvider),
new[]
{
typeof(AuthorizationPageApplicationModelProvider),
typeof(AuthorizationPageApplicationModelProvider),
typeof(DefaultPageApplicationModelProvider),
typeof(TempDataFilterPageApplicationModelProvider),
typeof(ViewDataAttributePageApplicationModelProvider),
typeof(ResponseCacheFilterApplicationModelProvider),
}
},
};
}
}
private void AssertServiceCountEquals(
IServiceCollection services,
Type serviceType,
int expectedServiceRegistrationCount)
{
var serviceDescriptors = services.Where(serviceDescriptor => serviceDescriptor.ServiceType == serviceType);
var actual = serviceDescriptors.Count();
Assert.True(
(expectedServiceRegistrationCount == actual),
$"Expected service type '{serviceType}' to be registered {expectedServiceRegistrationCount}" +
$" time(s) but was actually registered {actual} time(s)." +
string.Join(Environment.NewLine, serviceDescriptors.Select(sd => sd.ImplementationType)));
}
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.Fail($"Could not find an instance of {implementationType} registered as {serviceType}");
}
else if (matches.Length > 1)
{
var implementations = new List<Type>();
var sp = services.BuildServiceProvider();
foreach ( var service in matches )
{
if (service.ImplementationType is not null)
{
implementations.Add(service.ImplementationType);
}
else if (service.ImplementationInstance is not null)
{
implementations.Add(service.ImplementationInstance.GetType());
}
else if (service.ImplementationFactory is not null)
{
var instance = service.ImplementationFactory(sp);
implementations.Add(instance.GetType());
}
}
Assert.Fail($"Found multiple instances of {implementationType} registered as {serviceType}");
}
}
private IWebHostEnvironment GetHostingEnvironment()
{
var environment = new Mock<IWebHostEnvironment>();
environment
.Setup(e => e.ApplicationName)
.Returns(typeof(MvcServiceCollectionExtensionsTest).Assembly.GetName().Name);
environment.Setup(e => e.WebRootFileProvider)
.Returns(new NullFileProvider());
return environment.Object;
}
}
|