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
+ 5192957394
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
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": { }
+ }
+}