diff --git a/LICENSE b/LICENSE index 3e59e20..7e4a3d3 100644 --- a/LICENSE +++ b/LICENSE @@ -20,3 +20,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +This software includes code from the open source Microsoft ASP.NET web stack, +licensed under the Apache 2.0 license. You may obtain a copy of the license at +http://www.apache.org/licenses/LICENSE-2.0 \ No newline at end of file diff --git a/src/Autofac.Integration.WebApi/ActionFilterWrapper.cs b/src/Autofac.Integration.WebApi/ActionFilterWrapper.cs deleted file mode 100644 index 6faa8dd..0000000 --- a/src/Autofac.Integration.WebApi/ActionFilterWrapper.cs +++ /dev/null @@ -1,169 +0,0 @@ -// This software is part of the Autofac IoC container -// Copyright (c) 2012 Autofac Contributors -// https://autofac.org -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using Autofac.Features.Metadata; - -namespace Autofac.Integration.WebApi -{ - /// - /// Resolves a filter for the specified metadata for each controller request. - /// - [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Derived attribute adds filter override support")] - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - internal class ActionFilterWrapper : ActionFilterAttribute, IAutofacActionFilter - { - private readonly HashSet _allFilters; - - /// - /// Initializes a new instance of the class. - /// - /// The collection of filter metadata blocks that this wrapper should run. - public ActionFilterWrapper(HashSet filterMetadata) - { - if (filterMetadata == null) - { - throw new ArgumentNullException(nameof(filterMetadata)); - } - - _allFilters = filterMetadata; - } - - /// - /// Occurs after the action method is invoked. - /// - /// The context for the action. - /// A cancellation token for signaling task ending. - /// - /// Thrown if is . - /// - public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) - { - if (actionExecutedContext == null) - { - throw new ArgumentNullException(nameof(actionExecutedContext)); - } - - var dependencyScope = actionExecutedContext.Request.GetDependencyScope(); - var lifetimeScope = dependencyScope.GetRequestLifetimeScope(); - - var filters = lifetimeScope.Resolve>>>(); - - // Issue #16: OnActionExecuted needs to happen in the opposite order of OnActionExecuting. - foreach (var filter in filters.Where(this.FilterMatchesMetadata).Reverse()) - { - await filter.Value.Value.OnActionExecutedAsync(actionExecutedContext, cancellationToken); - } - } - - /// - /// Occurs before the action method is invoked. - /// - /// The context for the action. - /// A cancellation token for signaling task ending. - /// - /// Thrown if is . - /// - public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) - { - if (actionContext == null) - { - throw new ArgumentNullException(nameof(actionContext)); - } - - var dependencyScope = actionContext.Request.GetDependencyScope(); - var lifetimeScope = dependencyScope.GetRequestLifetimeScope(); - - var filters = lifetimeScope.Resolve>>>(); - - // Need to know the set of filters that we've executed so far, so - // we can go back through them. - var executedFilters = new List(); - - // Issue #16: OnActionExecuted needs to happen in the opposite order of OnActionExecuting. - foreach (var filter in filters.Where(this.FilterMatchesMetadata)) - { - await filter.Value.Value.OnActionExecutingAsync(actionContext, cancellationToken); - - // Issue #30: If an actionfilter sets a response, it should prevent the others from running - // their own OnActionExecuting methods, and call OnActionExecuted on already-run - // filters. - // The OnActionExecutedAsync method of the wrapper will never fire if there - // is a response set, so we must call the OnActionExecutedAsync of prior filters - // ourselves. - if (actionContext.Response != null) - { - await ExecuteManualOnActionExecutedAsync(executedFilters.AsEnumerable().Reverse(), actionContext, cancellationToken); - break; - } - - executedFilters.Add(filter.Value.Value); - } - } - - /// - /// Method to manually invoke OnActionExecutedAsync outside of the filter wrapper's own - /// OnActionExecuted async method. - /// - private async Task ExecuteManualOnActionExecutedAsync( - IEnumerable filters, - HttpActionContext actionContext, - CancellationToken cancellationToken) - { - HttpActionExecutedContext localExecutedContext = null; - - foreach (var alreadyRanFilter in filters) - { - if (localExecutedContext == null) - { - localExecutedContext = new HttpActionExecutedContext - { - ActionContext = actionContext, - Response = actionContext.Response - }; - } - - await alreadyRanFilter.OnActionExecutedAsync(localExecutedContext, cancellationToken); - } - } - - private bool FilterMatchesMetadata(Meta> filter) - { - var metadata = filter.Metadata.TryGetValue(AutofacWebApiFilterProvider.FilterMetadataKey, out var metadataAsObject) - ? metadataAsObject as FilterMetadata - : null; - - return _allFilters.Contains(metadata); - } - } -} \ No newline at end of file diff --git a/src/Autofac.Integration.WebApi/Autofac.Integration.WebApi.csproj b/src/Autofac.Integration.WebApi/Autofac.Integration.WebApi.csproj index d12d4a3..f3b875f 100644 --- a/src/Autofac.Integration.WebApi/Autofac.Integration.WebApi.csproj +++ b/src/Autofac.Integration.WebApi/Autofac.Integration.WebApi.csproj @@ -51,9 +51,10 @@ CodeAnalysisDictionary.xml - + + + - @@ -68,6 +69,7 @@ + diff --git a/src/Autofac.Integration.WebApi/AutofacActionFilterAdapter.cs b/src/Autofac.Integration.WebApi/AutofacActionFilterAdapter.cs new file mode 100644 index 0000000..ad3385e --- /dev/null +++ b/src/Autofac.Integration.WebApi/AutofacActionFilterAdapter.cs @@ -0,0 +1,140 @@ +// This software is part of the Autofac IoC container +// Copyright (c) 2012 Autofac Contributors +// https://autofac.org +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// Portions of this file come from the Microsoft ASP.NET web stack, licensed +// under the Apache 2.0 license. You may obtain a copy of the license at +// http://www.apache.org/licenses/LICENSE-2.0 + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; + +namespace Autofac.Integration.WebApi +{ + /// + /// This is an adapter responsible for wrapping the old style + /// and converting it to a . + /// + /// + /// The adapter from old -> new is registered in . + /// + internal class AutofacActionFilterAdapter : IAutofacContinuationActionFilter + { + private readonly IAutofacActionFilter _legacyFilter; + + public AutofacActionFilterAdapter(IAutofacActionFilter legacyFilter) + { + _legacyFilter = legacyFilter; + } + + public async Task ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func> continuation) + { + await _legacyFilter.OnActionExecutingAsync(actionContext, cancellationToken); + + if (actionContext.Response != null) + { + return actionContext.Response; + } + + return await CallOnActionExecutedAsync(actionContext, cancellationToken, continuation); + } + + /// + /// The content of this method is taken from the ActionFilterAttribute code in the ASP.NET source, since + /// that is basically the reference implementation for invoking an async filter's OnActionExecuted correctly. + /// + [SuppressMessage("Microsoft.CodeQuality", "CA1068", Justification = "Matching parameter order in original implementtion.")] + private async Task CallOnActionExecutedAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func> continuation) + { + cancellationToken.ThrowIfCancellationRequested(); + + HttpResponseMessage response = null; + ExceptionDispatchInfo exceptionInfo = null; + + try + { + response = await continuation(); + } + catch (Exception e) + { + exceptionInfo = ExceptionDispatchInfo.Capture(e); + } + + Exception exception; + + if (exceptionInfo == null) + { + exception = null; + } + else + { + exception = exceptionInfo.SourceException; + } + + HttpActionExecutedContext executedContext = new HttpActionExecutedContext(actionContext, exception) + { + Response = response + }; + + try + { + await _legacyFilter.OnActionExecutedAsync(executedContext, cancellationToken); + } + catch + { + // Catch is running because OnActionExecuted threw an exception, so we just want to re-throw. + // We also need to reset the response to forget about it since a filter threw an exception. + actionContext.Response = null; + throw; + } + + if (executedContext.Response != null) + { + return executedContext.Response; + } + + Exception newException = executedContext.Exception; + + if (newException != null) + { + if (newException == exception) + { + exceptionInfo.Throw(); + } + else + { + throw newException; + } + } + + throw new InvalidOperationException(); + } + } +} diff --git a/src/Autofac.Integration.WebApi/AutofacWebApiFilterProvider.cs b/src/Autofac.Integration.WebApi/AutofacWebApiFilterProvider.cs index cbc8b67..1889c68 100644 --- a/src/Autofac.Integration.WebApi/AutofacWebApiFilterProvider.cs +++ b/src/Autofac.Integration.WebApi/AutofacWebApiFilterProvider.cs @@ -150,8 +150,8 @@ private static void ResolveAllScopedFilterOverrides( ILifetimeScope lifeTimeScope, HttpActionDescriptor descriptor) { - ResolveScopedFilter( - filterContext, scope, lifeTimeScope, descriptor, hs => new ActionFilterOverrideWrapper(hs), AutofacFilterCategory.ActionFilterOverride); + ResolveScopedFilter( + filterContext, scope, lifeTimeScope, descriptor, hs => new ContinuationActionFilterOverrideWrapper(hs), AutofacFilterCategory.ActionFilterOverride); ResolveScopedFilter( filterContext, scope, lifeTimeScope, descriptor, hs => new AuthenticationFilterOverrideWrapper(hs), AutofacFilterCategory.AuthenticationFilterOverride); ResolveScopedFilter( @@ -162,8 +162,8 @@ private static void ResolveAllScopedFilterOverrides( private static void ResolveAllScopedFilters(FilterContext filterContext, FilterScope scope, ILifetimeScope lifeTimeScope, HttpActionDescriptor descriptor) { - ResolveScopedFilter( - filterContext, scope, lifeTimeScope, descriptor, hs => new ActionFilterWrapper(hs), AutofacFilterCategory.ActionFilter); + ResolveScopedFilter( + filterContext, scope, lifeTimeScope, descriptor, hs => new ContinuationActionFilterWrapper(hs), AutofacFilterCategory.ActionFilter); ResolveScopedFilter( filterContext, scope, lifeTimeScope, descriptor, hs => new AuthenticationFilterWrapper(hs), AutofacFilterCategory.AuthenticationFilter); ResolveScopedFilter( diff --git a/src/Autofac.Integration.WebApi/ActionFilterOverrideWrapper.cs b/src/Autofac.Integration.WebApi/ContinuationActionFilterOverrideWrapper.cs similarity index 80% rename from src/Autofac.Integration.WebApi/ActionFilterOverrideWrapper.cs rename to src/Autofac.Integration.WebApi/ContinuationActionFilterOverrideWrapper.cs index 304c25b..99ba89d 100644 --- a/src/Autofac.Integration.WebApi/ActionFilterOverrideWrapper.cs +++ b/src/Autofac.Integration.WebApi/ContinuationActionFilterOverrideWrapper.cs @@ -1,5 +1,5 @@ // This software is part of the Autofac IoC container -// Copyright (c) 2013 Autofac Contributors +// Copyright (c) 2012 Autofac Contributors // https://autofac.org // // Permission is hereby granted, free of charge, to any person @@ -32,14 +32,13 @@ namespace Autofac.Integration.WebApi /// /// Resolves a filter override for the specified metadata for each controller request. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - internal sealed class ActionFilterOverrideWrapper : ActionFilterWrapper, IOverrideFilter + internal sealed class ContinuationActionFilterOverrideWrapper : ContinuationActionFilterWrapper, IOverrideFilter { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The filter metadata. - public ActionFilterOverrideWrapper(HashSet filterMetadata) + public ContinuationActionFilterOverrideWrapper(HashSet filterMetadata) : base(filterMetadata) { } @@ -52,4 +51,4 @@ public Type FiltersToOverride get { return typeof(IActionFilter); } } } -} +} \ No newline at end of file diff --git a/src/Autofac.Integration.WebApi/ContinuationActionFilterWrapper.cs b/src/Autofac.Integration.WebApi/ContinuationActionFilterWrapper.cs new file mode 100644 index 0000000..5fc34c9 --- /dev/null +++ b/src/Autofac.Integration.WebApi/ContinuationActionFilterWrapper.cs @@ -0,0 +1,98 @@ +// This software is part of the Autofac IoC container +// Copyright (c) 2012 Autofac Contributors +// https://autofac.org +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using Autofac.Features.Metadata; + +namespace Autofac.Integration.WebApi +{ + /// + /// Resolves a filter for the specified metadata for each controller request. + /// + internal class ContinuationActionFilterWrapper : IActionFilter, IAutofacContinuationActionFilter + { + private readonly HashSet _allFilters; + + /// + /// Initializes a new instance of the class. + /// + /// The collection of filter metadata blocks that this wrapper should run. + public ContinuationActionFilterWrapper(HashSet filterMetadata) + { + if (filterMetadata == null) + { + throw new ArgumentNullException(nameof(filterMetadata)); + } + + _allFilters = filterMetadata; + } + + public bool AllowMultiple { get; } = true; + + public Task ExecuteActionFilterAsync( + HttpActionContext actionContext, + CancellationToken cancellationToken, + Func> continuation) + { + var dependencyScope = actionContext.Request.GetDependencyScope(); + var lifetimeScope = dependencyScope.GetRequestLifetimeScope(); + + var filters = lifetimeScope.Resolve>>>(); + + Func> result = continuation; + + Func> ChainContinuation(Func> next, IAutofacContinuationActionFilter innerFilter) + { + return () => innerFilter.ExecuteActionFilterAsync(actionContext, cancellationToken, next); + } + + // We go backwards for the beginning of the set of filters, where + // the last one invokes the provided continuation, the previous one invokes the last one, and so on, + // until there's a callback that invokes the first filter. + foreach (var filterStage in filters.Reverse().Where(FilterMatchesMetadata)) + { + result = ChainContinuation(result, filterStage.Value.Value); + } + + return result(); + } + + private bool FilterMatchesMetadata(Meta> filter) + { + var metadata = filter.Metadata.TryGetValue(AutofacWebApiFilterProvider.FilterMetadataKey, out var metadataAsObject) + ? metadataAsObject as FilterMetadata + : null; + + return _allFilters.Contains(metadata); + } + } +} \ No newline at end of file diff --git a/src/Autofac.Integration.WebApi/IAutofacContinuationActionFilter.cs b/src/Autofac.Integration.WebApi/IAutofacContinuationActionFilter.cs new file mode 100644 index 0000000..ab94264 --- /dev/null +++ b/src/Autofac.Integration.WebApi/IAutofacContinuationActionFilter.cs @@ -0,0 +1,54 @@ +// This software is part of the Autofac IoC container +// Copyright (c) 2012 Autofac Contributors +// https://autofac.org +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http.Controllers; + +namespace Autofac.Integration.WebApi +{ + /// + /// An action filter that will be created for each controller request, and + /// executes using continuations, so the async context is preserved. + /// + public interface IAutofacContinuationActionFilter + { + /// + /// The method called when the filter executes. The filter should call 'next' to + /// continue processing the request. + /// + /// The context of the current action. + /// A cancellation token for the request. + /// The function to call that invokes the next filter in the chain. + [SuppressMessage("Microsoft.CodeQuality", "CA1068", Justification = "Matching parameter order in IActionFilter.")] + Task ExecuteActionFilterAsync( + HttpActionContext actionContext, + CancellationToken cancellationToken, + Func> next); + } +} \ No newline at end of file diff --git a/src/Autofac.Integration.WebApi/RegistrationExtensions.cs b/src/Autofac.Integration.WebApi/RegistrationExtensions.cs index 2b74cbf..cff1099 100644 --- a/src/Autofac.Integration.WebApi/RegistrationExtensions.cs +++ b/src/Autofac.Integration.WebApi/RegistrationExtensions.cs @@ -36,6 +36,7 @@ using System.Web.Http.ModelBinding; using Autofac.Builder; using Autofac.Core; +using Autofac.Features.Metadata; using Autofac.Features.Scanning; namespace Autofac.Integration.WebApi @@ -242,10 +243,14 @@ public static void RegisterWebApiFilterProvider(this ContainerBuilder builder, H builder.Register(c => new AutofacWebApiFilterProvider(c.Resolve())) .As() .SingleInstance(); // It would be nice to scope this per request. + + // Register the adapter to turn the old IAutofacActionFilters into the new continuation style. + builder.RegisterAdapter( + legacy => new AutofacActionFilterAdapter(legacy)); } /// - /// Sets the provided registration to act as an for the specified controller action. + /// Sets the provided registration to act as an or for the specified controller action. /// /// The type of the controller. /// The registration. @@ -257,11 +262,11 @@ public static IRegistrationBuilder> actionSelector) where TController : IHttpController { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilter, actionSelector); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilter, actionSelector); } /// - /// Sets the provided registration to act as an for the specified controller. + /// Sets the provided registration to act as an or for the specified controller. /// /// The type of the controller. /// The registration. @@ -270,22 +275,22 @@ public static IRegistrationBuilder(this IRegistrationBuilder registration) where TController : IHttpController { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilter); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilter); } /// - /// Sets the provided registration to act as an for all controllers. + /// Sets the provided registration to act as an or for all controllers. /// /// The registration. /// A registration builder allowing further configuration of the component. public static IRegistrationBuilder AsWebApiActionFilterForAllControllers(this IRegistrationBuilder registration) { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilter, descriptor => true, FilterScope.Controller); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilter, (scope, descriptor) => true, FilterScope.Controller); } /// - /// Sets the provided registration to act as an , based on a predicate that filters which actions it is applied to. + /// Sets the provided registration to act as an or , based on a predicate that filters which actions it is applied to. /// /// The registration. /// A predicate that should return true if this filter should be applied to the specified action. @@ -297,11 +302,11 @@ public static IRegistrationBuilder predicate, FilterScope filterScope = FilterScope.Action) { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilter, predicate, filterScope); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilter, predicate, filterScope); } /// - /// Sets the provided registration to act as an , based on a predicate that filters which actions it is applied to. + /// Sets the provided registration to act as an or , based on a predicate that filters which actions it is applied to. /// /// The registration. /// A predicate that should return true if this filter should be applied to the specified action. @@ -313,11 +318,11 @@ public static IRegistrationBuilder predicate, FilterScope filterScope = FilterScope.Action) { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilter, predicate, filterScope); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilter, predicate, filterScope); } /// - /// Sets the provided registration to act as an override for the specified controller action. + /// Sets the provided registration to act as an or override for the specified controller action. /// /// The type of the controller. /// The registration. @@ -329,11 +334,11 @@ public static IRegistrationBuilder> actionSelector) where TController : IHttpController { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilterOverride, actionSelector); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilterOverride, actionSelector); } /// - /// Sets the provided registration to act as an override for the specified controller. + /// Sets the provided registration to act as an or override for the specified controller. /// /// The type of the controller. /// The registration. @@ -342,22 +347,22 @@ public static IRegistrationBuilder(this IRegistrationBuilder registration) where TController : IHttpController { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilterOverride); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilterOverride); } /// - /// Sets the provided registration to act as an override for all controllers. + /// Sets the provided registration to act as an or override for all controllers. /// /// The registration. /// A registration builder allowing further configuration of the component. public static IRegistrationBuilder AsWebApiActionFilterOverrideForAllControllers(this IRegistrationBuilder registration) { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilterOverride, descriptor => true, FilterScope.Controller); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilterOverride, (scope, descriptor) => true, FilterScope.Controller); } /// - /// Sets the provided registration to act as an override, based on a predicate that filters which actions it is applied to. + /// Sets the provided registration to act as an or override, based on a predicate that filters which actions it is applied to. /// /// The registration. /// A predicate that should return true if this filter should be applied to the specified action. @@ -369,11 +374,11 @@ public static IRegistrationBuilder predicate, FilterScope filterScope = FilterScope.Action) { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilterOverride, predicate, filterScope); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilterOverride, predicate, filterScope); } /// - /// Sets the provided registration to act as an override, based on a predicate that filters which actions it is applied to. + /// Sets the provided registration to act as an or override, based on a predicate that filters which actions it is applied to. /// /// The registration. /// A predicate that should return true if this filter should be applied to the specified action. @@ -385,7 +390,7 @@ public static IRegistrationBuilder predicate, FilterScope filterScope = FilterScope.Action) { - return AsFilterFor(registration, AutofacFilterCategory.ActionFilterOverride, predicate, filterScope); + return AsActionFilterFor(registration, AutofacFilterCategory.ActionFilterOverride, predicate, filterScope); } /// @@ -907,6 +912,17 @@ private static IRegistrationBuilder(registration, filterCategory, (lifetime, action) => predicate(action), filterScope); } + private static IRegistrationBuilder + AsActionFilterFor( + IRegistrationBuilder registration, + AutofacFilterCategory filterCategory, + Func predicate, + FilterScope filterScope) + { + return AsActionFilterFor(registration, filterCategory, (lifetime, action) => predicate(action), filterScope); + } + private static IRegistrationBuilder AsFilterFor( IRegistrationBuilder(); - if (!limitType.IsAssignableTo()) + // Get the filter metadata set. + registration = registration.GetOrCreateMetadata(out FilterMetadata filterMeta); + + var registrationMetadata = new FilterPredicateMetadata { - var message = string.Format( - CultureInfo.CurrentCulture, - RegistrationExtensionsResources.MustBeAssignableToFilterType, - limitType.FullName, - typeof(TFilter).FullName); - throw new ArgumentException(message, nameof(registration)); - } + Scope = filterScope, + FilterCategory = filterCategory, + Predicate = predicate + }; + + filterMeta.PredicateSet.Add(registrationMetadata); + + return registration.As(); + } + + private static IRegistrationBuilder + AsActionFilterFor( + IRegistrationBuilder registration, + AutofacFilterCategory filterCategory, + Func predicate, + FilterScope filterScope) + { + if (registration == null) throw new ArgumentNullException(nameof(registration)); + if (predicate == null) throw new ArgumentNullException(nameof(predicate)); + if (filterScope != FilterScope.Action && filterScope != FilterScope.Controller) + throw new InvalidEnumArgumentException(nameof(filterScope), (int)filterScope, typeof(FilterScope)); + + registration.ValidateActionFilterType(out var isLegacyFilterType); // Get the filter metadata set. registration = registration.GetOrCreateMetadata(out FilterMetadata filterMeta); @@ -944,7 +980,12 @@ private static IRegistrationBuilder(); + if (isLegacyFilterType) + { + return registration.As(); + } + + return registration.As(); } private static IRegistrationBuilder @@ -953,17 +994,30 @@ private static IRegistrationBuilder(); - if (!limitType.IsAssignableTo()) + // Get the filter metadata set. + registration = registration.GetOrCreateMetadata(out FilterMetadata filterMeta); + + var registrationMetadata = new FilterPredicateMetadata { - var message = string.Format( - CultureInfo.CurrentCulture, - RegistrationExtensionsResources.MustBeAssignableToFilterType, - limitType.FullName, - typeof(TFilter).FullName); - throw new ArgumentException(message, nameof(registration)); - } + Scope = FilterScope.Controller, + FilterCategory = filterCategory, + Predicate = (scope, descriptor) => descriptor.ControllerDescriptor.ControllerType == typeof(TController) + }; + + filterMeta.PredicateSet.Add(registrationMetadata); + + return registration.As(); + } + + private static IRegistrationBuilder + AsActionFilterFor(IRegistrationBuilder registration, AutofacFilterCategory filterCategory) + where TController : IHttpController + { + if (registration == null) throw new ArgumentNullException(nameof(registration)); + + registration.ValidateActionFilterType(out var isLegacyFilterType); // Get the filter metadata set. registration = registration.GetOrCreateMetadata(out FilterMetadata filterMeta); @@ -977,7 +1031,12 @@ private static IRegistrationBuilder(); + if (isLegacyFilterType) + { + return registration.As(); + } + + return registration.As(); } private static IRegistrationBuilder @@ -990,17 +1049,37 @@ private static IRegistrationBuilder(); - if (!limitType.IsAssignableTo()) + // Get the filter metadata set. + registration = registration.GetOrCreateMetadata(out FilterMetadata filterMeta); + + var method = GetMethodInfo(actionSelector); + + var registrationMetadata = new FilterPredicateMetadata { - var message = string.Format( - CultureInfo.CurrentCulture, - RegistrationExtensionsResources.MustBeAssignableToFilterType, - limitType.FullName, - typeof(TFilter).FullName); - throw new ArgumentException(message, nameof(registration)); - } + Scope = FilterScope.Action, + FilterCategory = filterCategory, + Predicate = (scope, descriptor) => typeof(TController).IsAssignableFrom(descriptor.ControllerDescriptor.ControllerType) && + ActionMethodMatches(descriptor, method) + }; + + filterMeta.PredicateSet.Add(registrationMetadata); + + return registration.As(); + } + + private static IRegistrationBuilder + AsActionFilterFor( + IRegistrationBuilder registration, + AutofacFilterCategory filterCategory, + Expression> actionSelector) + where TController : IHttpController + { + if (registration == null) throw new ArgumentNullException(nameof(registration)); + if (actionSelector == null) throw new ArgumentNullException(nameof(actionSelector)); + + registration.ValidateActionFilterType(out var isLegacyFilterType); // Get the filter metadata set. registration = registration.GetOrCreateMetadata(out FilterMetadata filterMeta); @@ -1017,7 +1096,51 @@ private static IRegistrationBuilder(); + if (isLegacyFilterType) + { + return registration.As(); + } + + return registration.As(); + } + + private static void ValidateFilterType(this IRegistrationBuilder registration) + { + var limitType = registration.ActivatorData.Activator.LimitType; + + if (!limitType.IsAssignableTo()) + { + var message = string.Format( + CultureInfo.CurrentCulture, + RegistrationExtensionsResources.MustBeAssignableToFilterType, + limitType.FullName, + typeof(TFilter).FullName); + throw new ArgumentException(message, nameof(registration)); + } + } + + private static void ValidateActionFilterType( + this IRegistrationBuilder registration, + out bool isLegacyFilterType) + { + var limitType = registration.ActivatorData.Activator.LimitType; + + isLegacyFilterType = false; + + if (limitType.IsAssignableTo()) + { + isLegacyFilterType = true; + } + else if (!limitType.IsAssignableTo()) + { + var message = string.Format( + CultureInfo.CurrentCulture, + RegistrationExtensionsResources.MustBeAssignableToActionFilterType, + limitType.FullName, + typeof(IAutofacActionFilter).FullName, + typeof(IAutofacContinuationActionFilter).FullName); + throw new ArgumentException(message, nameof(registration)); + } } private static void AsOverrideFor(ContainerBuilder builder, AutofacFilterCategory filterCategory) diff --git a/src/Autofac.Integration.WebApi/RegistrationExtensionsResources.Designer.cs b/src/Autofac.Integration.WebApi/RegistrationExtensionsResources.Designer.cs index f50224f..582b63d 100644 --- a/src/Autofac.Integration.WebApi/RegistrationExtensionsResources.Designer.cs +++ b/src/Autofac.Integration.WebApi/RegistrationExtensionsResources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18051 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace Autofac.Integration.WebApi { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class RegistrationExtensionsResources { @@ -78,6 +78,15 @@ internal static string ListMustNotBeEmptyOrContainNulls { } } + /// + /// Looks up a localized string similar to The type '{0}' must be assignable to '{1}' or '{2}'.. + /// + internal static string MustBeAssignableToActionFilterType { + get { + return ResourceManager.GetString("MustBeAssignableToActionFilterType", resourceCulture); + } + } + /// /// Looks up a localized string similar to The type '{0}' must be assignable to '{1}'.. /// diff --git a/src/Autofac.Integration.WebApi/RegistrationExtensionsResources.resx b/src/Autofac.Integration.WebApi/RegistrationExtensionsResources.resx index caa0153..789988d 100644 --- a/src/Autofac.Integration.WebApi/RegistrationExtensionsResources.resx +++ b/src/Autofac.Integration.WebApi/RegistrationExtensionsResources.resx @@ -123,6 +123,9 @@ Type list may not be empty or contain all null values. + + The type '{0}' must be assignable to '{1}' or '{2}'. + The type '{0}' must be assignable to '{1}'. diff --git a/test/Autofac.Integration.WebApi.Test/ActionFilterFixture.cs b/test/Autofac.Integration.WebApi.Test/ActionFilterFixture.cs index 3d54327..7d76461 100644 --- a/test/Autofac.Integration.WebApi.Test/ActionFilterFixture.cs +++ b/test/Autofac.Integration.WebApi.Test/ActionFilterFixture.cs @@ -73,12 +73,12 @@ protected override Action ConfigureControllerFilterOverride() diff --git a/test/Autofac.Integration.WebApi.Test/Autofac.Integration.WebApi.Test.csproj b/test/Autofac.Integration.WebApi.Test/Autofac.Integration.WebApi.Test.csproj index ba40634..0bf0e08 100644 --- a/test/Autofac.Integration.WebApi.Test/Autofac.Integration.WebApi.Test.csproj +++ b/test/Autofac.Integration.WebApi.Test/Autofac.Integration.WebApi.Test.csproj @@ -55,12 +55,13 @@ + - + @@ -70,7 +71,7 @@ - + @@ -92,6 +93,7 @@ + @@ -101,6 +103,8 @@ + + diff --git a/test/Autofac.Integration.WebApi.Test/AutofacFilterBaseFixture.cs b/test/Autofac.Integration.WebApi.Test/AutofacFilterBaseFixture.cs index fba1bc2..80dc235 100644 --- a/test/Autofac.Integration.WebApi.Test/AutofacFilterBaseFixture.cs +++ b/test/Autofac.Integration.WebApi.Test/AutofacFilterBaseFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Configuration; using System.Linq; using System.Web.Http; using System.Web.Http.Controllers; @@ -249,13 +250,17 @@ private void AssertSingleFilter( Action> configure) { var builder = new ContainerBuilder(); + var configuration = new HttpConfiguration(); builder.Register(c => new Logger()).InstancePerDependency(); configure(builder.Register(registration).InstancePerRequest()); + + // Need to do this so our adapter gets registered + builder.RegisterWebApiFilterProvider(configuration); + var container = builder.Build(); var provider = new AutofacWebApiFilterProvider(container); - var configuration = new HttpConfiguration { DependencyResolver = new AutofacWebApiDependencyResolver(container) }; + configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container); var actionDescriptor = BuildActionDescriptorForGetMethod(typeof(TController)); - var filterInfos = provider.GetFilters(configuration, actionDescriptor).ToArray(); var wrapperType = GetWrapperType(); @@ -270,9 +275,12 @@ private void AssertNoFilter( var builder = new ContainerBuilder(); builder.Register(c => new Logger()).InstancePerDependency(); configure(builder.Register(registration).InstancePerRequest()); + var configuration = new HttpConfiguration(); + builder.RegisterWebApiFilterProvider(configuration); + var container = builder.Build(); var provider = new AutofacWebApiFilterProvider(container); - var configuration = new HttpConfiguration { DependencyResolver = new AutofacWebApiDependencyResolver(container) }; + configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container); var actionDescriptor = BuildActionDescriptorForGetMethod(typeof(TController)); var filterInfos = provider.GetFilters(configuration, actionDescriptor).ToArray(); @@ -292,9 +300,12 @@ private void AssertMultipleFilters( builder.Register(c => new Logger()).InstancePerDependency(); configure1(builder.Register(registration1).InstancePerRequest()); configure2(builder.Register(registration2).InstancePerRequest()); + var configuration = new HttpConfiguration(); + builder.RegisterWebApiFilterProvider(configuration); var container = builder.Build(); var provider = new AutofacWebApiFilterProvider(container); - var configuration = new HttpConfiguration { DependencyResolver = new AutofacWebApiDependencyResolver(container) }; + configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container); + var actionDescriptor = BuildActionDescriptorForGetMethod(); var filterInfos = provider.GetFilters(configuration, actionDescriptor).ToArray(); @@ -309,9 +320,11 @@ private static void AssertOverrideFilter(Action r { var builder = new ContainerBuilder(); registration(builder); + var configuration = new HttpConfiguration(); + builder.RegisterWebApiFilterProvider(configuration); var container = builder.Build(); var provider = new AutofacWebApiFilterProvider(container); - var configuration = new HttpConfiguration { DependencyResolver = new AutofacWebApiDependencyResolver(container) }; + configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container); var actionDescriptor = BuildActionDescriptorForGetMethod(typeof(TController)); var filterInfos = provider.GetFilters(configuration, actionDescriptor).ToArray(); @@ -329,9 +342,11 @@ private void AssertOverrideFilter( var builder = new ContainerBuilder(); builder.Register(c => new Logger()).InstancePerDependency(); configure(builder.Register(registration).InstancePerRequest()); + var configuration = new HttpConfiguration(); + builder.RegisterWebApiFilterProvider(configuration); var container = builder.Build(); var provider = new AutofacWebApiFilterProvider(container); - var configuration = new HttpConfiguration { DependencyResolver = new AutofacWebApiDependencyResolver(container) }; + configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container); var actionDescriptor = BuildActionDescriptorForGetMethod(typeof(TController)); var filterInfos = provider.GetFilters(configuration, actionDescriptor).ToArray(); @@ -348,10 +363,12 @@ private void AssertMultiControllerRegistration( { var builder = new ContainerBuilder(); builder.Register(c => new Logger()).InstancePerDependency(); + var configuration = new HttpConfiguration(); configure(builder.Register(registration).InstancePerRequest()); + builder.RegisterWebApiFilterProvider(configuration); var container = builder.Build(); var provider = new AutofacWebApiFilterProvider(container); - var configuration = new HttpConfiguration { DependencyResolver = new AutofacWebApiDependencyResolver(container) }; + configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container); var wrapperType = GetWrapperType(); @@ -382,9 +399,11 @@ private void AssertMultiControllerRegistration( builder.Register(c => new Logger()).InstancePerDependency(); configure1(builder.Register(registration1).InstancePerRequest()); configure2(builder.Register(registration2).InstancePerRequest()); + var configuration = new HttpConfiguration(); + builder.RegisterWebApiFilterProvider(configuration); var container = builder.Build(); var provider = new AutofacWebApiFilterProvider(container); - var configuration = new HttpConfiguration { DependencyResolver = new AutofacWebApiDependencyResolver(container) }; + configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container); var wrapperType = GetWrapperType(); diff --git a/test/Autofac.Integration.WebApi.Test/AutofacWebApiFilterProviderFixture.cs b/test/Autofac.Integration.WebApi.Test/AutofacWebApiFilterProviderFixture.cs index 2ba1cf9..3dd77ec 100644 --- a/test/Autofac.Integration.WebApi.Test/AutofacWebApiFilterProviderFixture.cs +++ b/test/Autofac.Integration.WebApi.Test/AutofacWebApiFilterProviderFixture.cs @@ -55,23 +55,6 @@ public void ReturnsFiltersWithoutPropertyInjectionForUnregisteredDependencies() Assert.Null(filter.Logger); } - [Fact] - public void CanRegisterMultipleFilterTypesAgainstSingleService() - { - var builder = new ContainerBuilder(); - builder.RegisterInstance(new TestCombinationFilter()) - .AsWebApiActionFilterFor() - .AsWebApiAuthenticationFilterFor() - .AsWebApiAuthorizationFilterFor() - .AsWebApiExceptionFilterFor(); - var container = builder.Build(); - - Assert.NotNull(container.Resolve()); - Assert.NotNull(container.Resolve()); - Assert.NotNull(container.Resolve()); - Assert.NotNull(container.Resolve()); - } - [Fact] public void ResolvesMultipleFiltersOfDifferentTypes() { @@ -94,12 +77,12 @@ public void ResolvesMultipleFiltersOfDifferentTypes() .AsWebApiActionFilterFor() .InstancePerRequest(); + var configuration = new HttpConfiguration(); + builder.RegisterWebApiFilterProvider(configuration); var container = builder.Build(); var provider = new AutofacWebApiFilterProvider(container); - var configuration = new HttpConfiguration - { - DependencyResolver = new AutofacWebApiDependencyResolver(container) - }; + configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container); + var actionDescriptor = BuildActionDescriptorForGetMethod(); var filterInfos = provider.GetFilters(configuration, actionDescriptor).ToArray(); @@ -108,7 +91,7 @@ public void ResolvesMultipleFiltersOfDifferentTypes() Assert.Single(filters.OfType()); Assert.Single(filters.OfType()); Assert.Single(filters.OfType()); - Assert.Single(filters.OfType()); + Assert.Single(filters.OfType()); } private static ReflectedHttpActionDescriptor BuildActionDescriptorForGetMethod() diff --git a/test/Autofac.Integration.WebApi.Test/ActionFilterOverrideWrapperFixture.cs b/test/Autofac.Integration.WebApi.Test/ContinuationActionFilterOverrideWrapperFixture.cs similarity index 67% rename from test/Autofac.Integration.WebApi.Test/ActionFilterOverrideWrapperFixture.cs rename to test/Autofac.Integration.WebApi.Test/ContinuationActionFilterOverrideWrapperFixture.cs index d2083db..68d05ce 100644 --- a/test/Autofac.Integration.WebApi.Test/ActionFilterOverrideWrapperFixture.cs +++ b/test/Autofac.Integration.WebApi.Test/ContinuationActionFilterOverrideWrapperFixture.cs @@ -5,12 +5,12 @@ namespace Autofac.Integration.WebApi.Test { - public class ActionFilterOverrideWrapperFixture + public class ContinuationActionFilterOverrideWrapperFixture { [Fact] public void FiltersToOverrideReturnsCorrectType() { - var wrapper = new ActionFilterOverrideWrapper(new HashSet()); + var wrapper = new ContinuationActionFilterOverrideWrapper(new HashSet()); Assert.Equal(typeof(IActionFilter), wrapper.FiltersToOverride); } } diff --git a/test/Autofac.Integration.WebApi.Test/ActionFilterWrapperFixture.cs b/test/Autofac.Integration.WebApi.Test/ContinuationActionFilterWrapperFixture.cs similarity index 54% rename from test/Autofac.Integration.WebApi.Test/ActionFilterWrapperFixture.cs rename to test/Autofac.Integration.WebApi.Test/ContinuationActionFilterWrapperFixture.cs index 5eb162c..c0d2b2a 100644 --- a/test/Autofac.Integration.WebApi.Test/ActionFilterWrapperFixture.cs +++ b/test/Autofac.Integration.WebApi.Test/ContinuationActionFilterWrapperFixture.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Reflection; using System.Threading; +using System.Threading.Tasks; +using System.Transactions; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; @@ -12,12 +15,12 @@ namespace Autofac.Integration.WebApi.Test { - public class ActionFilterWrapperFixture + public class ContinuationActionFilterWrapperFixture { [Fact] public void RequiresFilterMetadata() { - var exception = Assert.Throws(() => new ActionFilterWrapper(null)); + var exception = Assert.Throws(() => new ContinuationActionFilterWrapper(null)); Assert.Equal("filterMetadata", exception.ParamName); } @@ -26,6 +29,8 @@ public async void WrapperResolvesActionFilterFromDependencyScope() { var builder = new ContainerBuilder(); builder.Register(c => new Logger()).InstancePerDependency(); + var configuration = new HttpConfiguration(); + builder.RegisterWebApiFilterProvider(configuration); var activationCount = 0; builder.Register(c => new TestActionFilter(c.Resolve())) .AsWebApiActionFilterFor(c => c.Get()) @@ -40,14 +45,10 @@ public async void WrapperResolvesActionFilterFromDependencyScope() var methodInfo = typeof(TestController).GetMethod("Get"); var actionDescriptor = CreateActionDescriptor(methodInfo); var actionContext = new HttpActionContext(controllerContext, actionDescriptor); - var httpActionExecutedContext = new HttpActionExecutedContext(actionContext, null); - var wrapper = new ActionFilterWrapper(filterMetadata.ToSingleFilterHashSet()); + var wrapper = new ContinuationActionFilterWrapper(filterMetadata.ToSingleFilterHashSet()); - await wrapper.OnActionExecutingAsync(actionContext, CancellationToken.None); - Assert.Equal(1, activationCount); - - await wrapper.OnActionExecutedAsync(httpActionExecutedContext, CancellationToken.None); + await wrapper.ExecuteActionFilterAsync(actionContext, CancellationToken.None, () => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))); Assert.Equal(1, activationCount); } @@ -70,6 +71,10 @@ public async void RunsFiltersInCorrectOrder() .InstancePerRequest() .GetMetadata(out var testFilter2Meta); + var configuration = new HttpConfiguration(); + + builder.RegisterWebApiFilterProvider(configuration); + var container = builder.Build(); var resolver = new AutofacWebApiDependencyResolver(container); @@ -77,15 +82,17 @@ public async void RunsFiltersInCorrectOrder() var methodInfo = typeof(TestController).GetMethod("Get"); var actionDescriptor = CreateActionDescriptor(methodInfo); var actionContext = new HttpActionContext(controllerContext, actionDescriptor); - var httpActionExecutedContext = new HttpActionExecutedContext(actionContext, null); - var wrapper = new ActionFilterWrapper(new HashSet + var wrapper = new ContinuationActionFilterWrapper(new HashSet { testFilter1Meta, testFilter2Meta }); - await wrapper.OnActionExecutingAsync(actionContext, CancellationToken.None); - await wrapper.OnActionExecutedAsync(httpActionExecutedContext, CancellationToken.None); + await wrapper.ExecuteActionFilterAsync( + actionContext, + CancellationToken.None, + () => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))); + Assert.Equal("TestActionFilter2.OnActionExecutingAsync", order[0]); Assert.Equal("TestActionFilter.OnActionExecutingAsync", order[1]); Assert.Equal("TestActionFilter.OnActionExecutedAsync", order[2]); @@ -126,6 +133,10 @@ public async void StopsIfFilterOnExecutingSetsResponse() .InstancePerRequest() .GetMetadata(out var testActionFilter3Metadata); + var configuration = new HttpConfiguration(); + + builder.RegisterWebApiFilterProvider(configuration); + var container = builder.Build(); var resolver = new AutofacWebApiDependencyResolver(container); @@ -134,7 +145,7 @@ public async void StopsIfFilterOnExecutingSetsResponse() var actionDescriptor = CreateActionDescriptor(methodInfo); var actionContext = new HttpActionContext(controllerContext, actionDescriptor); - var wrapper = new ActionFilterWrapper(new HashSet + var wrapper = new ContinuationActionFilterWrapper(new HashSet { testActionFilterMetadata, testActionFilterWithResponseMetadata, @@ -142,7 +153,10 @@ public async void StopsIfFilterOnExecutingSetsResponse() testActionFilter3Metadata }); - await wrapper.OnActionExecutingAsync(actionContext, CancellationToken.None); + await wrapper.ExecuteActionFilterAsync( + actionContext, + CancellationToken.None, + () => throw new Exception("Should never reach here because a filter set the response.")); Assert.Equal("TestActionFilter3.OnActionExecutingAsync", order[0]); Assert.Equal("TestActionFilter2.OnActionExecutingAsync", order[1]); @@ -152,6 +166,115 @@ public async void StopsIfFilterOnExecutingSetsResponse() Assert.Equal(5, order.Count); } + [Fact] + public void TransactionScopePreservedBetweenStandardFilters() + { + // Issue #34 - Async/await context lost between filters. + var builder = new ContainerBuilder(); + + TransactionScope scope = null; + + // Autofac filters will resolve in reverse order. + builder.Register(s => new TestCallbackActionFilter( + () => + Assert.NotNull(Transaction.Current), + () => + Assert.NotNull(Transaction.Current))) + .AsWebApiActionFilterFor(c => c.Get()) + .InstancePerRequest() + .GetMetadata(out var testFilter1Meta); + builder.Register(s => new TestCallbackActionFilter( + () => + scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled), + () => + { + Assert.NotNull(Transaction.Current); + scope.Dispose(); + Assert.Null(Transaction.Current); + })) + .AsWebApiActionFilterFor(c => c.Get()) + .InstancePerRequest() + .GetMetadata(out var testFilter2Meta); + + var configuration = new HttpConfiguration(); + + builder.RegisterWebApiFilterProvider(configuration); + + var container = builder.Build(); + + var resolver = new AutofacWebApiDependencyResolver(container); + var controllerContext = CreateControllerContext(resolver); + var methodInfo = typeof(TestController).GetMethod("Get"); + var actionDescriptor = CreateActionDescriptor(methodInfo); + var actionContext = new HttpActionContext(controllerContext, actionDescriptor); + var wrapper = new ContinuationActionFilterWrapper(new HashSet + { + testFilter1Meta, + testFilter2Meta + }); + + wrapper.ExecuteActionFilterAsync( + actionContext, + CancellationToken.None, + () => + { + Assert.NotNull(Transaction.Current); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + }).Wait(); + } + + + [Fact] + public void TransactionScopePreservedBetweenContinuationFilters() + { + // Issue #34 - Async/await context lost between filters. + var builder = new ContainerBuilder(); + + // Autofac filters will resolve in reverse order. + builder.Register(s => new TestContinuationActionFilter( + () => + Assert.NotNull(Transaction.Current), + () => + Assert.NotNull(Transaction.Current))) + .AsWebApiActionFilterFor(c => c.Get()) + .InstancePerRequest() + .GetMetadata(out var testFilter1Meta); + builder.Register(s => new TestContinuationActionFilterWithTransactionScope( + () => + Assert.Null(Transaction.Current), + () => + Assert.Null(Transaction.Current))) + .AsWebApiActionFilterFor(c => c.Get()) + .InstancePerRequest() + .GetMetadata(out var testFilter2Meta); + + var configuration = new HttpConfiguration(); + + builder.RegisterWebApiFilterProvider(configuration); + + var container = builder.Build(); + + var resolver = new AutofacWebApiDependencyResolver(container); + var controllerContext = CreateControllerContext(resolver); + var methodInfo = typeof(TestController).GetMethod("Get"); + var actionDescriptor = CreateActionDescriptor(methodInfo); + var actionContext = new HttpActionContext(controllerContext, actionDescriptor); + var wrapper = new ContinuationActionFilterWrapper(new HashSet + { + testFilter1Meta, + testFilter2Meta + }); + + wrapper.ExecuteActionFilterAsync( + actionContext, + CancellationToken.None, + () => + { + Assert.NotNull(Transaction.Current); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + }).Wait(); + } + private static HttpActionDescriptor CreateActionDescriptor(MethodInfo methodInfo) { var controllerDescriptor = new HttpControllerDescriptor { ControllerType = methodInfo.DeclaringType }; diff --git a/test/Autofac.Integration.WebApi.Test/FilterOrderingFixture.cs b/test/Autofac.Integration.WebApi.Test/FilterOrderingFixture.cs index 280b443..15e4643 100644 --- a/test/Autofac.Integration.WebApi.Test/FilterOrderingFixture.cs +++ b/test/Autofac.Integration.WebApi.Test/FilterOrderingFixture.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; +using Autofac.Features.Metadata; using Autofac.Integration.WebApi.Test.TestTypes; using Xunit; @@ -63,6 +65,11 @@ public async void FilterOrderForAllFilterTypes() builder.RegisterType>().AsWebApiExceptionFilterOverrideFor(c => c.Get()); builder.RegisterType>().AsWebApiExceptionFilterFor(c => c.Get()); builder.RegisterType>().AsWebApiExceptionFilterOverrideFor(c => c.Get()); + + var configuration = new HttpConfiguration(); + + builder.RegisterWebApiFilterProvider(configuration); + var container = builder.Build(); // Set up the filter provider so we can resolve the set of filters @@ -79,7 +86,7 @@ public async void FilterOrderForAllFilterTypes() // resolution of the actual filters internally, so you won't // actually see them in this list. We have to fake an execution // pipeline to see what's really there. - var configuration = new HttpConfiguration { DependencyResolver = new AutofacWebApiDependencyResolver(container) }; + configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container); var filterInfos = provider.GetFilters(configuration, actionDescriptor).ToArray(); // Fake execution of the filters to force the lazy initialization. @@ -106,10 +113,8 @@ public async void FilterOrderForAllFilterTypes() await fi.OnAuthorizationAsync(actionContext, token); } - foreach (var fi in filterInfos.Select(f => f.Instance).OfType()) - { - await fi.OnActionExecutingAsync(actionContext, token); - } + // Emulate the action filter execution pipeline. + await ExecuteContinuationFilters(actionContext, filterInfos.Select(f => f.Instance).OfType(), token); foreach (var fi in filterInfos.Select(f => f.Instance).OfType()) { @@ -167,6 +172,24 @@ public async void FilterOrderForAllFilterTypes() } } + private static async Task ExecuteContinuationFilters(HttpActionContext actionContext, IEnumerable filters, CancellationToken cancellationToken) + { + // Terminate the pipeline last. + Func> result = () => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + + Func> ChainContinuation(Func> next, IActionFilter innerFilter) + { + return () => innerFilter.ExecuteActionFilterAsync(actionContext, cancellationToken, next); + } + + foreach (var filterStage in filters.Reverse()) + { + result = ChainContinuation(result, filterStage); + } + + await result(); + } + private class A { } diff --git a/test/Autofac.Integration.WebApi.Test/RegistrationExtensionsFixture.cs b/test/Autofac.Integration.WebApi.Test/RegistrationExtensionsFixture.cs index bf916c1..e74ddb9 100644 --- a/test/Autofac.Integration.WebApi.Test/RegistrationExtensionsFixture.cs +++ b/test/Autofac.Integration.WebApi.Test/RegistrationExtensionsFixture.cs @@ -283,6 +283,11 @@ public void AsActionFilterForAllControllersMustBeActionFilter() () => builder.RegisterInstance(new object()).AsWebApiActionFilterForAllControllers()); Assert.Equal("registration", exception.ParamName); + Assert.StartsWith( + "The type 'System.Object' must be assignable to 'Autofac.Integration.WebApi.IAutofacActionFilter'" + + " or 'Autofac.Integration.WebApi.IAutofacContinuationActionFilter'.", + exception.Message, + StringComparison.CurrentCulture); } [Fact] @@ -294,6 +299,42 @@ public void AsActionFilterWhereRequiresPredicate() Assert.Equal("predicate", exception.ParamName); } + [Fact] + public void CanRegisterMultipleFilterTypesAgainstSingleService() + { + var builder = new ContainerBuilder(); + builder.RegisterInstance(new TestCombinationFilter()) + .AsWebApiActionFilterFor() + .AsWebApiAuthenticationFilterFor() + .AsWebApiAuthorizationFilterFor() + .AsWebApiExceptionFilterFor(); + + var configuration = new HttpConfiguration(); + builder.RegisterWebApiFilterProvider(configuration); + var container = builder.Build(); + + Assert.NotNull(container.Resolve()); + Assert.NotNull(container.Resolve()); + Assert.NotNull(container.Resolve()); + Assert.NotNull(container.Resolve()); + } + + [Fact] + public void AutofacActionFilterIsAdapted() + { + var builder = new ContainerBuilder(); + builder.RegisterInstance(new TestCombinationFilter()) + .AsWebApiActionFilterFor(); + + var configuration = new HttpConfiguration(); + + builder.RegisterWebApiFilterProvider(configuration); + + var container = builder.Build(); + + Assert.NotNull(container.Resolve()); + } + [Fact] public void AsAuthorizationFilterForRequiresActionSelector() { diff --git a/test/Autofac.Integration.WebApi.Test/TestTypes/TestCallbackActionFilter.cs b/test/Autofac.Integration.WebApi.Test/TestTypes/TestCallbackActionFilter.cs new file mode 100644 index 0000000..c27e37a --- /dev/null +++ b/test/Autofac.Integration.WebApi.Test/TestTypes/TestCallbackActionFilter.cs @@ -0,0 +1,61 @@ +// This software is part of the Autofac IoC container +// Copyright (c) 2012 Autofac Contributors +// https://autofac.org +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using System.Web.Http.ModelBinding; + +namespace Autofac.Integration.WebApi.Test.TestTypes +{ + public class TestCallbackActionFilter : IAutofacActionFilter + { + private readonly Action _executing; + private readonly Action _executed; + + public TestCallbackActionFilter(Action executing, Action executed) + { + _executing = executing; + _executed = executed; + } + + public Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) + { + _executing(); + return Task.FromResult(0); + } + + public Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) + { + _executed(); + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/test/Autofac.Integration.WebApi.Test/TestTypes/TestContinuationActionFilter.cs b/test/Autofac.Integration.WebApi.Test/TestTypes/TestContinuationActionFilter.cs new file mode 100644 index 0000000..fcb3077 --- /dev/null +++ b/test/Autofac.Integration.WebApi.Test/TestTypes/TestContinuationActionFilter.cs @@ -0,0 +1,56 @@ +// This software is part of the Autofac IoC container +// Copyright (c) 2012 Autofac Contributors +// https://autofac.org +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http.Controllers; + +namespace Autofac.Integration.WebApi.Test.TestTypes +{ + public class TestContinuationActionFilter : IAutofacContinuationActionFilter + { + private readonly Action _before; + private readonly Action _after; + + public TestContinuationActionFilter(Action before, Action after) + { + _before = before; + _after = after; + } + + public async Task ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func> next) + { + _before(); + + var result = await next(); + + _after(); + + return result; + } + } +} \ No newline at end of file diff --git a/test/Autofac.Integration.WebApi.Test/TestTypes/TestContinuationActionFilterWithTransactionScope.cs b/test/Autofac.Integration.WebApi.Test/TestTypes/TestContinuationActionFilterWithTransactionScope.cs new file mode 100644 index 0000000..933360d --- /dev/null +++ b/test/Autofac.Integration.WebApi.Test/TestTypes/TestContinuationActionFilterWithTransactionScope.cs @@ -0,0 +1,62 @@ +// This software is part of the Autofac IoC container +// Copyright (c) 2012 Autofac Contributors +// https://autofac.org +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; +using System.Web.Http.Controllers; + +namespace Autofac.Integration.WebApi.Test.TestTypes +{ + public class TestContinuationActionFilterWithTransactionScope : IAutofacContinuationActionFilter + { + private readonly Action _before; + private readonly Action _after; + + public TestContinuationActionFilterWithTransactionScope(Action before, Action after) + { + _before = before; + _after = after; + } + + public async Task ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func> next) + { + _before(); + + HttpResponseMessage result; + + using (new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + result = await next(); + } + + _after(); + + return result; + } + } +} \ No newline at end of file