-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Source Gen] Generate function executor code (#1309)
* Finally a green test for happy use case. * WIP * Handing async and void cases. More tests. * 🙈 * 🙊 * 🙉 * Clean up and addressing some PR feedback * Added the extension method to register the executor to service collection. * Cleanup * Rebased on main. Fixed package versions (to bump on only preview) * Minor cleanup * Updating tests to reflect the renaming of the public types (IInputBindingFeature) * Cleanup after rebasing on main. * Removed an unused line (from rebase merge) * nit fixes based on PR feedback. * Addressing PR feedback. raw literal string indendation is still driving me crazy! * Changing the build prop to "FunctionsEnableExecutorSourceGen" * Bit of a cleanup. Removed an unused type. * Simplified GetTypesDictionary method * Added summary comment for IsStatic prop.
- Loading branch information
Showing
17 changed files
with
823 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Emitter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading; | ||
|
||
namespace Microsoft.Azure.Functions.Worker.Sdk.Generators | ||
{ | ||
public partial class FunctionExecutorGenerator | ||
{ | ||
internal static class Emitter | ||
{ | ||
internal static string Emit(IEnumerable<ExecutableFunction> functions, CancellationToken cancellationToken) | ||
{ | ||
string result = $$""" | ||
// <auto-generated/> | ||
using System; | ||
using System.Threading.Tasks; | ||
using System.Collections.Generic; | ||
using Microsoft.Extensions.Hosting; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Azure.Functions.Worker.Context.Features; | ||
using Microsoft.Azure.Functions.Worker.Invocation; | ||
namespace Microsoft.Azure.Functions.Worker | ||
{ | ||
internal class DirectFunctionExecutor : IFunctionExecutor | ||
{ | ||
private readonly IFunctionActivator _functionActivator; | ||
{{GetTypesDictionary(functions)}} | ||
public DirectFunctionExecutor(IFunctionActivator functionActivator) | ||
{ | ||
_functionActivator = functionActivator ?? throw new ArgumentNullException(nameof(functionActivator)); | ||
} | ||
|
||
public async ValueTask ExecuteAsync(FunctionContext context) | ||
{ | ||
{{GetMethodBody(functions)}} | ||
} | ||
} | ||
public static class FunctionExecutorHostBuilderExtensions | ||
{ | ||
///<summary> | ||
/// Configures an optimized function executor to the invocation pipeline. | ||
///</summary> | ||
public static IHostBuilder ConfigureGeneratedFunctionExecutor(this IHostBuilder builder) | ||
{ | ||
return builder.ConfigureServices(s => | ||
{ | ||
s.AddSingleton<IFunctionExecutor, DirectFunctionExecutor>(); | ||
}); | ||
} | ||
} | ||
} | ||
"""; | ||
|
||
return result; | ||
} | ||
|
||
private static string GetTypesDictionary(IEnumerable<ExecutableFunction> functions) | ||
{ | ||
var classNames = functions.Where(f => !f.IsStatic).Select(f => f.ParentFunctionClassName).Distinct(); | ||
if (!classNames.Any()) | ||
{ | ||
return """ | ||
|
||
"""; | ||
} | ||
|
||
return $$""" | ||
private readonly Dictionary<string, Type> types = new() | ||
{ | ||
{{string.Join("\n", classNames.Select(c => $$""" { "{{c}}", Type.GetType("{{c}}")! }"""))}}, | ||
}; | ||
|
||
"""; | ||
} | ||
|
||
private static string GetMethodBody(IEnumerable<ExecutableFunction> functions) | ||
{ | ||
var sb = new StringBuilder(); | ||
sb.Append( | ||
""" | ||
var inputBindingFeature = context.Features.Get<IFunctionInputBindingFeature>()!; | ||
var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context)!; | ||
var inputArguments = inputBindingResult.Values; | ||
|
||
"""); | ||
foreach (ExecutableFunction function in functions) | ||
{ | ||
sb.Append($$""" | ||
|
||
if (string.Equals(context.FunctionDefinition.EntryPoint, "{{function.EntryPoint}}", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
"""); | ||
|
||
int functionParamCounter = 0; | ||
var functionParamList = new List<string>(); | ||
foreach (var argumentTypeName in function.ParameterTypeNames) | ||
{ | ||
functionParamList.Add($"({argumentTypeName})inputArguments[{functionParamCounter++}]"); | ||
} | ||
var methodParamsStr = string.Join(", ", functionParamList); | ||
|
||
if (!function.IsStatic) | ||
{ | ||
sb.Append($$""" | ||
|
||
var instanceType = types["{{function.ParentFunctionClassName}}"]; | ||
var i = _functionActivator.CreateInstance(instanceType, context) as {{function.ParentFunctionClassName}}; | ||
"""); | ||
} | ||
|
||
sb.Append(@" | ||
"); | ||
|
||
if (function.IsReturnValueAssignable) | ||
{ | ||
sb.Append(@$"context.GetInvocationResult().Value = "); | ||
} | ||
if (function.ShouldAwait) | ||
{ | ||
sb.Append("await "); | ||
} | ||
|
||
sb.Append(function.IsStatic | ||
? @$"{function.ParentFunctionClassName}.{function.MethodName}({methodParamsStr}); | ||
}}" | ||
: $@"i.{function.MethodName}({methodParamsStr}); | ||
}}"); | ||
} | ||
|
||
return sb.ToString(); | ||
} | ||
} | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.ExecutableFunction.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace Microsoft.Azure.Functions.Worker.Sdk.Generators | ||
{ | ||
/// <summary> | ||
/// A type which holds information about the functions which can be executed from an invocation. | ||
/// </summary> | ||
internal class ExecutableFunction | ||
{ | ||
/// <summary> | ||
/// False if the function returns Task or void. | ||
/// </summary> | ||
internal bool IsReturnValueAssignable { set; get; } | ||
|
||
/// <summary> | ||
/// Whether the function should be awaited or not for getting the result of execution. | ||
/// </summary> | ||
internal bool ShouldAwait { get; set; } | ||
|
||
/// <summary> | ||
/// The method name (which is part of EntryPoint property value). | ||
/// </summary> | ||
internal string MethodName { get; set; } = null!; | ||
|
||
/// <summary> | ||
/// A value indicating whether the function is static or not. | ||
/// </summary> | ||
internal bool IsStatic { get; set; } | ||
|
||
/// <summary> | ||
/// Ex: MyNamespace.MyClass.MyMethodName | ||
/// </summary> | ||
internal string EntryPoint { get; set; } = null!; | ||
|
||
/// <summary> | ||
/// Fully qualified type name of the parent class. | ||
/// Ex: MyNamespace.MyClass | ||
/// </summary> | ||
internal string ParentFunctionClassName { get; set; } = null!; | ||
|
||
/// <summary> | ||
/// A collection of fully qualified type names of the parameters of the function. | ||
/// </summary> | ||
internal IEnumerable<string> ParameterTypeNames { set; get; } = Enumerable.Empty<string>(); | ||
} | ||
} |
112 changes: 112 additions & 0 deletions
112
sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Parser.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
||
namespace Microsoft.Azure.Functions.Worker.Sdk.Generators | ||
{ | ||
public partial class FunctionExecutorGenerator | ||
{ | ||
internal sealed class Parser | ||
{ | ||
private readonly GeneratorExecutionContext _context; | ||
private readonly KnownTypes _knownTypes; | ||
|
||
internal Parser(GeneratorExecutionContext context) | ||
{ | ||
_context = context; | ||
_knownTypes = new KnownTypes(_context.Compilation); | ||
} | ||
|
||
private Compilation Compilation => _context.Compilation; | ||
|
||
internal ICollection<ExecutableFunction> GetFunctions(List<MethodDeclarationSyntax> methods) | ||
{ | ||
var functionList = new List<ExecutableFunction>(); | ||
|
||
foreach (MethodDeclarationSyntax method in methods) | ||
{ | ||
_context.CancellationToken.ThrowIfCancellationRequested(); | ||
var model = Compilation.GetSemanticModel(method.SyntaxTree); | ||
|
||
if (!FunctionsUtil.IsValidFunctionMethod(_context, Compilation, model, method, | ||
out _)) | ||
{ | ||
continue; | ||
} | ||
|
||
var methodName = method.Identifier.Text; | ||
var methodParameterList = new List<string>(method.ParameterList.Parameters.Count); | ||
|
||
foreach (var methodParam in method.ParameterList.Parameters) | ||
{ | ||
if (model.GetDeclaredSymbol(methodParam) is not IParameterSymbol parameterSymbol) | ||
{ | ||
continue; | ||
} | ||
|
||
methodParameterList.Add(parameterSymbol.Type.ToDisplayString()); | ||
} | ||
|
||
var methodSymbol = model.GetDeclaredSymbol(method)!; | ||
var fullyQualifiedClassName = methodSymbol.ContainingSymbol.ToDisplayString(); | ||
|
||
var function = new ExecutableFunction | ||
{ | ||
EntryPoint = $"{fullyQualifiedClassName}.{method.Identifier.ValueText}", | ||
ParameterTypeNames = methodParameterList, | ||
MethodName = methodName, | ||
ShouldAwait = IsTaskType(methodSymbol.ReturnType), | ||
IsReturnValueAssignable = IsReturnValueAssignable(methodSymbol), | ||
IsStatic = method.Modifiers.Any(SyntaxKind.StaticKeyword), | ||
ParentFunctionClassName = fullyQualifiedClassName | ||
}; | ||
|
||
functionList.Add(function); | ||
} | ||
|
||
return functionList; | ||
} | ||
|
||
/// <summary> | ||
/// Returns true if the symbol is Task/Task of T/ValueTask/ValueTask of T. | ||
/// </summary> | ||
private bool IsTaskType(ITypeSymbol typeSymbol) | ||
{ | ||
return | ||
SymbolEqualityComparer.Default.Equals(typeSymbol.OriginalDefinition, _knownTypes.TaskType) || | ||
SymbolEqualityComparer.Default.Equals(typeSymbol.OriginalDefinition, _knownTypes.TaskOfTType) || | ||
SymbolEqualityComparer.Default.Equals(typeSymbol.OriginalDefinition, _knownTypes.ValueTaskType) || | ||
SymbolEqualityComparer.Default.Equals(typeSymbol.OriginalDefinition, _knownTypes.ValueTaskOfTTypeOpt); | ||
} | ||
|
||
/// <summary> | ||
/// Is the return value of the method assignable to a variable? | ||
/// Returns True for methods which has Task or void as return type. | ||
/// </summary> | ||
private bool IsReturnValueAssignable(IMethodSymbol methodSymbol) | ||
{ | ||
if (methodSymbol.ReturnsVoid) | ||
{ | ||
return false; | ||
} | ||
|
||
if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType.OriginalDefinition, _knownTypes.TaskType)) | ||
{ | ||
return false; | ||
} | ||
|
||
if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType.OriginalDefinition, | ||
_knownTypes.ValueTaskType)) | ||
{ | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.