Compile middleware invoke method when extra args are provided

- Improves the performance when accessing scoped services in middleware
This commit is contained in:
David Fowler 2016-01-07 10:27:36 -08:00
parent e7bf0e71bb
commit ca8136b73c
4 changed files with 231 additions and 27 deletions

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
@ -16,7 +17,9 @@ namespace Microsoft.AspNet.Builder
/// </summary>
public static class UseMiddlewareExtensions
{
const string InvokeMethodName = "Invoke";
private const string InvokeMethodName = "Invoke";
private static readonly MethodInfo GetServiceInfo = typeof(UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static);
/// <summary>
/// Adds a middleware type to the application's request pipeline.
@ -49,7 +52,7 @@ namespace Microsoft.AspNet.Builder
throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName));
}
if (invokeMethods.Length == 0)
if (invokeMethods.Length == 0)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName));
}
@ -63,15 +66,20 @@ namespace Microsoft.AspNet.Builder
var parameters = methodinfo.GetParameters();
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName,nameof(HttpContext)));
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, nameof(HttpContext)));
}
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, new[] { next }.Concat(args).ToArray());
var ctorArgs = new object[args.Length + 1];
ctorArgs[0] = next;
Array.Copy(args, 0, ctorArgs, 1, args.Length);
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
if (parameters.Length == 1)
{
return (RequestDelegate)methodinfo.CreateDelegate(typeof(RequestDelegate), instance);
}
var factory = Compile<object>(methodinfo, parameters);
return context =>
{
var serviceProvider = context.RequestServices ?? applicationServices;
@ -80,20 +88,97 @@ namespace Microsoft.AspNet.Builder
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
}
var arguments = new object[parameters.Length];
arguments[0] = context;
for(var index = 1; index != parameters.Length; ++index)
{
var serviceType = parameters[index].ParameterType;
arguments[index] = serviceProvider.GetService(serviceType);
if (arguments[index] == null)
{
throw new Exception(string.Format("No service for type '{0}' has been registered.", serviceType));
}
}
return (Task)methodinfo.Invoke(instance, arguments);
return factory(instance, context, serviceProvider);
};
});
}
private static Func<T, HttpContext, IServiceProvider, Task> Compile<T>(MethodInfo methodinfo, ParameterInfo[] parameters)
{
// If we call something like
//
// public class Middleware
// {
// public Task Invoke(HttpContext context, ILoggerFactory loggeryFactory)
// {
//
// }
// }
//
// We'll end up with something like this:
// Generic version:
//
// Task Invoke(Middleware instance, HttpContext httpContext, IServiceprovider provider)
// {
// return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory));
// }
// Non generic version:
//
// Task Invoke(object instance, HttpContext httpContext, IServiceprovider provider)
// {
// return ((Middleware)instance).Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory));
// }
// context =>
// {
// var serviceProvider = context.RequestServices ?? applicationServices;
// if (serviceProvider == null)
// {
// throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
// }
//
// return Invoke(httpContext, serviceProvider);
// }
var middleware = typeof(T);
var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext");
var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider");
var instanceArg = Expression.Parameter(middleware, "middleware");
var methodArguments = new Expression[parameters.Length];
methodArguments[0] = httpContextArg;
for (int i = 1; i < parameters.Length; i++)
{
var parameterType = parameters[i].ParameterType;
if (parameterType.IsByRef)
{
throw new NotSupportedException(Resources.FormatException_InvokeDoesNotSupportRefOrOutParams(InvokeMethodName));
}
var parameterTypeExpression = new Expression[] {
providerArg,
Expression.Constant(parameterType, typeof(Type)),
Expression.Constant(methodinfo.DeclaringType, typeof(Type))
};
methodArguments[i] = Expression.Convert(Expression.Call(GetServiceInfo, parameterTypeExpression), parameterType);
}
Expression middlewareInstanceArg = instanceArg;
if (methodinfo.DeclaringType != typeof(T))
{
middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodinfo.DeclaringType);
}
var body = Expression.Call(middlewareInstanceArg, methodinfo, methodArguments);
var lambda = Expression.Lambda<Func<T, HttpContext, IServiceProvider, Task>>(body, instanceArg, httpContextArg, providerArg);
return lambda.Compile();
}
private static object GetService(IServiceProvider sp, Type type, Type middleware)
{
var service = sp.GetService(type);
if (service == null)
{
throw new InvalidOperationException(Resources.FormatException_InvokeMiddlewareNoService(type, middleware));
}
return service;
}
}
}

View File

