[Fixes #5161] Support running middleware pipeline as part of a resource filter
This commit is contained in:
parent
499fefcc03
commit
f7ee16170c
|
|
@ -85,6 +85,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
"ConfigureServices(...)"));
|
"ConfigureServices(...)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var middlewarePipelineBuilder = app.ApplicationServices.GetRequiredService<MiddlewareFilterBuilder>();
|
||||||
|
middlewarePipelineBuilder.ApplicationBuilder = app.New();
|
||||||
|
|
||||||
var routes = new RouteBuilder(app)
|
var routes = new RouteBuilder(app)
|
||||||
{
|
{
|
||||||
DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
|
DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,13 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
//
|
//
|
||||||
services.TryAddSingleton<MvcRouteHandler>(); // Only one per app
|
services.TryAddSingleton<MvcRouteHandler>(); // Only one per app
|
||||||
services.TryAddTransient<MvcAttributeRouteHandler>(); // Many per app
|
services.TryAddTransient<MvcAttributeRouteHandler>(); // Many per app
|
||||||
|
|
||||||
|
//
|
||||||
|
// Middleware pipeline filter related
|
||||||
|
//
|
||||||
|
services.TryAddSingleton<MiddlewareFilterConfigurationProvider>();
|
||||||
|
// This maintains a cache of middleware pipelines, so it needs to be a singleton
|
||||||
|
services.TryAddSingleton<MiddlewareFilterBuilder>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureDefaultServices(IServiceCollection services)
|
private static void ConfigureDefaultServices(IServiceCollection services)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a middleware pipeline provided the by the <see cref="MiddlewareFilterAttribute.ConfigurationType"/>.
|
||||||
|
/// The middleware pipeline will be treated as an async resource filter.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public class MiddlewareFilterAttribute : Attribute, IFilterFactory, IOrderedFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates a new instance of <see cref="MiddlewareFilterAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configurationType">A type which configures a middleware pipeline.</param>
|
||||||
|
public MiddlewareFilterAttribute(Type configurationType)
|
||||||
|
{
|
||||||
|
if (configurationType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(configurationType));
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationType = configurationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type ConfigurationType { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Order { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsReusable { get; } = true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
if (serviceProvider == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(serviceProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
var middlewarePipelineService = serviceProvider.GetRequiredService<MiddlewareFilterBuilder>();
|
||||||
|
var pipeline = middlewarePipelineService.GetPipeline(ConfigurationType);
|
||||||
|
|
||||||
|
return new MiddlewareFilter(pipeline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A feature in <see cref="Microsoft.AspNetCore.Http.HttpContext.Features"/> which is used to capture the
|
||||||
|
/// currently executing context of a resource filter. This feature is used in the final middleware
|
||||||
|
/// of a middleware filter's pipeline to keep the request flow through the rest of the MVC layers.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMiddlewareFilterFeature
|
||||||
|
{
|
||||||
|
ResourceExecutingContext ResourceExecutingContext { get; }
|
||||||
|
|
||||||
|
ResourceExecutionDelegate ResourceExecutionDelegate { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A filter which executes a user configured middleware pipeline.
|
||||||
|
/// </summary>
|
||||||
|
public class MiddlewareFilter : IAsyncResourceFilter
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _middlewarePipeline;
|
||||||
|
|
||||||
|
public MiddlewareFilter(RequestDelegate middlewarePipeline)
|
||||||
|
{
|
||||||
|
if (middlewarePipeline == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(middlewarePipeline));
|
||||||
|
}
|
||||||
|
|
||||||
|
_middlewarePipeline = middlewarePipeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
|
||||||
|
{
|
||||||
|
var httpContext = context.HttpContext;
|
||||||
|
|
||||||
|
// Capture the current context into the feature. This will later be used in the end middleware to continue
|
||||||
|
// the execution flow to later MVC layers.
|
||||||
|
// Example:
|
||||||
|
// this filter -> user-middleware1 -> user-middleware2 -> the-end-middleware -> resouce filters or model binding
|
||||||
|
var feature = new MiddlewareFilterFeature()
|
||||||
|
{
|
||||||
|
ResourceExecutionDelegate = next,
|
||||||
|
ResourceExecutingContext = context
|
||||||
|
};
|
||||||
|
httpContext.Features.Set<IMiddlewareFilterFeature>(feature);
|
||||||
|
|
||||||
|
return _middlewarePipeline(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a middleware pipeline after receiving the pipeline from a pipeline provider
|
||||||
|
/// </summary>
|
||||||
|
public class MiddlewareFilterBuilder
|
||||||
|
{
|
||||||
|
// 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the pipeline more
|
||||||
|
// once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
|
||||||
|
// threads but only one of the objects succeeds in creating a pipeline.
|
||||||
|
private readonly ConcurrentDictionary<Type, Lazy<RequestDelegate>> _pipelinesCache
|
||||||
|
= new ConcurrentDictionary<Type, Lazy<RequestDelegate>>();
|
||||||
|
private readonly MiddlewareFilterConfigurationProvider _configurationProvider;
|
||||||
|
|
||||||
|
public IApplicationBuilder ApplicationBuilder { get; set; }
|
||||||
|
|
||||||
|
public MiddlewareFilterBuilder(MiddlewareFilterConfigurationProvider configurationProvider)
|
||||||
|
{
|
||||||
|
_configurationProvider = configurationProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestDelegate GetPipeline(Type configurationType)
|
||||||
|
{
|
||||||
|
// Build the pipeline only once. This is similar to how middlewares registered in Startup are constructed.
|
||||||
|
|
||||||
|
var requestDelegate = _pipelinesCache.GetOrAdd(
|
||||||
|
configurationType,
|
||||||
|
key => new Lazy<RequestDelegate>(() => BuildPipeline(key)));
|
||||||
|
|
||||||
|
return requestDelegate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestDelegate BuildPipeline(Type middlewarePipelineProviderType)
|
||||||
|
{
|
||||||
|
if (ApplicationBuilder == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatMiddlewareFilterBuilder_NullApplicationBuilder(nameof(ApplicationBuilder)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var nestedAppBuilder = ApplicationBuilder.New();
|
||||||
|
|
||||||
|
// Get the 'Configure' method from the user provided type.
|
||||||
|
var configureDelegate = _configurationProvider.CreateConfigureDelegate(middlewarePipelineProviderType);
|
||||||
|
configureDelegate(nestedAppBuilder);
|
||||||
|
|
||||||
|
// The middleware resource filter, after receiving the request executes the user configured middleware
|
||||||
|
// pipeline. Since we want execution of the request to continue to later MVC layers (resource filters
|
||||||
|
// or model binding), add a middleware at the end of the user provided pipeline which make sure to continue
|
||||||
|
// this flow.
|
||||||
|
// Example:
|
||||||
|
// middleware filter -> user-middleware1 -> user-middleware2 -> end-middleware -> resouce filters or model binding
|
||||||
|
nestedAppBuilder.Run(async (httpContext) =>
|
||||||
|
{
|
||||||
|
var feature = httpContext.Features.Get<IMiddlewareFilterFeature>();
|
||||||
|
if (feature == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatMiddlewareFilterBuilder_NoMiddlewareFeature(nameof(IMiddlewareFilterFeature)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var resourceExecutionDelegate = feature.ResourceExecutionDelegate;
|
||||||
|
|
||||||
|
var resourceExecutedContext = await resourceExecutionDelegate();
|
||||||
|
|
||||||
|
// Ideally we want the experience of a middleware pipeline to behave the same as if it was registered,
|
||||||
|
// in Startup. In this scenario an exception thrown in a middelware later in the pipeline gets propagated
|
||||||
|
// back to earlier middleware.
|
||||||
|
// So check if a later resource filter threw an exception and propagate that back to the middleware pipeline.
|
||||||
|
if (!resourceExecutedContext.ExceptionHandled && resourceExecutedContext.Exception != null)
|
||||||
|
{
|
||||||
|
throw resourceExecutedContext.Exception;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return nestedAppBuilder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calls into user provided 'Configure' methods for configuring a middleware pipeline. The semantics of finding
|
||||||
|
/// the 'Configure' methods is similar to the application Startup class.
|
||||||
|
/// </summary>
|
||||||
|
public class MiddlewareFilterConfigurationProvider
|
||||||
|
{
|
||||||
|
public Action<IApplicationBuilder> CreateConfigureDelegate(Type configurationType)
|
||||||
|
{
|
||||||
|
if (configurationType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(configurationType));
|
||||||
|
}
|
||||||
|
|
||||||
|
var instance = Activator.CreateInstance(configurationType);
|
||||||
|
var configureDelegateBuilder = GetConfigureDelegateBuilder(configurationType);
|
||||||
|
return configureDelegateBuilder.Build(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConfigureBuilder GetConfigureDelegateBuilder(Type startupType)
|
||||||
|
{
|
||||||
|
var configureMethod = FindMethod(startupType, typeof(void));
|
||||||
|
return new ConfigureBuilder(configureMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInfo FindMethod(Type startupType, Type returnType = null)
|
||||||
|
{
|
||||||
|
var methodName = "Configure";
|
||||||
|
|
||||||
|
var methods = startupType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
|
||||||
|
var selectedMethods = methods.Where(method => method.Name.Equals(methodName)).ToList();
|
||||||
|
if (selectedMethods.Count > 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatMiddewareFilter_ConfigureMethodOverload(methodName));
|
||||||
|
}
|
||||||
|
|
||||||
|
var methodInfo = selectedMethods.FirstOrDefault();
|
||||||
|
if (methodInfo == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatMiddewareFilter_NoConfigureMethod(
|
||||||
|
methodName,
|
||||||
|
startupType.FullName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnType != null && methodInfo.ReturnType != returnType)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatMiddlewareFilter_InvalidConfigureReturnType(
|
||||||
|
methodInfo.Name,
|
||||||
|
startupType.FullName,
|
||||||
|
returnType.Name));
|
||||||
|
}
|
||||||
|
return methodInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConfigureBuilder
|
||||||
|
{
|
||||||
|
public ConfigureBuilder(MethodInfo configure)
|
||||||
|
{
|
||||||
|
MethodInfo = configure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MethodInfo MethodInfo { get; }
|
||||||
|
|
||||||
|
public Action<IApplicationBuilder> Build(object instance)
|
||||||
|
{
|
||||||
|
return (applicationBuilder) => Invoke(instance, applicationBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Invoke(object instance, IApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
var serviceProvider = builder.ApplicationServices;
|
||||||
|
var parameterInfos = MethodInfo.GetParameters();
|
||||||
|
var parameters = new object[parameterInfos.Length];
|
||||||
|
for (var index = 0; index < parameterInfos.Length; index++)
|
||||||
|
{
|
||||||
|
var parameterInfo = parameterInfos[index];
|
||||||
|
if (parameterInfo.ParameterType == typeof(IApplicationBuilder))
|
||||||
|
{
|
||||||
|
parameters[index] = builder;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parameters[index] = serviceProvider.GetRequiredService(parameterInfo.ParameterType);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatMiddlewareFilter_ServiceResolutionFail(
|
||||||
|
parameterInfo.ParameterType.FullName,
|
||||||
|
parameterInfo.Name,
|
||||||
|
MethodInfo.Name,
|
||||||
|
MethodInfo.DeclaringType.FullName),
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MethodInfo.Invoke(instance, parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
public class MiddlewareFilterFeature : IMiddlewareFilterFeature
|
||||||
|
{
|
||||||
|
public ResourceExecutingContext ResourceExecutingContext { get; set; }
|
||||||
|
|
||||||
|
public ResourceExecutionDelegate ResourceExecutionDelegate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1210,6 +1210,102 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("OutputFormattersAreRequired"), p0, p1, p2);
|
return string.Format(CultureInfo.CurrentCulture, GetString("OutputFormattersAreRequired"), p0, p1, p2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Having multiple overloads of method '{0}' is not supported.
|
||||||
|
/// </summary>
|
||||||
|
internal static string MiddewareFilter_ConfigureMethodOverload
|
||||||
|
{
|
||||||
|
get { return GetString("MiddewareFilter_ConfigureMethodOverload"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Having multiple overloads of method '{0}' is not supported.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMiddewareFilter_ConfigureMethodOverload(object p0)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("MiddewareFilter_ConfigureMethodOverload"), p0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A public method named '{0}' could not be found in the '{1}' type.
|
||||||
|
/// </summary>
|
||||||
|
internal static string MiddewareFilter_NoConfigureMethod
|
||||||
|
{
|
||||||
|
get { return GetString("MiddewareFilter_NoConfigureMethod"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A public method named '{0}' could not be found in the '{1}' type.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMiddewareFilter_NoConfigureMethod(object p0, object p1)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("MiddewareFilter_NoConfigureMethod"), p0, p1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find '{0}' in the feature list.
|
||||||
|
/// </summary>
|
||||||
|
internal static string MiddlewareFilterBuilder_NoMiddlewareFeature
|
||||||
|
{
|
||||||
|
get { return GetString("MiddlewareFilterBuilder_NoMiddlewareFeature"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not find '{0}' in the feature list.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMiddlewareFilterBuilder_NoMiddlewareFeature(object p0)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilterBuilder_NoMiddlewareFeature"), p0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// '{0}' property cannot be null.
|
||||||
|
/// </summary>
|
||||||
|
internal static string MiddlewareFilterBuilder_NullApplicationBuilder
|
||||||
|
{
|
||||||
|
get { return GetString("MiddlewareFilterBuilder_NullApplicationBuilder"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// '{0}' property cannot be null.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMiddlewareFilterBuilder_NullApplicationBuilder(object p0)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilterBuilder_NullApplicationBuilder"), p0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The '{0}' method in the type '{1}' must have a return type of '{2}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string MiddlewareFilter_InvalidConfigureReturnType
|
||||||
|
{
|
||||||
|
get { return GetString("MiddlewareFilter_InvalidConfigureReturnType"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The '{0}' method in the type '{1}' must have a return type of '{2}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMiddlewareFilter_InvalidConfigureReturnType(object p0, object p1, object p2)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilter_InvalidConfigureReturnType"), p0, p1, p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not resolve a service of type '{0}' for the parameter '{1}' of method '{2}' on type '{3}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string MiddlewareFilter_ServiceResolutionFail
|
||||||
|
{
|
||||||
|
get { return GetString("MiddlewareFilter_ServiceResolutionFail"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Could not resolve a service of type '{0}' for the parameter '{1}' of method '{2}' on type '{3}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMiddlewareFilter_ServiceResolutionFail(object p0, object p1, object p2, object p3)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilter_ServiceResolutionFail"), p0, p1, p2, p3);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -352,4 +352,22 @@
|
||||||
<data name="OutputFormattersAreRequired" xml:space="preserve">
|
<data name="OutputFormattersAreRequired" xml:space="preserve">
|
||||||
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to format a response.</value>
|
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to format a response.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="MiddewareFilter_ConfigureMethodOverload" xml:space="preserve">
|
||||||
|
<value>Multiple overloads of method '{0}' are not supported.</value>
|
||||||
|
</data>
|
||||||
|
<data name="MiddewareFilter_NoConfigureMethod" xml:space="preserve">
|
||||||
|
<value>A public method named '{0}' could not be found in the '{1}' type.</value>
|
||||||
|
</data>
|
||||||
|
<data name="MiddlewareFilterBuilder_NoMiddlewareFeature" xml:space="preserve">
|
||||||
|
<value>Could not find '{0}' in the feature list.</value>
|
||||||
|
</data>
|
||||||
|
<data name="MiddlewareFilterBuilder_NullApplicationBuilder" xml:space="preserve">
|
||||||
|
<value>The '{0}' property cannot be null.</value>
|
||||||
|
</data>
|
||||||
|
<data name="MiddlewareFilter_InvalidConfigureReturnType" xml:space="preserve">
|
||||||
|
<value>The '{0}' method in the type '{1}' must have a return type of '{2}'.</value>
|
||||||
|
</data>
|
||||||
|
<data name="MiddlewareFilter_ServiceResolutionFail" xml:space="preserve">
|
||||||
|
<value>Could not resolve a service of type '{0}' for the parameter '{1}' of method '{2}' on type '{3}'.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Builder.Internal;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
public class MiddlewareFilterAttributeTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CreatesMiddlewareFilter_WithConfiguredPipeline()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var middlewareFilterAttribute = new MiddlewareFilterAttribute(typeof(Pipeline1));
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton(new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider()));
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
var filterBuilderService = serviceProvider.GetRequiredService<MiddlewareFilterBuilder>();
|
||||||
|
filterBuilderService.ApplicationBuilder = new ApplicationBuilder(serviceProvider);
|
||||||
|
var configureCallCount = 0;
|
||||||
|
Pipeline1.ConfigurePipeline = (ab) =>
|
||||||
|
{
|
||||||
|
configureCallCount++;
|
||||||
|
ab.Use((httpCtxt, next) =>
|
||||||
|
{
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var filter = middlewareFilterAttribute.CreateInstance(serviceProvider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var middlewareFilter = Assert.IsType<MiddlewareFilter>(filter);
|
||||||
|
Assert.NotNull(middlewareFilter);
|
||||||
|
Assert.Equal(1, configureCallCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Pipeline1
|
||||||
|
{
|
||||||
|
public static Action<IApplicationBuilder> ConfigurePipeline { get; set; }
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
ConfigurePipeline(appBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Pipeline2
|
||||||
|
{
|
||||||
|
public static Action<IApplicationBuilder> ConfigurePipeline { get; set; }
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
ConfigurePipeline(appBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Builder.Internal;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
public class MiddlewareFilterBuilderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetPipeline_CallsInto_Configure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
|
||||||
|
var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider());
|
||||||
|
pipelineBuilderService.ApplicationBuilder = appBuilder;
|
||||||
|
var configureCount = 0;
|
||||||
|
Pipeline1.ConfigurePipeline = (ab) =>
|
||||||
|
{
|
||||||
|
configureCount++;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var pipeline = pipelineBuilderService.GetPipeline(typeof(Pipeline1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(pipeline);
|
||||||
|
Assert.Equal(1, configureCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPipeline_CallsIntoConfigure_OnlyOnce_ForTheSamePipelineType()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
|
||||||
|
var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider());
|
||||||
|
pipelineBuilderService.ApplicationBuilder = appBuilder;
|
||||||
|
var configureCount = 0;
|
||||||
|
Pipeline1.ConfigurePipeline = (ab) =>
|
||||||
|
{
|
||||||
|
configureCount++;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var pipeline1 = pipelineBuilderService.GetPipeline(typeof(Pipeline1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(pipeline1);
|
||||||
|
Assert.Equal(1, configureCount);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var pipeline2 = pipelineBuilderService.GetPipeline(typeof(Pipeline1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(pipeline2);
|
||||||
|
Assert.Same(pipeline1, pipeline2);
|
||||||
|
Assert.Equal(1, configureCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EndMiddleware_ThrowsException_WhenMiddleFeature_NotAvailable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
|
||||||
|
var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider());
|
||||||
|
pipelineBuilderService.ApplicationBuilder = appBuilder;
|
||||||
|
Pipeline1.ConfigurePipeline = (ab) =>
|
||||||
|
{
|
||||||
|
ab.Use((httpContext, next) =>
|
||||||
|
{
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var pipeline = pipelineBuilderService.GetPipeline(typeof(Pipeline1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(pipeline);
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline(new DefaultHttpContext()));
|
||||||
|
Assert.Equal(
|
||||||
|
"Could not find 'IMiddlewareFilterFeature' in the feature list.",
|
||||||
|
exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EndMiddleware_PropagatesBackException_ToEarlierMiddleware()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
var appBuilder = new ApplicationBuilder(services.BuildServiceProvider());
|
||||||
|
var pipelineBuilderService = new MiddlewareFilterBuilder(new MiddlewareFilterConfigurationProvider());
|
||||||
|
pipelineBuilderService.ApplicationBuilder = appBuilder;
|
||||||
|
Pipeline1.ConfigurePipeline = (ab) =>
|
||||||
|
{
|
||||||
|
ab.Use((httpCtxt, next) =>
|
||||||
|
{
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var middlewareFilterFeature = new MiddlewareFilterFeature();
|
||||||
|
middlewareFilterFeature.ResourceExecutionDelegate = () =>
|
||||||
|
{
|
||||||
|
var context = new ResourceExecutedContext(
|
||||||
|
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor(), new ModelStateDictionary()),
|
||||||
|
new List<IFilterMetadata>());
|
||||||
|
context.Exception = new InvalidOperationException("Error!!!");
|
||||||
|
return Task.FromResult(context);
|
||||||
|
};
|
||||||
|
var features = new FeatureCollection();
|
||||||
|
features.Set<IMiddlewareFilterFeature>(middlewareFilterFeature);
|
||||||
|
var httpContext = new DefaultHttpContext(features);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var pipeline = pipelineBuilderService.GetPipeline(typeof(Pipeline1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(pipeline);
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => pipeline(httpContext));
|
||||||
|
Assert.Equal("Error!!!", exception.Message);
|
||||||
|
}
|
||||||
|
private class Pipeline1
|
||||||
|
{
|
||||||
|
public static Action<IApplicationBuilder> ConfigurePipeline { get; set; }
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
ConfigurePipeline(appBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Pipeline2
|
||||||
|
{
|
||||||
|
public static Action<IApplicationBuilder> ConfigurePipeline { get; set; }
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
ConfigurePipeline(appBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Castle.Core.Logging;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
public class MiddlewareFilterConfigurationProviderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ValidConfigure_DoesNotThrow()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var provider = new MiddlewareFilterConfigurationProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var configureDelegate = provider.CreateConfigureDelegate(typeof(ValidConfigure_WithNoEnvironment));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(configureDelegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidConfigure_AndAdditionalServices_DoesNotThrow()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var loggerFactory = Mock.Of<ILoggerFactory>();
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton(loggerFactory);
|
||||||
|
services.AddSingleton(Mock.Of<IHostingEnvironment>());
|
||||||
|
var applicationBuilder = GetApplicationBuilder(services);
|
||||||
|
var provider = new MiddlewareFilterConfigurationProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var configureDelegate = provider.CreateConfigureDelegate(typeof(ValidConfigure_WithNoEnvironment_AdditionalServices));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(configureDelegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidType_NoConfigure_Throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(InvalidType_NoConfigure);
|
||||||
|
var provider = new MiddlewareFilterConfigurationProvider();
|
||||||
|
var expected = $"A public method named 'Configure' could not be found in the '{type.FullName}' type.";
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
provider.CreateConfigureDelegate(type);
|
||||||
|
});
|
||||||
|
Assert.Equal(expected, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidType_NoPublicConfigure_Throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(InvalidType_NoPublic_Configure);
|
||||||
|
var provider = new MiddlewareFilterConfigurationProvider();
|
||||||
|
var expected = $"A public method named 'Configure' could not be found in the '{type.FullName}' type.";
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
provider.CreateConfigureDelegate(type);
|
||||||
|
});
|
||||||
|
Assert.Equal(expected, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IApplicationBuilder GetApplicationBuilder(ServiceCollection services = null)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
{
|
||||||
|
services = new ServiceCollection();
|
||||||
|
}
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var applicationBuilder = new Mock<IApplicationBuilder>();
|
||||||
|
applicationBuilder
|
||||||
|
.SetupGet(a => a.ApplicationServices)
|
||||||
|
.Returns(serviceProvider);
|
||||||
|
|
||||||
|
return applicationBuilder.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ValidConfigure_WithNoEnvironment
|
||||||
|
{
|
||||||
|
public void Configure(IApplicationBuilder appBuilder) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ValidConfigure_WithNoEnvironment_AdditionalServices
|
||||||
|
{
|
||||||
|
public void Configure(
|
||||||
|
IApplicationBuilder appBuilder,
|
||||||
|
IHostingEnvironment hostingEnvironment,
|
||||||
|
ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
if (hostingEnvironment == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(hostingEnvironment));
|
||||||
|
}
|
||||||
|
if (loggerFactory == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(loggerFactory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ValidConfigure_WithEnvironment
|
||||||
|
{
|
||||||
|
public void ConfigureProduction(IApplicationBuilder appBuilder) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ValidConfigure_WithEnvironment_AdditionalServices
|
||||||
|
{
|
||||||
|
public void ConfigureProduction(
|
||||||
|
IApplicationBuilder appBuilder,
|
||||||
|
IHostingEnvironment hostingEnvironment,
|
||||||
|
ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
if (hostingEnvironment == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(hostingEnvironment));
|
||||||
|
}
|
||||||
|
if (loggerFactory == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(loggerFactory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MultipleConfigureWithEnvironments
|
||||||
|
{
|
||||||
|
public void ConfigureDevelopment(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureProduction(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InvalidConfigure_NoParameters
|
||||||
|
{
|
||||||
|
public void Configure()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InvalidType_NoConfigure
|
||||||
|
{
|
||||||
|
public void Foo(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InvalidType_NoPublic_Configure
|
||||||
|
{
|
||||||
|
private void Configure(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,516 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Builder.Internal;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
{
|
||||||
|
public class MiddlewareFilterTest
|
||||||
|
{
|
||||||
|
private readonly TestController _controller = new TestController();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MiddlewareFilter_SetsMiddlewareFilterFeature_OnExecution()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
RequestDelegate requestDelegate = (context) => Task.FromResult(true);
|
||||||
|
var middlwareFilter = new MiddlewareFilter(requestDelegate);
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var resourceExecutingContext = GetResourceExecutingContext(httpContext);
|
||||||
|
var resourceExecutionDelegate = GetResourceExecutionDelegate(httpContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await middlwareFilter.OnResourceExecutionAsync(resourceExecutingContext, resourceExecutionDelegate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var feature = resourceExecutingContext.HttpContext.Features.Get<IMiddlewareFilterFeature>();
|
||||||
|
Assert.NotNull(feature);
|
||||||
|
Assert.Same(resourceExecutingContext, feature.ResourceExecutingContext);
|
||||||
|
Assert.Same(resourceExecutionDelegate, feature.ResourceExecutionDelegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OnMiddlewareShortCircuit_DoesNotExecute_RestOfFilterPipeline()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHeader = "h1";
|
||||||
|
Pipeline1.ConfigurePipeline = (appBuilder) =>
|
||||||
|
{
|
||||||
|
appBuilder.Use((httpContext, next) =>
|
||||||
|
{
|
||||||
|
httpContext.Response.Headers.Add(expectedHeader, "");
|
||||||
|
return Task.FromResult(true); // short circuit the request
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var resourceFilter1 = new TestResourceFilter(TestResourceFilterAction.Passthrough);
|
||||||
|
var middlewareResourceFilter = new MiddlewareFilter(GetMiddlewarePipeline(typeof(Pipeline1)));
|
||||||
|
var exceptionThrowingResourceFilter = new TestResourceFilter(TestResourceFilterAction.ThrowException);
|
||||||
|
|
||||||
|
var invoker = CreateInvoker(
|
||||||
|
new IFilterMetadata[]
|
||||||
|
{
|
||||||
|
resourceFilter1,
|
||||||
|
middlewareResourceFilter,
|
||||||
|
exceptionThrowingResourceFilter,
|
||||||
|
},
|
||||||
|
actionThrows: true); // The action won't run
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await invoker.InvokeAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resourceExecutedContext = resourceFilter1.ResourceExecutedContext;
|
||||||
|
Assert.True(resourceExecutedContext.HttpContext.Response.Headers.ContainsKey(expectedHeader));
|
||||||
|
Assert.True(resourceExecutedContext.Canceled);
|
||||||
|
Assert.False(invoker.ControllerFactory.CreateCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Middleware filters are applied at Global, Controller & Action level
|
||||||
|
[Fact]
|
||||||
|
public async Task Multiple_MiddlewareFilters_ConcatsTheMiddlewarePipelines()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedHeader = "h1";
|
||||||
|
var expectedHeaderValue = "pipeline1-pipeline2";
|
||||||
|
Pipeline1.ConfigurePipeline = (appBuilder) =>
|
||||||
|
{
|
||||||
|
appBuilder.Use((httpContext, next) =>
|
||||||
|
{
|
||||||
|
httpContext.Response.Headers["h1"] = "pipeline1";
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Pipeline2.ConfigurePipeline = (appBuilder) =>
|
||||||
|
{
|
||||||
|
appBuilder.Use((httpContext, next) =>
|
||||||
|
{
|
||||||
|
httpContext.Response.Headers["h1"] = httpContext.Response.Headers["h1"] + "-pipeline2";
|
||||||
|
return Task.FromResult(true); // short circuits the request
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var resourceFilter1 = new TestResourceFilter(TestResourceFilterAction.Passthrough);
|
||||||
|
var middlewareResourceFilter1 = new MiddlewareFilter(GetMiddlewarePipeline(typeof(Pipeline1)));
|
||||||
|
var middlewareResourceFilter2 = new MiddlewareFilter(GetMiddlewarePipeline(typeof(Pipeline2)));
|
||||||
|
var exceptionThrowingResourceFilter = new TestResourceFilter(TestResourceFilterAction.ThrowException);
|
||||||
|
|
||||||
|
var invoker = CreateInvoker(
|
||||||
|
new IFilterMetadata[]
|
||||||
|
{
|
||||||
|
resourceFilter1, // This filter will pass through
|
||||||
|
middlewareResourceFilter1, // This filter will pass through
|
||||||
|
middlewareResourceFilter2, // This filter will short circuit
|
||||||
|
exceptionThrowingResourceFilter, // This shouldn't run
|
||||||
|
},
|
||||||
|
actionThrows: true); // The action won't run
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await invoker.InvokeAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resourceExecutedContext = resourceFilter1.ResourceExecutedContext;
|
||||||
|
var response = resourceExecutedContext.HttpContext.Response;
|
||||||
|
Assert.True(response.Headers.ContainsKey(expectedHeader));
|
||||||
|
Assert.Equal(expectedHeaderValue, response.Headers[expectedHeader]);
|
||||||
|
Assert.True(resourceExecutedContext.Canceled);
|
||||||
|
Assert.False(invoker.ControllerFactory.CreateCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UnhandledException_InMiddleware_PropagatesBackToInvoker()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = "Error!!!";
|
||||||
|
Pipeline1.ConfigurePipeline = (appBuilder) =>
|
||||||
|
{
|
||||||
|
appBuilder.Use((httpContext, next) =>
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(expectedMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var resourceFilter1 = new TestResourceFilter(TestResourceFilterAction.Passthrough);
|
||||||
|
var middlewareResourceFilter = new MiddlewareFilter(GetMiddlewarePipeline(typeof(Pipeline1)));
|
||||||
|
var exceptionThrowingResourceFilter = new TestResourceFilter(TestResourceFilterAction.ThrowException);
|
||||||
|
|
||||||
|
var invoker = CreateInvoker(
|
||||||
|
new IFilterMetadata[]
|
||||||
|
{
|
||||||
|
resourceFilter1,
|
||||||
|
middlewareResourceFilter,
|
||||||
|
exceptionThrowingResourceFilter, // This shouldn't run
|
||||||
|
},
|
||||||
|
actionThrows: true); // The action won't run
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await invoker.InvokeAsync());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExceptionThrownInMiddleware_CanBeHandled_ByEarlierMiddleware()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = "Error!!!";
|
||||||
|
Pipeline1.ConfigurePipeline = (appBuilder) =>
|
||||||
|
{
|
||||||
|
appBuilder.Use(async (httpContext, next) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
httpContext.Response.StatusCode = 500;
|
||||||
|
httpContext.Response.Headers.Add("Error", "Error!!!!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Pipeline2.ConfigurePipeline = (appBuilder) =>
|
||||||
|
{
|
||||||
|
appBuilder.Use((httpContext, next) =>
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(expectedMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var resourceFilter1 = new TestResourceFilter(TestResourceFilterAction.Passthrough);
|
||||||
|
var middlewareResourceFilter1 = new MiddlewareFilter(GetMiddlewarePipeline(typeof(Pipeline1)));
|
||||||
|
var middlewareResourceFilter2 = new MiddlewareFilter(GetMiddlewarePipeline(typeof(Pipeline2)));
|
||||||
|
var exceptionThrowingResourceFilter = new TestResourceFilter(TestResourceFilterAction.ThrowException);
|
||||||
|
|
||||||
|
var invoker = CreateInvoker(
|
||||||
|
new IFilterMetadata[]
|
||||||
|
{
|
||||||
|
resourceFilter1,
|
||||||
|
middlewareResourceFilter1,
|
||||||
|
middlewareResourceFilter2,
|
||||||
|
exceptionThrowingResourceFilter, // This shouldn't run
|
||||||
|
},
|
||||||
|
actionThrows: true); // The action won't run
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await invoker.InvokeAsync());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resourceExecutedContext = resourceFilter1.ResourceExecutedContext;
|
||||||
|
var response = resourceExecutedContext.HttpContext.Response;
|
||||||
|
Assert.Equal(500, response.StatusCode);
|
||||||
|
Assert.True(response.Headers.ContainsKey("Error"));
|
||||||
|
Assert.False(invoker.ControllerFactory.CreateCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResourceExecutingContext GetResourceExecutingContext(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
return new ResourceExecutingContext(
|
||||||
|
new ActionContext(httpContext, new RouteData(), new ActionDescriptor(), new ModelStateDictionary()),
|
||||||
|
new List<IFilterMetadata>(),
|
||||||
|
new List<IValueProviderFactory>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResourceExecutionDelegate GetResourceExecutionDelegate(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
return new ResourceExecutionDelegate(
|
||||||
|
() => Task.FromResult(new ResourceExecutedContext(new ActionContext(), new List<IFilterMetadata>())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestControllerActionInvoker CreateInvoker(
|
||||||
|
IFilterMetadata[] filters,
|
||||||
|
bool actionThrows = false)
|
||||||
|
{
|
||||||
|
var actionDescriptor = new ControllerActionDescriptor()
|
||||||
|
{
|
||||||
|
FilterDescriptors = new List<FilterDescriptor>(),
|
||||||
|
Parameters = new List<ParameterDescriptor>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (actionThrows)
|
||||||
|
{
|
||||||
|
actionDescriptor.MethodInfo = typeof(ControllerActionInvokerTest).GetMethod(
|
||||||
|
nameof(ControllerActionInvokerTest.ThrowingActionMethod));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
actionDescriptor.MethodInfo = typeof(ControllerActionInvokerTest).GetMethod(
|
||||||
|
nameof(ControllerActionInvokerTest.ActionMethod));
|
||||||
|
}
|
||||||
|
actionDescriptor.ControllerTypeInfo = typeof(ControllerActionInvokerTest).GetTypeInfo();
|
||||||
|
|
||||||
|
return CreateInvoker(filters, actionDescriptor, _controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestControllerActionInvoker CreateInvoker(
|
||||||
|
IFilterMetadata[] filters,
|
||||||
|
ControllerActionDescriptor actionDescriptor,
|
||||||
|
object controller)
|
||||||
|
{
|
||||||
|
var httpContext = GetHttpContext();
|
||||||
|
httpContext.Response.Body = new MemoryStream();
|
||||||
|
|
||||||
|
var options = new MvcOptions();
|
||||||
|
var optionsAccessor = new Mock<IOptions<MvcOptions>>();
|
||||||
|
optionsAccessor
|
||||||
|
.SetupGet(o => o.Value)
|
||||||
|
.Returns(options);
|
||||||
|
|
||||||
|
var actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor);
|
||||||
|
|
||||||
|
var filterProvider = new Mock<IFilterProvider>(MockBehavior.Strict);
|
||||||
|
filterProvider
|
||||||
|
.Setup(fp => fp.OnProvidersExecuting(It.IsAny<FilterProviderContext>()))
|
||||||
|
.Callback<FilterProviderContext>(context =>
|
||||||
|
{
|
||||||
|
foreach (var filterMetadata in filters)
|
||||||
|
{
|
||||||
|
context.Results.Add(new FilterItem(new FilterDescriptor(filterMetadata, FilterScope.Action))
|
||||||
|
{
|
||||||
|
Filter = filterMetadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
filterProvider
|
||||||
|
.Setup(fp => fp.OnProvidersExecuted(It.IsAny<FilterProviderContext>()))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
filterProvider
|
||||||
|
.SetupGet(fp => fp.Order)
|
||||||
|
.Returns(-1000);
|
||||||
|
|
||||||
|
var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
|
||||||
|
diagnosticSource.SubscribeWithAdapter(new TestDiagnosticListener());
|
||||||
|
|
||||||
|
var invoker = new TestControllerActionInvoker(
|
||||||
|
new[] { filterProvider.Object },
|
||||||
|
new MockControllerFactory(controller ?? this),
|
||||||
|
new TestControllerArgumentBinder(actionParameters: null),
|
||||||
|
new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
|
||||||
|
diagnosticSource,
|
||||||
|
actionContext,
|
||||||
|
new List<IValueProviderFactory>(),
|
||||||
|
maxAllowedErrorsInModelState: 200);
|
||||||
|
return invoker;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Pipeline1
|
||||||
|
{
|
||||||
|
public static Action<IApplicationBuilder> ConfigurePipeline { get; set; }
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
ConfigurePipeline(appBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Pipeline2
|
||||||
|
{
|
||||||
|
public static Action<IApplicationBuilder> ConfigurePipeline { get; set; }
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder appBuilder)
|
||||||
|
{
|
||||||
|
ConfigurePipeline(appBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpContext GetHttpContext()
|
||||||
|
{
|
||||||
|
var services = CreateServices();
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.RequestServices = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
return httpContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestDelegate GetMiddlewarePipeline(Type middlewarePipelineProviderType)
|
||||||
|
{
|
||||||
|
var applicationServices = new ServiceCollection();
|
||||||
|
var applicationBuilder = new ApplicationBuilder(applicationServices.BuildServiceProvider());
|
||||||
|
var middlewareFilterBuilderService = new MiddlewareFilterBuilder(
|
||||||
|
new MiddlewareFilterConfigurationProvider());
|
||||||
|
middlewareFilterBuilderService.ApplicationBuilder = applicationBuilder;
|
||||||
|
return middlewareFilterBuilderService.GetPipeline(middlewarePipelineProviderType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceCollection CreateServices()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MockControllerFactory : IControllerFactory
|
||||||
|
{
|
||||||
|
private object _controller;
|
||||||
|
|
||||||
|
public MockControllerFactory(object controller)
|
||||||
|
{
|
||||||
|
_controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CreateCalled { get; private set; }
|
||||||
|
|
||||||
|
public bool ReleaseCalled { get; private set; }
|
||||||
|
|
||||||
|
public ControllerContext ControllerContext { get; private set; }
|
||||||
|
|
||||||
|
public object CreateController(ControllerContext context)
|
||||||
|
{
|
||||||
|
ControllerContext = context;
|
||||||
|
CreateCalled = true;
|
||||||
|
return _controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseController(ControllerContext context, object controller)
|
||||||
|
{
|
||||||
|
Assert.NotNull(controller);
|
||||||
|
Assert.Same(_controller, controller);
|
||||||
|
ReleaseCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Verify()
|
||||||
|
{
|
||||||
|
if (CreateCalled && !ReleaseCalled)
|
||||||
|
{
|
||||||
|
Assert.False(true, "ReleaseController should have been called.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection().BuildServiceProvider();
|
||||||
|
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
|
||||||
|
return new ControllerActionInvokerCache(
|
||||||
|
descriptorProvider,
|
||||||
|
filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestControllerActionInvoker : ControllerActionInvoker
|
||||||
|
{
|
||||||
|
public TestControllerActionInvoker(
|
||||||
|
IFilterProvider[] filterProviders,
|
||||||
|
MockControllerFactory controllerFactory,
|
||||||
|
IControllerArgumentBinder argumentBinder,
|
||||||
|
ILogger logger,
|
||||||
|
DiagnosticSource diagnosticSource,
|
||||||
|
ActionContext actionContext,
|
||||||
|
IReadOnlyList<IValueProviderFactory> valueProviderFactories,
|
||||||
|
int maxAllowedErrorsInModelState)
|
||||||
|
: base(
|
||||||
|
CreateFilterCache(filterProviders),
|
||||||
|
controllerFactory,
|
||||||
|
argumentBinder,
|
||||||
|
logger,
|
||||||
|
diagnosticSource,
|
||||||
|
actionContext,
|
||||||
|
valueProviderFactories,
|
||||||
|
maxAllowedErrorsInModelState)
|
||||||
|
{
|
||||||
|
ControllerFactory = controllerFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MockControllerFactory ControllerFactory { get; }
|
||||||
|
|
||||||
|
public async override Task InvokeAsync()
|
||||||
|
{
|
||||||
|
await base.InvokeAsync();
|
||||||
|
|
||||||
|
// Make sure that the controller was disposed in every test that creates ones.
|
||||||
|
ControllerFactory.Verify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestControllerArgumentBinder : IControllerArgumentBinder
|
||||||
|
{
|
||||||
|
private readonly IDictionary<string, object> _actionParameters;
|
||||||
|
public TestControllerArgumentBinder(IDictionary<string, object> actionParameters)
|
||||||
|
{
|
||||||
|
_actionParameters = actionParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task BindArgumentsAsync(
|
||||||
|
ControllerContext controllerContext,
|
||||||
|
object controller,
|
||||||
|
IDictionary<string, object> arguments)
|
||||||
|
{
|
||||||
|
foreach (var entry in _actionParameters)
|
||||||
|
{
|
||||||
|
arguments.Add(entry.Key, entry.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestController
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private enum TestResourceFilterAction
|
||||||
|
{
|
||||||
|
ShortCircuit,
|
||||||
|
ThrowException,
|
||||||
|
Passthrough
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestResourceFilter : IAsyncResourceFilter
|
||||||
|
{
|
||||||
|
private readonly TestResourceFilterAction _action;
|
||||||
|
public TestResourceFilter(TestResourceFilterAction action)
|
||||||
|
{
|
||||||
|
_action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceExecutedContext ResourceExecutedContext { get; private set; }
|
||||||
|
|
||||||
|
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
|
||||||
|
{
|
||||||
|
if (_action == TestResourceFilterAction.ThrowException)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("This filter should not have been run!");
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (_action == TestResourceFilterAction.Passthrough)
|
||||||
|
{
|
||||||
|
ResourceExecutedContext = await next();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Result = new TestActionResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestActionResult : IActionResult
|
||||||
|
{
|
||||||
|
public Task ExecuteResultAsync(ActionContext context)
|
||||||
|
{
|
||||||
|
context.HttpContext.Response.StatusCode = 200;
|
||||||
|
return context.HttpContext.Response.WriteAsync("Shortcircuited");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -536,5 +536,23 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("text/plain", response.Content.Headers.ContentType.MediaType);
|
Assert.Equal("text/plain", response.Content.Headers.ContentType.MediaType);
|
||||||
Assert.Equal("Data:10", await response.Content.ReadAsStringAsync());
|
Assert.Equal("Data:10", await response.Content.ReadAsStringAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("en-US", "en-US")]
|
||||||
|
[InlineData("fr", "fr")]
|
||||||
|
[InlineData("ab-cd", "en-US")]
|
||||||
|
public async Task MiddlewareFilter_LocalizationMiddlewareRegistration_UsesRouteDataToFindCulture(
|
||||||
|
string culture,
|
||||||
|
string expected)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var response = await Client.GetAsync($"http://localhost/{culture}/MiddlewareFilterTest/CultureFromRouteData");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal(
|
||||||
|
$"CurrentCulture:{expected},CurrentUICulture:{expected}",
|
||||||
|
await response.Content.ReadAsStringAsync());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace FiltersWebSite
|
||||||
|
{
|
||||||
|
public class MiddlewareFilterTestController : Controller
|
||||||
|
{
|
||||||
|
[Route("{culture}/[controller]/[action]")]
|
||||||
|
[MiddlewareFilter(typeof(LocalizationPipeline))]
|
||||||
|
public IActionResult CultureFromRouteData()
|
||||||
|
{
|
||||||
|
return Content($"CurrentCulture:{CultureInfo.CurrentCulture.Name},CurrentUICulture:{CultureInfo.CurrentUICulture.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace FiltersWebSite
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public class TestSyncResourceFilter : Attribute, IResourceFilter, IOrderedFilter
|
||||||
|
{
|
||||||
|
public enum Action
|
||||||
|
{
|
||||||
|
PassThrough,
|
||||||
|
ThrowException,
|
||||||
|
Shortcircuit
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly string ExceptionMessage = $"Error!! in {nameof(TestSyncResourceFilter)}";
|
||||||
|
|
||||||
|
public readonly string ShortcircuitMessage = $"Shortcircuited by {nameof(TestSyncResourceFilter)}";
|
||||||
|
|
||||||
|
public Action FilterAction { get; set; }
|
||||||
|
|
||||||
|
public int Order { get; set; }
|
||||||
|
|
||||||
|
public void OnResourceExecuted(ResourceExecutedContext context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||||
|
{
|
||||||
|
if (FilterAction == Action.PassThrough)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (FilterAction == Action.ThrowException)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(ExceptionMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Result = new ContentResult()
|
||||||
|
{
|
||||||
|
Content = ShortcircuitMessage,
|
||||||
|
StatusCode = 400,
|
||||||
|
ContentType = "text/abcd"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public class TestAsyncResourceFilter : Attribute, IAsyncResourceFilter, IOrderedFilter
|
||||||
|
{
|
||||||
|
public enum Action
|
||||||
|
{
|
||||||
|
PassThrough,
|
||||||
|
ThrowException,
|
||||||
|
Shortcircuit
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly string ExceptionMessage = $"Error!! in {nameof(TestAsyncResourceFilter)}";
|
||||||
|
|
||||||
|
public readonly string ShortcircuitMessage = $"Shortcircuited by {nameof(TestAsyncResourceFilter)}";
|
||||||
|
|
||||||
|
public Action FilterAction { get; set; }
|
||||||
|
|
||||||
|
public int Order { get; set; }
|
||||||
|
|
||||||
|
public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
|
||||||
|
{
|
||||||
|
if (FilterAction == Action.PassThrough)
|
||||||
|
{
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
else if (FilterAction == Action.ThrowException)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(ExceptionMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Result = new ContentResult()
|
||||||
|
{
|
||||||
|
Content = ShortcircuitMessage,
|
||||||
|
StatusCode = 400,
|
||||||
|
ContentType = "text/abcd"
|
||||||
|
};
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Localization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Localization;
|
||||||
|
|
||||||
|
namespace FiltersWebSite
|
||||||
|
{
|
||||||
|
public class LocalizationPipeline
|
||||||
|
{
|
||||||
|
public void Configure(IApplicationBuilder applicationBuilder)
|
||||||
|
{
|
||||||
|
var supportedCultures = new[]
|
||||||
|
{
|
||||||
|
new CultureInfo("en-US"),
|
||||||
|
new CultureInfo("fr")
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new RequestLocalizationOptions()
|
||||||
|
{
|
||||||
|
DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US"),
|
||||||
|
SupportedCultures = supportedCultures,
|
||||||
|
SupportedUICultures = supportedCultures
|
||||||
|
};
|
||||||
|
options.RequestCultureProviders = new[] { new RouteDataRequestCultureProvider() { Options = options } };
|
||||||
|
|
||||||
|
applicationBuilder.UseRequestLocalization(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Localization;
|
||||||
|
|
||||||
|
namespace FiltersWebSite
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the culture information for a request via values in the route data.
|
||||||
|
/// </summary>
|
||||||
|
public class RouteDataRequestCultureProvider : RequestCultureProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The key that contains the culture name.
|
||||||
|
/// Defaults to "culture".
|
||||||
|
/// </summary>
|
||||||
|
public string RouteDataStringKey { get; set; } = "culture";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The key that contains the UI culture name. If not specified or no value is found,
|
||||||
|
/// <see cref="RouteDataStringKey"/> will be used.
|
||||||
|
/// Defaults to "ui-culture".
|
||||||
|
/// </summary>
|
||||||
|
public string UIRouteDataStringKey { get; set; } = "ui-culture";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
if (httpContext == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(httpContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
string culture = null;
|
||||||
|
string uiCulture = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(RouteDataStringKey))
|
||||||
|
{
|
||||||
|
culture = httpContext.GetRouteValue(RouteDataStringKey) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(UIRouteDataStringKey))
|
||||||
|
{
|
||||||
|
uiCulture = httpContext.GetRouteValue(UIRouteDataStringKey) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (culture == null && uiCulture == null)
|
||||||
|
{
|
||||||
|
// No values specified for either so no match
|
||||||
|
return Task.FromResult((ProviderCultureResult)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (culture != null && uiCulture == null)
|
||||||
|
{
|
||||||
|
// Value for culture but not for UI culture so default to culture value for both
|
||||||
|
uiCulture = culture;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (culture == null && uiCulture != null)
|
||||||
|
{
|
||||||
|
// Value for UI culture but not for culture so default to UI culture value for both
|
||||||
|
culture = uiCulture;
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerResultCulture = new ProviderCultureResult(culture, uiCulture);
|
||||||
|
|
||||||
|
return Task.FromResult(providerResultCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue