From ca8136b73c1d2c840a56708616a9b377d35e6580 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 7 Jan 2016 10:27:36 -0800 Subject: [PATCH] Compile middleware invoke method when extra args are provided - Improves the performance when accessing scoped services in middleware --- .../Extensions/UseMiddlewareExtensions.cs | 117 +++++++++++++++--- .../Properties/Resources.Designer.cs | 56 +++++++-- .../Resources.resx | 6 + .../UseMiddlewareTest.cs | 79 +++++++++++- 4 files changed, 231 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.AspNet.Http.Abstractions/Extensions/UseMiddlewareExtensions.cs b/src/Microsoft.AspNet.Http.Abstractions/Extensions/UseMiddlewareExtensions.cs index 42dbc1cc5a..6cb4c1b004 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/Extensions/UseMiddlewareExtensions.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/Extensions/UseMiddlewareExtensions.cs @@ -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 /// 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); /// /// 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(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 Compile(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>(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; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http.Abstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Http.Abstractions/Properties/Resources.Designer.cs index 82505c66e0..99f5b6bea3 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/Properties/Resources.Designer.cs @@ -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); - /// - /// '{0}' is not available. - /// - internal static string FormatException_PathMustStartWithSlash(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Exception_PathMustStartWithSlash"), p0); - } - /// /// '{0}' is not available. /// @@ -98,6 +90,54 @@ namespace Microsoft.AspNet.Http.Abstractions return string.Format(CultureInfo.CurrentCulture, GetString("Exception_UseMiddleMutlipleInvokes"), p0); } + /// + /// The path in '{0}' must start with '/'. + /// + internal static string Exception_PathMustStartWithSlash + { + get { return GetString("Exception_PathMustStartWithSlash"); } + } + + /// + /// The path in '{0}' must start with '/'. + /// + internal static string FormatException_PathMustStartWithSlash(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Exception_PathMustStartWithSlash"), p0); + } + + /// + /// Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'. + /// + internal static string Exception_InvokeMiddlewareNoService + { + get { return GetString("Exception_InvokeMiddlewareNoService"); } + } + + /// + /// Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'. + /// + internal static string FormatException_InvokeMiddlewareNoService(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Exception_InvokeMiddlewareNoService"), p0, p1); + } + + /// + /// The '{0}' method must not have ref or out parameters. + /// + internal static string Exception_InvokeDoesNotSupportRefOrOutParams + { + get { return GetString("Exception_InvokeDoesNotSupportRefOrOutParams"); } + } + + /// + /// The '{0}' method must not have ref or out parameters. + /// + 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); diff --git a/src/Microsoft.AspNet.Http.Abstractions/Resources.resx b/src/Microsoft.AspNet.Http.Abstractions/Resources.resx index 1c947c352b..414bad847d 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/Resources.resx +++ b/src/Microsoft.AspNet.Http.Abstractions/Resources.resx @@ -135,4 +135,10 @@ The path in '{0}' must start with '/'. + + Unable to resolve service for type '{0}' while attempting to Invoke middleware '{1}'. + + + The '{0}' method must not have ref or out parameters. + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Http.Abstractions.Tests/UseMiddlewareTest.cs b/test/Microsoft.AspNet.Http.Abstractions.Tests/UseMiddlewareTest.cs index 1839631d61..ee407d35f8 100644 --- a/test/Microsoft.AspNet.Http.Abstractions.Tests/UseMiddlewareTest.cs +++ b/test/Microsoft.AspNet.Http.Abstractions.Tests/UseMiddlewareTest.cs @@ -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(() => 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(() => 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(() => 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)