@ -10,14 +10,6 @@ namespace Microsoft.AspNet.Http.Abstractions
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNet.Http.Abstractions.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// '{0}' is not available.
/// </summary>
internal static string FormatException_PathMustStartWithSlash(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Exception_PathMustStartWithSlash"), p0);
}
/// <summary>
/// '{0}' is not available.
/// </summary>
@ -98,6 +90,54 @@ namespace Microsoft.AspNet.Http.Abstractions
return string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddleMutlipleInvokes"), p0);
}
/// <summary>
/// The path in '{0}' must start with '/'.
/// </summary>
internal static string Exception_PathMustStartWithSlash
{
get { return GetString("Exception_PathMustStartWithSlash"); }
}
/// <summary>
/// The path in '{0}' must start with '/'.
/// </summary>
internal static string FormatException_PathMustStartWithSlash(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Exception_PathMustStartWithSlash"), p0);
}
/// <summary>
/// Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'.
/// </summary>
internal static string Exception_InvokeMiddlewareNoService
{
get { return GetString("Exception_InvokeMiddlewareNoService"); }
}
/// <summary>
/// Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'.
/// </summary>
internal static string FormatException_InvokeMiddlewareNoService(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Exception_InvokeMiddlewareNoService"), p0, p1);
}
/// <summary>
/// The '{0}' method must not have ref or out parameters.
/// </summary>
internal static string Exception_InvokeDoesNotSupportRefOrOutParams
{
get { return GetString("Exception_InvokeDoesNotSupportRefOrOutParams"); }
}
/// <summary>
/// The '{0}' method must not have ref or out parameters.
/// </summary>
internal static string FormatException_InvokeDoesNotSupportRefOrOutParams(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Exception_InvokeDoesNotSupportRefOrOutParams"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -135,4 +135,10 @@
<data name="Exception_PathMustStartWithSlash" xml:space="preserve">
<value>The path in '{0}' must start with '/'.</value>
</data>
<data name="Exception_InvokeMiddlewareNoService" xml:space="preserve">
<value>Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'.</value>
</data>
<data name="Exception_InvokeDoesNotSupportRefOrOutParams" xml:space="preserve">
<value>The '{0}' method must not have ref or out parameters.</value>
</data>
</root>

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Builder.Internal;
using Microsoft.AspNet.Http.Abstractions;
using Microsoft.AspNet.Http.Internal;
using Xunit;
namespace Microsoft.AspNet.Http
@ -20,7 +21,7 @@ namespace Microsoft.AspNet.Http
builder.UseMiddleware(typeof(MiddlewareNoParametersStub));
var exception = Assert.Throws<InvalidOperationException>(() => builder.Build());
Assert.Equal(Resources.FormatException_UseMiddlewareNoParameters("Invoke",nameof(HttpContext)), exception.Message);
Assert.Equal(Resources.FormatException_UseMiddlewareNoParameters("Invoke", nameof(HttpContext)), exception.Message);
}
[Fact]
@ -35,7 +36,7 @@ namespace Microsoft.AspNet.Http
[Fact]
public void UseMiddleware_NoInvokeMethod_ThrowsException()
{
{
var mockServiceProvider = new DummyServiceProvider();
var builder = new ApplicationBuilder(mockServiceProvider);
builder.UseMiddleware(typeof(MiddlewareNoInvokeStub));
@ -53,14 +54,86 @@ namespace Microsoft.AspNet.Http
Assert.Equal(Resources.FormatException_UseMiddleMutlipleInvokes("Invoke"), exception.Message);
}
[Fact]
public async Task UseMiddleware_ThrowsIfArgCantBeResolvedFromContainer()
{
var mockServiceProvider = new DummyServiceProvider();
var builder = new ApplicationBuilder(mockServiceProvider);
builder.UseMiddleware(typeof(MiddlewareInjectInvokeNoService));
var app = builder.Build();
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => app(new DefaultHttpContext()));
Assert.Equal(Resources.FormatException_InvokeMiddlewareNoService(typeof(object), typeof(MiddlewareInjectInvokeNoService)), exception.Message);
}
[Fact]
public void UseMiddlewareWithInvokeArg()
{
var mockServiceProvider = new DummyServiceProvider();
var builder = new ApplicationBuilder(mockServiceProvider);
builder.UseMiddleware(typeof(MiddlewareInjectInvoke));
var app = builder.Build();
app(new DefaultHttpContext());
}
[Fact]
public void UseMiddlewareWithIvokeWithOutAndRefThrows()
{
var mockServiceProvider = new DummyServiceProvider();
var builder = new ApplicationBuilder(mockServiceProvider);
builder.UseMiddleware(typeof(MiddlewareInjectWithOutAndRefParams));
var exception = Assert.Throws<NotSupportedException>(() => builder.Build());
}
private class DummyServiceProvider : IServiceProvider
{
public object GetService(Type serviceType)
{
if (serviceType == typeof(IServiceProvider))
{
return this;
}
return null;
}
}
public class MiddlewareInjectWithOutAndRefParams
{
public MiddlewareInjectWithOutAndRefParams(RequestDelegate next)
{
}
public Task Invoke(HttpContext context, ref IServiceProvider sp1, out IServiceProvider sp2)
{
sp1 = null;
sp2 = null;
return Task.FromResult(0);
}
}
private class MiddlewareInjectInvokeNoService
{
public MiddlewareInjectInvokeNoService(RequestDelegate next)
{
}
public Task Invoke(HttpContext context, object value)
{
return Task.FromResult(0);
}
}
private class MiddlewareInjectInvoke
{
public MiddlewareInjectInvoke(RequestDelegate next)
{
}
public Task Invoke(HttpContext context, IServiceProvider provider)
{
return Task.FromResult(0);
}
}
private class MiddlewareNoParametersStub
{
public MiddlewareNoParametersStub(RequestDelegate next)
@ -84,7 +157,7 @@ namespace Microsoft.AspNet.Http
return 0;
}
}
private class MiddlewareNoInvokeStub
{
public MiddlewareNoInvokeStub(RequestDelegate next)