sdk/Sdk.Generators/ExtensionStartupRunnerGenerator.cs (140 lines of code) (raw):

// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using System.Collections.Generic; using System.Linq; using System.Text; using System.Collections.Immutable; namespace Microsoft.Azure.Functions.Worker.Sdk.Generators { /// <summary> /// Generates a class with a method which has code to call the "Configure" method /// of each of the participating extension's "WorkerExtensionStartup" implementations. /// Also adds the assembly attribute "WorkerExtensionStartupCodeExecutorInfo" /// and pass the information(the type) about the class we generated. /// We are also inheriting the generated class from the WorkerExtensionStartup class. /// (This is the same abstract class extension authors will implement for their extension specific startup code) /// We need the same signature as the extension's implementation as our class is an uber class which internally /// calls each of the extension's implementations. /// </summary> // Sample code generated (with one extensions participating in startup hook) // There will be one try-catch block for each extension participating in startup hook. //[assembly: WorkerExtensionStartupCodeExecutorInfo(typeof(Microsoft.Azure.Functions.Worker.WorkerExtensionStartupCodeExecutor))] // //internal class WorkerExtensionStartupCodeExecutor : WorkerExtensionStartup //{ // public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder) // { // try // { // new Microsoft.Azure.Functions.Worker.Extensions.Http.MyHttpExtensionStartup().Configure(applicationBuilder); // } // catch (Exception ex) // { // Console.Error.WriteLine("Error calling Configure on Microsoft.Azure.Functions.Worker.Extensions.Http.MyHttpExtensionStartup instance." + ex.ToString()); // } // } //} [Generator] public class ExtensionStartupRunnerGenerator : ISourceGenerator { /// <summary> /// The attribute which extension authors will apply on an assembly which contains their startup type. /// </summary> private const string AttributeTypeName = "WorkerExtensionStartupAttribute"; /// <summary> /// Fully qualified name of the above "WorkerExtensionStartupAttribute" attribute. /// </summary> private const string AttributeTypeFullName = "Microsoft.Azure.Functions.Worker.Core.WorkerExtensionStartupAttribute"; /// <summary> /// Fully qualified name of the base type which extension startup classes should implement. /// </summary> private const string StartupBaseClassName = "Microsoft.Azure.Functions.Worker.Core.WorkerExtensionStartup"; public void Execute(GeneratorExecutionContext context) { if (!context.IsRunningInAzureFunctionProject()) { return; } var extensionStartupTypeNames = GetExtensionStartupTypes(context); if (!extensionStartupTypeNames.Any()) { return; } var source = GenerateExtensionStartupRunner(context, extensionStartupTypeNames); var sourceText = SourceText.From(source, encoding: Encoding.UTF8); // Add the source code to the compilation context.AddSource($"WorkerExtensionStartupCodeExecutor.g.cs", sourceText); } /// <summary> /// Generates the extension startup source and applies te assembly attribute for the executor. /// </summary> /// <param name="extensionStartupTypeNames">The types to add to the configuration/bootstrapping process.</param> /// <returns>The generated source code.</returns> internal string GenerateExtensionStartupRunner(GeneratorExecutionContext context, IList<string> extensionStartupTypeNames) { string startupCodeExecutor = GenerateStartupCodeExecutorClass(extensionStartupTypeNames); var namespaceValue = FunctionsUtil.GetNamespaceForGeneratedCode(context); return $$""" // <auto-generated/> using System; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Core; [assembly: WorkerExtensionStartupCodeExecutorInfo(typeof({{namespaceValue}}.WorkerExtensionStartupCodeExecutor))] namespace {{namespaceValue}} { [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] {{Constants.GeneratedCodeAttribute}} internal class WorkerExtensionStartupCodeExecutor : global::Microsoft.Azure.Functions.Worker.Core.WorkerExtensionStartup { /// <summary> /// Configures the worker to register extension startup services. /// </summary> /// <param name="applicationBuilder">The <see cref="IFunctionsWorkerApplicationBuilder"/> to configure.</param> public override void Configure(global::Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder applicationBuilder) { {{startupCodeExecutor}} } } } """; } /// <summary> /// Gets the extension startup implementation type info from each of the participating extensions. /// Each entry in the return type collection includes full type name /// & a potential error message if the startup type is not valid. /// </summary> private IList<string> GetExtensionStartupTypes(GeneratorExecutionContext context) { IList<string>? typeNameList = null; // Extension authors should decorate their assembly with "WorkerExtensionStartup" attribute // if they want to participate in startup. foreach (var assembly in context.Compilation.SourceModule.ReferencedAssemblySymbols) { var extensionStartupAttribute = assembly.GetAttributes() .FirstOrDefault(a => (a.AttributeClass?.Name.Equals(AttributeTypeName, StringComparison.Ordinal) ?? false) && //Call GetFullName only if class name matches. a.AttributeClass.GetFullName() .Equals(AttributeTypeFullName, StringComparison.Ordinal)); if (extensionStartupAttribute != null) { // WorkerExtensionStartupAttribute has a constructor with one param, the type of startup implementation class. var firstConstructorParam = extensionStartupAttribute.ConstructorArguments[0]; if (firstConstructorParam.Value is not ITypeSymbol typeSymbol) { continue; } var fullTypeName = typeSymbol.ToDisplayString(); var hasAnyError = ReportDiagnosticErrorsIfAny(context, typeSymbol); if (!hasAnyError) { typeNameList ??= new List<string>(); typeNameList.Add(fullTypeName); } } } return typeNameList ?? ImmutableList<string>.Empty; } /// <summary> /// Check the startup type implementation is valid and report Diagnostic errors if it is not valid. /// </summary> private static bool ReportDiagnosticErrorsIfAny(GeneratorExecutionContext context, ITypeSymbol typeSymbol) { var hasAnyError = false; if (typeSymbol is INamedTypeSymbol namedTypeSymbol) { // Check public parameterless constructor exist for the type. var constructorExist = namedTypeSymbol.InstanceConstructors .Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public); if (!constructorExist) { context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ConstructorMissing, Location.None, typeSymbol.ToDisplayString())); hasAnyError = true; } // Check the extension startup class implements WorkerExtensionStartup abstract class. if (!namedTypeSymbol.BaseType!.GetFullName().Equals(StartupBaseClassName, StringComparison.Ordinal)) { context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.IncorrectBaseType, Location.None, typeSymbol.ToDisplayString(), StartupBaseClassName)); hasAnyError = true; } } return hasAnyError; } /// <summary> /// Writes a class with code which calls the Configure method on each implementation of participating extensions. /// We also have it implement the same "IWorkerExtensionStartup" interface which extension authors implement. /// </summary> private static string GenerateStartupCodeExecutorClass(IList<string> startupTypeNames) { var builder = new StringBuilder(); for (int i = 0; i < startupTypeNames.Count; i++) { var typeName = startupTypeNames[i]; if (i > 0) { builder.AppendLine(); } builder.Append($$""" try { new global::{{typeName}}().Configure(applicationBuilder); } catch (global::System.Exception ex) { global::System.Console.Error.WriteLine("Error calling Configure on {{typeName}} instance."+ex.ToString()); } """); } return builder.ToString(); } public void Initialize(GeneratorInitializationContext context) { } } }