From 1bd66ffda0263f7ab390c209d45a8c49e51d4ef4 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Sat, 19 Mar 2016 03:00:32 -0700 Subject: [PATCH] [Fixes #4089] Add support for application parts This commit introduces application parts as a concept on MVC. An application part is an abstraction that allows you to expose some feature or corncern in a way that is decoupled from their underlying source. Examples of this include types in an assembly, emdeded resources, files on disk etc. Application parts are configured during startup by adding or removing them from the application part manager available as part of IMvcBuilder and IMvcCoreBuilder. The application part manager provides the ability to populate features from the list of available application parts by using a list of application feature providers. Application feature providers are responsible for populating a given feature given a list of application parts. Examples of application providers can be a ControllerFeatureProvider that goes through the list of application parts, sees which one of those parts exposes types, determines which of those types are controller types, and adds them to a ControllerFeature that holds a list of all the types that will be considered controllers in the application. --- .../ApplicationParts/ApplicationPart.cs | 16 ++ .../ApplicationPartManager.cs | 47 ++++++ .../ApplicationParts/AssemblyPart.cs | 39 +++++ .../IApplicationFeatureProvider.cs | 13 ++ .../IApplicationFeatureProviderOfT.cs | 23 +++ .../DependencyInjection/IMvcBuilder.cs | 8 + .../DependencyInjection/IMvcCoreBuilder.cs | 8 + .../MvcCoreMvcBuilderExtensions.cs | 53 ++++++- .../MvcCoreMvcCoreBuilderExtensions.cs | 53 ++++++- .../MvcCoreServiceCollectionExtensions.cs | 40 ++++- .../Internal/MvcBuilder.cs | 13 +- .../Internal/MvcCoreBuilder.cs | 19 ++- .../Internal/MvcCoreMvcOptionsSetup.cs | 15 +- .../MvcServiceCollectionExtensions.cs | 2 +- .../ApplicationPartManagerTest.cs | 150 ++++++++++++++++++ .../ApplicationParts/AssemblyPartTest.cs | 35 ++++ .../MvcBuilderExtensionsTest.cs | 47 ++++++ .../MvcCoreBuilderExtensionsTest.cs | 60 +++++++ .../Binders/ComplexTypeModelBinderTest.cs | 5 +- .../MvcTestFixture.cs | 14 ++ .../ModelBindingTestHelper.cs | 2 + .../TestMvcOptions.cs | 5 +- .../Internal/DefaultTagHelperActivatorTest.cs | 3 + .../Internal/DefaultTagHelperFactoryTest.cs | 10 +- .../RazorViewEngineOptionsTest.cs | 3 +- .../MvcOptionsSetupTest.cs | 4 +- .../MvcServiceCollectionExtensionsTest.cs | 15 ++ 27 files changed, 681 insertions(+), 21 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPart.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProvider.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProviderOfT.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationPartManagerTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/AssemblyPartTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreBuilderExtensionsTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPart.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPart.cs new file mode 100644 index 0000000000..807af25a9d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPart.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.AspNetCore.Mvc.ApplicationParts +{ + /// + /// A part of an MVC application. + /// + public abstract class ApplicationPart + { + /// + /// Gets the name. + /// + public abstract string Name { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs new file mode 100644 index 0000000000..4d0c190931 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs @@ -0,0 +1,47 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + /// + /// Manages the parts and features of an MVC application. + /// + public class ApplicationPartManager + { + /// + /// Gets the list of s. + /// + public IList FeatureProviders { get; } = + new List(); + + /// + /// Gets the list of s. + /// + public IList ApplicationParts { get; } = + new List(); + + /// + /// Populates the given using the list of + /// s configured on the + /// . + /// + /// The type of the feature. + /// The feature instance to populate. + public void PopulateFeature(TFeature feature) + { + if (feature == null) + { + throw new ArgumentNullException(nameof(feature)); + } + + foreach (var provider in FeatureProviders.OfType>()) + { + provider.PopulateFeature(ApplicationParts, feature); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs new file mode 100644 index 0000000000..0147f59eb7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs @@ -0,0 +1,39 @@ +// 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.Reflection; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + /// + /// An backed by an . + /// + public class AssemblyPart : ApplicationPart + { + /// + /// Initalizes a new instance. + /// + /// + public AssemblyPart(Assembly assembly) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + Assembly = assembly; + } + + /// + /// Gets the of the . + /// + public Assembly Assembly { get; } + + /// + /// Gets the name of the . + /// + public override string Name => Assembly.GetName().Name; + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProvider.cs new file mode 100644 index 0000000000..2ee9ece9f0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProvider.cs @@ -0,0 +1,13 @@ +// 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 Microsoft.AspNetCore.Mvc.ApplicationParts +{ + /// + /// Marker interface for + /// implementations. + /// + public interface IApplicationFeatureProvider + { + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProviderOfT.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProviderOfT.cs new file mode 100644 index 0000000000..557c6287af --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProviderOfT.cs @@ -0,0 +1,23 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + /// + /// A provider for a given feature. + /// + /// The type of the feature. + public interface IApplicationFeatureProvider : IApplicationFeatureProvider + { + /// + /// Updates the intance. + /// + /// The list of s of the + /// application. + /// + /// The feature instance to populate. + void PopulateFeature(IEnumerable parts, TFeature feature); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/IMvcBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/IMvcBuilder.cs index 5e12b9da62..c1419690a4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/IMvcBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/IMvcBuilder.cs @@ -1,6 +1,8 @@ // 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.ApplicationParts; + namespace Microsoft.Extensions.DependencyInjection { /// @@ -12,5 +14,11 @@ namespace Microsoft.Extensions.DependencyInjection /// Gets the where MVC services are configured. /// IServiceCollection Services { get; } + + /// + /// Gets the where s + /// are configured. + /// + ApplicationPartManager PartManager { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/IMvcCoreBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/IMvcCoreBuilder.cs index 4c621afe1b..9263ebb514 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/IMvcCoreBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/IMvcCoreBuilder.cs @@ -1,6 +1,8 @@ // 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.ApplicationParts; + namespace Microsoft.Extensions.DependencyInjection { /// @@ -12,5 +14,11 @@ namespace Microsoft.Extensions.DependencyInjection /// Gets the where essential MVC services are configured. /// IServiceCollection Services { get; } + + /// + /// Gets the where s + /// are configured. + /// + ApplicationPartManager PartManager { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcBuilderExtensions.cs index 4c9c359b77..4c57152edc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcBuilderExtensions.cs @@ -3,11 +3,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Internal; -using System.Linq; namespace Microsoft.Extensions.DependencyInjection { @@ -58,6 +59,56 @@ namespace Microsoft.Extensions.DependencyInjection return builder; } + /// + /// Adds an to the list of on the + /// . + /// + /// The . + /// The of the . + /// The . + public static IMvcBuilder AddApplicationPart(this IMvcBuilder builder, Assembly assembly) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + builder.ConfigureApplicationPartManager(manager => manager.ApplicationParts.Add(new AssemblyPart(assembly))); + + return builder; + } + + /// + /// Configures the of the using + /// the given . + /// + /// The . + /// The + /// The . + public static IMvcBuilder ConfigureApplicationPartManager( + this IMvcBuilder builder, + Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + setupAction(builder.PartManager); + + return builder; + } + /// /// Register the specified as services and as a source for controller /// discovery. diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcCoreBuilderExtensions.cs index c4d57c6530..3ef94e1343 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcCoreBuilderExtensions.cs @@ -3,14 +3,15 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; -using System.Linq; namespace Microsoft.Extensions.DependencyInjection { @@ -143,6 +144,56 @@ namespace Microsoft.Extensions.DependencyInjection return builder.AddControllersAsServices(controllerAssemblies.AsEnumerable()); } + /// + /// Adds an to the list of on the + /// . + /// + /// The . + /// The of the . + /// The . + public static IMvcCoreBuilder AddApplicationPart(this IMvcCoreBuilder builder, Assembly assembly) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + builder.ConfigureApplicationPartManager(manager => manager.ApplicationParts.Add(new AssemblyPart(assembly))); + + return builder; + } + + /// + /// Configures the of the using + /// the given . + /// + /// The . + /// The + /// The . + public static IMvcCoreBuilder ConfigureApplicationPartManager( + this IMvcCoreBuilder builder, + Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + setupAction(builder.PartManager); + + return builder; + } + /// /// Registers controller types from the specified as services and as a source /// for controller discovery. diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 36d978c428..6c21cf5c65 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -3,10 +3,13 @@ using System; using System.Buffers; +using System.Linq; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -39,10 +42,45 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(services)); } + var partManager = GetApplicationPartManager(services); + services.TryAddSingleton(partManager); + ConfigureDefaultServices(services); AddMvcCoreServices(services); - return new MvcCoreBuilder(services); + var builder = new MvcCoreBuilder(services, partManager); + + return builder; + } + + private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services) + { + var manager = GetServiceFromCollection(services); + if (manager == null) + { + manager = new ApplicationPartManager(); + + var environment = GetServiceFromCollection(services); + if (environment == null) + { + return manager; + } + + var assemblies = new DefaultAssemblyProvider(environment).CandidateAssemblies; + foreach (var assembly in assemblies) + { + manager.ApplicationParts.Add(new AssemblyPart(assembly)); + } + } + + return manager; + } + + private static T GetServiceFromCollection(IServiceCollection services) + { + return (T)services + .FirstOrDefault(d => d.ServiceType == typeof(T)) + ?.ImplementationInstance; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcBuilder.cs index 84cc8468eb..d2a0db532e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc.Internal @@ -15,17 +16,27 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// Initializes a new instance. /// /// The to add services to. - public MvcBuilder(IServiceCollection services) + /// The of the application. + public MvcBuilder(IServiceCollection services, ApplicationPartManager manager) { if (services == null) { throw new ArgumentNullException(nameof(services)); } + if (manager == null) + { + throw new ArgumentNullException(nameof(manager)); + } + Services = services; + PartManager = manager; } /// public IServiceCollection Services { get; } + + /// + public ApplicationPartManager PartManager { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreBuilder.cs index eb64507a4e..5f2560179a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreBuilder.cs @@ -2,6 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc.Internal @@ -12,19 +15,31 @@ namespace Microsoft.AspNetCore.Mvc.Internal public class MvcCoreBuilder : IMvcCoreBuilder { /// - /// Initializes a new instance of . + /// Initializes a new instance. /// /// The to add services to. - public MvcCoreBuilder(IServiceCollection services) + /// The of the application. + public MvcCoreBuilder( + IServiceCollection services, + ApplicationPartManager manager) { if (services == null) { throw new ArgumentNullException(nameof(services)); } + if (manager == null) + { + throw new ArgumentNullException(nameof(manager)); + } + Services = services; + PartManager = manager; } + /// + public ApplicationPartManager PartManager { get; } + /// public IServiceCollection Services { get; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs index 44ddbea572..537a82a16c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs @@ -16,14 +16,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// /// Sets up default options for . /// - public class MvcCoreMvcOptionsSetup : ConfigureOptions + public class MvcCoreMvcOptionsSetup : IConfigureOptions { + private readonly IHttpRequestStreamReaderFactory _readerFactory; + public MvcCoreMvcOptionsSetup(IHttpRequestStreamReaderFactory readerFactory) - : base((options) => ConfigureMvc(options, readerFactory)) { + if (readerFactory == null) + { + throw new ArgumentNullException(nameof(readerFactory)); + } + + _readerFactory = readerFactory; } - public static void ConfigureMvc(MvcOptions options, IHttpRequestStreamReaderFactory readerFactory) + public void Configure(MvcOptions options) { // Set up default error messages var messageProvider = options.ModelBindingMessageProvider; @@ -38,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Set up ModelBinding options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider()); options.ModelBinderProviders.Add(new ServicesModelBinderProvider()); - options.ModelBinderProviders.Add(new BodyModelBinderProvider(readerFactory)); + options.ModelBinderProviders.Add(new BodyModelBinderProvider(_readerFactory)); options.ModelBinderProviders.Add(new HeaderModelBinderProvider()); options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider()); options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider()); diff --git a/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs index 0dfe509e96..355478f12a 100644 --- a/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs @@ -45,7 +45,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.AddCors(); - return new MvcBuilder(builder.Services); + return new MvcBuilder(builder.Services, builder.PartManager); } /// diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationPartManagerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationPartManagerTest.cs new file mode 100644 index 0000000000..27735d2693 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationPartManagerTest.cs @@ -0,0 +1,150 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public class ApplicationPartManagerTest + { + [Fact] + public void PopulateFeature_InvokesAllProvidersSequentially_ForAGivenFeature() + { + // Arrange + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new ControllersPart("ControllersPartA")); + manager.ApplicationParts.Add(new ViewComponentsPart("ViewComponentsPartB")); + manager.ApplicationParts.Add(new ControllersPart("ControllersPartC")); + manager.FeatureProviders.Add( + new ControllersFeatureProvider((f, v) => f.Values.Add($"ControllersFeatureProvider1{v}"))); + manager.FeatureProviders.Add( + new ControllersFeatureProvider((f, v) => f.Values.Add($"ControllersFeatureProvider2{v}"))); + + var feature = new ControllersFeature(); + var expectedResults = new[] { + "ControllersFeatureProvider1ControllersPartA", + "ControllersFeatureProvider1ControllersPartC", + "ControllersFeatureProvider2ControllersPartA", + "ControllersFeatureProvider2ControllersPartC" + }; + + // Act + manager.PopulateFeature(feature); + + // Assert + Assert.Equal(expectedResults, feature.Values.ToArray()); + } + + [Fact] + public void PopulateFeature_InvokesOnlyProviders_ForAGivenFeature() + { + // Arrange + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new ControllersPart("ControllersPart")); + manager.FeatureProviders.Add( + new ControllersFeatureProvider((f, v) => f.Values.Add($"ControllersFeatureProvider{v}"))); + manager.FeatureProviders.Add( + new NotControllersedFeatureProvider((f, v) => f.Values.Add($"ViewComponentsFeatureProvider{v}"))); + + var feature = new ControllersFeature(); + var expectedResults = new[] { "ControllersFeatureProviderControllersPart" }; + + // Act + manager.PopulateFeature(feature); + + // Assert + Assert.Equal(expectedResults, feature.Values.ToArray()); + } + + [Fact] + public void PopulateFeature_SkipProviders_ForOtherFeatures() + { + // Arrange + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new ViewComponentsPart("ViewComponentsPart")); + manager.FeatureProviders.Add( + new ControllersFeatureProvider((f, v) => f.Values.Add($"ControllersFeatureProvider{v}"))); + + var feature = new ControllersFeature(); + + // Act + manager.PopulateFeature(feature); + + // Assert + Assert.Empty(feature.Values.ToArray()); + } + + private class ControllersPart : ApplicationPart + { + public ControllersPart(string value) + { + Value = value; + } + + public override string Name => "Test"; + + public string Value { get; } + } + + private class ViewComponentsPart : ApplicationPart + { + public ViewComponentsPart(string value) + { + Value = value; + } + + public override string Name => "Other"; + + public string Value { get; } + } + + private class ControllersFeature + { + public IList Values { get; } = new List(); + } + + private class ViewComponentsFeature + { + public IList Values { get; } = new List(); + } + + private class NotControllersedFeatureProvider : IApplicationFeatureProvider + { + private readonly Action _operation; + + public NotControllersedFeatureProvider(Action operation) + { + _operation = operation; + } + + public void PopulateFeature(IEnumerable parts, ViewComponentsFeature feature) + { + foreach (var part in parts.OfType()) + { + _operation(feature, part.Value); + } + } + } + + private class ControllersFeatureProvider : IApplicationFeatureProvider + { + private readonly Action _operation; + + public ControllersFeatureProvider(Action operation) + { + _operation = operation; + } + + public void PopulateFeature(IEnumerable parts, ControllersFeature feature) + { + foreach (var part in parts.OfType()) + { + _operation(feature, part.Value); + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/AssemblyPartTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/AssemblyPartTest.cs new file mode 100644 index 0000000000..591026fa5d --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/AssemblyPartTest.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.Reflection; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public class AssemblyPartTest + { + [Fact] + public void AssemblyPart_Name_ReturnsAssemblyName() + { + // Arrange + var part = new AssemblyPart(typeof(AssemblyPartTest).GetTypeInfo().Assembly); + + // Act + var name = part.Name; + + // Assert + Assert.Equal("Microsoft.AspNetCore.Mvc.Core.Test", name); + } + + [Fact] + public void AssemblyPart_Assembly_ReturnsAssembly() + { + // Arrange + var assembly = typeof(AssemblyPartTest).GetTypeInfo().Assembly; + var part = new AssemblyPart(assembly); + + // Act & Assert + Assert.Equal(part.Assembly, assembly); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcBuilderExtensionsTest.cs index 3c1f1f611e..853330b643 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcBuilderExtensionsTest.cs @@ -3,7 +3,9 @@ using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.MvcServiceCollectionExtensionsTestControllers; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -13,6 +15,46 @@ namespace Microsoft.AspNetCore.Mvc { public class MvcBuilderExtensionsTest { + [Fact] + public void AddApplicationPart_AddsAnApplicationPart_ToTheListOfPartsOnTheBuilder() + { + // Arrange + var manager = new ApplicationPartManager(); + var builder = new MvcBuilder(Mock.Of(), manager); + + var assembly = typeof(MvcBuilder).GetTypeInfo().Assembly; + + // Act + var result = builder.AddApplicationPart(assembly); + + // Assert + Assert.Same(result, builder); + var part = Assert.Single(builder.PartManager.ApplicationParts); + var assemblyPart = Assert.IsType(part); + Assert.Equal(assembly, assemblyPart.Assembly); + } + + [Fact] + public void ConfigureApplicationParts_InvokesSetupAction() + { + // Arrange + var builder = new MvcBuilder( + Mock.Of(), + new ApplicationPartManager()); + + var part = new TestPart(); + + // Act + var result = builder.ConfigureApplicationPartManager(manager => + { + manager.ApplicationParts.Add(part); + }); + + // Assert + Assert.Same(result, builder); + Assert.Equal(new ApplicationPart[] { part }, builder.PartManager.ApplicationParts.ToArray()); + } + [Fact] public void WithControllersAsServices_AddsTypesToControllerTypeProviderAndServiceCollection() { @@ -50,6 +92,11 @@ namespace Microsoft.AspNetCore.Mvc Assert.Equal(controllerTypes, typeProvider.ControllerTypes.OrderBy(c => c.Name).Select(t => t.AsType())); Assert.Equal(ServiceLifetime.Singleton, services[3].Lifetime); } + + private class TestPart : ApplicationPart + { + public override string Name => "Test"; + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreBuilderExtensionsTest.cs new file mode 100644 index 0000000000..1cf271d476 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/MvcCoreBuilderExtensionsTest.cs @@ -0,0 +1,60 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.DependencyInjection +{ + public class MvcCoreBuilderExtensionsTest + { + [Fact] + public void AddApplicationPart_AddsAnApplicationPart_ToTheListOfPartsOnTheBuilder() + { + // Arrange + var manager = new ApplicationPartManager(); + var builder = new MvcCoreBuilder(Mock.Of(), manager); + var assembly = typeof(MvcCoreBuilder).GetTypeInfo().Assembly; + + // Act + var result = builder.AddApplicationPart(assembly); + + // Assert + Assert.Same(result, builder); + var part = Assert.Single(builder.PartManager.ApplicationParts); + var assemblyPart = Assert.IsType(part); + Assert.Equal(assembly, assemblyPart.Assembly); + } + + [Fact] + public void ConfigureApplicationParts_InvokesSetupAction() + { + // Arrange + var builder = new MvcCoreBuilder( + Mock.Of(), + new ApplicationPartManager()); + + var part = new TestPart(); + + // Act + var result = builder.ConfigureApplicationPartManager(manager => + { + manager.ApplicationParts.Add(part); + }); + + // Assert + Assert.Same(result, builder); + Assert.Equal(new ApplicationPart[] { part }, builder.PartManager.ApplicationParts.ToArray()); + } + + private class TestPart : ApplicationPart + { + public override string Name => "Test"; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs index 9079bf0544..0c965860f3 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ComplexTypeModelBinderTest.cs @@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders var bindingContext = CreateContext(GetMetadataForType(typeof(Person)), new Person()); var originalModel = bindingContext.Model; - var binder = new Mock(){ CallBase = true }; + var binder = new Mock() { CallBase = true }; binder .Setup(b => b.CreateModelPublic(It.IsAny())) .Verifiable(); @@ -1067,7 +1067,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders private static TestableComplexTypeModelBinder CreateBinder(ModelMetadata metadata) { var options = new TestOptionsManager(); - MvcCoreMvcOptionsSetup.ConfigureMvc(options.Value, new TestHttpRequestStreamReaderFactory()); + var setup = new MvcCoreMvcOptionsSetup(new TestHttpRequestStreamReaderFactory()); + setup.Configure(options.Value); var lastIndex = options.Value.ModelBinderProviders.Count - 1; Assert.IsType(options.Value.ModelBinderProviders[lastIndex]); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs index a41b6f0d25..f700b403a0 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs @@ -2,10 +2,13 @@ // 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.IO; using System.Net.Http; using System.Reflection; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Internal; @@ -68,6 +71,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var assemblyProvider = new StaticAssemblyProvider(); assemblyProvider.CandidateAssemblies.Add(startupAssembly); services.AddSingleton(assemblyProvider); + + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new AssemblyPart(startupAssembly)); + services.AddSingleton(manager); + } + + private class StaticAssemblyProvider : IAssemblyProvider + { + public IList CandidateAssemblies { get; } = new List(); + + IEnumerable IAssemblyProvider.CandidateAssemblies => CandidateAssemblies; } } } diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs index 12a3769f53..326643696e 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -99,6 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests private static IServiceProvider GetServices(Action updateOptions = null) { var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(new ApplicationPartManager()); serviceCollection.AddMvc(); serviceCollection .AddSingleton() diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TestMvcOptions.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TestMvcOptions.cs index 52a97647b5..9815f15eeb 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TestMvcOptions.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TestMvcOptions.cs @@ -5,6 +5,7 @@ using System.Buffers; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; @@ -20,7 +21,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests public TestMvcOptions() { Value = new MvcOptions(); - MvcCoreMvcOptionsSetup.ConfigureMvc(Value, new TestHttpRequestStreamReaderFactory()); + var optionsSetup = new MvcCoreMvcOptionsSetup(new TestHttpRequestStreamReaderFactory()); + optionsSetup.Configure(Value); + var collection = new ServiceCollection().AddOptions(); collection.AddSingleton(); collection.AddSingleton(); diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultTagHelperActivatorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultTagHelperActivatorTest.cs index 5ccc549f04..eee737ee0b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultTagHelperActivatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultTagHelperActivatorTest.cs @@ -2,8 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultTagHelperFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultTagHelperFactoryTest.cs index 2d40c3b4a1..28c2d4da61 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultTagHelperFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultTagHelperFactoryTest.cs @@ -4,8 +4,8 @@ using System; using System.IO; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { // Arrange var services = new ServiceCollection(); - var builder = new MvcCoreBuilder(services); + var builder = new MvcCoreBuilder(services, new ApplicationPartManager()); builder.InitializeTagHelper((h, vc) => { h.Name = name; @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { // Arrange var services = new ServiceCollection(); - var builder = new MvcCoreBuilder(services); + var builder = new MvcCoreBuilder(services, new ApplicationPartManager()); builder.InitializeTagHelper((h, _) => h.ViewContext = MakeViewContext(MakeHttpContext())); var httpContext = MakeHttpContext(services.BuildServiceProvider()); var viewContext = MakeViewContext(httpContext); @@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { // Arrange var services = new ServiceCollection(); - var builder = new MvcCoreBuilder(services); + var builder = new MvcCoreBuilder(services, new ApplicationPartManager()); builder.InitializeTagHelper((h, vc) => { h.Name = "Test 1"; @@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { // Arrange var services = new ServiceCollection(); - var builder = new MvcCoreBuilder(services); + var builder = new MvcCoreBuilder(services, new ApplicationPartManager()); builder.InitializeTagHelper((h, vc) => { h.Name = "Test 1"; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs index 0a0f53cd26..bd6b59f1ca 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineOptionsTest.cs @@ -1,6 +1,7 @@ // 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.ApplicationParts; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor var fileProvider = new TestFileProvider(); // Act - var builder = new MvcBuilder(services); + var builder = new MvcBuilder(services, new ApplicationPartManager()); builder.AddRazorOptions(options => { options.FileProviders.Add(fileProvider); diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs index a270bbc19e..4ae1d69223 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcOptionsSetupTest.cs @@ -9,6 +9,7 @@ using System.Xml; using System.Xml.Linq; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Internal; @@ -148,7 +149,7 @@ namespace Microsoft.AspNetCore.Mvc // Arrange & Act var options = GetOptions(services => { - var builder = new MvcCoreBuilder(services); + var builder = new MvcCoreBuilder(services, new ApplicationPartManager()); builder.AddXmlDataContractSerializerFormatters(); }); @@ -238,6 +239,7 @@ namespace Microsoft.AspNetCore.Mvc private static IServiceProvider GetServiceProvider(Action action = null) { var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(new ApplicationPartManager()); serviceCollection.AddMvc(); serviceCollection .AddSingleton() diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index d7ae2a0a57..ff4492cc08 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApiExplorer; @@ -41,6 +42,7 @@ namespace Microsoft.AspNetCore.Mvc { // Arrange var services = new ServiceCollection(); + services.AddSingleton(GetHostingEnvironment()); // Register a mock implementation of each service, AddMvcServices should add another implemenetation. foreach (var serviceType in MutliRegistrationServiceTypes) @@ -69,6 +71,7 @@ namespace Microsoft.AspNetCore.Mvc { // Arrange var services = new ServiceCollection(); + services.AddSingleton(GetHostingEnvironment()); // Register a mock implementation of each service, AddMvcServices should not replace it. foreach (var serviceType in SingleRegistrationServiceTypes) @@ -92,6 +95,7 @@ namespace Microsoft.AspNetCore.Mvc { // Arrange var services = new ServiceCollection(); + services.AddSingleton(GetHostingEnvironment()); // Act services.AddMvc(); @@ -123,6 +127,7 @@ namespace Microsoft.AspNetCore.Mvc get { var services = new ServiceCollection(); + services.AddSingleton(GetHostingEnvironment()); services.AddMvc(); var multiRegistrationServiceTypes = MutliRegistrationServiceTypes; @@ -266,5 +271,15 @@ namespace Microsoft.AspNetCore.Mvc $"Found multiple instances of {implementationType} registered as {serviceType}"); } } + + private IHostingEnvironment GetHostingEnvironment() + { + var environment = new Mock(); + environment + .Setup(e => e.ApplicationName) + .Returns(typeof(MvcServiceCollectionExtensionsTest).GetTypeInfo().Assembly.GetName().Name); + + return environment.Object; + } } }