From 7ed2de297e4744ee261d8fecbfc0f79df8c7916e Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 4 Sep 2014 13:20:02 -0700 Subject: [PATCH] moving global filters to options --- Mvc.sln | 13 + samples/MvcSample.Web/MvcSample.Web.kproj | 1 + samples/MvcSample.Web/Startup.cs | 11 +- src/Microsoft.AspNet.Mvc.Core/Controller.cs | 11 +- .../Filters/DefaultFilterProvider.cs | 14 +- .../Filters/DefaultGlobalFilterProvider.cs | 37 +++ .../Filters/FilterCollectionExtensions.cs | 105 ++++++++ .../Filters/FilterDescriptor.cs | 37 ++- .../Filters/IGlobalFilterProvider.cs | 18 ++ src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs | 16 +- .../ReflectedActionDescriptorProvider.cs | 11 +- .../ReflectedActionModel.cs | 13 +- .../ReflectedControllerModel.cs | 5 +- .../Rendering/IViewEngineProvider.cs | 2 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 4 + .../ActionAttributeTests.cs | 2 +- ...iscoveryConventionsActionSelectionTests.cs | 2 +- .../Filters/DefaultFilterProviderTest.cs | 224 ++++++++++++++++++ .../Filters/FilterCollectionExtensionsTest.cs | 117 +++++++++ .../ReflectedActionDescriptorProviderTests.cs | 7 +- .../TestGlobalFilterProvider.cs | 29 +++ .../FiltersTest.cs | 59 +++++ .../project.json | 3 +- .../Controllers/ProductsController.cs | 29 +++ .../Filters/GlobalExceptionFilter.cs | 19 ++ .../Filters/PassThroughActionFilter.cs | 11 + .../Filters/PassThroughResultFilter.cs | 11 + .../FiltersWebSite/FiltersWebSite.kproj | 30 +++ test/WebSites/FiltersWebSite/Startup.cs | 31 +++ test/WebSites/FiltersWebSite/project.json | 11 + 30 files changed, 848 insertions(+), 35 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/DefaultGlobalFilterProvider.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/FilterCollectionExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IGlobalFilterProvider.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Filters/DefaultFilterProviderTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Filters/FilterCollectionExtensionsTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/TestGlobalFilterProvider.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/FiltersTest.cs create mode 100644 test/WebSites/FiltersWebSite/Controllers/ProductsController.cs create mode 100644 test/WebSites/FiltersWebSite/Filters/GlobalExceptionFilter.cs create mode 100644 test/WebSites/FiltersWebSite/Filters/PassThroughActionFilter.cs create mode 100644 test/WebSites/FiltersWebSite/Filters/PassThroughResultFilter.cs create mode 100644 test/WebSites/FiltersWebSite/FiltersWebSite.kproj create mode 100644 test/WebSites/FiltersWebSite/Startup.cs create mode 100644 test/WebSites/FiltersWebSite/project.json diff --git a/Mvc.sln b/Mvc.sln index 7160ae03c8..a0d0a70a8c 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -74,6 +74,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "AntiForgeryWebSite", "test\ EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "AddServicesWebSite", "test\WebSites\AddServicesWebSite\AddServicesWebSite.kproj", "{6A0B65CE-6B01-40D0-840D-EFF3680D1547}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FiltersWebSite", "test\WebSites\FiltersWebSite\FiltersWebSite.kproj", "{1976AC4A-FEA4-4587-A158-D9F79736D2B6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -374,6 +376,16 @@ Global {6A0B65CE-6B01-40D0-840D-EFF3680D1547}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {6A0B65CE-6B01-40D0-840D-EFF3680D1547}.Release|Mixed Platforms.Build.0 = Release|Any CPU {6A0B65CE-6B01-40D0-840D-EFF3680D1547}.Release|x86.ActiveCfg = Release|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Release|Any CPU.Build.0 = Release|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1976AC4A-FEA4-4587-A158-D9F79736D2B6}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -409,5 +421,6 @@ Global {C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {A353B17E-A940-4CE8-8BF9-179E24A9041F} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {6A0B65CE-6B01-40D0-840D-EFF3680D1547} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {1976AC4A-FEA4-4587-A158-D9F79736D2B6} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/samples/MvcSample.Web/MvcSample.Web.kproj b/samples/MvcSample.Web/MvcSample.Web.kproj index 4ba2a5664d..bc8c95face 100644 --- a/samples/MvcSample.Web/MvcSample.Web.kproj +++ b/samples/MvcSample.Web/MvcSample.Web.kproj @@ -15,6 +15,7 @@ 2.0 + 51929 57394 diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index 1c4aed6f8a..81131c7e7f 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -4,13 +4,13 @@ using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; using Microsoft.Framework.ConfigurationModel; using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; using MvcSample.Web.Filters; using MvcSample.Web.Services; #if ASPNET50 using Autofac; using Microsoft.Framework.DependencyInjection.Autofac; -using Microsoft.Framework.OptionsModel; #endif namespace MvcSample.Web @@ -42,6 +42,10 @@ namespace MvcSample.Web // sample's assemblies are loaded. This prevents loading controllers from other assemblies // when the sample is used in the Functional Tests. services.AddTransient>(); + services.SetupOptions(options => + { + options.Filters.Add(typeof(PassThroughAttribute), order: 17); + }); // Create the autofac container ContainerBuilder builder = new ContainerBuilder(); @@ -72,6 +76,11 @@ namespace MvcSample.Web // sample's assemblies are loaded. This prevents loading controllers from other assemblies // when the sample is used in the Functional Tests. services.AddTransient>(); + + services.SetupOptions(options => + { + options.Filters.Add(typeof(PassThroughAttribute), order: 17); + }); }); } diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index da5b5d98f4..9b3e79c39a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -12,7 +12,7 @@ using Microsoft.AspNet.Mvc.Rendering; namespace Microsoft.AspNet.Mvc { - public class Controller : IActionFilter, IAsyncActionFilter + public class Controller : IActionFilter, IAsyncActionFilter, IOrderedFilter { private DynamicViewData _viewBag; @@ -80,6 +80,15 @@ namespace Microsoft.AspNet.Mvc } } + int IOrderedFilter.Order + { + get + { + // Controller-filter methods run closest the action by default. + return int.MaxValue; + } + } + /// /// Creates a object that renders a view to the response. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/DefaultFilterProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/DefaultFilterProvider.cs index a16df81fe0..d9f9514166 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/DefaultFilterProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/DefaultFilterProvider.cs @@ -10,12 +10,9 @@ namespace Microsoft.AspNet.Mvc.Filters { public class DefaultFilterProvider : INestedProvider { - private readonly ITypeActivator _typeActivator; - - public DefaultFilterProvider(IServiceProvider serviceProvider, ITypeActivator typeActivator) + public DefaultFilterProvider(IServiceProvider serviceProvider) { ServiceProvider = serviceProvider; - _typeActivator = typeActivator; } public int Order @@ -78,15 +75,6 @@ namespace Microsoft.AspNet.Mvc.Filters private void InsertControllerAsFilter(FilterProviderContext context, IFilter controllerFilter) { - // If the controller implements a filter, and doesn't specify order, then it should - // run closest to the action. - var order = Int32.MaxValue; - var orderedControllerFilter = controllerFilter as IOrderedFilter; - if (orderedControllerFilter != null) - { - order = orderedControllerFilter.Order; - } - var descriptor = new FilterDescriptor(controllerFilter, FilterScope.Controller); var item = new FilterItem(descriptor, controllerFilter); diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/DefaultGlobalFilterProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/DefaultGlobalFilterProvider.cs new file mode 100644 index 0000000000..0fce8c272f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/DefaultGlobalFilterProvider.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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; +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Mvc.Filters +{ + /// + /// An implementation of based on . + /// + public class DefaultGlobalFilterProvider : IGlobalFilterProvider + { + private readonly IReadOnlyList _filters; + + /// + /// Creates a new instance of . + /// + /// The options accessor for . + public DefaultGlobalFilterProvider(IOptionsAccessor optionsAccessor) + { + var filters = optionsAccessor.Options.Filters; + _filters = filters.ToList(); + } + + /// + public IReadOnlyList Filters + { + get + { + return _filters; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/FilterCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/FilterCollectionExtensions.cs new file mode 100644 index 0000000000..73f4dd6601 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/FilterCollectionExtensions.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Mvc.Core; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Extension methods for adding filters to the global filters collection. + /// + public static class FilterCollectionExtensions + { + /// + /// Adds a type representing an to a filter collection. + /// + /// A collection of . + /// Type representing an . + /// An representing the added type. + /// + /// Filter instances will be created using . + /// Use to register a service as a filter. + /// + public static IFilter Add( + [NotNull] this ICollection filters, + [NotNull] Type filterType) + { + return Add(filters, filterType, order: 0); + } + + /// + /// Adds a type representing an to a filter collection. + /// + /// A collection of . + /// Type representing an . + /// The order of the added filter. + /// An representing the added type. + /// + /// Filter instances will be created using . + /// Use to register a service as a filter. + /// + public static IFilter Add( + [NotNull] this ICollection filters, + [NotNull] Type filterType, + int order) + { + if (!typeof(IFilter).IsAssignableFrom(filterType)) + { + var message = Resources.FormatTypeMustDeriveFromType(filterType.FullName, typeof(IFilter).FullName); + throw new ArgumentException(message, nameof(filterType)); + } + + var filter = new TypeFilterAttribute(filterType) { Order = order }; + filters.Add(filter); + return filter; + } + + /// + /// Adds a type representing an to a filter collection. + /// + /// A collection of . + /// Type representing an . + /// An representing the added service type. + /// + /// Filter instances will created through dependency injection. Use + /// to register a service that will be created via + /// type activation. + /// + public static IFilter AddService( + [NotNull] this ICollection filters, + [NotNull] Type filterType) + { + return AddService(filters, filterType, order: 0); + } + + /// + /// Adds a type representing an to a filter collection. + /// + /// A collection of . + /// Type representing an . + /// The order of the added filter. + /// An representing the added service type. + /// + /// Filter instances will created through dependency injection. Use + /// to register a service that will be created via + /// type activation. + /// + public static IFilter AddService( + [NotNull] this ICollection filters, + [NotNull] Type filterType, + int order) + { + if (!typeof(IFilter).IsAssignableFrom(filterType)) + { + var message = Resources.FormatTypeMustDeriveFromType(filterType.FullName, typeof(IFilter).FullName); + throw new ArgumentException(message, nameof(filterType)); + } + + var filter = new ServiceFilterAttribute(filterType) { Order = order }; + filters.Add(filter); + return filter; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/FilterDescriptor.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/FilterDescriptor.cs index ddade4f560..44d9e62d1a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/FilterDescriptor.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/FilterDescriptor.cs @@ -3,8 +3,34 @@ namespace Microsoft.AspNet.Mvc { + /// + /// Descriptor for an . + /// + /// + /// describes an with an order and scope. + /// + /// Order and scope control the execution order of filters. Filters with a higher value of Order execute + /// later in the pipeline. + /// + /// When filters have the same Order, the Scope value is used to determine the order of execution. Filters + /// with a higher value of Scope execute later in the pipeline. See for commonly + /// used scopes. + /// + /// For implementions, the filter runs only after an exception has occurred, + /// and so the observed order of execution will be opposite that of other filters. + /// public class FilterDescriptor { + /// + /// Creates a new . + /// + /// The . + /// The filter scope. + /// + /// If the implements , then the value of + /// will be taken from . Otherwise the value + /// of will default to 0. + /// public FilterDescriptor([NotNull] IFilter filter, int filterScope) { Filter = filter; @@ -18,10 +44,19 @@ namespace Microsoft.AspNet.Mvc } } + /// + /// The instance. + /// public IFilter Filter { get; private set; } - public int Order { get; private set; } + /// + /// The filter order. + /// + public int Order { get; set; } + /// + /// The filter scope. + /// public int Scope { get; private set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IGlobalFilterProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IGlobalFilterProvider.cs new file mode 100644 index 0000000000..600d859f83 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IGlobalFilterProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Mvc.Filters +{ + /// + /// Provides access to the collection of for globally registered filters. + /// + public interface IGlobalFilterProvider + { + /// + /// Gets the collection of . + /// + IReadOnlyList Filters { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs index bb947d4fcb..895b0941f5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.OptionDescriptors; using Microsoft.AspNet.Mvc.ReflectedModelBuilder; @@ -25,6 +26,7 @@ namespace Microsoft.AspNet.Mvc ValueProviderFactories = new List(); OutputFormatters = new List(); InputFormatters = new List(); + Filters = new List(); } /// @@ -51,13 +53,19 @@ namespace Microsoft.AspNet.Mvc } /// - /// Get a list of the which are used to construct + /// Gets a list of which are used to construct filters that + /// apply to all actions. + /// + public ICollection Filters { get; private set; } + + /// + /// Gets a list of the which are used to construct /// a list of by . /// public List OutputFormatters { get; private set; } /// - /// Get a list of the which are used to construct + /// Gets a list of the which are used to construct /// a list of by . /// public List InputFormatters { get; private set; } @@ -86,13 +94,13 @@ namespace Microsoft.AspNet.Mvc } /// - /// Get a list of the used by the + /// Gets a list of the used by the /// . /// public List ModelBinders { get; private set; } /// - /// Get a list of the s used by + /// Gets a list of the s used by /// . /// public List ModelValidatorProviders { get; } = diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs index 0b08315ec9..7e4cbb2602 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Mvc.ReflectedModelBuilder; using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Routing; @@ -27,19 +28,19 @@ namespace Microsoft.AspNet.Mvc private readonly IControllerAssemblyProvider _controllerAssemblyProvider; private readonly IActionDiscoveryConventions _conventions; - private readonly IEnumerable _globalFilters; + private readonly IReadOnlyList _globalFilters; private readonly IEnumerable _modelConventions; private readonly IInlineConstraintResolver _constraintResolver; public ReflectedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider, IActionDiscoveryConventions conventions, - IEnumerable globalFilters, + IGlobalFilterProvider globalFilters, IOptionsAccessor optionsAccessor, IInlineConstraintResolver constraintResolver) { _controllerAssemblyProvider = controllerAssemblyProvider; _conventions = conventions; - _globalFilters = globalFilters ?? Enumerable.Empty(); + _globalFilters = globalFilters.Filters; _modelConventions = optionsAccessor.Options.ApplicationModelConventions; _constraintResolver = constraintResolver; } @@ -352,8 +353,8 @@ namespace Microsoft.AspNet.Mvc IEnumerable controllerFilters, IEnumerable globalFilters) { - actionDescriptor.FilterDescriptors = actionFilters - .Select(f => new FilterDescriptor(f, FilterScope.Action)) + actionDescriptor.FilterDescriptors = + actionFilters.Select(f => new FilterDescriptor(f, FilterScope.Action)) .Concat(controllerFilters.Select(f => new FilterDescriptor(f, FilterScope.Controller))) .Concat(globalFilters.Select(f => new FilterDescriptor(f, FilterScope.Global))) .OrderBy(d => d, FilterDescriptorOrderComparer.Comparer) diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs index 944511ada4..1fe10bdced 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedActionModel.cs @@ -18,7 +18,16 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder // is needed to so that the result of ToList() is List Attributes = actionMethod.GetCustomAttributes(inherit: true).OfType().ToList(); - Filters = Attributes.OfType().ToList(); + Filters = Attributes + .OfType() + .ToList(); + + var routeTemplateAttribute = Attributes.OfType().FirstOrDefault(); + if (routeTemplateAttribute != null) + { + AttributeRouteModel = new ReflectedAttributeRouteModel(routeTemplateAttribute); + } + HttpMethods = new List(); Parameters = new List(); } @@ -39,4 +48,4 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder public ReflectedAttributeRouteModel AttributeRouteModel { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedControllerModel.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedControllerModel.cs index 97cf0e593d..760e616bba 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedControllerModel.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedModelBuilder/ReflectedControllerModel.cs @@ -21,7 +21,10 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder // is needed to so that the result of ToList() is List Attributes = ControllerType.GetCustomAttributes(inherit: true).OfType().ToList(); - Filters = Attributes.OfType().ToList(); + Filters = Attributes + .OfType() + .ToList(); + RouteConstraints = Attributes.OfType().ToList(); AttributeRoutes = Attributes.OfType() diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/IViewEngineProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/IViewEngineProvider.cs index c6cef3e82d..5fed0e6e1d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/IViewEngineProvider.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/IViewEngineProvider.cs @@ -15,4 +15,4 @@ namespace Microsoft.AspNet.Mvc.Rendering /// IReadOnlyList ViewEngines { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 5458c69779..fe4fbd6de0 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -79,6 +79,10 @@ namespace Microsoft.AspNet.Mvc yield return describe.Instance( new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false)); + // The IGlobalFilterProvider is used to build the action descriptors (likely once) and so should + // remain transient to avoid keeping it in memory. + yield return describe.Transient(); + yield return describe.Transient, DefaultFilterProvider>(); yield return describe.Transient(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs index 540e45fa0b..2e810cbf8d 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs @@ -226,7 +226,7 @@ namespace Microsoft.AspNet.Mvc return new ReflectedActionDescriptorProvider( controllerAssemblyProvider, actionDiscoveryConventions, - null, + new TestGlobalFilterProvider(), new MockMvcOptionsAccessor(), Mock.Of()); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs index f853b9b446..859943f24f 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionDiscoveryConventionsActionSelectionTests.cs @@ -196,7 +196,7 @@ namespace Microsoft.AspNet.Mvc return new ReflectedActionDescriptorProvider( controllerAssemblyProvider.Object, actionDiscoveryConventions, - null, + new TestGlobalFilterProvider(), new MockMvcOptionsAccessor(), Mock.Of()); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/DefaultFilterProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/DefaultFilterProviderTest.cs new file mode 100644 index 0000000000..e5c3c6ef12 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/DefaultFilterProviderTest.cs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.ComponentModel.Design; +using System.Linq; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; +using Xunit; +using Moq; + +namespace Microsoft.AspNet.Mvc.Filters +{ + public class DefaultFilterProviderTest + { + [Fact] + public void DefaultFilterProvider_UsesFilter_WhenItsNotIFilterFactory() + { + // Arrange + var filter = Mock.Of(); + + var context = CreateFilterContext(new List() + { + new FilterItem(new FilterDescriptor(filter, FilterScope.Global)), + }); + + var provider = CreateProvider(); + + //System.Diagnostics.Debugger.Launch(); + //System.Diagnostics.Debugger.Break(); + // Act + provider.Invoke(context, () => { }); + var results = context.Results; + + // Assert + var item = Assert.Single(results); + Assert.Same(filter, item.Filter); + Assert.Same(filter, item.Descriptor.Filter); + Assert.Equal(0, item.Descriptor.Order); + } + + [Fact] + public void DefaultFilterProvider_UsesFilterFactory() + { + // Arrange + var filter = Mock.Of(); + + var filterFactory = new Mock(); + filterFactory + .Setup(ff => ff.CreateInstance(It.IsAny())) + .Returns(filter); + + var context = CreateFilterContext(new List() + { + new FilterItem(new FilterDescriptor(filterFactory.Object, FilterScope.Global)), + }); + + var provider = CreateProvider(); + + // Act + provider.Invoke(context, () => { }); + var results = context.Results; + + // Assert + var item = Assert.Single(results); + Assert.Same(filter, item.Filter); + Assert.Same(filterFactory.Object, item.Descriptor.Filter); + Assert.Equal(0, item.Descriptor.Order); + } + + [Fact] + public void DefaultFilterProvider_UsesFilterFactory_WithOrder() + { + // Arrange + var filter = Mock.Of(); + + var filterFactory = new Mock(); + filterFactory + .Setup(ff => ff.CreateInstance(It.IsAny())) + .Returns(filter); + + filterFactory.As().SetupGet(ff => ff.Order).Returns(17); + + var context = CreateFilterContext(new List() + { + new FilterItem(new FilterDescriptor(filterFactory.Object, FilterScope.Global)), + }); + + var provider = CreateProvider(); + + // Act + provider.Invoke(context, () => { }); + var results = context.Results; + + // Assert + var item = Assert.Single(results); + Assert.Same(filter, item.Filter); + Assert.Same(filterFactory.Object, item.Descriptor.Filter); + Assert.Equal(17, item.Descriptor.Order); + } + + [Fact] + public void DefaultFilterProvider_UsesFilterFactory_WithIFilterContainer() + { + // Arrange + var filter = new Mock(); + filter.SetupAllProperties(); + + var filterFactory = new Mock(); + filterFactory + .Setup(ff => ff.CreateInstance(It.IsAny())) + .Returns(filter.As().Object); + + var context = CreateFilterContext(new List() + { + new FilterItem(new FilterDescriptor(filterFactory.Object, FilterScope.Global)), + }); + + var provider = CreateProvider(); + + // Act + provider.Invoke(context, () => { }); + var results = context.Results; + + // Assert + var item = Assert.Single(results); + Assert.Same(filter.Object, item.Filter); + Assert.Same(filterFactory.Object, ((IFilterContainer)item.Filter).FilterDefinition); + Assert.Same(filterFactory.Object, item.Descriptor.Filter); + Assert.Equal(0, item.Descriptor.Order); + } + + [Fact] + public void DefaultFilterProvider_InsertsController_DefaultOrder() + { + // Arrange + var filter1 = new Mock(); + filter1.SetupGet(f => f.Order).Returns(int.MaxValue); + + var filter2 = new Mock(); + filter2.SetupGet(f => f.Order).Returns(int.MaxValue); + + var context = CreateFilterContext(new List() + { + new FilterItem(new FilterDescriptor(filter1.Object, FilterScope.Global)), + new FilterItem(new FilterDescriptor(filter2.Object, FilterScope.Action)), + }); + + var controller = new Controller(); + context.ActionContext.Controller = controller; + + var provider = CreateProvider(); + + // Act + provider.Invoke(context, () => { }); + var results = context.Results; + + // Assert + var controllerItem = results[1]; + Assert.Same(controller, controllerItem.Filter); + Assert.Same(controller, controllerItem.Descriptor.Filter); + Assert.Equal(FilterScope.Controller, controllerItem.Descriptor.Scope); + Assert.Equal(Int32.MaxValue, controllerItem.Descriptor.Order); + } + + [Fact] + public void DefaultFilterProvider_InsertsController_CustomOrder() + { + // Arrange + var filter1 = new Mock(); + filter1.SetupGet(f => f.Order).Returns(0); + + var filter2 = new Mock(); + filter2.SetupGet(f => f.Order).Returns(int.MaxValue); + + var context = CreateFilterContext(new List() + { + new FilterItem(new FilterDescriptor(filter1.Object, FilterScope.Global)), + new FilterItem(new FilterDescriptor(filter2.Object, FilterScope.Action)), + }); + + var controller = new Mock(); + controller.SetupGet(f => f.Order).Returns(17); + + context.ActionContext.Controller = controller.Object; + + var provider = CreateProvider(); + + // Act + provider.Invoke(context, () => { }); + var results = context.Results; + + // Assert + var controllerItem = results[1]; + Assert.Same(controller.Object, controllerItem.Filter); + Assert.Same(controller.Object, controllerItem.Descriptor.Filter); + Assert.Equal(FilterScope.Controller, controllerItem.Descriptor.Scope); + Assert.Equal(17, controllerItem.Descriptor.Order); + } + + private DefaultFilterProvider CreateProvider() + { + var services = new ServiceContainer(); + + return new DefaultFilterProvider(services); + } + + private FilterProviderContext CreateFilterContext(List items) + { + var actionContext = CreateActionContext(); + actionContext.ActionDescriptor.FilterDescriptors = new List( + items.Select(item => item.Descriptor)); + + return new FilterProviderContext(actionContext, items); + } + + private ActionContext CreateActionContext() + { + return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FilterCollectionExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FilterCollectionExtensionsTest.cs new file mode 100644 index 0000000000..996a3e5e4f --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/FilterCollectionExtensionsTest.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.ObjectModel; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class FilterCollectionExtensionsTest + { + [Fact] + public void Add_UsesTypeFilterAttribute() + { + // Arrange + var collection = new Collection(); + + // Act + var added = collection.Add(typeof(MyFilter)); + + // Assert + var typeFilter = Assert.IsType(added); + Assert.Equal(typeof(MyFilter), typeFilter.ImplementationType); + Assert.Same(typeFilter, Assert.Single(collection)); + } + + [Fact] + public void Add_WithOrder_SetsOrder() + { + // Arrange + var collection = new Collection(); + + // Act + var added = collection.Add(typeof(MyFilter), 17); + + // Assert + Assert.Equal(17, Assert.IsAssignableFrom(added).Order); + } + + [Fact] + public void Add_ThrowsOnNonIFilter() + { + // Arrange + var collection = new Collection(); + + var expectedMessage = + "The type 'Microsoft.AspNet.Mvc.FilterCollectionExtensionsTest+NonFilter' must derive from " + + "'Microsoft.AspNet.Mvc.IFilter'." + Environment.NewLine + + "Parameter name: filterType"; + + // Act & Assert + var ex = Assert.Throws(() => { collection.Add(typeof(NonFilter)); }); + + // Assert + Assert.Equal(expectedMessage, ex.Message); + } + + [Fact] + public void AddService_UsesServiceFilterAttribute() + { + // Arrange + var collection = new Collection(); + + // Act + var added = collection.AddService(typeof(MyFilter)); + + // Assert + var serviceFilter = Assert.IsType(added); + Assert.Equal(typeof(MyFilter), serviceFilter.ServiceType); + Assert.Same(serviceFilter, Assert.Single(collection)); + } + + [Fact] + public void AddService_SetsOrder() + { + // Arrange + var collection = new Collection(); + + // Act + var added = collection.AddService(typeof(MyFilter), 17); + + // Assert + Assert.Equal(17, Assert.IsAssignableFrom(added).Order); + } + + [Fact] + public void AddService_ThrowsOnNonIFilter() + { + // Arrange + var collection = new Collection(); + + var expectedMessage = + "The type 'Microsoft.AspNet.Mvc.FilterCollectionExtensionsTest+NonFilter' must derive from " + + "'Microsoft.AspNet.Mvc.IFilter'." + Environment.NewLine + + "Parameter name: filterType"; + + // Act & Assert + var ex = Assert.Throws(() => { collection.AddService(typeof(NonFilter)); }); + + // Assert + Assert.Equal(expectedMessage, ex.Message); + } + + private class MyFilter : IFilter, IOrderedFilter + { + public int Order + { + get; + set; + } + } + + private class NonFilter + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs index 069689b43b..7da7bd1375 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionDescriptorProviderTests.cs @@ -9,6 +9,7 @@ using Microsoft.AspNet.Routing; using Microsoft.AspNet.Mvc.Routing; using Moq; using Xunit; +using Microsoft.AspNet.Mvc.Filters; namespace Microsoft.AspNet.Mvc.Test { @@ -1029,7 +1030,7 @@ namespace Microsoft.AspNet.Mvc.Test var provider = new ReflectedActionDescriptorProvider( assemblyProvider.Object, conventions, - filters, + new TestGlobalFilterProvider(filters), new MockMvcOptionsAccessor(), Mock.Of()); @@ -1049,7 +1050,7 @@ namespace Microsoft.AspNet.Mvc.Test var provider = new ReflectedActionDescriptorProvider( assemblyProvider.Object, conventions, - Enumerable.Empty(), + new TestGlobalFilterProvider(), new MockMvcOptionsAccessor(), Mock.Of()); @@ -1068,7 +1069,7 @@ namespace Microsoft.AspNet.Mvc.Test var provider = new ReflectedActionDescriptorProvider( assemblyProvider.Object, conventions, - null, + new TestGlobalFilterProvider(), new MockMvcOptionsAccessor(), null); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/TestGlobalFilterProvider.cs b/test/Microsoft.AspNet.Mvc.Core.Test/TestGlobalFilterProvider.cs new file mode 100644 index 0000000000..9ee6347502 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/TestGlobalFilterProvider.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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; +using Microsoft.AspNet.Mvc.Filters; + +namespace Microsoft.AspNet.Mvc +{ + public class TestGlobalFilterProvider : IGlobalFilterProvider + { + public TestGlobalFilterProvider() + : this(null) + { + } + + public TestGlobalFilterProvider(IEnumerable filters) + { + var filterList = new List(); + Filters = filterList; + + if (filters != null) + { + filterList.AddRange(filters); + } + } + + public IReadOnlyList Filters { get; private set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/FiltersTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/FiltersTest.cs new file mode 100644 index 0000000000..b505f3f5a2 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/FiltersTest.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Net; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class FiltersTest + { + private readonly IServiceProvider _services = TestHelper.CreateServices("FiltersWebSite"); + private readonly Action _app = new FiltersWebSite.Startup().Configure; + + [Fact] + public async Task ListAllFilters() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/Products/GetPrice/5"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal(19.95m, result); + + var filters = response.Headers.GetValues("filters"); + Assert.Equal( + new string[] + { + // This one uses order to set itself 'first' even though it appears on the controller + "FiltersWebSite.PassThroughActionFilter", + + // Configured as global with default order + "FiltersWebSite.GlobalExceptionFilter", + + // Configured on the controller with default order + "FiltersWebSite.PassThroughResultFilter", + + // Configured on the action with default order + "FiltersWebSite.PassThroughActionFilter", + + // The controller itself + "FiltersWebSite.ProductsController", + }, + filters); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 17561a23d1..82ca7dda62 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -8,7 +8,8 @@ "AntiForgeryWebSite": "", "BasicWebSite": "", "CompositeViewEngine": "", - "ConnegWebsite": "", + "ConnegWebsite": "", + "FiltersWebSite": "", "FormatterWebSite": "", "InlineConstraintsWebSite": "", "Microsoft.AspNet.TestHost": "1.0.0-*", diff --git a/test/WebSites/FiltersWebSite/Controllers/ProductsController.cs b/test/WebSites/FiltersWebSite/Controllers/ProductsController.cs new file mode 100644 index 0000000000..e457849b29 --- /dev/null +++ b/test/WebSites/FiltersWebSite/Controllers/ProductsController.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; +using System.Linq; + +namespace FiltersWebSite +{ + // This controller will list the filters that are configured for each action in a header. + // This exercises the merging of filters with the global filters collection. + [PassThroughActionFilter(Order = -2)] + [PassThroughResultFilter] + public class ProductsController : Controller + { + [PassThroughActionFilter] + public decimal GetPrice(int id) + { + return 19.95m; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + // Log the filter names in a header + context.HttpContext.Response.Headers.Add( + "filters", + context.Filters.Select(f => f.GetType().FullName).ToArray()); + } + } +} \ No newline at end of file diff --git a/test/WebSites/FiltersWebSite/Filters/GlobalExceptionFilter.cs b/test/WebSites/FiltersWebSite/Filters/GlobalExceptionFilter.cs new file mode 100644 index 0000000000..15151d5e46 --- /dev/null +++ b/test/WebSites/FiltersWebSite/Filters/GlobalExceptionFilter.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace FiltersWebSite +{ + public class GlobalExceptionFilter : IExceptionFilter + { + public void OnException(ExceptionContext context) + { + context.Result = new ContentResult() + { + Content = "GlobalExceptionFilter.OnException", + ContentType = "text/plain", + }; + } + } +} \ No newline at end of file diff --git a/test/WebSites/FiltersWebSite/Filters/PassThroughActionFilter.cs b/test/WebSites/FiltersWebSite/Filters/PassThroughActionFilter.cs new file mode 100644 index 0000000000..b1eb7188ee --- /dev/null +++ b/test/WebSites/FiltersWebSite/Filters/PassThroughActionFilter.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace FiltersWebSite +{ + public class PassThroughActionFilter : ActionFilterAttribute + { + } +} \ No newline at end of file diff --git a/test/WebSites/FiltersWebSite/Filters/PassThroughResultFilter.cs b/test/WebSites/FiltersWebSite/Filters/PassThroughResultFilter.cs new file mode 100644 index 0000000000..d56ea51169 --- /dev/null +++ b/test/WebSites/FiltersWebSite/Filters/PassThroughResultFilter.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace FiltersWebSite +{ + public class PassThroughResultFilter : ResultFilterAttribute + { + } +} \ No newline at end of file diff --git a/test/WebSites/FiltersWebSite/FiltersWebSite.kproj b/test/WebSites/FiltersWebSite/FiltersWebSite.kproj new file mode 100644 index 0000000000..e64716c7e1 --- /dev/null +++ b/test/WebSites/FiltersWebSite/FiltersWebSite.kproj @@ -0,0 +1,30 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + Debug + AnyCPU + + + + 1976ac4a-fea4-4587-a158-d9f79736d2b6 + Web + FiltersWebSite + + + ConsoleDebugger + + + WebDebugger + + + + + 2.0 + 25980 + + + \ No newline at end of file diff --git a/test/WebSites/FiltersWebSite/Startup.cs b/test/WebSites/FiltersWebSite/Startup.cs new file mode 100644 index 0000000000..9cd88a4a27 --- /dev/null +++ b/test/WebSites/FiltersWebSite/Startup.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; + +namespace FiltersWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(configuration); + + services.SetupOptions(options => + { + options.Filters.Add(new GlobalExceptionFilter()); + }); + }); + + app.UseMvc(); + } + } +} diff --git a/test/WebSites/FiltersWebSite/project.json b/test/WebSites/FiltersWebSite/project.json new file mode 100644 index 0000000000..fc12010a7d --- /dev/null +++ b/test/WebSites/FiltersWebSite/project.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "Microsoft.AspNet.Mvc": "", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "" + }, + "frameworks" : { + "aspnet50": { }, + "aspnetcore50": { } + } +}