diff --git a/DotNetWorker.sln b/DotNetWorker.sln index d413bddc8..8561726ba 100644 --- a/DotNetWorker.sln +++ b/DotNetWorker.sln @@ -132,8 +132,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker.Extensions.Tests", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetIntegration", "samples\AspNetIntegration\AspNetIntegration.csproj", "{D2F67410-9933-42E8-B04A-E17634D83A30}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net7Worker", "samples\Net7Worker\Net7Worker.csproj", "{EC1A321B-70F6-420B-85D4-56C7869BB71B}" -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependentAssemblyWithFunctions", "test\DependentAssemblyWithFunctions\DependentAssemblyWithFunctions.csproj", "{AB6E1E7A-0D2C-4086-9487-566887C1E753}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependentAssemblyWithFunctions", "test\DependentAssemblyWithFunctions\DependentAssemblyWithFunctions.csproj", "{AB6E1E7A-0D2C-4086-9487-566887C1E753}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker.Extensions.Http.AspNetCore.Tests", "test\extensions\Worker.Extensions.Http.AspNetCore.Tests\Worker.Extensions.Http.AspNetCore.Tests.csproj", "{D8E79B53-9A44-46CC-9D7A-E9494FC8CAF4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker.Extensions.Http.AspNetCore.Analyzers", "extensions\Worker.Extensions.Http.AspNetCore.Analyzers\Worker.Extensions.Http.AspNetCore.Analyzers.csproj", "{7B6C2920-7A02-43B2-8DA0-7B76B9FAFC6B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net7Worker", "samples\Net7Worker\Net7Worker.csproj", "{BE1F79C3-24FA-4BC8-BAB2-C1AD321B13FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependentAssemblyWithFunctions.NetStandard", "test\DependentAssemblyWithFunctions.NetStandard\DependentAssemblyWithFunctions.NetStandard.csproj", "{198DA072-F94F-4585-A744-97B3DAC21686}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -329,14 +336,26 @@ Global {D2F67410-9933-42E8-B04A-E17634D83A30}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2F67410-9933-42E8-B04A-E17634D83A30}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2F67410-9933-42E8-B04A-E17634D83A30}.Release|Any CPU.Build.0 = Release|Any CPU - {EC1A321B-70F6-420B-85D4-56C7869BB71B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC1A321B-70F6-420B-85D4-56C7869BB71B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC1A321B-70F6-420B-85D4-56C7869BB71B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC1A321B-70F6-420B-85D4-56C7869BB71B}.Release|Any CPU.Build.0 = Release|Any CPU {AB6E1E7A-0D2C-4086-9487-566887C1E753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AB6E1E7A-0D2C-4086-9487-566887C1E753}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB6E1E7A-0D2C-4086-9487-566887C1E753}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB6E1E7A-0D2C-4086-9487-566887C1E753}.Release|Any CPU.Build.0 = Release|Any CPU + {D8E79B53-9A44-46CC-9D7A-E9494FC8CAF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8E79B53-9A44-46CC-9D7A-E9494FC8CAF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8E79B53-9A44-46CC-9D7A-E9494FC8CAF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8E79B53-9A44-46CC-9D7A-E9494FC8CAF4}.Release|Any CPU.Build.0 = Release|Any CPU + {BE1F79C3-24FA-4BC8-BAB2-C1AD321B13FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE1F79C3-24FA-4BC8-BAB2-C1AD321B13FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE1F79C3-24FA-4BC8-BAB2-C1AD321B13FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE1F79C3-24FA-4BC8-BAB2-C1AD321B13FF}.Release|Any CPU.Build.0 = Release|Any CPU + {7B6C2920-7A02-43B2-8DA0-7B76B9FAFC6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B6C2920-7A02-43B2-8DA0-7B76B9FAFC6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B6C2920-7A02-43B2-8DA0-7B76B9FAFC6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B6C2920-7A02-43B2-8DA0-7B76B9FAFC6B}.Release|Any CPU.Build.0 = Release|Any CPU + {198DA072-F94F-4585-A744-97B3DAC21686}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {198DA072-F94F-4585-A744-97B3DAC21686}.Debug|Any CPU.Build.0 = Debug|Any CPU + {198DA072-F94F-4585-A744-97B3DAC21686}.Release|Any CPU.ActiveCfg = Release|Any CPU + {198DA072-F94F-4585-A744-97B3DAC21686}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -394,8 +413,11 @@ Global {286F9EE3-00AE-4EFA-BFD8-A2E58BC809D2} = {FD7243E4-BF18-43F8-8744-BA1D17ACF378} {17BDCE12-6964-4B87-B2AC-68CE270A3E9A} = {FD7243E4-BF18-43F8-8744-BA1D17ACF378} {D2F67410-9933-42E8-B04A-E17634D83A30} = {9D6603BD-7EA2-4D11-A69C-0D9E01317FD6} - {EC1A321B-70F6-420B-85D4-56C7869BB71B} = {9D6603BD-7EA2-4D11-A69C-0D9E01317FD6} {AB6E1E7A-0D2C-4086-9487-566887C1E753} = {B5821230-6E0A-4535-88A9-ED31B6F07596} + {D8E79B53-9A44-46CC-9D7A-E9494FC8CAF4} = {AA4D318D-101B-49E7-A4EC-B34165AEDB33} + {7B6C2920-7A02-43B2-8DA0-7B76B9FAFC6B} = {A7B4FF1E-3DF7-4F28-9333-D0961CDDF702} + {BE1F79C3-24FA-4BC8-BAB2-C1AD321B13FF} = {9D6603BD-7EA2-4D11-A69C-0D9E01317FD6} + {198DA072-F94F-4585-A744-97B3DAC21686} = {B5821230-6E0A-4535-88A9-ED31B6F07596} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {497D2ED4-A13E-4BCA-8D29-F30CA7D0EA4A} diff --git a/build/AspNetCore.slnf b/build/AspNetCore.slnf new file mode 100644 index 000000000..ea7297aa9 --- /dev/null +++ b/build/AspNetCore.slnf @@ -0,0 +1,9 @@ +{ + "solution": { + "path": "..\\DotNetWorker.sln", + "projects": [ + "extensions\\Worker.Extensions.Http.AspNetCore\\src\\Worker.Extensions.Http.AspNetCore.csproj", + "extensions\\Worker.Extensions.Http.AspNetCore.Analyzers\\Worker.Extensions.Http.AspNetCore.Analyzers.csproj" + ] + } +} \ No newline at end of file diff --git a/build/install-dotnet.yml b/build/install-dotnet.yml index 5b3e2dfc2..1cdaa3c38 100644 --- a/build/install-dotnet.yml +++ b/build/install-dotnet.yml @@ -4,7 +4,13 @@ steps: displayName: 'Install .NET6 SDK' inputs: packageType: 'sdk' - version: "6.0.412" + version: "6.x" + +- task: UseDotNet@2 + displayName: 'Install .NET7 SDK' + inputs: + packageType: 'sdk' + version: "7.x" # The SDK we use to build - task: UseDotNet@2 diff --git a/build/pipelines/templates/extensions-ci.yml b/build/pipelines/templates/extensions-ci.yml index ec42b2a19..2742b8619 100644 --- a/build/pipelines/templates/extensions-ci.yml +++ b/build/pipelines/templates/extensions-ci.yml @@ -2,6 +2,13 @@ parameters: - name: ExtensionDirectory type: string default: not-specified +- name: RunExtensionTests + displayName: Run Extension Tests? + type: boolean + default: false +- name: Solution + type: string + default: jobs: - job: "Build_And_Test_Windows" @@ -23,14 +30,14 @@ jobs: arguments: '-c Release -p:BuildNumber=$(buildNumber) -p:IsLocalBuild=False' projects: ${{ parameters.ExtensionDirectory }}/src/*.csproj - # Extensions test structure must be defined - # - task: DotNetCoreCLI@2 - # displayName: 'Run tests' - # inputs: - # command: 'test' - # arguments: '--no-build -c Release' - # projects: | - # test/**/*Tests.csproj + - ${{ if eq(parameters.RunExtensionTests, true) }}: + - task: DotNetCoreCLI@2 + displayName: 'Run tests' + inputs: + command: 'test' + arguments: '-c Release' + projects: | + test/${{ parameters.ExtensionDirectory }}.Tests/**/*Tests.csproj - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@2 displayName: 'ESRP CodeSigning - Authenticode' @@ -76,7 +83,11 @@ jobs: command: 'custom' custom: 'pack' arguments: '--no-build -c Release -o packages -p:BuildNumber=$(buildNumber) -p:IsLocalBuild=False' - projects: ${{ parameters.ExtensionDirectory }}/src/*.csproj + ${{if parameters.Solution}}: + projects: | + **\${{ parameters.Solution }} + ${{ else }}: + projects: ${{ parameters.ExtensionDirectory }}/src/*.csproj - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@2 displayName: 'ESRP CodeSigning: Nupkg' diff --git a/docs/analyzer-rules/AZFW0014.md b/docs/analyzer-rules/AZFW0014.md new file mode 100644 index 000000000..b9ec2f72a --- /dev/null +++ b/docs/analyzer-rules/AZFW0014.md @@ -0,0 +1,25 @@ +# AZFW0011: Missing Registration for ASP.NET Core Integration + +| | Value | +|-|-| +| **Rule ID** |AZFW00014| +| **Category** |[Usage]| +| **Severity** |Error| + +## Cause + +This rule is triggered when worker using the ASP.NET Core Integration uses `ConfigureFunctionsWorkerDefaults()`. + +## Rule description + +When using the ASP.NET Core Integration, the worker must configure the worker using the `ConfigureFunctionsWebApplication()` method. This rule will be violated when the worker uses `ConfigureFunctionsWorkerDefaults()`. + + +## How to fix violations + +Replace usage of `ConfigureFunctionsWorkerDefaults` with `ConfigureFunctionsWebApplication` method in the Function App. Refer to [public documentation](https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide#aspnet-core-integration) for guidance on ASP.NET Core Integration. + + +## When to suppress + +It is okay to suppress this if `ConfigureFunctionsWebApplication` is called indirectly through a different API. \ No newline at end of file diff --git a/extensions/Worker.Extensions.EventGrid/release_notes.md b/extensions/Worker.Extensions.EventGrid/release_notes.md index 9e82bcaf3..709dd6e9e 100644 --- a/extensions/Worker.Extensions.EventGrid/release_notes.md +++ b/extensions/Worker.Extensions.EventGrid/release_notes.md @@ -4,6 +4,6 @@ - My change description (#PR/#issue) --> -### Microsoft.Azure.Functions.Worker.Extensions.EventGrid +### Microsoft.Azure.Functions.Worker.Extensions.EventGrid 3.4.1 -- +- Updated `Microsoft.Azure.WebJobs.Extensions.EventGrid` reference to 3.3.1 \ No newline at end of file diff --git a/extensions/Worker.Extensions.EventGrid/src/Properties/AssemblyInfo.cs b/extensions/Worker.Extensions.EventGrid/src/Properties/AssemblyInfo.cs index 974875917..98bec0107 100644 --- a/extensions/Worker.Extensions.EventGrid/src/Properties/AssemblyInfo.cs +++ b/extensions/Worker.Extensions.EventGrid/src/Properties/AssemblyInfo.cs @@ -4,5 +4,5 @@ using System.Runtime.CompilerServices; using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; -[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.EventGrid", "3.3.0")] +[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.EventGrid", "3.3.1")] [assembly: InternalsVisibleTo("Microsoft.Azure.Functions.Worker.Extensions.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")] diff --git a/extensions/Worker.Extensions.EventGrid/src/Worker.Extensions.EventGrid.csproj b/extensions/Worker.Extensions.EventGrid/src/Worker.Extensions.EventGrid.csproj index e340ab872..4f81cbc12 100644 --- a/extensions/Worker.Extensions.EventGrid/src/Worker.Extensions.EventGrid.csproj +++ b/extensions/Worker.Extensions.EventGrid/src/Worker.Extensions.EventGrid.csproj @@ -6,7 +6,7 @@ Azure Event Grid extensions for .NET isolated functions - 3.4.0 + 3.4.1 false diff --git a/extensions/Worker.Extensions.EventHubs/release_notes.md b/extensions/Worker.Extensions.EventHubs/release_notes.md index d3055b4f2..3d6e918a8 100644 --- a/extensions/Worker.Extensions.EventHubs/release_notes.md +++ b/extensions/Worker.Extensions.EventHubs/release_notes.md @@ -6,4 +6,4 @@ ### Microsoft.Azure.Functions.Worker.Extensions.EventHubs -- \ No newline at end of file +- diff --git a/extensions/Worker.Extensions.EventHubs/src/Properties/AssemblyInfo.cs b/extensions/Worker.Extensions.EventHubs/src/Properties/AssemblyInfo.cs index bd052b1e8..ca3bea305 100644 --- a/extensions/Worker.Extensions.EventHubs/src/Properties/AssemblyInfo.cs +++ b/extensions/Worker.Extensions.EventHubs/src/Properties/AssemblyInfo.cs @@ -4,5 +4,5 @@ using System.Runtime.CompilerServices; using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; -[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.EventHubs", "5.5.0")] +[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.EventHubs", "6.0.1")] [assembly: InternalsVisibleTo("Microsoft.Azure.Functions.Worker.Extensions.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")] diff --git a/extensions/Worker.Extensions.EventHubs/src/Worker.Extensions.EventHubs.csproj b/extensions/Worker.Extensions.EventHubs/src/Worker.Extensions.EventHubs.csproj index d77620fde..e828ecf02 100644 --- a/extensions/Worker.Extensions.EventHubs/src/Worker.Extensions.EventHubs.csproj +++ b/extensions/Worker.Extensions.EventHubs/src/Worker.Extensions.EventHubs.csproj @@ -6,7 +6,7 @@ Azure Event Hubs extensions for .NET isolated functions - 5.6.0 + 6.0.1 diff --git a/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/CodeFixForRegistrationInASPNetCoreIntegration.cs b/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/CodeFixForRegistrationInASPNetCoreIntegration.cs new file mode 100644 index 000000000..28e9fd3d5 --- /dev/null +++ b/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/CodeFixForRegistrationInASPNetCoreIntegration.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using System.Collections.Immutable; +using System.Threading.Tasks; +using System.Composition; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Threading; +using System.Linq; + +namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CodeFixForRegistrationInASPNetCoreIntegration)), Shared] + public sealed class CodeFixForRegistrationInASPNetCoreIntegration : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create(DiagnosticDescriptors.CorrectRegistrationExpectedInAspNetIntegration.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + private const string ExpectedRegistrationMethod = "ConfigureFunctionsWebApplication"; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics.First(); + context.RegisterCodeFix(new ChangeConfigurationForASPNetIntegration(context.Document, diagnostic), diagnostic); + + return Task.CompletedTask; + } + + /// + /// CodeAction implementation which fixes the method configuration for ASP.NET Core Integration. + /// + private sealed class ChangeConfigurationForASPNetIntegration : CodeAction + { + private readonly Document _document; + private readonly Diagnostic _diagnostic; + + internal ChangeConfigurationForASPNetIntegration(Document document, Diagnostic diagnostic) + { + this._document = document; + this._diagnostic = diagnostic; + } + + public override string Title => "Change configuration for ASP.NET Core Integration"; + + public override string EquivalenceKey => null; + + /// + /// Returns an updated Document with correct method configuration for ASP.NET Core Integration. + /// + protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) + { + SyntaxNode root = await _document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var currentNode = root.FindNode(this._diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + + var newNode = currentNode.ReplaceNode(currentNode, SyntaxFactory.IdentifierName(ExpectedRegistrationMethod)); + + SyntaxNode newSyntaxRoot = root.ReplaceNode(currentNode, newNode); + + return _document.WithSyntaxRoot(newSyntaxRoot); + } + } + } +} diff --git a/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/DiagnosticDescriptors.cs b/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/DiagnosticDescriptors.cs new file mode 100644 index 000000000..a95118f5c --- /dev/null +++ b/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/DiagnosticDescriptors.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore +{ + internal class DiagnosticDescriptors + { + public const string Usage = "Usage"; + + private static DiagnosticDescriptor Create(string id, string title, string messageFormat, string category, DiagnosticSeverity severity) + { + var helpLink = $"https://aka.ms/azfw-rules?ruleid={id}"; + return new DiagnosticDescriptor(id, title, messageFormat, category, severity, isEnabledByDefault: true, helpLinkUri: helpLink); + } + + public static DiagnosticDescriptor CorrectRegistrationExpectedInAspNetIntegration { get; } + = Create(id: "AZFW0014", title: "Missing expected registration of ASP.NET Core Integration services", messageFormat: "The registration for method '{0}' is expected for ASP.NET Core Integration.", + category: Usage, severity: DiagnosticSeverity.Error); + } +} diff --git a/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/RegistrationExpectedInASPNetIntegration.cs b/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/RegistrationExpectedInASPNetIntegration.cs new file mode 100644 index 000000000..d8d73f732 --- /dev/null +++ b/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/RegistrationExpectedInASPNetIntegration.cs @@ -0,0 +1,100 @@ +// 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.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore +{ + /// + /// Analyzer to verify whether expected registration is present for ASP.NET Core Integration. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class RegistrationExpectedInASPNetIntegration : DiagnosticAnalyzer + { + /// Diagnostics supported by the analyzer + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptors.CorrectRegistrationExpectedInAspNetIntegration); + + private const string ExpectedRegistrationMethod = "ConfigureFunctionsWebApplication"; + private const string IncorrectRegistrationMethod = "ConfigureFunctionsWorkerDefaults"; + + /// Initialization method + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + } + + private static void AnalyzeMethod(SymbolAnalysisContext context) + { + var symbol = (IMethodSymbol)context.Symbol; + + if (!IsMainMethod(symbol)) + { + return; + } + + var syntaxReference = symbol.DeclaringSyntaxReferences.FirstOrDefault(); + var root = syntaxReference.SyntaxTree.GetRoot(); + var methodCallExpressions = root.DescendantNodes().OfType(); + + if (methodCallExpressions is null) + { + return; + } + + var incorrectMethodCallExpressions = methodCallExpressions.Where(invocation => (invocation.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.ValueText == IncorrectRegistrationMethod); + var incorrectMethodInvocationPresent = incorrectMethodCallExpressions.Any(); + + if (!incorrectMethodInvocationPresent) + { + return; + } + + //Finding exact location of method call + Location location = GetSymbolLocation(root, incorrectMethodCallExpressions); + + var expectedMethodInvocationPresent = methodCallExpressions.Any(invocation => (invocation.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.ValueText == ExpectedRegistrationMethod); + + if (!expectedMethodInvocationPresent) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptors.CorrectRegistrationExpectedInAspNetIntegration, location, ExpectedRegistrationMethod); + context.ReportDiagnostic(diagnostic); + } + } + + // Checks if a method symbol is a Main method. This also checks for implicit main in top-level statements + private static bool IsMainMethod(IMethodSymbol symbol) + { + return symbol?.IsStatic == true && symbol.Name switch + { + "Main" => true, + "$Main" => true, + "
$" => true, + _ => false + }; + } + + private static Location GetSymbolLocation(SyntaxNode root, IEnumerable methodCallExpressions) + { + Location location = Location.None; + + if (methodCallExpressions != null) + { + var lineSpan = methodCallExpressions.FirstOrDefault().GetLocation().SourceSpan; + var node = root.DescendantNodes(lineSpan) + .First(n => lineSpan.Contains(n.FullSpan)).DescendantNodes() + .OfType().FirstOrDefault(c => c.Identifier.Text == IncorrectRegistrationMethod); + + location = node.GetLocation(); + } + + return location; + } + } +} diff --git a/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/Worker.Extensions.Http.AspNetCore.Analyzers.csproj b/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/Worker.Extensions.Http.AspNetCore.Analyzers.csproj new file mode 100644 index 000000000..9708cff9d --- /dev/null +++ b/extensions/Worker.Extensions.Http.AspNetCore.Analyzers/Worker.Extensions.Http.AspNetCore.Analyzers.csproj @@ -0,0 +1,34 @@ + + + + 1.0.1 + Library + true + false + netstandard2.0 + Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Analyzers + This package provides development time code analysis for ASP.NET Core extensions for .NET isolated functions. + Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Analyzers + Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Analyzers + disable + $(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/extensions/Worker.Extensions.Http.AspNetCore/ci.yml b/extensions/Worker.Extensions.Http.AspNetCore/ci.yml index 25fd4892b..cbc7c7acf 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/ci.yml +++ b/extensions/Worker.Extensions.Http.AspNetCore/ci.yml @@ -7,6 +7,7 @@ trigger: paths: include: - extensions/Worker.Extensions.Http.AspNetCore/ + - test/extensions/Worker.Extensions.Http.AspNetCore.Tests/ pr: branches: @@ -17,8 +18,11 @@ pr: paths: include: - extensions/Worker.Extensions.Http.AspNetCore/ + - test/extensions/Worker.Extensions.Http.AspNetCore.Tests/ extends: template: ../../build/pipelines/templates/extensions-ci.yml parameters: ExtensionDirectory: extensions/Worker.Extensions.Http.AspNetCore + Solution: AspNetCore.slnf + RunExtensionTests: true diff --git a/extensions/Worker.Extensions.Http.AspNetCore/release_notes.md b/extensions/Worker.Extensions.Http.AspNetCore/release_notes.md index 7b0801352..d673c5e12 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/release_notes.md +++ b/extensions/Worker.Extensions.Http.AspNetCore/release_notes.md @@ -4,6 +4,13 @@ - My change description (#PR/#issue) --> -### Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore +### Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore 1.1.1 -- +- New overload added to `ConfigureFunctionsWebApplication` to take a `HostBuilderContext` (#1925). Thank you @vmcbaptista +- Added support for the `HttpRequestData` and `HttpResponseData` models, backed by ASP.NET Core. (#1932) +- Updated `Microsoft.Azure.Functions.Worker.Core` dependency + +### Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Analyzers 1.0.0 + +- Analyzer to detect missing ASP.NET Core Integration registration (#1917) +- Code fix suggestion for correct registration in ASP.NET core integration (#1992) \ No newline at end of file diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/AspNetMiddleware/InvokeFunctionMiddleware.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/AspNetMiddleware/InvokeFunctionMiddleware.cs deleted file mode 100644 index a34aff6de..000000000 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/AspNetMiddleware/InvokeFunctionMiddleware.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore -{ - internal class InvokeFunctionMiddleware - { - public InvokeFunctionMiddleware(RequestDelegate next) - { - } - - public Task Invoke(HttpContext context) - { - return context.InvokeFunctionAsync(); - } - } -} diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionContext/FunctionContextExtensions.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionContext/FunctionContextExtensions.cs index d829cbdcb..892a3c6dc 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionContext/FunctionContextExtensions.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionContext/FunctionContextExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore; @@ -27,5 +28,24 @@ public static class FunctionContextExtensions return null; } + + /// + /// Gets the for the . + /// + /// The . + /// The for the context. + /// + internal static bool TryGetRequest(this FunctionContext context, [NotNullWhen(true)] out HttpRequest? request) + { + request = null; + + if (context.Items.TryGetValue(Constants.HttpContextKey, out var requestContext) + && requestContext is HttpContext httpContext) + { + request = httpContext.Request; + } + + return request is not null; + } } } diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsHostBuilderExtensions.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsHostBuilderExtensions.cs index 5df1b4fad..91ac96221 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsHostBuilderExtensions.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsHostBuilderExtensions.cs @@ -34,11 +34,25 @@ public static IHostBuilder ConfigureFunctionsWebApplication(this IHostBuilder bu /// The same instance of the for chaining. public static IHostBuilder ConfigureFunctionsWebApplication(this IHostBuilder builder, Action configureWorker) { - builder.ConfigureFunctionsWorkerDefaults(workerAppBuilder => + return builder.ConfigureFunctionsWebApplication((_, workerAppBuilder) => { - workerAppBuilder.UseAspNetCoreIntegration(); configureWorker?.Invoke(workerAppBuilder); }); + } + + /// + /// Configures the worker to use the ASP.NET Core integration, enabling advanced HTTP features. + /// + /// The to configure. + /// The worker configure callback. + /// The same instance of the for chaining. + public static IHostBuilder ConfigureFunctionsWebApplication(this IHostBuilder builder, Action configureWorker) + { + builder.ConfigureFunctionsWorkerDefaults((hostBuilderContext, workerAppBuilder) => + { + workerAppBuilder.UseAspNetCoreIntegration(); + configureWorker?.Invoke(hostBuilderContext, workerAppBuilder); + }); builder.ConfigureAspNetCoreIntegration(); diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs index 6e8d5b7f2..aacd7c65a 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs @@ -47,12 +47,23 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next await next(context); - if (context.GetInvocationResult()?.Value is IActionResult actionResult) + var invocationResult = context.GetInvocationResult(); + + if (invocationResult?.Value is IActionResult actionResult) { ActionContext actionContext = new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor()); await actionResult.ExecuteResultAsync(actionContext); } + else if (invocationResult?.Value is AspNetCoreHttpResponseData) + { + // The AspNetCoreHttpResponseData implementation is + // simply a wrapper over the underlying HttpResponse and + // all APIs manipulate the request. + // There's no need to return this result as no additional + // processing is required. + invocationResult.Value = null; + } // allows asp.net middleware to continue _coordinator.CompleteFunctionInvocation(invocationId); diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/HttpContextConverter.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpContextConverter.cs index 41a38e5cc..4ab717b2b 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/HttpContextConverter.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpContextConverter.cs @@ -1,9 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Azure.Functions.Worker.Converters; +using Microsoft.Azure.Functions.Worker.Http; namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore { @@ -16,15 +18,17 @@ public ValueTask ConvertAsync(ConverterContext context) { object? target = null; - if (context.TargetType == typeof(HttpRequest)) + if (context.TargetType == typeof(HttpRequest) + && context.FunctionContext.TryGetRequest(out var request)) { - if (context.FunctionContext.Items.TryGetValue(Constants.HttpContextKey, out var requestContext) - && requestContext is HttpContext httpContext) - { - target = httpContext.Request; - } + target = request; } - + else if (context.TargetType == typeof(HttpRequestData) + && context.FunctionContext.TryGetRequest(out request)) + { + target = new AspNetCoreHttpRequestData(request, context.FunctionContext); + } + if (target is not null) { return new ValueTask(ConversionResult.Success(target)); diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpHeadersCollection.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpHeadersCollection.cs new file mode 100644 index 000000000..f495076f2 --- /dev/null +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpHeadersCollection.cs @@ -0,0 +1,211 @@ +// 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 System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using HeadersEnumerable = System.Collections.Generic.IEnumerable>; + +namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore +{ + internal sealed class AspNetCoreHttpRequestHeadersCollection : AspNetCoreHttpHeadersCollection + { + public AspNetCoreHttpRequestHeadersCollection(HttpRequest request) + : base(request.Headers) + { + var requestFeature = request.HttpContext.Features.Get()!; + requestFeature.Headers = this; + } + } + + internal sealed class AspNetCoreHttpResponseHeadersCollection : AspNetCoreHttpHeadersCollection + { + private IHeaderDictionary _originalResponseHeaders; + public AspNetCoreHttpResponseHeadersCollection(HttpResponse request) + : base(request.Headers) + { + var requestFeature = request.HttpContext.Features.Get()!; + _originalResponseHeaders = requestFeature.Headers; + requestFeature.Headers = this; + + // Even though we're replacing the response feature headers, we need to make sure + // headers are written back to the original reference before sent to the client as + // that reference is still used. + request.OnStarting(ProcessResponseStarting); + } + + public AspNetCoreHttpResponseHeadersCollection(HttpResponse request, HttpHeadersCollection headers) + : base(headers) + { + var requestFeature = request.HttpContext.Features.Get()!; + _originalResponseHeaders = requestFeature.Headers; + requestFeature.Headers = this; + } + + private Task ProcessResponseStarting() + { + _originalResponseHeaders.Clear(); + foreach (var item in ((HeadersEnumerable)this)) + { + _originalResponseHeaders.Add(item.Key, item.Value); + } + + return Task.CompletedTask; + } + } + + internal abstract class AspNetCoreHttpHeadersCollection : HttpHeadersCollection, IHeaderDictionary + { + public AspNetCoreHttpHeadersCollection(IHeaderDictionary headers) + { + foreach ( var header in headers) + { + TryAddWithoutValidation(header.Key, (IEnumerable)header.Value); + } + } + + public AspNetCoreHttpHeadersCollection(HttpHeadersCollection headers) + { + foreach (var header in headers) + { + TryAddWithoutValidation(header.Key, header.Value); + } + } + + StringValues IHeaderDictionary.this[string key] + { + get + { + if (TryGetValues(key, out var value)) + { + return new StringValues(value.ToArray()); + } + + return StringValues.Empty; + } + set + { + TryAddWithoutValidation(key, (IEnumerable)value); + } + } + + StringValues IDictionary.this[string key] { get => ((IHeaderDictionary)this)[key]; set => ((IHeaderDictionary)this)[key] = value; } + + public long? ContentLength + { + get + { + var headerValue = ((IHeaderDictionary)this)[HeaderNames.ContentLength]; + if (headerValue.Count == 1 && + !string.IsNullOrEmpty(headerValue[0]) && + HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(headerValue[0]).Trim(), out long value)) + { + return value; + } + + return null; + } + set + { + if (value.HasValue) + { + ((IHeaderDictionary)this)[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(value.GetValueOrDefault()); + } + else + { + Remove(HeaderNames.ContentLength); + } + } + } + + + public StringValues this[string key] + { + get + { + if (TryGetValues(key, out var value)) + { + return new StringValues(value.ToArray()); + } + + return StringValues.Empty; + } + set + { + TryAddWithoutValidation(key, (IEnumerable)value); + } + } + + public ICollection Keys => NonValidated.Select(x => x.Key).ToList(); + + public ICollection Values => NonValidated.Select(x => new StringValues(x.Value.ToArray())).ToList(); + + public int Count => base.NonValidated.Count; + + public bool IsReadOnly => false; + + public void Add(string key, StringValues value) + { + _ = TryAddWithoutValidation(key, (IEnumerable)value); + } + + public void Add(KeyValuePair item) => Add(item.Key, item.Value); + + public bool Contains(KeyValuePair item) + { + if (!TryGetValues(item.Key, out var value) + || !StringValues.Equals(new StringValues(value!.ToArray()), item.Value)) + { + return false; + } + return true; + } + + public bool ContainsKey(string key) => base.Contains(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + if (this.Contains(item)) + { + return base.Remove(item.Key); + } + + return false; + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out StringValues value) + { + if (TryGetValues(key, out var values)) + { + value = new StringValues(values.ToArray()); + return true; + } + + value = default; + return false; + } + + protected IEnumerator> GetHeaders() + { + foreach (var item in NonValidated) + { + yield return new KeyValuePair(item.Key, new StringValues(item.Value.ToArray())); + } + } + + IEnumerator> HeadersEnumerable.GetEnumerator() + => GetHeaders(); + } +} diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpRequestData.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpRequestData.cs new file mode 100644 index 000000000..3faa0425c --- /dev/null +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpRequestData.cs @@ -0,0 +1,64 @@ +// 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 System.Collections.Generic; +using System.IO; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Azure.Functions.Worker.Http; + +namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore +{ + internal sealed class AspNetCoreHttpRequestData : HttpRequestData + { + private readonly HttpRequest _httpRequest; + private readonly Uri _uri; + private readonly Lazy> _cookies; + private readonly Lazy _headers; + + public AspNetCoreHttpRequestData(HttpRequest request, FunctionContext context) + : base(context) + { + _httpRequest = request ?? throw new ArgumentNullException(nameof(request)); + + _uri = new Uri(_httpRequest.GetEncodedUrl()); + + // Currently, this is a one way sync. + // Further changes to the cookies collection will not be reflected in the request. + // We can revisit this and inject a feature to enable sync in the future, if needed. + _cookies = new(CreateCookiesCollection); + _headers = new(() => new AspNetCoreHttpRequestHeadersCollection(_httpRequest)); + } + + public override Stream Body => _httpRequest.Body; + + public override HttpHeadersCollection Headers => _headers.Value; + + public override IReadOnlyCollection Cookies => _cookies.Value; + + public override Uri Url => _uri; + + public override IEnumerable Identities => _httpRequest.HttpContext.User.Identities; + + public override string Method => _httpRequest.Method; + + public override HttpResponseData CreateResponse() + { + return new AspNetCoreHttpResponseData(_httpRequest.HttpContext.Response, FunctionContext); + } + + private IReadOnlyCollection CreateCookiesCollection() + { + var cookies = new List(); + foreach (var item in _httpRequest.Cookies) + { + var cookie = new HttpCookie(item.Key, item.Value); + cookies.Add(cookie); + } + + return cookies; + } + } +} diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionContext/AspNetCoreHttpRequestDataFeature.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpRequestDataFeature.cs similarity index 69% rename from extensions/Worker.Extensions.Http.AspNetCore/src/FunctionContext/AspNetCoreHttpRequestDataFeature.cs rename to extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpRequestDataFeature.cs index e8dd82fe5..e53eadd0f 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionContext/AspNetCoreHttpRequestDataFeature.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpRequestDataFeature.cs @@ -13,12 +13,13 @@ private AspNetCoreHttpRequestDataFeature() { } + /// + /// Gets the singleton instance of the class. + /// public static AspNetCoreHttpRequestDataFeature Instance { get; } = new AspNetCoreHttpRequestDataFeature(); + /// public ValueTask GetHttpRequestDataAsync(FunctionContext context) - { - throw new NotSupportedException($"The method {nameof(GetHttpRequestDataAsync)} " + - $"is not supported when using the ASP.NET Core integration. Use the GetHttpContext method of {nameof(FunctionContext)} to access the HttpContext for the request."); - } + => context.TryGetRequest(out var request) ? new(new AspNetCoreHttpRequestData(request, context)) : default; } } diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpResponseData.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpResponseData.cs new file mode 100644 index 000000000..99b3bd483 --- /dev/null +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreHttpResponseData.cs @@ -0,0 +1,45 @@ +// 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 System.IO; +using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker.Http; + +namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore +{ + internal sealed class AspNetCoreHttpResponseData : HttpResponseData + { + private readonly HttpResponse _httpResponse; + private readonly Lazy _cookies; + private Lazy _headers; + + public AspNetCoreHttpResponseData(HttpResponse httpResponse, FunctionContext context) + : base(context) + { + _httpResponse = httpResponse; + _cookies = new(() => new AspNetCoreResponseCookies(_httpResponse)); + _headers = new(() => new AspNetCoreHttpResponseHeadersCollection(_httpResponse)); + } + + public override HttpHeadersCollection Headers + { + get => _headers.Value; + set => _headers = new(new AspNetCoreHttpResponseHeadersCollection(_httpResponse, value)); + } + + public override Stream Body + { + get => _httpResponse.Body; + set => _httpResponse.Body = value; + } + public override HttpStatusCode StatusCode + { + get => (HttpStatusCode)_httpResponse.StatusCode; + set => _httpResponse.StatusCode = (int)value; + } + + public override HttpCookies Cookies => _cookies.Value; + } +} diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreResponseCookies.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreResponseCookies.cs new file mode 100644 index 000000000..251e4e216 --- /dev/null +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/HttpDataModel/AspNetCoreResponseCookies.cs @@ -0,0 +1,58 @@ +// 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.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker.Http; + +namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore +{ + internal sealed class AspNetCoreResponseCookies : HttpCookies + { + private readonly HttpResponse _httpResponse; + + public AspNetCoreResponseCookies(HttpResponse httpResponse) + { + _httpResponse = httpResponse; + } + + public override void Append(string name, string value) + { + _httpResponse.Cookies.Append(name, value); + } + + public override void Append(IHttpCookie cookie) + { + if (cookie is null) + { + throw new ArgumentNullException(nameof(cookie)); + } + + var cookieOptions = new CookieOptions + { + Domain = cookie.Domain, + Path = cookie.Path, + Expires = cookie.Expires, + HttpOnly = cookie.HttpOnly ?? false, + MaxAge = cookie.MaxAge is null ? null : TimeSpan.FromSeconds(cookie.MaxAge.Value), + SameSite = ConvertSameSite(cookie.SameSite), + Secure = cookie.Secure ?? false + }; + + _httpResponse.Cookies.Append(cookie.Name, cookie.Value, cookieOptions); + } + + private static SameSiteMode ConvertSameSite(SameSite sameSite) + { + return sameSite switch + { + SameSite.ExplicitNone or SameSite.None => SameSiteMode.None, + SameSite.Lax => SameSiteMode.Lax, + SameSite.Strict => SameSiteMode.Strict, + _ => default, + }; + } + + public override IHttpCookie CreateNew() => throw new NotSupportedException(); + } +} diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/Properties/AssemblyInfo.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/Properties/AssemblyInfo.cs index 982103208..33f6f1705 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/Properties/AssemblyInfo.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/Properties/AssemblyInfo.cs @@ -4,4 +4,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.Azure.Functions.Worker.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")] +[assembly: InternalsVisibleTo("Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/Worker.Extensions.Http.AspNetCore.csproj b/extensions/Worker.Extensions.Http.AspNetCore/src/Worker.Extensions.Http.AspNetCore.csproj index 5718e34b8..02baa04b9 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/Worker.Extensions.Http.AspNetCore.csproj +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/Worker.Extensions.Http.AspNetCore.csproj @@ -6,7 +6,7 @@ ASP.NET Core extensions for .NET isolated functions - 1.0.0 + 1.1.1 net6.0 @@ -18,6 +18,7 @@ + diff --git a/extensions/Worker.Extensions.Kafka/release_notes.md b/extensions/Worker.Extensions.Kafka/release_notes.md index ca17cb549..e73c591ab 100644 --- a/extensions/Worker.Extensions.Kafka/release_notes.md +++ b/extensions/Worker.Extensions.Kafka/release_notes.md @@ -6,4 +6,4 @@ ### Microsoft.Azure.Functions.Worker.Extensions.Kafka -- \ No newline at end of file +- diff --git a/extensions/Worker.Extensions.Kafka/src/KafkaOutputAttribute.cs b/extensions/Worker.Extensions.Kafka/src/KafkaOutputAttribute.cs index da2954f3f..8ee86ecab 100644 --- a/extensions/Worker.Extensions.Kafka/src/KafkaOutputAttribute.cs +++ b/extensions/Worker.Extensions.Kafka/src/KafkaOutputAttribute.cs @@ -128,5 +128,20 @@ public KafkaOutputAttribute(string brokerList, string topic) /// being sent to cluster. Larger value allows more batching results in high throughput. /// public int LingerMs { get; set; } = 5; + + /// + /// URL for the Avro Schema Registry + /// + public string SchemaRegistryUrl { get; set; } + + /// + /// Username for the Avro Schema Registry + /// + public string SchemaRegistryUsername { get; set; } + + /// + /// Password for the Avro Schema Registry + /// + public string SchemaRegistryPassword { get; set; } } } diff --git a/extensions/Worker.Extensions.Kafka/src/KafkaTriggerAttribute.cs b/extensions/Worker.Extensions.Kafka/src/KafkaTriggerAttribute.cs index c5ea4e79a..6708f7593 100644 --- a/extensions/Worker.Extensions.Kafka/src/KafkaTriggerAttribute.cs +++ b/extensions/Worker.Extensions.Kafka/src/KafkaTriggerAttribute.cs @@ -112,6 +112,21 @@ public KafkaTriggerAttribute(string brokerList, string topic) /// public long LagThreshold { get; set; } = 1000; + /// + /// URL for the Avro Schema Registry + /// + public string SchemaRegistryUrl { get; set; } + + /// + /// Username for the Avro Schema Registry + /// + public string SchemaRegistryUsername { get; set; } + + /// + /// Password for the Avro Schema Registry + /// + public string SchemaRegistryPassword { get; set; } + /// /// Gets or sets the configuration to enable batch processing of events. Default value is "false". /// diff --git a/extensions/Worker.Extensions.Kafka/src/Worker.Extensions.Kafka.csproj b/extensions/Worker.Extensions.Kafka/src/Worker.Extensions.Kafka.csproj index b5ca20c0f..bbf3f596c 100644 --- a/extensions/Worker.Extensions.Kafka/src/Worker.Extensions.Kafka.csproj +++ b/extensions/Worker.Extensions.Kafka/src/Worker.Extensions.Kafka.csproj @@ -5,7 +5,7 @@ Kafka extensions for .NET isolated functions - 3.9.0 + 3.10.0 false diff --git a/extensions/Worker.Extensions.ServiceBus/release_notes.md b/extensions/Worker.Extensions.ServiceBus/release_notes.md index aeba67dd4..2afb5597e 100644 --- a/extensions/Worker.Extensions.ServiceBus/release_notes.md +++ b/extensions/Worker.Extensions.ServiceBus/release_notes.md @@ -4,6 +4,8 @@ - My change description (#PR/#issue) --> -### Microsoft.Azure.Functions.Worker.Extensions.ServiceBus +### Microsoft.Azure.Functions.Worker.Extensions.ServiceBus 5.14.1 -- +- Fixed issue where deadlettering a message without specifying properties to modify could throw + an exception from out of proc extension. +- Include underlying exception details in RpcException when a failure occurs. diff --git a/extensions/Worker.Extensions.ServiceBus/src/Properties/AssemblyInfo.cs b/extensions/Worker.Extensions.ServiceBus/src/Properties/AssemblyInfo.cs index 26b1b7c2e..2ef5f3d28 100644 --- a/extensions/Worker.Extensions.ServiceBus/src/Properties/AssemblyInfo.cs +++ b/extensions/Worker.Extensions.ServiceBus/src/Properties/AssemblyInfo.cs @@ -4,5 +4,5 @@ using System.Runtime.CompilerServices; using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; -[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.ServiceBus", "5.12.0")] +[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.ServiceBus", "5.13.3")] [assembly: InternalsVisibleTo("Microsoft.Azure.Functions.Worker.Extensions.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")] diff --git a/extensions/Worker.Extensions.ServiceBus/src/Proto/settlement.proto b/extensions/Worker.Extensions.ServiceBus/src/Proto/settlement.proto new file mode 100644 index 000000000..969e94357 --- /dev/null +++ b/extensions/Worker.Extensions.ServiceBus/src/Proto/settlement.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "google/protobuf/wrappers.proto"; + +// this namespace will be shared between isolated worker and WebJobs extension so make it somewhat generic +option csharp_namespace = "Microsoft.Azure.ServiceBus.Grpc"; + +// The settlement service definition. +service Settlement { + // Completes a message + rpc Complete (CompleteRequest) returns (google.protobuf.Empty) {} + + // Abandons a message + rpc Abandon (AbandonRequest) returns (google.protobuf.Empty) {} + + // Deadletters a message + rpc Deadletter (DeadletterRequest) returns (google.protobuf.Empty) {} + + // Defers a message + rpc Defer (DeferRequest) returns (google.protobuf.Empty) {} +} + +// The complete message request containing the locktoken. +message CompleteRequest { + string locktoken = 1; +} + +// The abandon message request containing the locktoken and properties to modify. +message AbandonRequest { + string locktoken = 1; + bytes propertiesToModify = 2; +} + +// The deadletter message request containing the locktoken and properties to modify along with the reason/description. +message DeadletterRequest { + string locktoken = 1; + bytes propertiesToModify = 2; + google.protobuf.StringValue deadletterReason = 3; + google.protobuf.StringValue deadletterErrorDescription = 4; +} + +// The defer message request containing the locktoken and properties to modify. +message DeferRequest { + string locktoken = 1; + bytes propertiesToModify = 2; +} \ No newline at end of file diff --git a/extensions/Worker.Extensions.ServiceBus/src/ServiceBusExtensionStartup.cs b/extensions/Worker.Extensions.ServiceBus/src/ServiceBusExtensionStartup.cs new file mode 100644 index 000000000..5be4b027b --- /dev/null +++ b/extensions/Worker.Extensions.ServiceBus/src/ServiceBusExtensionStartup.cs @@ -0,0 +1,27 @@ +// Copyright (c) Jacob Viau. All rights reserved. +// Licensed under the MIT. See LICENSE file in the project root for full license information. + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Core; +using Microsoft.Azure.Functions.Worker.Extensions.Rpc; +using Microsoft.Azure.ServiceBus.Grpc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +[assembly: WorkerExtensionStartup(typeof(ServiceBusExtensionStartup))] + +namespace Microsoft.Azure.Functions.Worker +{ + public sealed class ServiceBusExtensionStartup : WorkerExtensionStartup + { + public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder) + { + applicationBuilder.Services.AddTransient(sp => + { + IOptions options = sp.GetRequiredService>(); + return new Settlement.SettlementClient(options.Value.CallInvoker); + }); + applicationBuilder.Services.AddWorkerRpc(); + } + } +} \ No newline at end of file diff --git a/extensions/Worker.Extensions.ServiceBus/src/ServiceBusMessageActions.cs b/extensions/Worker.Extensions.ServiceBus/src/ServiceBusMessageActions.cs new file mode 100644 index 000000000..d02de18db --- /dev/null +++ b/extensions/Worker.Extensions.ServiceBus/src/ServiceBusMessageActions.cs @@ -0,0 +1,381 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Google.Protobuf; +using Microsoft.Azure.Amqp; +using Microsoft.Azure.Amqp.Encoding; +using Microsoft.Azure.Functions.Worker.Converters; +using Microsoft.Azure.ServiceBus.Grpc; +using Type = System.Type; + +namespace Microsoft.Azure.Functions.Worker +{ + [InputConverter(typeof(ServiceBusMessageActionsConverter))] + public class ServiceBusMessageActions + { + private readonly Settlement.SettlementClient _settlement; + + /// The size, in bytes, to use as a buffer for stream operations. + private const int StreamBufferSizeInBytes = 512; + + /// The set of mappings from CLR types to AMQP types for property values. + private static readonly IReadOnlyDictionary AmqpPropertyTypeMap = new Dictionary + { + { typeof(byte), AmqpType.Byte }, + { typeof(sbyte), AmqpType.SByte }, + { typeof(char), AmqpType.Char }, + { typeof(short), AmqpType.Int16 }, + { typeof(ushort), AmqpType.UInt16 }, + { typeof(int), AmqpType.Int32 }, + { typeof(uint), AmqpType.UInt32 }, + { typeof(long), AmqpType.Int64 }, + { typeof(ulong), AmqpType.UInt64 }, + { typeof(float), AmqpType.Single }, + { typeof(double), AmqpType.Double }, + { typeof(decimal), AmqpType.Decimal }, + { typeof(bool), AmqpType.Boolean }, + { typeof(Guid), AmqpType.Guid }, + { typeof(string), AmqpType.String }, + { typeof(Uri), AmqpType.Uri }, + { typeof(DateTime), AmqpType.DateTime }, + { typeof(DateTimeOffset), AmqpType.DateTimeOffset }, + { typeof(TimeSpan), AmqpType.TimeSpan }, + }; + + internal ServiceBusMessageActions(Settlement.SettlementClient settlement) + { + _settlement = settlement; + } + + /// + /// Initializes a new instance of the class for mocking use in testing. + /// + /// + /// This constructor exists only to support mocking. When used, class state is not fully initialized, and + /// will not function correctly; virtual members are meant to be mocked. + /// + protected ServiceBusMessageActions() + { + } + + /// + public virtual async Task CompleteMessageAsync( + ServiceBusReceivedMessage message, + CancellationToken cancellationToken = default) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + await _settlement.CompleteAsync(new() { Locktoken = message.LockToken }, cancellationToken: cancellationToken); + } + + /// + public virtual async Task AbandonMessageAsync( + ServiceBusReceivedMessage message, + IDictionary? propertiesToModify = default, + CancellationToken cancellationToken = default) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + var request = new AbandonRequest() + { + Locktoken = message.LockToken, + }; + if (propertiesToModify != null) + { + request.PropertiesToModify = ConvertToByteString(propertiesToModify); + } + await _settlement.AbandonAsync(request, cancellationToken: cancellationToken); + } + + /// + public virtual async Task DeadLetterMessageAsync( + ServiceBusReceivedMessage message, + Dictionary? propertiesToModify = default, + string? deadLetterReason = default, + string? deadLetterErrorDescription = default, + CancellationToken cancellationToken = default) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + var request = new DeadletterRequest() + { + Locktoken = message.LockToken, + DeadletterReason = deadLetterReason, + DeadletterErrorDescription = deadLetterErrorDescription + }; + if (propertiesToModify != null) + { + request.PropertiesToModify = ConvertToByteString(propertiesToModify); + } + await _settlement.DeadletterAsync(request, cancellationToken: cancellationToken); + } + + /// + public virtual async Task DeferMessageAsync( + ServiceBusReceivedMessage message, + IDictionary? propertiesToModify = default, + CancellationToken cancellationToken = default) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + var request = new DeferRequest() + { + Locktoken = message.LockToken, + }; + if (propertiesToModify != null) + { + request.PropertiesToModify = ConvertToByteString(propertiesToModify); + } + await _settlement.DeferAsync(request, cancellationToken: cancellationToken); + } + + internal static ByteString ConvertToByteString(IDictionary propertiesToModify) + { + var map = new AmqpMap(); + foreach (KeyValuePair kvp in propertiesToModify) + { + if (TryCreateAmqpPropertyValueFromNetProperty(kvp.Value, out var amqpValue)) + { + map[new MapKey(kvp.Key)] = amqpValue; + } + else + { + throw new NotSupportedException( + string.Format( + CultureInfo.CurrentCulture, + "The key `{0}` has a value of type `{1}` which is not supported for AMQP transport." + + "The list of supported types can be found here: https://learn.microsoft.com/dotnet/api/azure.messaging.servicebus.servicebusmessage.applicationproperties?view=azure-dotnet#remarks", + kvp.Key, + kvp.Value?.GetType().Name)); + } + } + + using ByteBuffer buffer = new ByteBuffer(256, true); + AmqpCodec.EncodeMap(map, buffer); + return ByteString.CopyFrom(buffer.Buffer, 0, buffer.Length); + } + + /// + /// Attempts to create an AMQP property value for a given event property. + /// + /// + /// The value of the event property to create an AMQP property value for. + /// The AMQP property value that was created. + /// true to allow an AMQP map to be translated to additional types supported only by a message body; otherwise, false. + /// + /// true if an AMQP property value was able to be created; otherwise, false. + /// + private static bool TryCreateAmqpPropertyValueFromNetProperty( + object? propertyValue, + out object? amqpPropertyValue, + bool allowBodyTypes = false) + { + amqpPropertyValue = null; + + if (propertyValue == null) + { + return true; + } + + switch (GetTypeIdentifier(propertyValue)) + { + case AmqpType.Byte: + case AmqpType.SByte: + case AmqpType.Int16: + case AmqpType.Int32: + case AmqpType.Int64: + case AmqpType.UInt16: + case AmqpType.UInt32: + case AmqpType.UInt64: + case AmqpType.Single: + case AmqpType.Double: + case AmqpType.Boolean: + case AmqpType.Decimal: + case AmqpType.Char: + case AmqpType.Guid: + case AmqpType.DateTime: + case AmqpType.String: + amqpPropertyValue = propertyValue; + break; + + case AmqpType.Stream: + case AmqpType.Unknown when propertyValue is Stream: + amqpPropertyValue = ReadStreamToArraySegment((Stream)propertyValue); + break; + + case AmqpType.Uri: + amqpPropertyValue = new DescribedType((AmqpSymbol)AmqpMessageConstants.Uri, ((Uri)propertyValue).AbsoluteUri); + break; + + case AmqpType.DateTimeOffset: + amqpPropertyValue = new DescribedType((AmqpSymbol)AmqpMessageConstants.DateTimeOffset, ((DateTimeOffset)propertyValue).UtcTicks); + break; + + case AmqpType.TimeSpan: + amqpPropertyValue = new DescribedType((AmqpSymbol)AmqpMessageConstants.TimeSpan, ((TimeSpan)propertyValue).Ticks); + break; + + case AmqpType.Unknown when allowBodyTypes && propertyValue is byte[] byteArray: + amqpPropertyValue = new ArraySegment(byteArray); + break; + + case AmqpType.Unknown when allowBodyTypes && propertyValue is IDictionary dict: + amqpPropertyValue = new AmqpMap(dict); + break; + + case AmqpType.Unknown when allowBodyTypes && propertyValue is IList: + amqpPropertyValue = propertyValue; + break; + + case AmqpType.Unknown: + var exception = new SerializationException(string.Format(CultureInfo.CurrentCulture, "Serialization failed due to an unsupported type, {0}.", propertyValue.GetType().FullName)); + throw exception; + } + + return (amqpPropertyValue != null); + } + + /// + /// Converts a stream to an representation. + /// + /// + /// The stream to read and capture in memory. + /// + /// The containing the stream data. + /// + private static ArraySegment ReadStreamToArraySegment(Stream stream) + { + switch (stream) + { + case { Length: < 1 }: + return default; + + case BufferListStream bufferListStream: + return bufferListStream.ReadBytes((int)stream.Length); + + case MemoryStream memStreamSource: + { + using var memStreamCopy = new MemoryStream((int)(memStreamSource.Length - memStreamSource.Position)); + memStreamSource.CopyTo(memStreamCopy, StreamBufferSizeInBytes); + if (!memStreamCopy.TryGetBuffer(out ArraySegment segment)) + { + segment = new ArraySegment(memStreamCopy.ToArray()); + } + return segment; + } + + default: + { + using var memStreamCopy = new MemoryStream(StreamBufferSizeInBytes); + stream.CopyTo(memStreamCopy, StreamBufferSizeInBytes); + if (!memStreamCopy.TryGetBuffer(out ArraySegment segment)) + { + segment = new ArraySegment(memStreamCopy.ToArray()); + } + return segment; + } + } + } + + /// + /// Represents the supported AMQP property types. + /// + /// + /// + /// WARNING: + /// These values are synchronized between Azure services and the client + /// library. You must consult with the Event Hubs/Service Bus service team before making + /// changes, including adding a new member. + /// + /// When adding a new member, remember to always do so before the Unknown + /// member. + /// + /// + private enum AmqpType + { + Null, + Byte, + SByte, + Char, + Int16, + UInt16, + Int32, + UInt32, + Int64, + UInt64, + Single, + Double, + Decimal, + Boolean, + Guid, + String, + Uri, + DateTime, + DateTimeOffset, + TimeSpan, + Stream, + Unknown + } + + /// + /// Gets the AMQP property type identifier for a given + /// value. + /// + /// + /// The value to determine the type identifier for. + /// + /// The that was identified for the given . + /// + private static AmqpType GetTypeIdentifier(object? value) => ToAmqpPropertyType(value?.GetType()); + + /// + /// Translates the given to the corresponding + /// . + /// + /// + /// The type to convert to an AMQP type. + /// + /// The AMQP property type that best matches the specified . + /// + private static AmqpType ToAmqpPropertyType(Type? type) + { + if (type == null) + { + return AmqpType.Null; + } + + if (AmqpPropertyTypeMap.TryGetValue(type, out AmqpType amqpType)) + { + return amqpType; + } + + return AmqpType.Unknown; + } + + internal static class AmqpMessageConstants + { + public const string Vendor = "com.microsoft"; + public const string TimeSpan = Vendor + ":timespan"; + public const string Uri = Vendor + ":uri"; + public const string DateTimeOffset = Vendor + ":datetime-offset"; + } + } +} \ No newline at end of file diff --git a/extensions/Worker.Extensions.ServiceBus/src/ServiceBusMessageActionsConverter.cs b/extensions/Worker.Extensions.ServiceBus/src/ServiceBusMessageActionsConverter.cs new file mode 100644 index 000000000..cfdf3beb1 --- /dev/null +++ b/extensions/Worker.Extensions.ServiceBus/src/ServiceBusMessageActionsConverter.cs @@ -0,0 +1,35 @@ +// 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 System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker.Converters; +using Microsoft.Azure.ServiceBus.Grpc; + +namespace Microsoft.Azure.Functions.Worker +{ + /// + /// Converter to bind to type parameter. + /// + internal class ServiceBusMessageActionsConverter : IInputConverter + { + private readonly Settlement.SettlementClient _settlement; + + public ServiceBusMessageActionsConverter(Settlement.SettlementClient settlement) + { + _settlement = settlement; + } + + public ValueTask ConvertAsync(ConverterContext context) + { + try + { + return new ValueTask(ConversionResult.Success(new ServiceBusMessageActions(_settlement))); + } + catch (Exception exception) + { + return new ValueTask(ConversionResult.Failed(exception)); + } + } + } +} \ No newline at end of file diff --git a/extensions/Worker.Extensions.ServiceBus/src/Worker.Extensions.ServiceBus.csproj b/extensions/Worker.Extensions.ServiceBus/src/Worker.Extensions.ServiceBus.csproj index b08a98d49..de567876a 100644 --- a/extensions/Worker.Extensions.ServiceBus/src/Worker.Extensions.ServiceBus.csproj +++ b/extensions/Worker.Extensions.ServiceBus/src/Worker.Extensions.ServiceBus.csproj @@ -6,7 +6,7 @@ Azure Service Bus extensions for .NET isolated functions - 5.13.0 + 5.14.1 false @@ -15,12 +15,15 @@ - + + + + @@ -28,4 +31,7 @@ + + + diff --git a/extensions/Worker.Extensions.SignalRService/release_notes.md b/extensions/Worker.Extensions.SignalRService/release_notes.md index 0198f2ccf..cb6d2671f 100644 --- a/extensions/Worker.Extensions.SignalRService/release_notes.md +++ b/extensions/Worker.Extensions.SignalRService/release_notes.md @@ -3,6 +3,8 @@ +- Update dependency `Microsoft.Azure.SignalR.Management` to 1.22.0 +- Update dependency `Microsoft.Azure.WebJobs.Extensions.SignalRService` to 1.12.0 ### Microsoft.Azure.Functions.Worker.Extensions.SignalRService 1.11.0 diff --git a/extensions/Worker.Extensions.SignalRService/src/Properties/AssemblyInfo.cs b/extensions/Worker.Extensions.SignalRService/src/Properties/AssemblyInfo.cs index 1888b4fde..ac92f990c 100644 --- a/extensions/Worker.Extensions.SignalRService/src/Properties/AssemblyInfo.cs +++ b/extensions/Worker.Extensions.SignalRService/src/Properties/AssemblyInfo.cs @@ -3,4 +3,4 @@ using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; -[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.SignalRService", "1.11.0")] +[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.SignalRService", "1.12.0")] diff --git a/extensions/Worker.Extensions.SignalRService/src/Worker.Extensions.SignalRService.csproj b/extensions/Worker.Extensions.SignalRService/src/Worker.Extensions.SignalRService.csproj index 19bc7b280..09ebb6e13 100644 --- a/extensions/Worker.Extensions.SignalRService/src/Worker.Extensions.SignalRService.csproj +++ b/extensions/Worker.Extensions.SignalRService/src/Worker.Extensions.SignalRService.csproj @@ -6,7 +6,7 @@ Azure SignalR Service extensions for .NET isolated functions annotations - 1.11.0 + 1.12.0 false @@ -19,7 +19,7 @@ - + diff --git a/extensions/Worker.Extensions.Storage.Blobs/src/BlobStorageConverter.cs b/extensions/Worker.Extensions.Storage.Blobs/src/BlobStorageConverter.cs index 578bda206..c3597cc4b 100644 --- a/extensions/Worker.Extensions.Storage.Blobs/src/BlobStorageConverter.cs +++ b/extensions/Worker.Extensions.Storage.Blobs/src/BlobStorageConverter.cs @@ -244,8 +244,7 @@ private async Task GetBlobBinaryDataAsync(BlobContainerClient containerC private async Task GetBlobStreamAsync(BlobContainerClient containerClient, string blobName) { var client = CreateBlobClient(containerClient, blobName); - var download = await client.DownloadStreamingAsync(); - return download.Value.Content; + return await client.OpenReadAsync(); } private BlobContainerClient CreateBlobContainerClient(string connectionName, string containerName) diff --git a/extensions/Worker.Extensions.Timer/release_notes.md b/extensions/Worker.Extensions.Timer/release_notes.md index 322d068be..4c9928a15 100644 --- a/extensions/Worker.Extensions.Timer/release_notes.md +++ b/extensions/Worker.Extensions.Timer/release_notes.md @@ -4,6 +4,6 @@ - My change description (#PR/#issue) --> -### Microsoft.Azure.Functions.Worker.Extensions.Timer +### Microsoft.Azure.Functions.Worker.Extensions.Timer 4.3.0 - Add `BindingCapabilities` attribute to TimerTrigger to express function-level retry capabilities. (#1457) \ No newline at end of file diff --git a/extensions/Worker.Extensions.Timer/src/Worker.Extensions.Timer.csproj b/extensions/Worker.Extensions.Timer/src/Worker.Extensions.Timer.csproj index c48caadcf..bd668a4e5 100644 --- a/extensions/Worker.Extensions.Timer/src/Worker.Extensions.Timer.csproj +++ b/extensions/Worker.Extensions.Timer/src/Worker.Extensions.Timer.csproj @@ -6,7 +6,7 @@ Timer extensions for .NET isolated functions - 4.2.0 + 4.3.0 diff --git a/global.json b/global.json index afc32cee3..4ac08fdaf 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.306", + "version": "8.0.100", "rollForward": "latestFeature" }, "msbuild-sdks": { diff --git a/host/azure-pipelines.yml b/host/azure-pipelines.yml index deb7d5476..e3164c011 100644 --- a/host/azure-pipelines.yml +++ b/host/azure-pipelines.yml @@ -46,9 +46,10 @@ stages: displayName: "Copy needed files" inputs: SourceFolder: "$(Build.ArtifactStagingDirectory)/output/linux" - # Publish output will include many other files. We only need the FunctionsNetHost & libnethost.so + # Publish output will include many other files. We only need the FunctionsNetHost, FunctionsNetHost.dbg & libnethost.so Contents: | FunctionsNetHost + FunctionsNetHost.dbg libnethost.so TargetFolder: "$(Build.ArtifactStagingDirectory)/output/linux_filtered" @@ -80,9 +81,10 @@ stages: displayName: "Copy needed files" inputs: SourceFolder: "$(Build.ArtifactStagingDirectory)/output/windows" - # Publish output will include many other files. We only need FunctionsNetHost.exe & nethost.dll + # Publish output will include many other files. We only need FunctionsNetHost.exe, pdb & nethost.dll Contents: | FunctionsNetHost.exe + FunctionsNetHost.pdb nethost.dll TargetFolder: "$(Build.ArtifactStagingDirectory)/output/windows_filtered" diff --git a/host/src/FunctionsNetHost/AppLoader/AppLoader.cs b/host/src/FunctionsNetHost/AppLoader/AppLoader.cs index fd3005688..c09eba28f 100644 --- a/host/src/FunctionsNetHost/AppLoader/AppLoader.cs +++ b/host/src/FunctionsNetHost/AppLoader/AppLoader.cs @@ -1,10 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System.Diagnostics; using System.Runtime.InteropServices; namespace FunctionsNetHost { + // If having problems with the managed host, enable the following: + // Environment.SetEnvironmentVariable("COREHOST_TRACE", "1"); + // In Unix environment, you need to run the below command in the terminal to set the environment variable. + // export COREHOST_TRACE=1 + /// /// Manages loading hostfxr & worker assembly. /// @@ -14,48 +20,36 @@ internal sealed class AppLoader : IDisposable private IntPtr _hostContextHandle = IntPtr.Zero; private bool _disposed; - internal AppLoader() - { - LoadHostfxrLibrary(); - } - - private void LoadHostfxrLibrary() - { - // If having problems with the managed host, enable the following: - // Environment.SetEnvironmentVariable("COREHOST_TRACE", "1"); - // In Unix environment, you need to run the below command in the terminal to set the environment variable. - // export COREHOST_TRACE=1 - - var hostfxrFullPath = NetHost.GetHostFxrPath(); - Logger.LogTrace($"hostfxr path:{hostfxrFullPath}"); - - _hostfxrHandle = NativeLibrary.Load(hostfxrFullPath); - if (_hostfxrHandle == IntPtr.Zero) - { - Logger.Log($"Failed to load hostfxr. hostfxr path:{hostfxrFullPath}"); - return; - } - - Logger.LogTrace($"hostfxr library loaded successfully."); - } - internal int RunApplication(string? assemblyPath) { ArgumentNullException.ThrowIfNull(assemblyPath, nameof(assemblyPath)); unsafe { - var parameters = new HostFxr.hostfxr_initialize_parameters + var parameters = new NetHost.get_hostfxr_parameters { - size = sizeof(HostFxr.hostfxr_initialize_parameters) + size = sizeof(NetHost.get_hostfxr_parameters), + assembly_path = GetCharArrayPointer(assemblyPath) }; - var error = HostFxr.Initialize(1, new[] { assemblyPath }, ref parameters, out _hostContextHandle); + var hostfxrFullPath = NetHost.GetHostFxrPath(¶meters); + Logger.LogTrace($"hostfxr path:{hostfxrFullPath}"); + + _hostfxrHandle = NativeLibrary.Load(hostfxrFullPath); + + if (_hostfxrHandle == IntPtr.Zero) + { + Logger.Log($"Failed to load hostfxr. hostfxrFullPath:{hostfxrFullPath}"); + return -1; + } + + Logger.LogTrace($"hostfxr loaded."); + + var error = HostFxr.Initialize(1, new[] { assemblyPath }, IntPtr.Zero, out _hostContextHandle); if (_hostContextHandle == IntPtr.Zero) { - Logger.Log( - $"Failed to initialize the .NET Core runtime. Assembly path:{assemblyPath}"); + Logger.Log($"Failed to initialize the .NET Core runtime. Assembly path:{assemblyPath}"); return -1; } @@ -95,13 +89,22 @@ private void Dispose(bool disposing) if (_hostContextHandle != IntPtr.Zero) { - NativeLibrary.Free(_hostContextHandle); - Logger.LogTrace($"Freed hostcontext handle"); + HostFxr.Close(_hostContextHandle); + Logger.LogTrace($"Closed hostcontext handle"); _hostContextHandle = IntPtr.Zero; } _disposed = true; } } + + private static unsafe char* GetCharArrayPointer(string assemblyPath) + { +#if OS_LINUX + return (char*)Marshal.StringToHGlobalAnsi(assemblyPath).ToPointer(); +#else + return (char*)Marshal.StringToHGlobalUni(assemblyPath).ToPointer(); +#endif + } } } diff --git a/host/src/FunctionsNetHost/AppLoader/HostFxr.cs b/host/src/FunctionsNetHost/AppLoader/HostFxr.cs index 7655ca919..7e0e7709b 100644 --- a/host/src/FunctionsNetHost/AppLoader/HostFxr.cs +++ b/host/src/FunctionsNetHost/AppLoader/HostFxr.cs @@ -14,37 +14,33 @@ public unsafe struct hostfxr_initialize_parameters public char* dotnet_root; }; - [LibraryImport("hostfxr", EntryPoint = "hostfxr_initialize_for_dotnet_command_line")] - public unsafe static partial int Initialize( - int argc, - [MarshalAs(UnmanagedType.LPArray, ArraySubType = + [LibraryImport("hostfxr", EntryPoint = "hostfxr_initialize_for_dotnet_command_line", #if OS_LINUX - UnmanagedType.LPStr + StringMarshalling = StringMarshalling.Utf8 #else - UnmanagedType.LPWStr + StringMarshalling = StringMarshalling.Utf16 #endif - )] string[] argv, - ref hostfxr_initialize_parameters parameters, - out IntPtr host_context_handle - ); + )] + public unsafe static partial int Initialize( + int argc, + string[] argv, + IntPtr parameters, + out IntPtr host_context_handle + ); [LibraryImport("hostfxr", EntryPoint = "hostfxr_run_app")] public static partial int Run(IntPtr host_context_handle); - [LibraryImport("hostfxr", EntryPoint = "hostfxr_set_runtime_property_value")] - public static partial int SetAppContextData(IntPtr host_context_handle, [MarshalAs( -#if OS_LINUX - UnmanagedType.LPStr -#else - UnmanagedType.LPWStr -#endif - )] string name, [MarshalAs( + [LibraryImport("hostfxr", EntryPoint = "hostfxr_set_runtime_property_value", #if OS_LINUX - UnmanagedType.LPStr + StringMarshalling = StringMarshalling.Utf8 #else - UnmanagedType.LPWStr + StringMarshalling = StringMarshalling.Utf16 #endif - )] string value); + )] + public static partial int SetAppContextData(IntPtr host_context_handle, string name, string value); + [LibraryImport("hostfxr", EntryPoint = "hostfxr_close")] + public static partial int Close(IntPtr host_context_handle); } } diff --git a/host/src/FunctionsNetHost/AppLoader/NetHost.cs b/host/src/FunctionsNetHost/AppLoader/NetHost.cs index 61397c29f..3a7d70207 100644 --- a/host/src/FunctionsNetHost/AppLoader/NetHost.cs +++ b/host/src/FunctionsNetHost/AppLoader/NetHost.cs @@ -7,18 +7,28 @@ namespace FunctionsNetHost { internal class NetHost { + public unsafe struct get_hostfxr_parameters + { + public nint size; + + // Optional.Path to the application assembly, + // If specified, hostfxr is located from this directory if present (For self-contained deployments) + public char* assembly_path; + public char* dotnet_root; + } + [DllImport("nethost", CharSet = CharSet.Auto)] - private static extern int get_hostfxr_path( + private unsafe static extern int get_hostfxr_path( [Out] char[] buffer, [In] ref int buffer_size, - IntPtr reserved); + get_hostfxr_parameters* parameters); - internal static string GetHostFxrPath() + internal unsafe static string GetHostFxrPath(get_hostfxr_parameters* parameters) { char[] buffer = new char[200]; int bufferSize = buffer.Length; - int rc = get_hostfxr_path(buffer, ref bufferSize, IntPtr.Zero); + int rc = get_hostfxr_path(buffer, ref bufferSize, parameters); if (rc != 0) { diff --git a/host/src/FunctionsNetHost/Native/NativeExports.cs b/host/src/FunctionsNetHost/Native/NativeExports.cs index 2d44f3015..f5ef01554 100644 --- a/host/src/FunctionsNetHost/Native/NativeExports.cs +++ b/host/src/FunctionsNetHost/Native/NativeExports.cs @@ -10,24 +10,10 @@ namespace FunctionsNetHost public static class NativeExports { [UnmanagedCallersOnly(EntryPoint = "get_application_properties")] - public static int GetApplicationProperties(NativeHostData nativeHostData) + public static unsafe int GetApplicationProperties(NativeHostData* nativeHostData) { Logger.LogTrace("NativeExports.GetApplicationProperties method invoked."); - - try - { - var nativeHostApplication = NativeHostApplication.Instance; - GCHandle gch = GCHandle.Alloc(nativeHostApplication, GCHandleType.Pinned); - IntPtr pNativeApplication = gch.AddrOfPinnedObject(); - nativeHostData.PNativeApplication = pNativeApplication; - - return 1; - } - catch (Exception ex) - { - Logger.Log($"Error in NativeExports.GetApplicationProperties: {ex}"); - return 0; - } + return 1; } [UnmanagedCallersOnly(EntryPoint = "register_callbacks")] diff --git a/host/src/FunctionsNetHost/global.json b/host/src/FunctionsNetHost/global.json index 27928313f..989a69caf 100644 --- a/host/src/FunctionsNetHost/global.json +++ b/host/src/FunctionsNetHost/global.json @@ -1,7 +1,6 @@ { "sdk": { - "version": "8.0.100-rc.2.23502.2", - "allowPrerelease": true, + "version": "8.0.100", "rollForward": "latestMinor" } } \ No newline at end of file diff --git a/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec b/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec index 0f198f5a2..5e5c77c4e 100644 --- a/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec +++ b/host/tools/build/Microsoft.Azure.Functions.DotnetIsolatedNativeHost.nuspec @@ -4,7 +4,7 @@ Microsoft.Azure.Functions.DotNetIsolatedNativeHost Microsoft Azure Functions dotnet-isolated native host dotnet-isolated azure-functions azure - 1.0.2 + 1.0.3 Microsoft Microsoft https://github.com/Azure/azure-functions-dotnet-worker diff --git a/release_notes.md b/release_notes.md index d210c4639..b5b553cdc 100644 --- a/release_notes.md +++ b/release_notes.md @@ -4,16 +4,19 @@ - My change description (#PR/#issue) --> -### Microsoft.Azure.Functions.Worker (metapackage) +### Microsoft.Azure.Functions.Worker (metapackage) 1.20.0 -- +- Updated to `Microsoft.Azure.Functions.Worker.Core` 1.16.0 +- Updated to `Microsoft.Azure.Functions.Worker.Grpc` 1.15.0 -### Microsoft.Azure.Functions.Worker.Core +### Microsoft.Azure.Functions.Worker.Core 1.16.0 - Adding optional parameter support (#1868) +- Unsealed `Microsoft.Azure.Functions.Worker.Http.HttpHeadersCollection` -### Microsoft.Azure.Functions.Worker.Grpc +### Microsoft.Azure.Functions.Worker.Grpc 1.15.0 - Added support for handling the new command line arguments with "functions-" prefix. (#1897) - Adding optional parameter support (#1868) +- Enhancements to interop in hosted placeholder scenarios \ No newline at end of file diff --git a/samples/AspNetIntegration/AspNetIntegration.csproj b/samples/AspNetIntegration/AspNetIntegration.csproj index 5a5bfa51f..7cf66cefe 100644 --- a/samples/AspNetIntegration/AspNetIntegration.csproj +++ b/samples/AspNetIntegration/AspNetIntegration.csproj @@ -1,6 +1,6 @@  - net7.0 + net8.0 v4 Exe enable @@ -8,7 +8,7 @@ - + diff --git a/samples/AspNetIntegration/RoutingMiddleware.cs b/samples/AspNetIntegration/RoutingMiddleware.cs index 70c56c7b2..e0abd8007 100644 --- a/samples/AspNetIntegration/RoutingMiddleware.cs +++ b/samples/AspNetIntegration/RoutingMiddleware.cs @@ -22,7 +22,7 @@ public Task Invoke(FunctionContext context, FunctionExecutionDelegate next) { string? displayName = endpoint.DisplayName; string? routePattern = endpoint.RoutePattern.RawText; - IReadOnlyList? httpMethods = (endpoint.Metadata.Single() as HttpMethodMetadata)?.HttpMethods; + IReadOnlyList? httpMethods = (endpoint.Metadata.Single(b=>b is HttpMethodMetadata) as HttpMethodMetadata)?.HttpMethods; } // continue along with function execution diff --git a/samples/AspNetIntegration/SimpleHttpTrigger/SimpleHttpTrigger.cs b/samples/AspNetIntegration/SimpleHttpTrigger/SimpleHttpTrigger.cs index b7f25ee5a..a4cbbcd20 100644 --- a/samples/AspNetIntegration/SimpleHttpTrigger/SimpleHttpTrigger.cs +++ b/samples/AspNetIntegration/SimpleHttpTrigger/SimpleHttpTrigger.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; namespace AspNetIntegration { @@ -17,4 +18,17 @@ public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] Http } // } + + public class SimpleHttpTriggerHttpData + { + [Function("SimpleHttpTriggerHttpData")] + public async Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req) + { + var response = req.CreateResponse(); + + await response.WriteStringAsync("Welcome to Azure Functions (HttpData)"); + + return response; + } + } } diff --git a/samples/Configuration/Configuration.csproj b/samples/Configuration/Configuration.csproj index c3f8a40ab..ecd847dd6 100644 --- a/samples/Configuration/Configuration.csproj +++ b/samples/Configuration/Configuration.csproj @@ -1,14 +1,14 @@  - net7.0 + net8.0 v4 Exe - - - + + + diff --git a/samples/CustomMiddleware/CustomMiddleware.csproj b/samples/CustomMiddleware/CustomMiddleware.csproj index 074690b62..9d24472e8 100644 --- a/samples/CustomMiddleware/CustomMiddleware.csproj +++ b/samples/CustomMiddleware/CustomMiddleware.csproj @@ -1,7 +1,7 @@  false - net7.0 + net8.0 latest v4 Exe @@ -13,10 +13,10 @@ - - + + - + diff --git a/samples/EntityFramework/EntityFramework.csproj b/samples/EntityFramework/EntityFramework.csproj index ce669b25f..da9d2668f 100644 --- a/samples/EntityFramework/EntityFramework.csproj +++ b/samples/EntityFramework/EntityFramework.csproj @@ -1,19 +1,19 @@  - net7.0 + net8.0 v4 Exe - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/samples/Extensions/Extensions.csproj b/samples/Extensions/Extensions.csproj index 0d66a0883..812770e69 100644 --- a/samples/Extensions/Extensions.csproj +++ b/samples/Extensions/Extensions.csproj @@ -1,26 +1,26 @@  - net7.0 + net8.0 v4 Exe <_FunctionsSkipCleanOutput>true - + - + - + @@ -32,4 +32,9 @@ Never + + + + + \ No newline at end of file diff --git a/samples/Extensions/ServiceBus/ServiceBusReceivedMessageFunctions.cs b/samples/Extensions/ServiceBus/ServiceBusReceivedMessageFunctions.cs index 838ee63b3..fad50fecb 100644 --- a/samples/Extensions/ServiceBus/ServiceBusReceivedMessageFunctions.cs +++ b/samples/Extensions/ServiceBus/ServiceBusReceivedMessageFunctions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; @@ -74,5 +75,20 @@ public void ServiceBusReceivedMessageWithStringProperties( _logger.LogInformation("Delivery Count: {count}", message.DeliveryCount); _logger.LogInformation("Delivery Count: {count}", deliveryCount); } + // + [Function(nameof(ServiceBusMessageActionsFunction))] + public async Task ServiceBusMessageActionsFunction( + [ServiceBusTrigger("queue", Connection = "ServiceBusConnection")] + ServiceBusReceivedMessage message, + ServiceBusMessageActions messageActions) + { + _logger.LogInformation("Message ID: {id}", message.MessageId); + _logger.LogInformation("Message Body: {body}", message.Body); + _logger.LogInformation("Message Content-Type: {contentType}", message.ContentType); + + // Complete the message + await messageActions.CompleteMessageAsync(message); + } + // } } diff --git a/samples/Extensions/local.settings.json b/samples/Extensions/local.settings.json index 3a4a9217a..5241ee372 100644 --- a/samples/Extensions/local.settings.json +++ b/samples/Extensions/local.settings.json @@ -3,7 +3,7 @@ "Values": { "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "AzureWebJobsStorage": "", - "CosmosConnection": "", + "CosmosDBConnection": "", "CosmosDb": "ItemDb", "CosmosContainerIn": "ItemContainerIn", "CosmosContainerOut": "ItemContainerOut", diff --git a/samples/FunctionApp/FunctionApp.csproj b/samples/FunctionApp/FunctionApp.csproj index 1c378e023..74f5f40d5 100644 --- a/samples/FunctionApp/FunctionApp.csproj +++ b/samples/FunctionApp/FunctionApp.csproj @@ -1,7 +1,7 @@  false - net7.0 + net8.0 v4 Exe <_FunctionsSkipCleanOutput>true @@ -14,7 +14,7 @@ - + @@ -35,10 +35,10 @@ - - - + diff --git a/sdk/Sdk.Generators/Constants.cs b/sdk/Sdk.Generators/Constants.cs index c64b01633..68a25a127 100644 --- a/sdk/Sdk.Generators/Constants.cs +++ b/sdk/Sdk.Generators/Constants.cs @@ -12,6 +12,7 @@ internal static class Languages internal static class BuildProperties { + internal const string MSBuildTargetFrameworkIdentifier = "build_property.TargetFrameworkIdentifier"; internal const string MSBuildRootNamespace = "build_property.RootNamespace"; internal const string GeneratedCodeNamespace = "build_property.FunctionsGeneratedCodeNamespace"; internal const string EnableSourceGen = "build_property.FunctionsEnableMetadataSourceGen"; diff --git a/sdk/Sdk.Generators/ExtensionStartupRunnerGenerator.cs b/sdk/Sdk.Generators/ExtensionStartupRunnerGenerator.cs index 48754e943..163627c79 100644 --- a/sdk/Sdk.Generators/ExtensionStartupRunnerGenerator.cs +++ b/sdk/Sdk.Generators/ExtensionStartupRunnerGenerator.cs @@ -96,8 +96,14 @@ internal string GenerateExtensionStartupRunner(GeneratorExecutionContext context namespace {{namespaceValue}} { + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class WorkerExtensionStartupCodeExecutor : WorkerExtensionStartup { + /// + /// Configures the worker to register extension startup services. + /// + /// The to configure. public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder) { {{startupCodeExecutor}} diff --git a/sdk/Sdk.Generators/Extensions/IMethodSymbolExtensions.cs b/sdk/Sdk.Generators/Extensions/IMethodSymbolExtensions.cs new file mode 100644 index 000000000..f85aae1a7 --- /dev/null +++ b/sdk/Sdk.Generators/Extensions/IMethodSymbolExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Azure.Functions.Worker.Sdk.Generators +{ + internal static class IMethodSymbolExtensions + { + /// + /// Determines the visibility of an azure function method. + /// The visibility is determined by the following rules: + /// 1. If the method is public, and all containing types are public, return Public + /// 2. If the method is public, but one or more containing types are not public, return PublicButContainingTypeNotVisible + /// 3. If the method is not public, return NotPublic + /// + /// The instance representing an azure function method. + /// + internal static FunctionMethodVisibility GetVisibility(this IMethodSymbol methodSymbol) + { + // Check if the symbol itself is public + if (methodSymbol.DeclaredAccessibility == Accessibility.Public) + { + // Check if any containing type is not public + INamedTypeSymbol containingType = methodSymbol.ContainingType; + while (containingType != null) + { + if (containingType.DeclaredAccessibility != Accessibility.Public) + { + return FunctionMethodVisibility.PublicButContainingTypeNotVisible; + } + containingType = containingType.ContainingType; + } + + // If both the symbol and all containing types are public, return PublicAndVisible + return FunctionMethodVisibility.Public; + } + + // If the symbol itself is not public, return NotPublic + return FunctionMethodVisibility.NotPublic; + } + } +} diff --git a/sdk/Sdk.Generators/Extensions/StringExtensions.cs b/sdk/Sdk.Generators/Extensions/StringExtensions.cs index c028242dd..838053830 100644 --- a/sdk/Sdk.Generators/Extensions/StringExtensions.cs +++ b/sdk/Sdk.Generators/Extensions/StringExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Microsoft.Azure.Functions.Worker.Sdk.Generators { @@ -29,5 +30,20 @@ public static string TrimStringsFromEnd(this string str, IReadOnlyList s return result; } + + /// + /// Returns a copy of the string where the first character is in lower case. + /// + public static string ToLowerFirstCharacter(this string str) + { + if (!string.IsNullOrEmpty(str)) + { + return Char.ToLowerInvariant(str[0]) + str.Substring(1); + } + else + { + return str; + } + } } } diff --git a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Emitter.cs b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Emitter.cs index abf0e51bf..c7488061a 100644 --- a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Emitter.cs +++ b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Emitter.cs @@ -13,8 +13,12 @@ public partial class FunctionExecutorGenerator { internal static class Emitter { - internal static string Emit(GeneratorExecutionContext context, IEnumerable functions, bool includeAutoRegistrationCode) + private const string WorkerCoreAssemblyName = "Microsoft.Azure.Functions.Worker.Core"; + + internal static string Emit(GeneratorExecutionContext context, IEnumerable executableFunctions, bool includeAutoRegistrationCode) { + var functions = executableFunctions.ToList(); + var defaultExecutorNeeded = functions.Any(f => f.Visibility == FunctionMethodVisibility.PublicButContainingTypeNotVisible); string result = $$""" // @@ -28,20 +32,27 @@ internal static string Emit(GeneratorExecutionContext context, IEnumerable _defaultExecutor;" : string.Empty)}} {{GetTypesDictionary(functions)}} public DirectFunctionExecutor(IFunctionActivator functionActivator) { _functionActivator = functionActivator ?? throw new ArgumentNullException(nameof(functionActivator)); } + /// public async ValueTask ExecuteAsync(FunctionContext context) { - {{GetMethodBody(functions)}} - } + {{GetMethodBody(functions, defaultExecutorNeeded)}} + }{{(defaultExecutorNeeded ? $"{Environment.NewLine}{EmitCreateDefaultExecutorMethod(context)}" : string.Empty)}} } + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class FunctionExecutorHostBuilderExtensions { /// @@ -61,20 +72,40 @@ public static IHostBuilder ConfigureGeneratedFunctionExecutor(this IHostBuilder return result; } + private static string EmitCreateDefaultExecutorMethod(GeneratorExecutionContext context) + { + var workerCoreAssembly = context.Compilation.SourceModule.ReferencedAssemblySymbols.Single(a => a.Name == WorkerCoreAssemblyName); + var assemblyIdentity = workerCoreAssembly.Identity; + + return $$""" + + private IFunctionExecutor CreateDefaultExecutorInstance(FunctionContext context) + { + var defaultExecutorFullName = "Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor, {{assemblyIdentity}}"; + var defaultExecutorType = Type.GetType(defaultExecutorFullName); + + return ActivatorUtilities.CreateInstance(context.InstanceServices, defaultExecutorType) as IFunctionExecutor; + } + """; + } + private static string GetTypesDictionary(IEnumerable functions) { - var classNames = functions.Where(f => !f.IsStatic).Select(f => f.ParentFunctionClassName).Distinct(); - if (!classNames.Any()) - { - return """ + // Build a dictionary of type names and its full qualified names (including assembly identity) + var typesDict = functions + .Where(f => !f.IsStatic) + .GroupBy(f => f.ParentFunctionClassName) + .ToDictionary(k => k.First().ParentFunctionClassName, v => v.First().AssemblyIdentity); - """; + if (typesDict.Count == 0) + { + return ""; } return $$""" - private readonly Dictionary types = new() + private readonly Dictionary types = new Dictionary() { - {{string.Join($",{Environment.NewLine} ", classNames.Select(c => $$""" { "{{c}}", Type.GetType("{{c}}")! }"""))}} + {{string.Join($",{Environment.NewLine} ", typesDict.Select(c => $$""" { "{{c.Key}}", Type.GetType("{{c.Key}}, {{c.Value}}") }"""))}} }; """; @@ -86,8 +117,16 @@ private static string GetAutoConfigureStartupClass(bool includeAutoRegistrationC { string result = $$""" + /// + /// Auto startup class to register the custom implementation generated for the current worker. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] public class FunctionExecutorAutoStartup : IAutoConfigureStartup { + /// + /// Configures the to use the custom implementation generated for the current worker. + /// + /// The instance to use for service registration. public void Configure(IHostBuilder hostBuilder) { hostBuilder.ConfigureGeneratedFunctionExecutor(); @@ -100,62 +139,84 @@ public void Configure(IHostBuilder hostBuilder) return ""; } - private static string GetMethodBody(IEnumerable functions) + private static string GetMethodBody(IEnumerable functions, bool anyDefaultExecutor) { var sb = new StringBuilder(); sb.Append( - """ - var inputBindingFeature = context.Features.Get()!; - var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context)!; + $$""" + var inputBindingFeature = context.Features.Get(); + var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context); var inputArguments = inputBindingResult.Values; - + {{(anyDefaultExecutor ? $" _defaultExecutor = new Lazy(() => CreateDefaultExecutorInstance(context));{Environment.NewLine}" : string.Empty)}} """); + + bool first = true; + foreach (ExecutableFunction function in functions) { + var fast = function.Visibility == FunctionMethodVisibility.Public; sb.Append($$""" - if (string.Equals(context.FunctionDefinition.EntryPoint, "{{function.EntryPoint}}", StringComparison.OrdinalIgnoreCase)) + {{(first ? string.Empty : "else ")}}if (string.Equals(context.FunctionDefinition.EntryPoint, "{{function.EntryPoint}}", StringComparison.Ordinal)) { + {{(fast ? EmitFastPath(function) : EmitSlowPath())}} + } """); + first = false; + } - int functionParamCounter = 0; - var functionParamList = new List(); - foreach (var argumentTypeName in function.ParameterTypeNames) - { - functionParamList.Add($"({argumentTypeName})inputArguments[{functionParamCounter++}]"); - } - var methodParamsStr = string.Join(", ", functionParamList); + return sb.ToString(); + } - if (!function.IsStatic) - { - sb.Append($$""" + private static string EmitFastPath(ExecutableFunction function) + { + var sb = new StringBuilder(); + int functionParamCounter = 0; + var functionParamList = new List(); + foreach (var argumentTypeName in function.ParameterTypeNames) + { + functionParamList.Add($"({argumentTypeName})inputArguments[{functionParamCounter++}]"); + } + var methodParamsStr = string.Join(", ", functionParamList); - var instanceType = types["{{function.ParentFunctionClassName}}"]; - var i = _functionActivator.CreateInstance(instanceType, context) as {{function.ParentFunctionClassName}}; + if (!function.IsStatic) + { + sb.Append($$""" + var instanceType = types["{{function.ParentFunctionClassName}}"]; + var i = _functionActivator.CreateInstance(instanceType, context) as {{function.ParentFunctionFullyQualifiedClassName}}; """); - } + } + if (!function.IsStatic) + { sb.Append(@" "); + } + else + { + 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}); - }}"); + if (function.IsReturnValueAssignable) + { + sb.Append("context.GetInvocationResult().Value = "); + } + if (function.ShouldAwait) + { + sb.Append("await "); } + sb.Append(function.IsStatic + ? $"{function.ParentFunctionFullyQualifiedClassName}.{function.MethodName}({methodParamsStr});" + : $"i.{function.MethodName}({methodParamsStr});"); return sb.ToString(); } + + private static string EmitSlowPath() + { + return + " await _defaultExecutor.Value.ExecuteAsync(context);"; + } } } } diff --git a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.ExecutableFunction.cs b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.ExecutableFunction.cs index e3467240d..2dbcfe7b6 100644 --- a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.ExecutableFunction.cs +++ b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.ExecutableFunction.cs @@ -37,14 +37,31 @@ internal class ExecutableFunction internal string EntryPoint { get; set; } = null!; /// - /// Fully qualified type name of the parent class. + /// Type name of the parent class in default symbol format. /// Ex: MyNamespace.MyClass /// internal string ParentFunctionClassName { get; set; } = null!; + /// + /// Fully qualified type name of the parent class. + /// Ex: global::MyNamespace.MyClass + /// + internal string ParentFunctionFullyQualifiedClassName { get; set; } = null!; + /// /// A collection of fully qualified type names of the parameters of the function. /// internal IEnumerable ParameterTypeNames { set; get; } = Enumerable.Empty(); + + /// + /// Get a value indicating the visibility of the executable function. + /// + internal FunctionMethodVisibility Visibility { get; set; } + + /// + /// Gets the assembly identity of the function. + /// ex: FooAssembly, Version=1.2.3.4, Culture=neutral, PublicKeyToken=9475d07f10cb09df + /// + internal string AssemblyIdentity { get; set; } = null!; } } diff --git a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Parser.cs b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Parser.cs index ae160a342..fd6c570fc 100644 --- a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Parser.cs +++ b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Parser.cs @@ -2,9 +2,8 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.Azure.Functions.Worker.Sdk.Generators { @@ -23,46 +22,38 @@ internal Parser(GeneratorExecutionContext context) private Compilation Compilation => _context.Compilation; - internal ICollection GetFunctions(List methods) + internal ICollection GetFunctions(IEnumerable methods) { var functionList = new List(); - foreach (MethodDeclarationSyntax method in methods) + foreach (IMethodSymbol method in methods.Where(m=>m.DeclaredAccessibility == Accessibility.Public)) { _context.CancellationToken.ThrowIfCancellationRequested(); - var model = Compilation.GetSemanticModel(method.SyntaxTree); - if (!FunctionsUtil.IsValidFunctionMethod(_context, Compilation, model, method)) - { - continue; - } + var methodName = method.Name; + var methodParameterList = new List(); - var methodName = method.Identifier.Text; - var methodParameterList = new List(method.ParameterList.Parameters.Count); - - foreach (var methodParam in method.ParameterList.Parameters) + foreach (IParameterSymbol parameterSymbol in method.Parameters) { - if (model.GetDeclaredSymbol(methodParam) is not IParameterSymbol parameterSymbol) - { - continue; - } - var fullyQualifiedTypeName = parameterSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); methodParameterList.Add(fullyQualifiedTypeName); } - var methodSymbol = model.GetDeclaredSymbol(method)!; - var fullyQualifiedClassName = methodSymbol.ContainingSymbol.ToDisplayString(); + var defaultFormatClassName = method.ContainingSymbol.ToDisplayString(); + var fullyQualifiedClassName = method.ContainingSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var function = new ExecutableFunction { - EntryPoint = $"{fullyQualifiedClassName}.{method.Identifier.ValueText}", + EntryPoint = $"{defaultFormatClassName}.{method.Name}", ParameterTypeNames = methodParameterList, MethodName = methodName, - ShouldAwait = IsTaskType(methodSymbol.ReturnType), - IsReturnValueAssignable = IsReturnValueAssignable(methodSymbol), - IsStatic = method.Modifiers.Any(SyntaxKind.StaticKeyword), - ParentFunctionClassName = fullyQualifiedClassName + ShouldAwait = IsTaskType(method.ReturnType), + IsReturnValueAssignable = IsReturnValueAssignable(method), + IsStatic = method.IsStatic, + ParentFunctionClassName = defaultFormatClassName, + ParentFunctionFullyQualifiedClassName = fullyQualifiedClassName, + Visibility = method.GetVisibility(), + AssemblyIdentity = method.ContainingAssembly.Identity.GetDisplayName(), }; functionList.Add(function); diff --git a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.cs b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.cs index 4bbea665f..1f239e3b5 100644 --- a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.cs +++ b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.cs @@ -1,8 +1,11 @@ // 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using FunctionMethodSyntaxReceiver = Microsoft.Azure.Functions.Worker.Sdk.Generators.FunctionMetadataProviderGenerator.FunctionMethodSyntaxReceiver; @@ -23,25 +26,53 @@ public void Execute(GeneratorExecutionContext context) return; } - if (context.SyntaxReceiver is not FunctionMethodSyntaxReceiver receiver || receiver.CandidateMethods.Count == 0) + if (context.SyntaxReceiver is not FunctionMethodSyntaxReceiver receiver) { return; } - var parser = new Parser(context); - var functions = parser.GetFunctions(receiver.CandidateMethods); + var entryAssemblyFuncs = GetSymbolsMethodSyntaxes(receiver.CandidateMethods, context); + var dependentFuncs = GetDependentAssemblyFunctionsSymbols(context); + var allMethods = entryAssemblyFuncs.Concat(dependentFuncs); - if (functions.Count == 0) + if (!allMethods.Any()) { return; } + var parser = new Parser(context); + var functions = parser.GetFunctions(allMethods); var shouldIncludeAutoGeneratedAttributes = ShouldIncludeAutoGeneratedAttributes(context); var text = Emitter.Emit(context, functions, shouldIncludeAutoGeneratedAttributes); context.AddSource(Constants.FileNames.GeneratedFunctionExecutor, SourceText.From(text, Encoding.UTF8)); } + private IEnumerable GetSymbolsMethodSyntaxes(List methods, GeneratorExecutionContext context) + { + foreach (MethodDeclarationSyntax method in methods) + { + var model = context.Compilation.GetSemanticModel(method.SyntaxTree); + + if (FunctionsUtil.IsValidFunctionMethod(context, context.Compilation, model, method)) + { + IMethodSymbol? methodSymbol = (IMethodSymbol)model.GetDeclaredSymbol(method)!; + yield return methodSymbol; + } + } + } + + /// + /// Collect methods with Function attributes on them from dependent/referenced assemblies. + /// + private static IEnumerable GetDependentAssemblyFunctionsSymbols(GeneratorExecutionContext context) + { + var visitor = new ReferencedAssemblyMethodVisitor(context.Compilation); + visitor.Visit(context.Compilation.SourceModule); + + return visitor.FunctionMethods; + } + private static bool ShouldIncludeAutoGeneratedAttributes(GeneratorExecutionContext context) { if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Emitter.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Emitter.cs index 63016583a..7619ba452 100644 --- a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Emitter.cs +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Emitter.cs @@ -34,11 +34,17 @@ public string Emit(GeneratorExecutionContext context, IReadOnlyList + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); @@ -46,7 +52,10 @@ public Task> GetFunctionMetadataAsync(string d return Task.FromResult(metadataList.ToImmutableArray()); } } - + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -72,8 +81,16 @@ private static string GetAutoConfigureStartupClass(bool includeAutoRegistrationC { string result = $$""" + /// + /// Auto startup class to register the custom implementation generated for the current worker. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] public class FunctionMetadataProviderAutoStartup : IAutoConfigureStartup { + /// + /// Configures the to use the custom implementation generated for the current worker. + /// + /// The instance to use for service registration. public void Configure(IHostBuilder hostBuilder) { hostBuilder.ConfigureGeneratedFunctionMetadataProvider(); diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs index 5d4ce348a..f8c01a50a 100644 --- a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs @@ -8,8 +8,6 @@ using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.Azure.Functions.Worker.Sdk.Generators { @@ -42,23 +40,23 @@ public Parser(GeneratorExecutionContext context) /// Takes in candidate methods from the user compilation and parses them to return function metadata info as GeneratorFunctionMetadata. /// /// List of candidate methods from the syntax receiver. - public IReadOnlyList GetFunctionMetadataInfo(List methods) + /// An instance of . Optional. + public IReadOnlyList GetFunctionMetadataInfo(List methods, FunctionsMetadataParsingContext? parsingContext = null) { var result = ImmutableArray.CreateBuilder(); - // Loop through the candidate methods (methods with any attribute associated with them) - foreach (IMethodSymbol method in methods) + // Loop through the candidate methods (methods with any attribute associated with them) which are public. + foreach (IMethodSymbol method in methods.Where(m => m.DeclaredAccessibility == Accessibility.Public)) { CancellationToken.ThrowIfCancellationRequested(); - string? funcName = null; - if (!FunctionsUtil.TryGetFunctionName(method, Compilation, out funcName)) + if (!FunctionsUtil.TryGetFunctionName(method, Compilation, out var funcName)) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, Location.None, method.Name)); // would only reach here if the function attribute or method was not loaded, resulting in failure to retrieve name } var assemblyName = method.ContainingAssembly.Name; - var scriptFile = Path.Combine(assemblyName + ".dll"); + var scriptFile = $"{assemblyName}{parsingContext?.ScriptFileExtension ?? ".dll"}"; var newFunction = new GeneratorFunctionMetadata { @@ -96,9 +94,9 @@ private bool TryGetBindings(IMethodSymbol method, out IList>? methodOutputBindings) + if (!TryGetMethodOutputBinding(method, out bool hasMethodOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? methodOutputBindings) || !TryGetParameterInputAndTriggerBindings(method, out bool supportsRetryOptions, out hasHttpTrigger, out IList>? parameterInputAndTriggerBindings) - || !TryGetReturnTypeBindings(method, hasHttpTrigger, hasOutputBinding, out IList>? returnTypeBindings)) + || !TryGetReturnTypeBindings(method, hasHttpTrigger, hasMethodOutputBinding, out IList>? returnTypeBindings)) { bindings = null; return false; @@ -131,12 +129,12 @@ private bool TryGetBindings(IMethodSymbol method, out IList /// Checks for and returns any OutputBinding attributes associated with the method. /// - private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? bindingsList) + private bool TryGetMethodOutputBinding(IMethodSymbol method, out bool hasMethodOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? bindingsList) { var attributes = method!.GetAttributes(); // methodSymbol is not null here because it's checked in IsValidAzureFunction which is called before bindings are collected/created AttributeData? outputBindingAttribute = null; - hasOutputBinding = false; + hasMethodOutputBinding = false; retryOptions = null; foreach (var attribute in attributes) @@ -151,8 +149,8 @@ private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasOutputBi if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass?.BaseType, _knownFunctionMetadataTypes.OutputBindingAttribute)) { - // There can only be one output binding associated with a function. If there is more than one, we return a diagnostic error here. - if (hasOutputBinding) + // There can only be one method output binding associated with a function. If there is more than one, we return a diagnostic error here. + if (hasMethodOutputBinding) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, Location.None, new object[] { "Method", method.Name })); bindingsList = null; @@ -160,7 +158,7 @@ private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasOutputBi } outputBindingAttribute = attribute; - hasOutputBinding = true; + hasMethodOutputBinding = true; } } @@ -402,7 +400,7 @@ private bool DoesConverterSupportTargetType(List converterAdverti /// /// Checks for and returns any bindings found in the Return Type of the method /// - private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, bool hasOutputBinding, out IList>? bindingsList) + private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, bool hasMethodOutputBinding, out IList>? bindingsList) { ITypeSymbol? returnTypeSymbol = method.ReturnType; bindingsList = new List>(); @@ -442,7 +440,7 @@ private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, } else { - if (!TryGetReturnTypePropertyBindings(returnTypeSymbol, hasHttpTrigger, hasOutputBinding, out bindingsList)) + if (!TryGetReturnTypePropertyBindings(returnTypeSymbol, hasHttpTrigger, hasMethodOutputBinding, out bindingsList)) { bindingsList = null; return false; @@ -453,10 +451,11 @@ private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, return true; } - private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool hasHttpTrigger, bool hasOutputBinding, out IList>? bindingsList) + private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool hasHttpTrigger, bool hasMethodOutputBinding, out IList>? bindingsList) { var members = returnTypeSymbol.GetMembers(); var foundHttpOutput = false; + var returnTypeHasOutputBindings = false; bindingsList = new List>(); // initialize this without size, because it will be difficult to predict how many bindings we can find here in the user code. foreach (var prop in returnTypeSymbol.GetMembers().Where(a => a is IPropertySymbol)) @@ -505,23 +504,16 @@ private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool bindingsList.Add(bindingDict!); - hasOutputBinding = true; + returnTypeHasOutputBindings = true; foundPropertyOutputAttr = true; } } } } - if (hasHttpTrigger && !foundHttpOutput) + if (hasHttpTrigger && !foundHttpOutput && !hasMethodOutputBinding && !returnTypeHasOutputBindings) { - if (!hasOutputBinding) - { - bindingsList.Add(GetHttpReturnBinding(Constants.FunctionMetadataBindingProps.ReturnBindingName)); - } - else - { - bindingsList.Add(GetHttpReturnBinding(Constants.FunctionMetadataBindingProps.HttpResponseBindingName)); - } + bindingsList.Add(GetHttpReturnBinding(Constants.FunctionMetadataBindingProps.ReturnBindingName)); } return true; @@ -552,7 +544,7 @@ private bool TryCreateBindingDict(AttributeData bindingAttrData, string bindingN string attributeName = bindingAttrData.AttributeClass!.Name; // properly format binding types by removing "Attribute" and "Input" descriptors - string bindingType = attributeName.TrimStringsFromEnd(_functionsStringNamesToRemove); + string bindingType = attributeName.TrimStringsFromEnd(_functionsStringNamesToRemove).ToLowerFirstCharacter(); // Set binding direction string bindingDirection = SymbolEqualityComparer.Default.Equals(bindingAttrData.AttributeClass?.BaseType, _knownFunctionMetadataTypes.OutputBindingAttribute) ? "Out" : "In"; diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.cs index 665bf2f44..11736adf8 100644 --- a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.cs +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.cs @@ -1,6 +1,7 @@ // 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 System.Collections.Generic; using System.Linq; using System.Text; @@ -18,6 +19,10 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators [Generator] public partial class FunctionMetadataProviderGenerator : ISourceGenerator { + private const string NetFxTargetFrameworkIdentifierValue = ".NETFramework"; + private const string ExecutableFileExtension = ".exe"; + private const string DynamicLinkLibraryFileExtension = ".dll"; + public void Execute(GeneratorExecutionContext context) { if (context.SyntaxReceiver is not FunctionMethodSyntaxReceiver receiver || receiver.CandidateMethods.Count == 0) @@ -37,10 +42,17 @@ public void Execute(GeneratorExecutionContext context) // attempt to parse user compilation var p = new Parser(context); - var entryAssemblyFuncs = GetEntryAssemblyFunctions(receiver.CandidateMethods, context); - var dependentFuncs = GetDependentAssemblyFunctions(context); + var entryAssemblyFunctionSymbols = GetEntryAssemblyFunctions(receiver.CandidateMethods, context); + var dependentAssemblyFunctionSymbols = GetDependentAssemblyFunctions(context); + + var entryAssemblyParsingContext = new FunctionsMetadataParsingContext + { + ScriptFileExtension = GetScriptFileExtensionForEntryPointAssemblyFunctions(context) + }; + var entryAssemblyFunctions = p.GetFunctionMetadataInfo(entryAssemblyFunctionSymbols.ToList(), entryAssemblyParsingContext); + var dependentAssemblyFunctions = p.GetFunctionMetadataInfo(dependentAssemblyFunctionSymbols.ToList()); - IReadOnlyList functionMetadataInfo = p.GetFunctionMetadataInfo(entryAssemblyFuncs.Concat(dependentFuncs).ToList()); + IReadOnlyList functionMetadataInfo = entryAssemblyFunctions.Concat(dependentAssemblyFunctions).ToList(); // Proceed to generate the file if function metadata info was successfully returned if (functionMetadataInfo.Count > 0) @@ -63,6 +75,14 @@ public void Initialize(GeneratorInitializationContext context) context.RegisterForSyntaxNotifications(() => new FunctionMethodSyntaxReceiver()); } + // For dependent assemblies, it will be always "dll". We only need to find out for entry point assembly. + private static string GetScriptFileExtensionForEntryPointAssemblyFunctions(GeneratorExecutionContext context) + { + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(Constants.BuildProperties.MSBuildTargetFrameworkIdentifier, out var value); + + return string.Equals(value, NetFxTargetFrameworkIdentifierValue, StringComparison.OrdinalIgnoreCase) ? ExecutableFileExtension : DynamicLinkLibraryFileExtension; + } + private static bool ShouldIncludeAutoGeneratedAttributes(GeneratorExecutionContext context) { if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( @@ -93,34 +113,10 @@ private IEnumerable GetEntryAssemblyFunctions(List private IEnumerable GetDependentAssemblyFunctions(GeneratorExecutionContext context) { - foreach (var assembly in context.Compilation.SourceModule.ReferencedAssemblySymbols) - { - var namespaceSymbols = assembly.GlobalNamespace.GetMembers(); + var visitor = new ReferencedAssemblyMethodVisitor(context.Compilation); + visitor.Visit(context.Compilation.SourceModule); - foreach (var namespaceSymbol in namespaceSymbols) - { - var namespaceMembers = namespaceSymbol.GetMembers(); - - foreach (var m in namespaceMembers) - { - if (m is INamedTypeSymbol namedType) - { - var typeMembers = namedType.GetMembers(); - - foreach (var typeMember in typeMembers) - { - if (typeMember is IMethodSymbol method) - { - if (FunctionsUtil.IsFunctionSymbol(method, context.Compilation)) - { - yield return method; - } - } - } - } - } - } - } + return visitor.FunctionMethods; } } } diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionsMetadataParsingContext.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionsMetadataParsingContext.cs new file mode 100644 index 000000000..93fcbeb6b --- /dev/null +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionsMetadataParsingContext.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information.; + +namespace Microsoft.Azure.Functions.Worker.Sdk.Generators +{ + internal sealed class FunctionsMetadataParsingContext + { + internal string? ScriptFileExtension { get; set; } + } +} diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/ReferencedAssemblyMethodVisitor.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/ReferencedAssemblyMethodVisitor.cs new file mode 100644 index 000000000..97c288c1e --- /dev/null +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/ReferencedAssemblyMethodVisitor.cs @@ -0,0 +1,75 @@ +// 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 System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Azure.Functions.Worker.Sdk.Generators +{ + /// + /// Visits all symbols from referenced assemblies and returns all methods which are valid Azure Functions. + /// + internal sealed class ReferencedAssemblyMethodVisitor : SymbolVisitor + { + private readonly Compilation _compilation; + + /// + /// Gets all methods which are valid Azure Functions. + /// + internal readonly List FunctionMethods = new(); + + internal ReferencedAssemblyMethodVisitor(Compilation compilation) + { + _compilation = compilation ?? throw new ArgumentNullException(nameof(compilation)); + } + + public override void VisitModule(IModuleSymbol moduleSymbol) + { + foreach (var assemblySymbol in moduleSymbol.ReferencedAssemblySymbols) + { + assemblySymbol.Accept(this); + } + } + + public override void VisitAssembly(IAssemblySymbol symbol) + { + var namespaceSymbol = symbol.GlobalNamespace; + namespaceSymbol.Accept(this); + } + + public override void VisitNamespace(INamespaceSymbol symbol) + { + // Get classes in this namespace or child namespaces + var classesOrNamespaces = symbol.GetMembers() + .Where(a => a.Kind is SymbolKind.Namespace or SymbolKind.NamedType); + + foreach (var childSymbol in classesOrNamespaces) + { + childSymbol.Accept(this); + } + } + + public override void VisitNamedType(INamedTypeSymbol symbol) + { + // Get methods in this class or nested child classes + var methodsOrClasses = symbol.GetMembers() + .Where(a => a.Kind is SymbolKind.NamedType or SymbolKind.Method); + + foreach (var childSymbol in methodsOrClasses) + { + childSymbol.Accept(this); + } + } + + public override void VisitMethod(IMethodSymbol methodSymbol) + { + if (methodSymbol.MethodKind == MethodKind.Ordinary && + FunctionsUtil.IsFunctionSymbol(methodSymbol, _compilation)) + { + FunctionMethods.Add(methodSymbol); + } + } + } +} diff --git a/sdk/Sdk.Generators/FunctionMethodVisibility.cs b/sdk/Sdk.Generators/FunctionMethodVisibility.cs new file mode 100644 index 000000000..558ce5054 --- /dev/null +++ b/sdk/Sdk.Generators/FunctionMethodVisibility.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.Azure.Functions.Worker.Sdk.Generators +{ + /// + /// Represents the visibility of an "azure function" method and its parent classes. + /// + internal enum FunctionMethodVisibility + { + /// + /// The method and it's parent classes are public & visible. + /// + Public, + + /// + /// The method is public, but one or more of its parent classes are not public. + /// + PublicButContainingTypeNotVisible, + + /// + /// The method is not public. + /// + NotPublic + } +} diff --git a/sdk/Sdk.Generators/FunctionsUtil.cs b/sdk/Sdk.Generators/FunctionsUtil.cs index 7f5046aed..0ba73d454 100644 --- a/sdk/Sdk.Generators/FunctionsUtil.cs +++ b/sdk/Sdk.Generators/FunctionsUtil.cs @@ -80,17 +80,12 @@ internal static string GetFullyQualifiedMethodName(IMethodSymbol method) /// internal static string GetNamespaceForGeneratedCode(GeneratorExecutionContext context) { - // If csproj has the msbuild property specified, use it's value. - if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(Constants.BuildProperties.GeneratedCodeNamespace, out var namespaceValue) - && !string.IsNullOrWhiteSpace(namespaceValue)) - { - return namespaceValue; - } + // If user has not provided a custom namespace explicitly, + // our msbuild target will set the RootNamespace msbuild property value as the value of this property. + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(Constants.BuildProperties.GeneratedCodeNamespace, out var namespaceValue); - // Get the "RootNamespace" msbuild property value.(This gets populated in Microsoft.NET.Sdk.props and can be overridden by user in their function app) - context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(Constants.BuildProperties.MSBuildRootNamespace, out var rootNamespaceValue); + return namespaceValue!; - return rootNamespaceValue!; } } } diff --git a/sdk/Sdk.Generators/Sdk.Generators.csproj b/sdk/Sdk.Generators/Sdk.Generators.csproj index fef28b16e..cbe6ac94e 100644 --- a/sdk/Sdk.Generators/Sdk.Generators.csproj +++ b/sdk/Sdk.Generators/Sdk.Generators.csproj @@ -10,7 +10,7 @@ false true 1 - 2 + 5 true diff --git a/sdk/Sdk/Sdk.csproj b/sdk/Sdk/Sdk.csproj index 9ba0c848a..c61be03ff 100644 --- a/sdk/Sdk/Sdk.csproj +++ b/sdk/Sdk/Sdk.csproj @@ -1,9 +1,8 @@  - 16 - 0 - -preview2 + 17 + -preview1 netstandard2.0;net472 Microsoft.Azure.Functions.Worker.Sdk This package provides development time support for the Azure Functions .NET Worker. diff --git a/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.props b/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.props index d24ff04fb..d7e98339a 100644 --- a/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.props +++ b/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.props @@ -26,6 +26,7 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and + -### Microsoft.Azure.Functions.Worker.Sdk 1.16.0-preview2 +### Microsoft.Azure.Functions.Worker.Sdk 1.17.0-preview1 - Improve incremental build support for worker extension project inner build (https://github.com/Azure/azure-functions-dotnet-worker/pull/1749) - Now builds to intermediate output path @@ -13,12 +13,3 @@ - Integrate inner build with existing .NET SDK targets (https://github.com/Azure/azure-functions-dotnet-worker/pull/1861) - Targets have been refactored to participate with `CopyToOutputDirectory` and `CopyToPublishDirectory` instead of manually copying - Incremental build support further improved - -### Microsoft.Azure.Functions.Worker.Sdk.Analyzers (delete if not updated) - -- - -### Microsoft.Azure.Functions.Worker.Sdk.Generators - -- - diff --git a/src/DotNetWorker.ApplicationInsights/DotNetWorker.ApplicationInsights.csproj b/src/DotNetWorker.ApplicationInsights/DotNetWorker.ApplicationInsights.csproj index 39df83d84..68d727cab 100644 --- a/src/DotNetWorker.ApplicationInsights/DotNetWorker.ApplicationInsights.csproj +++ b/src/DotNetWorker.ApplicationInsights/DotNetWorker.ApplicationInsights.csproj @@ -6,9 +6,10 @@ Microsoft.Azure.Functions.Worker.ApplicationInsights Microsoft.Azure.Functions.Worker.ApplicationInsights 1 - 0 + 1 0 README.md + $(BeforePack);GetReleaseNotes diff --git a/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs b/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs index a80496cb3..66c9e710b 100644 --- a/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs +++ b/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs @@ -26,6 +26,11 @@ public static IServiceCollection ConfigureFunctionsApplicationInsights(this ISer throw new ArgumentNullException(nameof(services)); } + services.AddSingleton, AppServiceOptionsInitializer>(); + services.AddSingleton(); + services.AddSingleton>(p => p.GetRequiredService()); + services.AddSingleton(p => p.GetRequiredService()); + services.AddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/DotNetWorker.ApplicationInsights/Initializers/AppServiceEnvironmentVariableMonitor.cs b/src/DotNetWorker.ApplicationInsights/Initializers/AppServiceEnvironmentVariableMonitor.cs new file mode 100644 index 000000000..419ba40fe --- /dev/null +++ b/src/DotNetWorker.ApplicationInsights/Initializers/AppServiceEnvironmentVariableMonitor.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers; + +internal class AppServiceEnvironmentVariableMonitor : BackgroundService, IOptionsChangeTokenSource +{ + private readonly TimeSpan _refreshInterval; + + private IChangeToken _changeToken; + private CancellationTokenSource _cancellationTokenSource = new(); + + private readonly Dictionary _monitoredVariableCache = new(StringComparer.OrdinalIgnoreCase); + + public AppServiceEnvironmentVariableMonitor() : this(TimeSpan.FromSeconds(5)) + { + } + + public AppServiceEnvironmentVariableMonitor(TimeSpan refreshInterval) + { + _refreshInterval = refreshInterval; + _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token); + } + + public string Name => string.Empty; + + public IChangeToken GetChangeToken() => _changeToken; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + bool changeDetected = false; + + foreach (string envVar in AppServiceOptionsInitializer.EnvironmentVariablesToMonitor) + { + string? currentVal = Environment.GetEnvironmentVariable(envVar); + _monitoredVariableCache.TryGetValue(envVar, out string? cachedVal); + + if (!string.Equals(currentVal, cachedVal, StringComparison.Ordinal)) + { + changeDetected = true; + _monitoredVariableCache[envVar] = currentVal; + } + } + + if (changeDetected) + { + var oldTokenSource = Interlocked.Exchange(ref _cancellationTokenSource, new CancellationTokenSource()); + Interlocked.Exchange(ref _changeToken, new CancellationChangeToken(_cancellationTokenSource.Token)); + + if (!oldTokenSource.IsCancellationRequested) + { + oldTokenSource.Cancel(); + oldTokenSource.Dispose(); + } + } + + try + { + await Task.Delay(_refreshInterval, stoppingToken); + } + catch (OperationCanceledException) + { + // happens during normal shutdown + break; + } + } + } +} diff --git a/src/DotNetWorker.ApplicationInsights/Initializers/AppServiceOptions.cs b/src/DotNetWorker.ApplicationInsights/Initializers/AppServiceOptions.cs new file mode 100644 index 000000000..a1998f43e --- /dev/null +++ b/src/DotNetWorker.ApplicationInsights/Initializers/AppServiceOptions.cs @@ -0,0 +1,11 @@ +namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers; + +internal class AppServiceOptions +{ + public string? AzureWebsiteName { get; set; } + + public string? AzureWebsiteSlotName { get; set; } + + public string? AzureWebsiteCloudRoleName { get; set; } +} + diff --git a/src/DotNetWorker.ApplicationInsights/Initializers/AppServiceOptionsInitializer.cs b/src/DotNetWorker.ApplicationInsights/Initializers/AppServiceOptionsInitializer.cs new file mode 100644 index 000000000..005408b43 --- /dev/null +++ b/src/DotNetWorker.ApplicationInsights/Initializers/AppServiceOptionsInitializer.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers; +using Microsoft.Extensions.Options; + +internal class AppServiceOptionsInitializer : IConfigureOptions +{ + internal const string AzureWebsiteName = "WEBSITE_SITE_NAME"; + internal const string AzureWebsiteSlotName = "WEBSITE_SLOT_NAME"; + internal const string AzureWebsiteCloudRoleName = "WEBSITE_CLOUD_ROLENAME"; + internal const string DefaultProductionSlotName = "production"; + + internal static string[] EnvironmentVariablesToMonitor = new[] { AzureWebsiteName, AzureWebsiteSlotName, AzureWebsiteCloudRoleName }; + + public void Configure(AppServiceOptions options) + { + options.AzureWebsiteName = Environment.GetEnvironmentVariable(AzureWebsiteName); + options.AzureWebsiteCloudRoleName = Environment.GetEnvironmentVariable(AzureWebsiteCloudRoleName); + + // Compute the slot name by appending non-production slot to the site name (i.e. mysite-staging) + string slotName = Environment.GetEnvironmentVariable(AzureWebsiteSlotName); + options.AzureWebsiteSlotName = GetAzureWebsiteUniqueSlotName(options.AzureWebsiteName, slotName); + } + + /// + /// Gets a value that uniquely identifies the site and slot. + /// + private static string? GetAzureWebsiteUniqueSlotName(string? websiteName, string? slotName) + { + if (!string.IsNullOrEmpty(slotName) && + !string.Equals(slotName, DefaultProductionSlotName, StringComparison.OrdinalIgnoreCase)) + { + websiteName += $"-{slotName}"; + } + + return websiteName?.ToLowerInvariant(); + } +} diff --git a/src/DotNetWorker.ApplicationInsights/Initializers/FunctionsRoleEnvironmentTelmetryInitializer.cs b/src/DotNetWorker.ApplicationInsights/Initializers/FunctionsRoleEnvironmentTelmetryInitializer.cs index 91d7f02f2..90a5e7b7d 100644 --- a/src/DotNetWorker.ApplicationInsights/Initializers/FunctionsRoleEnvironmentTelmetryInitializer.cs +++ b/src/DotNetWorker.ApplicationInsights/Initializers/FunctionsRoleEnvironmentTelmetryInitializer.cs @@ -6,6 +6,7 @@ using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; +using Microsoft.Extensions.Options; namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers { @@ -17,13 +18,15 @@ namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers /// internal class FunctionsRoleEnvironmentTelemetryInitializer : ITelemetryInitializer { - internal const string AzureWebsiteName = "WEBSITE_SITE_NAME"; - internal const string AzureWebsiteSlotName = "WEBSITE_SLOT_NAME"; - internal const string AzureWebsiteCloudRoleName = "WEBSITE_CLOUD_ROLENAME"; - private const string DefaultProductionSlotName = "production"; - private const string WebAppSuffix = ".azurewebsites.net"; + internal const string WebAppSuffix = ".azurewebsites.net"; private readonly ConcurrentDictionary _siteNodeNames = new(StringComparer.OrdinalIgnoreCase); + private readonly IOptionsMonitor _appServiceOptions; + + public FunctionsRoleEnvironmentTelemetryInitializer(IOptionsMonitor appServiceOptions) + { + _appServiceOptions = appServiceOptions; + } /// /// Initializes device context. @@ -36,49 +39,27 @@ public void Initialize(ITelemetry telemetry) return; } - var siteSlotName = new Lazy(() => - { - // We cannot cache these values as the environment variables can change on the fly. - return GetAzureWebsiteUniqueSlotName(); - }); - - var websiteCloudRoleName = Environment.GetEnvironmentVariable(AzureWebsiteCloudRoleName); + var options = _appServiceOptions.CurrentValue; - if (!string.IsNullOrEmpty(websiteCloudRoleName)) + if (!string.IsNullOrEmpty(options.AzureWebsiteCloudRoleName)) { - telemetry.Context.Cloud.RoleName = websiteCloudRoleName; + telemetry.Context.Cloud.RoleName = options.AzureWebsiteCloudRoleName; } else { - telemetry.Context.Cloud.RoleName = siteSlotName.Value; + telemetry.Context.Cloud.RoleName = options.AzureWebsiteSlotName; } var internalContext = telemetry.Context.GetInternalContext(); - if (!string.IsNullOrEmpty(siteSlotName.Value)) + if (!string.IsNullOrEmpty(options.AzureWebsiteSlotName)) { - internalContext.NodeName = _siteNodeNames.GetOrAdd(siteSlotName.Value!, p => + internalContext.NodeName = _siteNodeNames.GetOrAdd(options.AzureWebsiteSlotName!, p => { // maintain previous behavior of node having the full url return p += WebAppSuffix; }); } } - - /// - /// Gets a value that uniquely identifies the site and slot. - /// - private static string? GetAzureWebsiteUniqueSlotName() - { - var name = Environment.GetEnvironmentVariable(AzureWebsiteName); - var slotName = Environment.GetEnvironmentVariable(AzureWebsiteSlotName); - - if (!string.IsNullOrEmpty(slotName) && - !string.Equals(slotName, DefaultProductionSlotName, StringComparison.OrdinalIgnoreCase)) - { - name += $"-{slotName}"; - } - - return name?.ToLowerInvariant(); - } } } + diff --git a/src/DotNetWorker.ApplicationInsights/release_notes.md b/src/DotNetWorker.ApplicationInsights/release_notes.md index 760bc777b..c5932c681 100644 --- a/src/DotNetWorker.ApplicationInsights/release_notes.md +++ b/src/DotNetWorker.ApplicationInsights/release_notes.md @@ -1,3 +1,3 @@ ## What's Changed -- GA release (no functional changes) \ No newline at end of file +- Moving hot-path environment variable checks to a background task (#1996) \ No newline at end of file diff --git a/src/DotNetWorker.Core/DotNetWorker.Core.csproj b/src/DotNetWorker.Core/DotNetWorker.Core.csproj index f3a4f7298..d8922f194 100644 --- a/src/DotNetWorker.Core/DotNetWorker.Core.csproj +++ b/src/DotNetWorker.Core/DotNetWorker.Core.csproj @@ -8,7 +8,7 @@ Microsoft.Azure.Functions.Worker.Core Microsoft.Azure.Functions.Worker.Core true - 15 + 16 0 diff --git a/src/DotNetWorker.Core/Http/HttpCookie.cs b/src/DotNetWorker.Core/Http/HttpCookie.cs index 1c411292d..2ba12cb56 100644 --- a/src/DotNetWorker.Core/Http/HttpCookie.cs +++ b/src/DotNetWorker.Core/Http/HttpCookie.cs @@ -27,7 +27,7 @@ public HttpCookie(string name, string value) public string? Domain { get; set; } /// - /// Gets or sets experation date of the cookie. An experation date sets the + /// Gets or sets expiration date of the cookie. An expiration date sets the /// cookie to expire at a specific date instead of when the client closes. /// NOTE: It is generally recommended that you use MaxAge over Expires. /// diff --git a/src/DotNetWorker.Core/Http/HttpHeadersCollection.cs b/src/DotNetWorker.Core/Http/HttpHeadersCollection.cs index 2cd8d7540..d4cc6b11a 100644 --- a/src/DotNetWorker.Core/Http/HttpHeadersCollection.cs +++ b/src/DotNetWorker.Core/Http/HttpHeadersCollection.cs @@ -10,7 +10,7 @@ namespace Microsoft.Azure.Functions.Worker.Http /// /// A collection of HTTP Headers /// - public sealed class HttpHeadersCollection : HttpHeaders + public class HttpHeadersCollection : HttpHeaders { /// /// Initializes an empty collection of HTTP Headers diff --git a/src/DotNetWorker.Core/Http/HttpRequestDataExtensions.cs b/src/DotNetWorker.Core/Http/HttpRequestDataExtensions.cs index b3fcbef08..f8b50c469 100644 --- a/src/DotNetWorker.Core/Http/HttpRequestDataExtensions.cs +++ b/src/DotNetWorker.Core/Http/HttpRequestDataExtensions.cs @@ -5,12 +5,11 @@ using System.IO; using System.Net; using System.Text; +using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.DependencyInjection; using Azure.Core.Serialization; -using System.Threading; -using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.Azure.Functions.Worker.Http { @@ -37,7 +36,7 @@ public static class HttpRequestDataExtensions return null; } - using (var reader = new StreamReader(request.Body, bufferSize: 1024, detectEncodingFromByteOrderMarks: true, encoding: encoding, leaveOpen: true)) + using (var reader = new StreamReader(request.Body, bufferSize: 1024, detectEncodingFromByteOrderMarks: true, encoding: encoding ?? Encoding.UTF8, leaveOpen: true)) { return await reader.ReadToEndAsync(); } @@ -61,7 +60,7 @@ public static class HttpRequestDataExtensions return null; } - using (var reader = new StreamReader(request.Body, bufferSize: 1024, detectEncodingFromByteOrderMarks: true, encoding: encoding, leaveOpen: true)) + using (var reader = new StreamReader(request.Body, bufferSize: 1024, detectEncodingFromByteOrderMarks: true, encoding: encoding ?? Encoding.UTF8, leaveOpen: true)) { return reader.ReadToEnd(); } @@ -122,9 +121,8 @@ public static class HttpRequestDataExtensions } return new ValueTask(result.AsTask().ContinueWith(t => TryCast(t.Result))); - } - - + } + /// /// Creates a response for the the provided . /// @@ -139,4 +137,4 @@ public static HttpResponseData CreateResponse(this HttpRequestData request, Http return response; } } -} +} diff --git a/src/DotNetWorker.Core/Http/IHttpCookie.cs b/src/DotNetWorker.Core/Http/IHttpCookie.cs index 26d0d6528..fbf611eba 100644 --- a/src/DotNetWorker.Core/Http/IHttpCookie.cs +++ b/src/DotNetWorker.Core/Http/IHttpCookie.cs @@ -16,7 +16,7 @@ public interface IHttpCookie string? Domain { get; } /// - /// Gets or sets experation date of the cookie. An experation date sets the + /// Gets or sets expiration date of the cookie. An expiration date sets the /// cookie to expire at a specific date instead of when the client closes. /// NOTE: It is generally recommended that you use MaxAge over Expires. /// diff --git a/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj b/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj index 37c590ea4..e21475cb0 100644 --- a/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj +++ b/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj @@ -8,7 +8,7 @@ Microsoft.Azure.Functions.Worker.Grpc Microsoft.Azure.Functions.Worker.Grpc true - 14 + 15 0 true diff --git a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeHost.cs b/src/DotNetWorker.Grpc/NativeHostIntegration/NativeHost.cs deleted file mode 100644 index 2b16e7d6a..000000000 --- a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeHost.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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 System.Runtime.InteropServices; - -namespace Microsoft.Azure.Functions.Worker.Grpc.NativeHostIntegration -{ - [StructLayout(LayoutKind.Sequential)] - internal struct NativeHost - { - public IntPtr pNativeApplication; - } -} diff --git a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeMethods.cs b/src/DotNetWorker.Grpc/NativeHostIntegration/NativeMethods.cs index 390d45efe..ebaeea44c 100644 --- a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeMethods.cs +++ b/src/DotNetWorker.Grpc/NativeHostIntegration/NativeMethods.cs @@ -17,38 +17,27 @@ static NativeMethods() NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, ImportResolver); } - public static NativeHost GetNativeHostData() - { - var result = get_application_properties(out var hostData); - if (result == 1) - { - return hostData; - } - - throw new InvalidOperationException($"Invalid result returned from get_application_properties: {result}"); - } - - public static void RegisterCallbacks(NativeSafeHandle nativeApplication, + public static void RegisterCallbacks( delegate* unmanaged requestCallback, IntPtr grpcHandler) { - _ = register_callbacks(nativeApplication, requestCallback, grpcHandler); + _ = register_callbacks(IntPtr.Zero, requestCallback, grpcHandler); } - public static void SendStreamingMessage(NativeSafeHandle nativeApplication, StreamingMessage streamingMessage) + public static void SendStreamingMessage(StreamingMessage streamingMessage) { byte[] bytes = streamingMessage.ToByteArray(); - _ = send_streaming_message(nativeApplication, bytes, bytes.Length); + fixed (byte* ptr = bytes) + { + _ = send_streaming_message(IntPtr.Zero, ptr, bytes.Length); + } } - [DllImport(NativeWorkerDll, CharSet = CharSet.Auto)] - private static extern int get_application_properties(out NativeHost hostData); - - [DllImport(NativeWorkerDll, CharSet = CharSet.Auto)] - private static extern int send_streaming_message(NativeSafeHandle pInProcessApplication, byte[] streamingMessage, int streamingMessageSize); + [DllImport(NativeWorkerDll)] + private static extern int send_streaming_message(IntPtr pInProcessApplication, byte* streamingMessage, int streamingMessageSize); - [DllImport(NativeWorkerDll, CharSet = CharSet.Auto)] - private static extern unsafe int register_callbacks(NativeSafeHandle pInProcessApplication, + [DllImport(NativeWorkerDll)] + private static extern unsafe int register_callbacks(IntPtr pInProcessApplication, delegate* unmanaged requestCallback, IntPtr grpcHandler); diff --git a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeWorkerClient.cs b/src/DotNetWorker.Grpc/NativeHostIntegration/NativeWorkerClient.cs index aa7f39984..14f32035c 100644 --- a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeWorkerClient.cs +++ b/src/DotNetWorker.Grpc/NativeHostIntegration/NativeWorkerClient.cs @@ -15,17 +15,15 @@ internal class NativeWorkerClient : IWorkerClient private readonly IMessageProcessor _messageProcessor; private readonly ChannelReader _outputChannelReader; private readonly ChannelWriter _outputChannelWriter; - private readonly NativeSafeHandle _application; private GCHandle _gcHandle; private readonly Channel _inbound = Channel.CreateUnbounded(); - public NativeWorkerClient(IMessageProcessor messageProcessor, GrpcHostChannel outputChannel, NativeHost nativeHostData) + public NativeWorkerClient(IMessageProcessor messageProcessor, GrpcHostChannel outputChannel) { _messageProcessor = messageProcessor; _outputChannelReader = outputChannel.Channel.Reader; _outputChannelWriter = outputChannel.Channel.Writer; - _application = new NativeSafeHandle(nativeHostData.pNativeApplication); } public Task StartAsync(CancellationToken cancellationToken) @@ -37,7 +35,7 @@ public Task StartAsync(CancellationToken cancellationToken) public unsafe void Start() { _gcHandle = GCHandle.Alloc(this); - NativeMethods.RegisterCallbacks(_application, &HandleRequest, (IntPtr)_gcHandle); + NativeMethods.RegisterCallbacks(&HandleRequest, (IntPtr)_gcHandle); _ = ProcessInbound(); _ = ProcessOutbound(); @@ -55,7 +53,7 @@ private async Task ProcessOutbound() { await foreach (StreamingMessage msg in _outputChannelReader.ReadAllAsync()) { - NativeMethods.SendStreamingMessage(_application, msg); + NativeMethods.SendStreamingMessage(msg); } } diff --git a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeWorkerClientFactory.cs b/src/DotNetWorker.Grpc/NativeHostIntegration/NativeWorkerClientFactory.cs index 75c40e3fe..20ce548db 100644 --- a/src/DotNetWorker.Grpc/NativeHostIntegration/NativeWorkerClientFactory.cs +++ b/src/DotNetWorker.Grpc/NativeHostIntegration/NativeWorkerClientFactory.cs @@ -14,8 +14,7 @@ public NativeWorkerClientFactory(GrpcHostChannel hostChannel) public IWorkerClient CreateClient(IMessageProcessor messageProcessor) { - var nativeHostData = NativeMethods.GetNativeHostData(); - return new NativeWorkerClient(messageProcessor, _hostChannel, nativeHostData); + return new NativeWorkerClient(messageProcessor, _hostChannel); } } } diff --git a/src/DotNetWorker/DotNetWorker.csproj b/src/DotNetWorker/DotNetWorker.csproj index 5ea6ff28a..6b28164a5 100644 --- a/src/DotNetWorker/DotNetWorker.csproj +++ b/src/DotNetWorker/DotNetWorker.csproj @@ -8,7 +8,7 @@ Microsoft.Azure.Functions.Worker Microsoft.Azure.Functions.Worker true - 19 + 20 0 diff --git a/src/ci.yml b/src/ci.yml index 932c113f5..73248ca17 100644 --- a/src/ci.yml +++ b/src/ci.yml @@ -24,6 +24,7 @@ pr: - sdk/ - samples/FunctionApp - tools/ + - extensions/ jobs: - job: "Build_And_Test_Windows" diff --git a/test/DependentAssemblyWithFunctions.NetStandard/DependentAssemblyWithFunctions.NetStandard.csproj b/test/DependentAssemblyWithFunctions.NetStandard/DependentAssemblyWithFunctions.NetStandard.csproj new file mode 100644 index 000000000..77009bcc2 --- /dev/null +++ b/test/DependentAssemblyWithFunctions.NetStandard/DependentAssemblyWithFunctions.NetStandard.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + + + + + + diff --git a/test/DependentAssemblyWithFunctions.NetStandard/NetStandardClassLibraryClass1.cs b/test/DependentAssemblyWithFunctions.NetStandard/NetStandardClassLibraryClass1.cs new file mode 100644 index 000000000..276880c08 --- /dev/null +++ b/test/DependentAssemblyWithFunctions.NetStandard/NetStandardClassLibraryClass1.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace DependentAssemblyWithFunctions.NetStandard +{ + public sealed class NetStandardClassLibraryClass1 + { + private readonly ILogger _logger; + + public NetStandardClassLibraryClass1(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("NetStandardClassLibraryClass1Function1")] + public HttpResponseData Run1([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("NetStandardClassLibraryClass1Function1"); + throw new NotImplementedException(); + } + + [Function("NetStandardClassLibraryClass1Function2Async")] + public Task Run2([HttpTrigger(AuthorizationLevel.Admin, "get", "post")] HttpRequestData req) + { + _logger.LogInformation("NetStandardClassLibraryClass1Function2Async"); + throw new NotImplementedException(); + } + } +} diff --git a/test/DependentAssemblyWithFunctions/DependencyFunction.cs b/test/DependentAssemblyWithFunctions/DependencyFunction.cs index 605b45343..12517de95 100644 --- a/test/DependentAssemblyWithFunctions/DependencyFunction.cs +++ b/test/DependentAssemblyWithFunctions/DependencyFunction.cs @@ -1,8 +1,8 @@ - -using System.Net; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information.; + using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; -using Microsoft.Extensions.Logging; namespace DependentAssemblyWithFunctions { diff --git a/test/DependentAssemblyWithFunctions/InternalFunction.cs b/test/DependentAssemblyWithFunctions/InternalFunction.cs index 6a34b7388..5cf10a5c2 100644 --- a/test/DependentAssemblyWithFunctions/InternalFunction.cs +++ b/test/DependentAssemblyWithFunctions/InternalFunction.cs @@ -1,4 +1,7 @@ -using Microsoft.Azure.Functions.Worker; +// (c).NET Foundation.All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; namespace DependentAssemblyWithFunctions @@ -11,5 +14,11 @@ public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "g { throw new NotImplementedException(); } + + [Function("ThisShouldBeSkippedBecauseMethodNotPublic")] + internal static HttpResponseData Run2([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req) + { + throw new NotImplementedException(); + } } } diff --git a/test/DependentAssemblyWithFunctions/NestedNamespaceFunction.cs b/test/DependentAssemblyWithFunctions/NestedNamespaceFunction.cs new file mode 100644 index 000000000..daecaeb59 --- /dev/null +++ b/test/DependentAssemblyWithFunctions/NestedNamespaceFunction.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information.; + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; + +namespace MyCompany.MyProduct.MyApp +{ + public class HttpFunctions + { + [Function("NestedNamespaceFunc1")] + public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/DependentAssemblyWithFunctions/NestedTypesFunction.cs b/test/DependentAssemblyWithFunctions/NestedTypesFunction.cs new file mode 100644 index 000000000..b8e790513 --- /dev/null +++ b/test/DependentAssemblyWithFunctions/NestedTypesFunction.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information.; + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; + +namespace MyCompany.MyProduct.MyApp +{ + public sealed class Foo + { + public sealed class Bar + { + [Function("NestedTypeFunc")] + public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs b/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs index b9714011c..b5eb82701 100644 --- a/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs +++ b/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs @@ -3,9 +3,14 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility.Implementation; +using Microsoft.Azure.Functions.Tests; +using Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers; using Microsoft.Azure.Functions.Worker.Context.Features; using Microsoft.Azure.Functions.Worker.Diagnostics; using Microsoft.Azure.Functions.Worker.Tests.Features; @@ -17,57 +22,70 @@ namespace Microsoft.Azure.Functions.Worker.Tests.ApplicationInsights; -public class EndToEndTests : IDisposable +public class EndToEndTests { + private const string RoleName = "RoleName"; + private readonly TestTelemetryChannel _channel; - private readonly IHost _host; - private readonly IFunctionsApplication _application; - private readonly IInvocationFeaturesFactory _invocationFeaturesFactory; + private IFunctionsApplication _application; + private IInvocationFeaturesFactory _invocationFeaturesFactory; private readonly AppInsightsFunctionDefinition _funcDefinition = new(); public EndToEndTests() { _channel = new TestTelemetryChannel(); + } - _host = new HostBuilder() - .ConfigureServices(services => - { - var functionsBuilder = services.AddFunctionsWorkerCore(); - functionsBuilder.UseDefaultWorkerMiddleware(); - - services.AddApplicationInsightsTelemetryWorkerService(options => - { + private IHost InitializeHost() + { + var host = new HostBuilder() + .ConfigureServices(services => + { + var functionsBuilder = services.AddFunctionsWorkerCore(); + functionsBuilder.UseDefaultWorkerMiddleware(); + + services.AddApplicationInsightsTelemetryWorkerService(options => + { #pragma warning disable CS0618 // Obsolete member. Test case, this is fine to use. - options.InstrumentationKey = "abc"; + options.InstrumentationKey = "abc"; #pragma warning restore CS0618 // Obsolete member. Test case, this is fine to use. - // keep things more deterministic for tests - options.EnableAdaptiveSampling = false; - options.EnableDependencyTrackingTelemetryModule = false; - }); + // keep things more deterministic for tests + options.EnableAdaptiveSampling = false; + options.EnableDependencyTrackingTelemetryModule = false; + options.EnablePerformanceCounterCollectionModule = false; + options.EnableEventCounterCollectionModule = false; + options.EnableHeartbeat = false; + }); - services.ConfigureFunctionsApplicationInsights(); - services.AddDefaultInputConvertersToWorkerOptions(); + services.ConfigureFunctionsApplicationInsights(); - // Register our own in-memory channel - services.AddSingleton(_channel); - services.AddSingleton(_ => new Mock().Object); - }) - .Build(); + // override this so tests don't have to wait + services.AddSingleton(p => new AppServiceEnvironmentVariableMonitor(TimeSpan.FromMilliseconds(50))); - _application = _host.Services.GetService(); - _invocationFeaturesFactory = _host.Services.GetService(); + services.AddDefaultInputConvertersToWorkerOptions(); + + // Register our own in-memory channel + services.AddSingleton(_channel); + services.AddSingleton(_ => new Mock().Object); + }) + .Build(); + + _application = host.Services.GetService(); + _invocationFeaturesFactory = host.Services.GetService(); _application.LoadFunction(_funcDefinition); + + return host; } - private FunctionContext CreateContext() + private FunctionContext CreateContext(IHost host) { var invocation = new TestFunctionInvocation(functionId: _funcDefinition.Id); var features = _invocationFeaturesFactory.Create(); features.Set(invocation); - var inputConversionProvider = _host.Services.GetRequiredService(); + var inputConversionProvider = host.Services.GetRequiredService(); inputConversionProvider.TryCreate(typeof(DefaultInputConversionFeature), out var inputConversion); features.Set(new TestFunctionBindingsFeature()); features.Set(inputConversion); @@ -78,7 +96,10 @@ private FunctionContext CreateContext() [Fact] public async Task Logger_SendsTraceAndDependencyTelemetry() { - var context = CreateContext(); + using var _ = SetupDefaultEnvironmentVariables(); + using var host = InitializeHost(); + + var context = CreateContext(host); await _application.InvokeFunctionAsync(context); @@ -94,7 +115,10 @@ public async Task Logger_SendsTraceAndDependencyTelemetry() [Fact] public async Task Logger_Exception_SendsTraceAndExceptionAndDependencyTelemetry() { - var context = CreateContext(); + using var _ = SetupDefaultEnvironmentVariables(); + using var host = InitializeHost(); + + var context = CreateContext(host); context.Items["_throw"] = true; await Assert.ThrowsAsync(() => _application.InvokeFunctionAsync(context)); @@ -109,6 +133,40 @@ public async Task Logger_Exception_SendsTraceAndExceptionAndDependencyTelemetry( t => ValidateTraceTelemetry((TraceTelemetry)t, context, activity)); } + [Fact] + public async Task Telemetry_Updates_After_Swap() + { + // Env vars can change during a swap. These should automatically update. + using var _ = SetupDefaultEnvironmentVariables(); + using var host = InitializeHost(); + + // these tests don't use a running host; explicitly start this hosted service + var monitor = host.Services.GetRequiredService(); + await monitor.StartAsync(CancellationToken.None); + + var telemetryClient = host.Services.GetService(); + telemetryClient.TrackTrace("before swap"); + + using var swapped = new TestScopedEnvironmentVariable(new Dictionary + { + { AppServiceOptionsInitializer.AzureWebsiteName, "SwappedRoleName" }, + { AppServiceOptionsInitializer.AzureWebsiteSlotName, "staging" } + }); + + // ensure the monitor has refreshed env var cache + await Task.Delay(100); + + telemetryClient.TrackTrace("after swap"); + + IEnumerable telemetries = await WaitForTelemetries(expectedCount: 2); + Assert.Equal(2, telemetries.Count()); + var beforeSwap = telemetries.Last(); + var afterSwap = telemetries.First(); + + ValidateCommonTelemetry(beforeSwap); + ValidateCommonTelemetry(afterSwap, "SwappedRoleName-staging"); + } + private async Task> WaitForTelemetries(int expectedCount) { IEnumerable telemetries = null; @@ -133,6 +191,8 @@ private static void ValidateDependencyTelemetry(DependencyTelemetry dependency, Assert.Equal(context.InvocationId, dependency.Properties[TraceConstants.AttributeFaasExecution]); Assert.Contains(TraceConstants.AttributeSchemaUrl, dependency.Properties.Keys); + + ValidateCommonTelemetry(dependency); } private static void ValidateTraceTelemetry(TraceTelemetry trace, FunctionContext context, Activity activity) @@ -145,6 +205,8 @@ private static void ValidateTraceTelemetry(TraceTelemetry trace, FunctionContext Assert.Equal(context.InvocationId, trace.Properties[FunctionInvocationScope.FunctionInvocationIdKey]); Assert.Equal(activity.RootId, trace.Context.Operation.Id); + + ValidateCommonTelemetry(trace); } private static void ValidateExceptionTelemetry(ExceptionTelemetry exception, FunctionContext context, Activity activity) @@ -157,10 +219,28 @@ private static void ValidateExceptionTelemetry(ExceptionTelemetry exception, Fun Assert.Contains("boom!", edi.Message); Assert.Equal(activity.RootId, exception.Context.Operation.Id); + + ValidateCommonTelemetry(exception); + } + + private static void ValidateCommonTelemetry(ITelemetry telemetry, string expectedSiteName = RoleName) + { + // tests will set this when swapping out env vars + var internalContext = telemetry.Context.GetInternalContext(); + + expectedSiteName = expectedSiteName.ToLowerInvariant(); + + Assert.Equal(expectedSiteName, telemetry.Context.Cloud.RoleName); + Assert.Equal($"{expectedSiteName}{FunctionsRoleEnvironmentTelemetryInitializer.WebAppSuffix}", internalContext.NodeName); } - public void Dispose() + + private static IDisposable SetupDefaultEnvironmentVariables() { - _host?.Dispose(); + return new TestScopedEnvironmentVariable(new Dictionary + { + { AppServiceOptionsInitializer.AzureWebsiteName, RoleName }, + { AppServiceOptionsInitializer.AzureWebsiteSlotName, AppServiceOptionsInitializer.DefaultProductionSlotName } + }); } internal class AppInsightsFunctionDefinition : FunctionDefinition diff --git a/test/DotNetWorkerTests/DotNetWorkerTests.csproj b/test/DotNetWorkerTests/DotNetWorkerTests.csproj index d488afe14..75956fc5b 100644 --- a/test/DotNetWorkerTests/DotNetWorkerTests.csproj +++ b/test/DotNetWorkerTests/DotNetWorkerTests.csproj @@ -9,6 +9,7 @@ preview ..\..\key.snk disable + true diff --git a/test/DotNetWorkerTests/Http/HttpRequestDataExtensionsTests.cs b/test/DotNetWorkerTests/Http/HttpRequestDataExtensionsTests.cs index 5e59819ac..8e88d6bbd 100644 --- a/test/DotNetWorkerTests/Http/HttpRequestDataExtensionsTests.cs +++ b/test/DotNetWorkerTests/Http/HttpRequestDataExtensionsTests.cs @@ -1,6 +1,7 @@ // 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 System.IO; using System.Text; using System.Text.Json.Serialization; @@ -14,9 +15,104 @@ namespace Microsoft.Azure.Functions.Worker.Tests { public class HttpRequestDataExtensionsTests - { + { + /// + /// Tests if ReadAsString throws ArgumentNullException when the HttpRequestData is null. + /// + [Fact] + public void ReadAsString_RequestIsNull_ThrowsArgumentNullException() + { + // Arrange, Act & Assert + Assert.Throws(() => HttpRequestDataExtensions.ReadAsString(null!)); + } - [Fact] + /// + /// Tests if ReadAsString correctly reads the body with the default UTF-8 encoding. + /// + [Fact] + public void ReadAsString_BodyIsNotNull_ReadsBodyCorrectly() + { + // Arrange + FunctionContext context = TestFunctionContext.Create(); + var body = "Test String"; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(body)); + var request = new TestHttpRequestData(context, body: stream); + + // Act + var result = request.ReadAsString(); + + // Assert + Assert.Equal("Test String", result); + } + + /// + /// Tests if ReadAsString correctly reads the body with a specified encoding. + /// + [Fact] + public void ReadAsString_WithEncoding_ReadsBodyCorrectly() + { + // Arrange + FunctionContext context = TestFunctionContext.Create(); + var body = "Test String"; + var stream = new MemoryStream(Encoding.ASCII.GetBytes(body)); + var request = new TestHttpRequestData(context, body: stream); + + // Act + var result = request.ReadAsString(Encoding.ASCII); + + // Assert + Assert.Equal("Test String", result); + } + + /// + /// Tests if ReadAsStringAsync throws ArgumentNullException when the HttpRequestData is null. + /// + [Fact] + public async Task ReadAsStringAsync_RequestIsNull_ThrowsArgumentNullException() + { + // Arrange, Act & Assert + await Assert.ThrowsAsync(() => HttpRequestDataExtensions.ReadAsStringAsync(null!)); + } + + /// + /// Tests if ReadAsStringAsync correctly reads the body with the default UTF-8 encoding. + /// + [Fact] + public async Task ReadAsStringAsync_BodyIsNotNull_ReadsBodyCorrectly() + { + // Arrange + FunctionContext context = TestFunctionContext.Create(); + var body = "Test String"; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(body)); + var request = new TestHttpRequestData(context, body: stream); + + // Act + var result = await request.ReadAsStringAsync(); + + // Assert + Assert.Equal("Test String", result); + } + + /// + /// Tests if ReadAsStringAsync correctly reads the body with a specified encoding. + /// + [Fact] + public async Task ReadAsStringAsync_WithEncoding_ReadsBodyCorrectly() + { + // Arrange + FunctionContext context = TestFunctionContext.Create(); + var body = "Test String"; + var stream = new MemoryStream(Encoding.ASCII.GetBytes(body)); + var request = new TestHttpRequestData(context, body: stream); + + // Act + var result = await request.ReadAsStringAsync(Encoding.ASCII); + + // Assert + Assert.Equal("Test String", result); + } + + [Fact] public async Task ReadAsJsonAsync_SimpleOverload_AppliesDefaults() { FunctionContext context = TestFunctionContext.Create(); @@ -32,7 +128,7 @@ public async Task ReadAsJsonAsync_SimpleOverload_AppliesDefaults() Assert.Equal(42, result.SomeInt); } - [Fact] + [Fact] public async Task ReadAsJsonAsync_SerializerOverload_AppliesSerializer() { FunctionContext context = TestFunctionContext.Create(); @@ -51,13 +147,12 @@ public async Task ReadAsJsonAsync_SerializerOverload_AppliesSerializer() public class RequestPoco { [JsonProperty("jsonnetname")] - [JsonPropertyName("textjsonname")] + [JsonPropertyName("textjsonname")] public string Name { get; set; } [JsonProperty("jsonnetint")] - [JsonPropertyName("textjsonint")] + [JsonPropertyName("textjsonint")] public int SomeInt { get; set; } - } + } } -} - +} diff --git a/test/DotNetWorkerTests/Properties/AssemblyInfo.cs b/test/DotNetWorkerTests/Properties/AssemblyInfo.cs index 2d7df5e1b..98f17e623 100644 --- a/test/DotNetWorkerTests/Properties/AssemblyInfo.cs +++ b/test/DotNetWorkerTests/Properties/AssemblyInfo.cs @@ -6,4 +6,6 @@ [assembly: CollectionBehavior(DisableTestParallelization = true)] [assembly: InternalsVisibleTo("Microsoft.Azure.Functions.Worker.Extensions.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")] +[assembly: InternalsVisibleTo("Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")] + diff --git a/test/E2ETests/E2EApps/E2EApp/Http/BasicHttpFunctions.cs b/test/E2ETests/E2EApps/E2EApp/Http/BasicHttpFunctions.cs index f9ea25e69..4cb1a949c 100644 --- a/test/E2ETests/E2EApps/E2EApp/Http/BasicHttpFunctions.cs +++ b/test/E2ETests/E2EApps/E2EApp/Http/BasicHttpFunctions.cs @@ -11,6 +11,32 @@ namespace Microsoft.Azure.Functions.Worker.E2EApp { public static class BasicHttpFunctions { + [Function("HelloPascal")] + public static HttpResponseData Hello( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + FunctionContext context) + { + var logger = context.GetLogger(nameof(Hello)); + logger.LogInformation(".NET Worker HTTP trigger function processed a request"); + + var response = req.CreateResponse(HttpStatusCode.OK); + response.WriteString("Hello!"); + return response; + } + + [Function("HelloAllCaps")] + public static HttpResponseData HELLO( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + FunctionContext context) + { + var logger = context.GetLogger(nameof(HELLO)); + logger.LogInformation(".NET Worker HTTP trigger function processed a request"); + + var response = req.CreateResponse(HttpStatusCode.OK); + response.WriteString("HELLO!"); + return response; + } + [Function(nameof(HelloFromQuery))] public static HttpResponseData HelloFromQuery( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, diff --git a/test/E2ETests/E2ETests/Fixtures/FunctionAppFixture.cs b/test/E2ETests/E2ETests/Fixtures/FunctionAppFixture.cs index ef4cc1481..6674d0cad 100644 --- a/test/E2ETests/E2ETests/Fixtures/FunctionAppFixture.cs +++ b/test/E2ETests/E2ETests/Fixtures/FunctionAppFixture.cs @@ -61,6 +61,8 @@ public async Task InitializeAsync() _funcProcess.StartInfo.ArgumentList.Add("PocoAfterRouteParameters"); _funcProcess.StartInfo.ArgumentList.Add("ExceptionFunction"); _funcProcess.StartInfo.ArgumentList.Add("PocoWithoutBindingSource"); + _funcProcess.StartInfo.ArgumentList.Add("HelloPascal"); + _funcProcess.StartInfo.ArgumentList.Add("HelloAllCaps"); } await CosmosDBHelpers.TryCreateDocumentCollectionsAsync(_logger); diff --git a/test/E2ETests/E2ETests/HttpEndToEndTests.cs b/test/E2ETests/E2ETests/HttpEndToEndTests.cs index 5c6da07b6..764043d40 100644 --- a/test/E2ETests/E2ETests/HttpEndToEndTests.cs +++ b/test/E2ETests/E2ETests/HttpEndToEndTests.cs @@ -24,6 +24,8 @@ public HttpEndToEndTests(FunctionAppFixture fixture, ITestOutputHelper testOutpu } [Theory] + [InlineData("HelloPascal", "", HttpStatusCode.OK, "Hello!")] + [InlineData("HelloAllCaps", "", HttpStatusCode.OK, "HELLO!")] [InlineData("HelloFromQuery", "?name=Test", HttpStatusCode.OK, "Hello Test")] [InlineData("HelloFromQuery", "?name=John&lastName=Doe", HttpStatusCode.OK, "Hello John")] [InlineData("HelloFromQuery", "?emptyProperty=&name=Jane", HttpStatusCode.OK, "Hello Jane")] diff --git a/test/E2ETests/E2ETests/Storage/BlobEndToEndTests.cs b/test/E2ETests/E2ETests/Storage/BlobEndToEndTests.cs index 1ee1c7e79..21e56a552 100644 --- a/test/E2ETests/E2ETests/Storage/BlobEndToEndTests.cs +++ b/test/E2ETests/E2ETests/Storage/BlobEndToEndTests.cs @@ -81,7 +81,7 @@ public async Task BlobTrigger_String_Succeeds() Assert.Equal("Hello World", result); } - [Fact] + [Fact(Skip = "TODO: https://github.com/Azure/azure-functions-dotnet-worker/issues/1935")] public async Task BlobTrigger_Stream_Succeeds() { string key = "StreamTriggerOutput: "; diff --git a/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs b/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs index 95c8cc09e..0c1d3aa1a 100644 --- a/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs +++ b/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs @@ -806,7 +806,7 @@ public void CardinalityManyFunctions(string functionName, string entryPoint, boo b => ValidateTrigger(b, cardinalityMany)); AssertDictionary(extensions, new Dictionary(){ - { "Microsoft.Azure.WebJobs.Extensions.EventHubs", "5.5.0" } + { "Microsoft.Azure.WebJobs.Extensions.EventHubs", "6.0.1" } }); void ValidateTrigger(ExpandoObject b, bool many) @@ -978,7 +978,7 @@ public void ServiceBus_SDKTypeBindings() AssertDictionary(extensions, new Dictionary { - { "Microsoft.Azure.WebJobs.Extensions.ServiceBus", "5.12.0" }, + { "Microsoft.Azure.WebJobs.Extensions.ServiceBus", "5.13.3" }, }); var serviceBusTriggerFunction = functions.Single(p => p.Name == nameof(SDKTypeBindings_ServiceBus.ServiceBusTriggerFunction)); @@ -1031,7 +1031,7 @@ public void EventHubs_SDKTypeBindings() AssertDictionary(extensions, new Dictionary { - { "Microsoft.Azure.WebJobs.Extensions.EventHubs", "5.5.0" }, + { "Microsoft.Azure.WebJobs.Extensions.EventHubs", "6.0.1" }, }); var eventHubTriggerFunction = functions.Single(p => p.Name == nameof(SDKTypeBindings_EventHubs.EventHubTriggerFunction)); diff --git a/test/Sdk.Generator.Tests/ExtensionStartupRunnerGeneratorTests.cs b/test/Sdk.Generator.Tests/ExtensionStartupRunnerGeneratorTests.cs index 2c0d1dd98..b02cc8302 100644 --- a/test/Sdk.Generator.Tests/ExtensionStartupRunnerGeneratorTests.cs +++ b/test/Sdk.Generator.Tests/ExtensionStartupRunnerGeneratorTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Azure.Functions.Tests.WorkerExtensionsSample; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Testing; using Worker.Extensions.Sample_IncorrectImplementation; using Xunit; @@ -19,8 +20,14 @@ public class Foo } """; - [Fact] - public async Task StartupExecutorCodeGetsGenerated() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task StartupExecutorCodeGetsGenerated(LanguageVersion languageVersion) { // Source generation is based on referenced assembly. var referencedExtensionAssemblies = new[] @@ -39,8 +46,14 @@ public async Task StartupExecutorCodeGetsGenerated() namespace TestProject { + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class WorkerExtensionStartupCodeExecutor : WorkerExtensionStartup { + /// + /// Configures the worker to register extension startup services. + /// + /// The to configure. public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder) { try @@ -60,11 +73,18 @@ await TestHelpers.RunTestAsync( referencedExtensionAssemblies, InputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async Task StartupExecutorCodeDoesNotGetsGeneratedWheNoExtensionAssembliesAreReferenced() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task StartupExecutorCodeDoesNotGetsGeneratedWheNoExtensionAssembliesAreReferenced(LanguageVersion languageVersion) { // source gen will happen only when an assembly with worker startup type is defined. var referencedExtensionAssemblies = Array.Empty(); @@ -76,11 +96,18 @@ await TestHelpers.RunTestAsync( referencedExtensionAssemblies, InputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async Task DiagnosticErrorsAreReportedWhenStartupTypeIsInvalid() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task DiagnosticErrorsAreReportedWhenStartupTypeIsInvalid(LanguageVersion languageVersion) { var referencedExtensionAssemblies = new[] { @@ -103,8 +130,14 @@ public async Task DiagnosticErrorsAreReportedWhenStartupTypeIsInvalid() namespace MyCompany.MyProject.MyApp { + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class WorkerExtensionStartupCodeExecutor : WorkerExtensionStartup { + /// + /// Configures the worker to register extension startup services. + /// + /// The to configure. public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder) { try @@ -142,7 +175,8 @@ await TestHelpers.RunTestAsync( expectedGeneratedFileName, expectedOutput, expectedDiagnosticResults, - buildPropertiesDict); + buildPropertiesDict, + languageVersion: languageVersion); } } } diff --git a/test/Sdk.Generator.Tests/FunctionExecutor/DependentAssemblyTest.cs b/test/Sdk.Generator.Tests/FunctionExecutor/DependentAssemblyTest.cs new file mode 100644 index 000000000..57e31e283 --- /dev/null +++ b/test/Sdk.Generator.Tests/FunctionExecutor/DependentAssemblyTest.cs @@ -0,0 +1,151 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Xunit; + +namespace Microsoft.Azure.Functions.SdkGeneratorTests +{ + public partial class FunctionExecutorGeneratorTests + { + public class DependentAssemblyTest + { + private readonly Assembly[] _referencedAssemblies; + + public DependentAssemblyTest() + { + var abstractionsExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Abstractions.dll"); + var httpExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Http.dll"); + var hostingExtension = Assembly.LoadFrom("Microsoft.Extensions.Hosting.dll"); + var diExtension = Assembly.LoadFrom("Microsoft.Extensions.DependencyInjection.dll"); + var hostingAbExtension = Assembly.LoadFrom("Microsoft.Extensions.Hosting.Abstractions.dll"); + var diAbExtension = Assembly.LoadFrom("Microsoft.Extensions.DependencyInjection.Abstractions.dll"); + var dependentAssembly = Assembly.LoadFrom("DependentAssemblyWithFunctions.dll"); + + _referencedAssemblies = new[] + { + abstractionsExtension, + httpExtension, + hostingExtension, + hostingAbExtension, + diExtension, + diAbExtension, + dependentAssembly + }; + } + + [Fact] + public async Task FunctionsFromDependentAssembly() + { + const string inputSourceCode = """ + using System; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Http; + namespace MyCompany + { + public class MyHttpTriggers + { + [Function("FunctionA")] + public HttpResponseData Foo([HttpTrigger(AuthorizationLevel.User, "get")] HttpRequestData r, FunctionContext c) + { + return r.CreateResponse(System.Net.HttpStatusCode.OK); + } + } + } + """; + var expected = $$""" + // + using System; + using System.Threading.Tasks; + using System.Collections.Generic; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Context.Features; + using Microsoft.Azure.Functions.Worker.Invocation; + namespace TestProject + { + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class DirectFunctionExecutor : IFunctionExecutor + { + private readonly IFunctionActivator _functionActivator; + private Lazy _defaultExecutor; + private readonly Dictionary types = new Dictionary() + { + { "MyCompany.MyHttpTriggers", Type.GetType("MyCompany.MyHttpTriggers, TestProject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null") }, + { "DependentAssemblyWithFunctions.DependencyFunction", Type.GetType("DependentAssemblyWithFunctions.DependencyFunction, DependentAssemblyWithFunctions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null") }, + { "MyCompany.MyProduct.MyApp.HttpFunctions", Type.GetType("MyCompany.MyProduct.MyApp.HttpFunctions, DependentAssemblyWithFunctions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null") }, + { "MyCompany.MyProduct.MyApp.Foo.Bar", Type.GetType("MyCompany.MyProduct.MyApp.Foo.Bar, DependentAssemblyWithFunctions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null") } + }; + + public DirectFunctionExecutor(IFunctionActivator functionActivator) + { + _functionActivator = functionActivator ?? throw new ArgumentNullException(nameof(functionActivator)); + } + + /// + public async ValueTask ExecuteAsync(FunctionContext context) + { + var inputBindingFeature = context.Features.Get(); + var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context); + var inputArguments = inputBindingResult.Values; + _defaultExecutor = new Lazy(() => CreateDefaultExecutorInstance(context)); + + if (string.Equals(context.FunctionDefinition.EntryPoint, "MyCompany.MyHttpTriggers.Foo", StringComparison.Ordinal)) + { + var instanceType = types["MyCompany.MyHttpTriggers"]; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.MyHttpTriggers; + context.GetInvocationResult().Value = i.Foo((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0], (global::Microsoft.Azure.Functions.Worker.FunctionContext)inputArguments[1]); + } + else if (string.Equals(context.FunctionDefinition.EntryPoint, "DependentAssemblyWithFunctions.DependencyFunction.Run", StringComparison.Ordinal)) + { + var instanceType = types["DependentAssemblyWithFunctions.DependencyFunction"]; + var i = _functionActivator.CreateInstance(instanceType, context) as global::DependentAssemblyWithFunctions.DependencyFunction; + context.GetInvocationResult().Value = i.Run((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0]); + } + else if (string.Equals(context.FunctionDefinition.EntryPoint, "DependentAssemblyWithFunctions.InternalFunction.Run", StringComparison.Ordinal)) + { + await _defaultExecutor.Value.ExecuteAsync(context); + } + else if (string.Equals(context.FunctionDefinition.EntryPoint, "DependentAssemblyWithFunctions.StaticFunction.Run", StringComparison.Ordinal)) + { + context.GetInvocationResult().Value = global::DependentAssemblyWithFunctions.StaticFunction.Run((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0], (global::Microsoft.Azure.Functions.Worker.FunctionContext)inputArguments[1]); + } + else if (string.Equals(context.FunctionDefinition.EntryPoint, "MyCompany.MyProduct.MyApp.HttpFunctions.Run", StringComparison.Ordinal)) + { + var instanceType = types["MyCompany.MyProduct.MyApp.HttpFunctions"]; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.MyProduct.MyApp.HttpFunctions; + context.GetInvocationResult().Value = i.Run((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0]); + } + else if (string.Equals(context.FunctionDefinition.EntryPoint, "MyCompany.MyProduct.MyApp.Foo.Bar.Run", StringComparison.Ordinal)) + { + var instanceType = types["MyCompany.MyProduct.MyApp.Foo.Bar"]; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.MyProduct.MyApp.Foo.Bar; + context.GetInvocationResult().Value = i.Run((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0]); + } + } + + private IFunctionExecutor CreateDefaultExecutorInstance(FunctionContext context) + { + var defaultExecutorFullName = "Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor, Microsoft.Azure.Functions.Worker.Core, Version=1.16.0.0, Culture=neutral, PublicKeyToken=551316b6919f366c"; + var defaultExecutorType = Type.GetType(defaultExecutorFullName); + + return ActivatorUtilities.CreateInstance(context.InstanceServices, defaultExecutorType) as IFunctionExecutor; + } + } + {{GetExpectedExtensionMethodCode()}} + } + """.Replace("'", "\""); + + await TestHelpers.RunTestAsync( + _referencedAssemblies, + inputSourceCode, + Constants.FileNames.GeneratedFunctionExecutor, + expected); + } + } + } +} diff --git a/test/Sdk.Generator.Tests/FunctionExecutor/FunctionExecutorGeneratorTests.cs b/test/Sdk.Generator.Tests/FunctionExecutor/FunctionExecutorGeneratorTests.cs index 1d71aeb85..759d7b496 100644 --- a/test/Sdk.Generator.Tests/FunctionExecutor/FunctionExecutorGeneratorTests.cs +++ b/test/Sdk.Generator.Tests/FunctionExecutor/FunctionExecutorGeneratorTests.cs @@ -8,6 +8,7 @@ using Azure.Storage.Queues.Models; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -16,7 +17,7 @@ namespace Microsoft.Azure.Functions.SdkGeneratorTests { - public class FunctionExecutorGeneratorTests + public partial class FunctionExecutorGeneratorTests { // A super set of assemblies we need for all tests in the file. private readonly Assembly[] _referencedAssemblies = new[] @@ -37,8 +38,14 @@ public class FunctionExecutorGeneratorTests typeof(IHostBuilder).Assembly }; - [Fact] - public async Task FunctionsFromMultipleClasses() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task FunctionsFromMultipleClasses(LanguageVersion languageVersion) { const string inputSourceCode = @" using System; @@ -110,14 +117,16 @@ public void Run2([QueueTrigger(""myqueue-items"")] string message) using Microsoft.Azure.Functions.Worker.Invocation; namespace TestProject {{ + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class DirectFunctionExecutor : IFunctionExecutor {{ private readonly IFunctionActivator _functionActivator; - private readonly Dictionary types = new() + private readonly Dictionary types = new Dictionary() {{ - {{ ""MyCompany.MyHttpTriggers"", Type.GetType(""MyCompany.MyHttpTriggers"")! }}, - {{ ""MyCompany.MyHttpTriggers2"", Type.GetType(""MyCompany.MyHttpTriggers2"")! }}, - {{ ""MyCompany.QueueTriggers"", Type.GetType(""MyCompany.QueueTriggers"")! }} + {{ ""MyCompany.MyHttpTriggers"", Type.GetType(""MyCompany.MyHttpTriggers, TestProject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"") }}, + {{ ""MyCompany.MyHttpTriggers2"", Type.GetType(""MyCompany.MyHttpTriggers2, TestProject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"") }}, + {{ ""MyCompany.QueueTriggers"", Type.GetType(""MyCompany.QueueTriggers, TestProject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"") }} }}; public DirectFunctionExecutor(IFunctionActivator functionActivator) @@ -125,38 +134,39 @@ public DirectFunctionExecutor(IFunctionActivator functionActivator) _functionActivator = functionActivator ?? throw new ArgumentNullException(nameof(functionActivator)); }} + /// public async ValueTask ExecuteAsync(FunctionContext context) {{ - var inputBindingFeature = context.Features.Get()!; - var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context)!; + var inputBindingFeature = context.Features.Get(); + var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context); var inputArguments = inputBindingResult.Values; - if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.Foo"", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.Foo"", StringComparison.Ordinal)) {{ var instanceType = types[""MyCompany.MyHttpTriggers""]; - var i = _functionActivator.CreateInstance(instanceType, context) as MyCompany.MyHttpTriggers; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.MyHttpTriggers; context.GetInvocationResult().Value = i.Foo((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0], (global::Microsoft.Azure.Functions.Worker.FunctionContext)inputArguments[1]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers2.Bar"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers2.Bar"", StringComparison.Ordinal)) {{ var instanceType = types[""MyCompany.MyHttpTriggers2""]; - var i = _functionActivator.CreateInstance(instanceType, context) as MyCompany.MyHttpTriggers2; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.MyHttpTriggers2; context.GetInvocationResult().Value = i.Bar((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.Foo.MyAsyncStaticMethod"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.Foo.MyAsyncStaticMethod"", StringComparison.Ordinal)) {{ - context.GetInvocationResult().Value = await MyCompany.Foo.MyAsyncStaticMethod((string)inputArguments[0]); + context.GetInvocationResult().Value = await global::MyCompany.Foo.MyAsyncStaticMethod((string)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.QueueTriggers.Run"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.QueueTriggers.Run"", StringComparison.Ordinal)) {{ var instanceType = types[""MyCompany.QueueTriggers""]; - var i = _functionActivator.CreateInstance(instanceType, context) as MyCompany.QueueTriggers; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.QueueTriggers; i.Run((global::Azure.Storage.Queues.Models.QueueMessage)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.QueueTriggers.Run2"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.QueueTriggers.Run2"", StringComparison.Ordinal)) {{ var instanceType = types[""MyCompany.QueueTriggers""]; - var i = _functionActivator.CreateInstance(instanceType, context) as MyCompany.QueueTriggers; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.QueueTriggers; i.Run2((string)inputArguments[0]); }} }} @@ -168,11 +178,18 @@ public async ValueTask ExecuteAsync(FunctionContext context) _referencedAssemblies, inputSourceCode, Constants.FileNames.GeneratedFunctionExecutor, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async Task MultipleFunctionsDependencyInjection() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task MultipleFunctionsDependencyInjection(LanguageVersion languageVersion) { string inputSourceCode = @" using System.Net; @@ -217,12 +234,14 @@ public HttpResponseData Run2([HttpTrigger(AuthorizationLevel.User, ""get"")] Htt using Microsoft.Azure.Functions.Worker.Invocation; namespace MyCompany.MyProject.MyApp {{ + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class DirectFunctionExecutor : IFunctionExecutor {{ private readonly IFunctionActivator _functionActivator; - private readonly Dictionary types = new() + private readonly Dictionary types = new Dictionary() {{ - {{ ""MyCompany.MyHttpTriggers"", Type.GetType(""MyCompany.MyHttpTriggers"")! }} + {{ ""MyCompany.MyHttpTriggers"", Type.GetType(""MyCompany.MyHttpTriggers, TestProject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"") }} }}; public DirectFunctionExecutor(IFunctionActivator functionActivator) @@ -230,22 +249,23 @@ public DirectFunctionExecutor(IFunctionActivator functionActivator) _functionActivator = functionActivator ?? throw new ArgumentNullException(nameof(functionActivator)); }} + /// public async ValueTask ExecuteAsync(FunctionContext context) {{ - var inputBindingFeature = context.Features.Get()!; - var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context)!; + var inputBindingFeature = context.Features.Get(); + var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context); var inputArguments = inputBindingResult.Values; - if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.Run1"", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.Run1"", StringComparison.Ordinal)) {{ var instanceType = types[""MyCompany.MyHttpTriggers""]; - var i = _functionActivator.CreateInstance(instanceType, context) as MyCompany.MyHttpTriggers; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.MyHttpTriggers; context.GetInvocationResult().Value = i.Run1((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.Run2"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.Run2"", StringComparison.Ordinal)) {{ var instanceType = types[""MyCompany.MyHttpTriggers""]; - var i = _functionActivator.CreateInstance(instanceType, context) as MyCompany.MyHttpTriggers; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.MyHttpTriggers; context.GetInvocationResult().Value = i.Run2((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0], (global::Microsoft.Azure.Functions.Worker.FunctionContext)inputArguments[1]); }} }} @@ -264,11 +284,18 @@ await TestHelpers.RunTestAsync( inputSourceCode, Constants.FileNames.GeneratedFunctionExecutor, expectedOutput, - buildPropertiesDictionary: buildPropertiesDict); + buildPropertiesDictionary: buildPropertiesDict, + languageVersion: languageVersion); } - [Fact] - public async Task StaticMethods() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task StaticMethods(LanguageVersion languageVersion) { var inputSourceCode = @" using System; @@ -316,8 +343,10 @@ public class BlobTriggers [Function(nameof(BlobTriggers))] public static async Task Run([BlobTrigger(""items/{name}"", Connection = ""ConStr"")] Stream stream, string name) { - using var blobStreamReader = new StreamReader(stream); - var content = await blobStreamReader.ReadToEndAsync(); + using (var blobStreamReader = new StreamReader(stream)) + { + var content = await blobStreamReader.ReadToEndAsync(); + } } } public class EventHubTriggers @@ -352,6 +381,8 @@ public static Task RunAsync1([EventHubTrigger(""items"", Connection = ""Con"")] using Microsoft.Azure.Functions.Worker.Invocation; namespace TestProject {{ + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class DirectFunctionExecutor : IFunctionExecutor {{ private readonly IFunctionActivator _functionActivator; @@ -361,55 +392,56 @@ public DirectFunctionExecutor(IFunctionActivator functionActivator) _functionActivator = functionActivator ?? throw new ArgumentNullException(nameof(functionActivator)); }} + /// public async ValueTask ExecuteAsync(FunctionContext context) {{ - var inputBindingFeature = context.Features.Get()!; - var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context)!; + var inputBindingFeature = context.Features.Get(); + var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context); var inputArguments = inputBindingResult.Values; - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyTaskStaticMethod"", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyTaskStaticMethod"", StringComparison.Ordinal)) {{ - await FunctionApp26.MyQTriggers.MyTaskStaticMethod((string)inputArguments[0]); + await global::FunctionApp26.MyQTriggers.MyTaskStaticMethod((string)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyAsyncStaticMethod"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyAsyncStaticMethod"", StringComparison.Ordinal)) {{ - context.GetInvocationResult().Value = await FunctionApp26.MyQTriggers.MyAsyncStaticMethod((string)inputArguments[0]); + context.GetInvocationResult().Value = await global::FunctionApp26.MyQTriggers.MyAsyncStaticMethod((string)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyVoidStaticMethod"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyVoidStaticMethod"", StringComparison.Ordinal)) {{ - FunctionApp26.MyQTriggers.MyVoidStaticMethod((string)inputArguments[0]); + global::FunctionApp26.MyQTriggers.MyVoidStaticMethod((string)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyAsyncStaticMethodWithReturn"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyAsyncStaticMethodWithReturn"", StringComparison.Ordinal)) {{ - context.GetInvocationResult().Value = await FunctionApp26.MyQTriggers.MyAsyncStaticMethodWithReturn((string)inputArguments[0], (string)inputArguments[1]); + context.GetInvocationResult().Value = await global::FunctionApp26.MyQTriggers.MyAsyncStaticMethodWithReturn((string)inputArguments[0], (string)inputArguments[1]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyValueTaskOfTStaticAsyncMethod"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyValueTaskOfTStaticAsyncMethod"", StringComparison.Ordinal)) {{ - context.GetInvocationResult().Value = await FunctionApp26.MyQTriggers.MyValueTaskOfTStaticAsyncMethod((string)inputArguments[0]); + context.GetInvocationResult().Value = await global::FunctionApp26.MyQTriggers.MyValueTaskOfTStaticAsyncMethod((string)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyValueTaskStaticAsyncMethod2"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.MyQTriggers.MyValueTaskStaticAsyncMethod2"", StringComparison.Ordinal)) {{ - await FunctionApp26.MyQTriggers.MyValueTaskStaticAsyncMethod2((string)inputArguments[0]); + await global::FunctionApp26.MyQTriggers.MyValueTaskStaticAsyncMethod2((string)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.BlobTriggers.Run"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.BlobTriggers.Run"", StringComparison.Ordinal)) {{ - await FunctionApp26.BlobTriggers.Run((global::System.IO.Stream)inputArguments[0], (string)inputArguments[1]); + await global::FunctionApp26.BlobTriggers.Run((global::System.IO.Stream)inputArguments[0], (string)inputArguments[1]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.EventHubTriggers.Run1"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.EventHubTriggers.Run1"", StringComparison.Ordinal)) {{ - FunctionApp26.EventHubTriggers.Run1((global::Azure.Messaging.EventHubs.EventData[])inputArguments[0]); + global::FunctionApp26.EventHubTriggers.Run1((global::Azure.Messaging.EventHubs.EventData[])inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.EventHubTriggers.Run2"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.EventHubTriggers.Run2"", StringComparison.Ordinal)) {{ - context.GetInvocationResult().Value = FunctionApp26.EventHubTriggers.Run2((global::Azure.Messaging.EventHubs.EventData)inputArguments[0]); + context.GetInvocationResult().Value = global::FunctionApp26.EventHubTriggers.Run2((global::Azure.Messaging.EventHubs.EventData)inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.EventHubTriggers.RunAsync1"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.EventHubTriggers.RunAsync1"", StringComparison.Ordinal)) {{ - await FunctionApp26.EventHubTriggers.RunAsync1((global::Azure.Messaging.EventHubs.EventData[])inputArguments[0]); + await global::FunctionApp26.EventHubTriggers.RunAsync1((global::Azure.Messaging.EventHubs.EventData[])inputArguments[0]); }} - if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.EventHubTriggers.RunAsync2"", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""FunctionApp26.EventHubTriggers.RunAsync2"", StringComparison.Ordinal)) {{ - await FunctionApp26.EventHubTriggers.RunAsync2((global::Azure.Messaging.EventHubs.EventData[])inputArguments[0]); + await global::FunctionApp26.EventHubTriggers.RunAsync2((global::Azure.Messaging.EventHubs.EventData[])inputArguments[0]); }} }} }} @@ -420,13 +452,24 @@ public async ValueTask ExecuteAsync(FunctionContext context) _referencedAssemblies, inputSourceCode, Constants.FileNames.GeneratedFunctionExecutor, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task VerifyAutoConfigureStartupTypeEmitted(bool includeAutoStartupType) + [InlineData(true, LanguageVersion.CSharp7_3)] + [InlineData(true, LanguageVersion.CSharp8)] + [InlineData(true, LanguageVersion.CSharp9)] + [InlineData(true, LanguageVersion.CSharp10)] + [InlineData(true, LanguageVersion.CSharp11)] + [InlineData(true, LanguageVersion.Latest)] + [InlineData(false, LanguageVersion.CSharp7_3)] + [InlineData(false, LanguageVersion.CSharp8)] + [InlineData(false, LanguageVersion.CSharp9)] + [InlineData(false, LanguageVersion.CSharp10)] + [InlineData(false, LanguageVersion.CSharp11)] + [InlineData(false, LanguageVersion.Latest)] + public async Task VerifyAutoConfigureStartupTypeEmitted(bool includeAutoStartupType, LanguageVersion languageVersion) { string inputSourceCode = @" using System.Net; @@ -459,12 +502,14 @@ public HttpResponseData Run1([HttpTrigger(AuthorizationLevel.User, ""get"")] Htt using Microsoft.Azure.Functions.Worker.Invocation; namespace TestProject {{ + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class DirectFunctionExecutor : IFunctionExecutor {{ private readonly IFunctionActivator _functionActivator; - private readonly Dictionary types = new() + private readonly Dictionary types = new Dictionary() {{ - {{ ""MyCompany.MyHttpTriggers"", Type.GetType(""MyCompany.MyHttpTriggers"")! }} + {{ ""MyCompany.MyHttpTriggers"", Type.GetType(""MyCompany.MyHttpTriggers, TestProject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"") }} }}; public DirectFunctionExecutor(IFunctionActivator functionActivator) @@ -472,16 +517,17 @@ public DirectFunctionExecutor(IFunctionActivator functionActivator) _functionActivator = functionActivator ?? throw new ArgumentNullException(nameof(functionActivator)); }} + /// public async ValueTask ExecuteAsync(FunctionContext context) {{ - var inputBindingFeature = context.Features.Get()!; - var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context)!; + var inputBindingFeature = context.Features.Get(); + var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context); var inputArguments = inputBindingResult.Values; - if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.Run1"", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.Run1"", StringComparison.Ordinal)) {{ var instanceType = types[""MyCompany.MyHttpTriggers""]; - var i = _functionActivator.CreateInstance(instanceType, context) as MyCompany.MyHttpTriggers; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.MyHttpTriggers; context.GetInvocationResult().Value = i.Run1((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0]); }} }} @@ -499,7 +545,190 @@ await TestHelpers.RunTestAsync( inputSourceCode, Constants.FileNames.GeneratedFunctionExecutor, expectedOutput, - buildPropertiesDictionary: buildPropertiesDict); + buildPropertiesDictionary: buildPropertiesDict, + languageVersion: languageVersion); + } + + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task ClassWithSameNameAsNamespace(LanguageVersion languageVersion) + { + const string inputSourceCode = @" +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Azure.Storage.Queues.Models; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +namespace TestProject +{ + public class TestProject + { + [Function(""FunctionA"")] + public HttpResponseData Foo([HttpTrigger(AuthorizationLevel.User, ""get"")] HttpRequestData r, FunctionContext c) + { + return r.CreateResponse(System.Net.HttpStatusCode.OK); + } + + [Function(""FunctionB"")] + public static HttpResponseData FooStatic([HttpTrigger(AuthorizationLevel.User, ""get"")] HttpRequestData r, FunctionContext c) + { + return r.CreateResponse(System.Net.HttpStatusCode.OK); + } + } +} +"; + var expectedOutput = $@"// +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Context.Features; +using Microsoft.Azure.Functions.Worker.Invocation; +namespace TestProject +{{ + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class DirectFunctionExecutor : IFunctionExecutor + {{ + private readonly IFunctionActivator _functionActivator; + private readonly Dictionary types = new Dictionary() + {{ + {{ ""TestProject.TestProject"", Type.GetType(""TestProject.TestProject, TestProject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"") }} + }}; + + public DirectFunctionExecutor(IFunctionActivator functionActivator) + {{ + _functionActivator = functionActivator ?? throw new ArgumentNullException(nameof(functionActivator)); + }} + + /// + public async ValueTask ExecuteAsync(FunctionContext context) + {{ + var inputBindingFeature = context.Features.Get(); + var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context); + var inputArguments = inputBindingResult.Values; + + if (string.Equals(context.FunctionDefinition.EntryPoint, ""TestProject.TestProject.Foo"", StringComparison.Ordinal)) + {{ + var instanceType = types[""TestProject.TestProject""]; + var i = _functionActivator.CreateInstance(instanceType, context) as global::TestProject.TestProject; + context.GetInvocationResult().Value = i.Foo((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0], (global::Microsoft.Azure.Functions.Worker.FunctionContext)inputArguments[1]); + }} + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""TestProject.TestProject.FooStatic"", StringComparison.Ordinal)) + {{ + context.GetInvocationResult().Value = global::TestProject.TestProject.FooStatic((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0], (global::Microsoft.Azure.Functions.Worker.FunctionContext)inputArguments[1]); + }} + }} + }} +{GetExpectedExtensionMethodCode()} +}}".Replace("'", "\""); + + await TestHelpers.RunTestAsync( + _referencedAssemblies, + inputSourceCode, + Constants.FileNames.GeneratedFunctionExecutor, + expectedOutput, + languageVersion: languageVersion); + } + + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task FunctionsWithSameNameExceptForCasing(LanguageVersion languageVersion) + { + const string inputSourceCode = @" +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Azure.Storage.Queues.Models; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +namespace MyCompany +{ + public class MyHttpTriggers + { + [Function(""FunctionA"")] + public HttpResponseData Hello([HttpTrigger(AuthorizationLevel.User, ""get"")] HttpRequestData r, FunctionContext c) + { + return r.CreateResponse(System.Net.HttpStatusCode.OK); + } + + [Function(""FunctionB"")] + public static HttpResponseData HELLO([HttpTrigger(AuthorizationLevel.User, ""get"")] HttpRequestData r, FunctionContext c) + { + return r.CreateResponse(System.Net.HttpStatusCode.OK); + } + } +} +"; + var expectedOutput = $@"// +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Context.Features; +using Microsoft.Azure.Functions.Worker.Invocation; +namespace TestProject +{{ + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class DirectFunctionExecutor : IFunctionExecutor + {{ + private readonly IFunctionActivator _functionActivator; + private readonly Dictionary types = new Dictionary() + {{ + {{ ""MyCompany.MyHttpTriggers"", Type.GetType(""MyCompany.MyHttpTriggers, TestProject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"") }} + }}; + + public DirectFunctionExecutor(IFunctionActivator functionActivator) + {{ + _functionActivator = functionActivator ?? throw new ArgumentNullException(nameof(functionActivator)); + }} + + /// + public async ValueTask ExecuteAsync(FunctionContext context) + {{ + var inputBindingFeature = context.Features.Get(); + var inputBindingResult = await inputBindingFeature.BindFunctionInputAsync(context); + var inputArguments = inputBindingResult.Values; + + if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.Hello"", StringComparison.Ordinal)) + {{ + var instanceType = types[""MyCompany.MyHttpTriggers""]; + var i = _functionActivator.CreateInstance(instanceType, context) as global::MyCompany.MyHttpTriggers; + context.GetInvocationResult().Value = i.Hello((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0], (global::Microsoft.Azure.Functions.Worker.FunctionContext)inputArguments[1]); + }} + else if (string.Equals(context.FunctionDefinition.EntryPoint, ""MyCompany.MyHttpTriggers.HELLO"", StringComparison.Ordinal)) + {{ + context.GetInvocationResult().Value = global::MyCompany.MyHttpTriggers.HELLO((global::Microsoft.Azure.Functions.Worker.Http.HttpRequestData)inputArguments[0], (global::Microsoft.Azure.Functions.Worker.FunctionContext)inputArguments[1]); + }} + }} + }} +{GetExpectedExtensionMethodCode()} +}}".Replace("'", "\""); + + await TestHelpers.RunTestAsync( + _referencedAssemblies, + inputSourceCode, + Constants.FileNames.GeneratedFunctionExecutor, + expectedOutput, + languageVersion: languageVersion); } private static string GetExpectedExtensionMethodCode(bool includeAutoStartupType = false) @@ -507,6 +736,10 @@ private static string GetExpectedExtensionMethodCode(bool includeAutoStartupType if (includeAutoStartupType) { return """ + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class FunctionExecutorHostBuilderExtensions { /// @@ -520,8 +753,16 @@ public static IHostBuilder ConfigureGeneratedFunctionExecutor(this IHostBuilder }); } } + /// + /// Auto startup class to register the custom implementation generated for the current worker. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] public class FunctionExecutorAutoStartup : IAutoConfigureStartup { + /// + /// Configures the to use the custom implementation generated for the current worker. + /// + /// The instance to use for service registration. public void Configure(IHostBuilder hostBuilder) { hostBuilder.ConfigureGeneratedFunctionExecutor(); @@ -531,6 +772,10 @@ public void Configure(IHostBuilder hostBuilder) } return """ + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class FunctionExecutorHostBuilderExtensions { /// diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs index d88a710c5..8c0cbafa5 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.CodeAnalysis.CSharp; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -37,9 +38,19 @@ public AutoConfigureStartupTypeTests() } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task VerifyAutoGeneratedCodeAttributesAreEmitted(bool includeAutoStartupType) + [InlineData(true, LanguageVersion.CSharp7_3)] + [InlineData(true, LanguageVersion.CSharp8)] + [InlineData(true, LanguageVersion.CSharp9)] + [InlineData(true, LanguageVersion.CSharp10)] + [InlineData(true, LanguageVersion.CSharp11)] + [InlineData(true, LanguageVersion.Latest)] + [InlineData(false, LanguageVersion.CSharp7_3)] + [InlineData(false, LanguageVersion.CSharp8)] + [InlineData(false, LanguageVersion.CSharp9)] + [InlineData(false, LanguageVersion.CSharp10)] + [InlineData(false, LanguageVersion.CSharp11)] + [InlineData(false, LanguageVersion.Latest)] + public async Task VerifyAutoGeneratedCodeAttributesAreEmitted(bool includeAutoStartupType, LanguageVersion languageVersion) { string inputCode = """ using System; @@ -74,13 +85,19 @@ public static void HttpTrigger([HttpTrigger(AuthorizationLevel.Admin, "get", "po namespace TestProject {{ + /// + /// Custom implementation that returns function metadata definitions for the current worker.""/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider {{ + /// public Task> GetFunctionMetadataAsync(string directory) {{ var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@""{{""""name"""":""""req"""",""""type"""":""""HttpTrigger"""",""""direction"""":""""In"""",""""authLevel"""":""""Admin"""",""""methods"""":[""""get"""",""""post""""],""""route"""":""""/api2""""}}""); + Function0RawBindings.Add(@""{{""""name"""":""""req"""",""""type"""":""""httpTrigger"""",""""direction"""":""""In"""",""""authLevel"""":""""Admin"""",""""methods"""":[""""get"""",""""post""""],""""route"""":""""/api2""""}}""); var Function0 = new DefaultFunctionMetadata {{ @@ -107,7 +124,8 @@ await TestHelpers.RunTestAsync( inputCode, expectedGeneratedFileName, expectedOutput, - buildPropertiesDictionary: buildPropertiesDict); + buildPropertiesDictionary: buildPropertiesDict, + languageVersion: languageVersion); } } @@ -116,6 +134,9 @@ private static string GetExpectedExtensionMethodCode(bool includeAutoStartupType if (includeAutoStartupType) { return """ + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -131,8 +152,16 @@ public static IHostBuilder ConfigureGeneratedFunctionMetadataProvider(this IHost return builder; } } + /// + /// Auto startup class to register the custom implementation generated for the current worker. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] public class FunctionMetadataProviderAutoStartup : IAutoConfigureStartup { + /// + /// Configures the to use the custom implementation generated for the current worker. + /// + /// The instance to use for service registration. public void Configure(IHostBuilder hostBuilder) { hostBuilder.ConfigureGeneratedFunctionMetadataProvider(); @@ -142,6 +171,9 @@ public void Configure(IHostBuilder hostBuilder) } return """ + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.NetFx.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.NetFx.cs new file mode 100644 index 000000000..b2cd05c50 --- /dev/null +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.NetFx.cs @@ -0,0 +1,175 @@ +// 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.Reflection; +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Microsoft.Azure.Functions.SdkGeneratorTests +{ + public partial class FunctionMetadataProviderGeneratorTests + { + public class DependentAssemblyTestNetFx + { + private readonly Assembly[] _referencedExtensionAssemblies; + + public DependentAssemblyTestNetFx() + { + // load all extensions used in tests (match extensions tested on E2E app? Or include ALL extensions?) + var abstractionsExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Abstractions.dll"); + var httpExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Http.dll"); + var hostingExtension = Assembly.LoadFrom("Microsoft.Extensions.Hosting.dll"); + var diExtension = Assembly.LoadFrom("Microsoft.Extensions.DependencyInjection.dll"); + var hostingAbExtension = Assembly.LoadFrom("Microsoft.Extensions.Hosting.Abstractions.dll"); + var diAbExtension = Assembly.LoadFrom("Microsoft.Extensions.DependencyInjection.Abstractions.dll"); + var dependentAssembly = Assembly.LoadFrom("DependentAssemblyWithFunctions.NetStandard.dll"); + + _referencedExtensionAssemblies = new[] + { + abstractionsExtension, + httpExtension, + hostingExtension, + hostingAbExtension, + diExtension, + diAbExtension, + dependentAssembly + }; + } + + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task FunctionInDependentAssemblyTest(LanguageVersion languageVersion) + { + string inputCode = """ + using System; + using System.Collections.Generic; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Http; + + namespace FunctionApp + { + public static class EntryPointAssemblyHttpFunctions + { + [Function(nameof(EntryPointAssemblyHttpFunctions))] + public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, FunctionContext executionContext) + { + throw new NotImplementedException(); + } + } + } + """; + + string expectedGeneratedFileName = $"GeneratedFunctionMetadataProvider.g.cs"; + string expectedOutput = """ + // + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Text.Json; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + + namespace TestProject + { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider + { + /// + public Task> GetFunctionMetadataAsync(string directory) + { + var metadataList = new List(); + var Function0RawBindings = new List(); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function0 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "EntryPointAssemblyHttpFunctions", + EntryPoint = "FunctionApp.EntryPointAssemblyHttpFunctions.Run", + RawBindings = Function0RawBindings, + ScriptFile = "TestProject.exe" + }; + metadataList.Add(Function0); + var Function1RawBindings = new List(); + Function1RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Function"",""methods"":[""get"",""post""]}"); + Function1RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function1 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "NetStandardClassLibraryClass1Function1", + EntryPoint = "DependentAssemblyWithFunctions.NetStandard.NetStandardClassLibraryClass1.Run1", + RawBindings = Function1RawBindings, + ScriptFile = "DependentAssemblyWithFunctions.NetStandard.dll" + }; + metadataList.Add(Function1); + var Function2RawBindings = new List(); + Function2RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Admin"",""methods"":[""get"",""post""]}"); + Function2RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function2 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "NetStandardClassLibraryClass1Function2Async", + EntryPoint = "DependentAssemblyWithFunctions.NetStandard.NetStandardClassLibraryClass1.Run2", + RawBindings = Function2RawBindings, + ScriptFile = "DependentAssemblyWithFunctions.NetStandard.dll" + }; + metadataList.Add(Function2); + + return Task.FromResult(metadataList.ToImmutableArray()); + } + } + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// + public static class WorkerHostBuilderFunctionMetadataProviderExtension + { + /// + /// Adds the GeneratedFunctionMetadataProvider to the service collection. + /// During initialization, the worker will return generated function metadata instead of relying on the Azure Functions host for function indexing. + /// + public static IHostBuilder ConfigureGeneratedFunctionMetadataProvider(this IHostBuilder builder) + { + builder.ConfigureServices(s => + { + s.AddSingleton(); + }); + return builder; + } + } + } + """; + + var buildPropertiesDict = new Dictionary() + { + { Constants.BuildProperties.MSBuildTargetFrameworkIdentifier, ".NETFramework"} + }; + await TestHelpers.RunTestAsync( + _referencedExtensionAssemblies, + inputCode, + expectedGeneratedFileName, + expectedOutput, + buildPropertiesDictionary: buildPropertiesDict, + languageVersion: languageVersion); + } + } + } +} diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs index f5cdd7ff4..169d6aa92 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs @@ -74,13 +74,19 @@ public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "g namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -93,7 +99,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function0); var Function1RawBindings = new List(); - Function1RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function1RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); Function1RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function1 = new DefaultFunctionMetadata @@ -106,7 +112,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function1); var Function2RawBindings = new List(); - Function2RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function2RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); Function2RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function2 = new DefaultFunctionMetadata @@ -119,7 +125,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function2); var Function3RawBindings = new List(); - Function3RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function3RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); Function3RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function3 = new DefaultFunctionMetadata @@ -131,11 +137,40 @@ public Task> GetFunctionMetadataAsync(string d ScriptFile = "DependentAssemblyWithFunctions.dll" }; metadataList.Add(Function3); + var Function4RawBindings = new List(); + Function4RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function4RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function4 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "NestedNamespaceFunc1", + EntryPoint = "MyCompany.MyProduct.MyApp.HttpFunctions.Run", + RawBindings = Function4RawBindings, + ScriptFile = "DependentAssemblyWithFunctions.dll" + }; + metadataList.Add(Function4); + var Function5RawBindings = new List(); + Function5RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function5RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function5 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "NestedTypeFunc", + EntryPoint = "MyCompany.MyProduct.MyApp.Foo.Bar.Run", + RawBindings = Function5RawBindings, + ScriptFile = "DependentAssemblyWithFunctions.dll" + }; + metadataList.Add(Function5); return Task.FromResult(metadataList.ToImmutableArray()); } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs index d52f5cd29..9ac5e4f2a 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Reflection; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Testing; using Xunit; @@ -43,8 +44,14 @@ public DiagnosticResultTests() }; } - [Fact] - public async void MultipleOutputBindingsOnMethodFails() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void MultipleOutputBindingsOnMethodFails(LanguageVersion languageVersion) { var inputCode = @"using System; using System.Net; @@ -85,11 +92,18 @@ await TestHelpers.RunTestAsync( inputCode, expectedGeneratedFileName, expectedOutput, - expectedDiagnosticResults); + expectedDiagnosticResults, + languageVersion: languageVersion); } - [Fact] - public async void MultipleOutputBindingsOnPropertyFails() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void MultipleOutputBindingsOnPropertyFails(LanguageVersion languageVersion) { var inputCode = @"using System.Net; using Microsoft.Azure.Functions.Worker; @@ -139,11 +153,18 @@ await TestHelpers.RunTestAsync( inputCode, expectedGeneratedFileName, expectedOutput, - expectedDiagnosticResults); + expectedDiagnosticResults, + languageVersion: languageVersion); } - [Fact] - public async void MultipleHttpResponseBindingsFails() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void MultipleHttpResponseBindingsFails(LanguageVersion languageVersion) { var inputCode = @"using System; using System.Threading.Tasks; @@ -188,11 +209,18 @@ await TestHelpers.RunTestAsync( inputCode, expectedGeneratedFileName, expectedOutput, - expectedDiagnosticResults); + expectedDiagnosticResults, + languageVersion: languageVersion); } - [Fact] - public async void InvalidRetryOptionsFailure() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void InvalidRetryOptionsFailure(LanguageVersion languageVersion) { var inputCode = @"using System; using System.Threading.Tasks; @@ -225,7 +253,8 @@ await TestHelpers.RunTestAsync( inputCode, expectedGeneratedFileName, expectedOutput, - expectedDiagnosticResults); + expectedDiagnosticResults, + languageVersion: languageVersion); } } } diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs index 9423010c3..e7834b15a 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Testing; using Xunit; @@ -94,13 +95,19 @@ public class EventHubsInput namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""One""{{rawBindingSuffix}} + Function0RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""One""{{rawBindingSuffix}} """); @@ -120,6 +127,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -219,13 +229,19 @@ IEnumerator IEnumerable.GetEnumerator() namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many""{{rawBindingSuffix}} + Function0RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many""{{rawBindingSuffix}} """); @@ -245,6 +261,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -270,8 +289,14 @@ await TestHelpers.RunTestAsync( expectedOutputBuilder.ToString()); } - [Fact] - public async void EnumerableGenericInputFunction() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void EnumerableGenericInputFunction(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -311,13 +336,19 @@ public static void EnumerableBinaryInputFunction([EventHubTrigger("test", Con namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many""}"); + Function0RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many""}"); var Function0 = new DefaultFunctionMetadata { @@ -333,6 +364,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -355,11 +389,18 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async void EnumerableStringClassesAsInputFunctions() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void EnumerableStringClassesAsInputFunctions(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -450,13 +491,19 @@ public class EnumerableStringNestedGenericTestClass : EnumerableStringTestCl namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""String""}"); + Function0RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""String""}"); var Function0 = new DefaultFunctionMetadata { @@ -468,7 +515,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function0); var Function1RawBindings = new List(); - Function1RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""String""}"); + Function1RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""String""}"); var Function1 = new DefaultFunctionMetadata { @@ -480,7 +527,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function1); var Function2RawBindings = new List(); - Function2RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""String""}"); + Function2RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""String""}"); var Function2 = new DefaultFunctionMetadata { @@ -492,7 +539,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function2); var Function3RawBindings = new List(); - Function3RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""String""}"); + Function3RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""String""}"); var Function3 = new DefaultFunctionMetadata { @@ -508,6 +555,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -530,11 +580,18 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async void EnumerableBinaryClassesAsInputFunctions() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void EnumerableBinaryClassesAsInputFunctions(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -599,13 +656,19 @@ public class EnumerableBinaryNestedTestClass : EnumerableBinaryTestClass namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""Binary""}"); + Function0RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""Binary""}"); var Function0 = new DefaultFunctionMetadata { @@ -617,7 +680,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function0); var Function1RawBindings = new List(); - Function1RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""Binary""}"); + Function1RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many"",""dataType"":""Binary""}"); var Function1 = new DefaultFunctionMetadata { @@ -633,6 +696,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -655,11 +721,18 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async void PocoInputFunctions() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void PocoInputFunctions(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -711,13 +784,19 @@ public class Poco namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many""}"); + Function0RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many""}"); var Function0 = new DefaultFunctionMetadata { @@ -729,7 +808,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function0); var Function1RawBindings = new List(); - Function1RawBindings.Add(@"{""name"":""input"",""type"":""EventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many""}"); + Function1RawBindings.Add(@"{""name"":""input"",""type"":""eventHubTrigger"",""direction"":""In"",""eventHubName"":""test"",""connection"":""EventHubConnectionAppSetting"",""cardinality"":""Many""}"); var Function1 = new DefaultFunctionMetadata { @@ -745,6 +824,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -767,11 +849,18 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async void CardinalityManyWithNonIterableInputFails() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void CardinalityManyWithNonIterableInputFails(LanguageVersion languageVersion) { var inputCode = @"using System; using System.Net; @@ -810,7 +899,8 @@ await TestHelpers.RunTestAsync( inputCode, expectedGeneratedFileName, expectedOutput, - expectedDiagnosticResults); + expectedDiagnosticResults, + languageVersion: languageVersion); } } } diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs index b49afe09b..3aac634e1 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.CodeAnalysis.CSharp; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -36,8 +37,14 @@ public HttpTriggerTests() }; } - [Fact] - public async Task GenerateSimpleHttpTriggerMetadataTest() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task GenerateSimpleHttpTriggerMetadataTest(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -73,13 +80,19 @@ public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "g namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -96,6 +109,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -118,11 +134,18 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async void BasicHttpFunctionWithNoResponse() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void BasicHttpFunctionWithNoResponse(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -159,13 +182,19 @@ public static void HttpTrigger([HttpTrigger(AuthorizationLevel.Admin, "get", "po namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Admin"",""methods"":[""get"",""post""],""route"":""/api2""}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Admin"",""methods"":[""get"",""post""],""route"":""/api2""}"); var Function0 = new DefaultFunctionMetadata { @@ -181,6 +210,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -203,11 +235,18 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async void ReturnTypeJustHttp() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void ReturnTypeJustHttp(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -249,13 +288,19 @@ public class JustHttp namespace MyCompany.MyProject.MyApp { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""methods"":[""get""],""dataType"":""String""}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""methods"":[""get""],""dataType"":""String""}"); Function0RawBindings.Add(@"{""name"":""httpResponseProp"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -272,6 +317,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -300,7 +348,8 @@ await TestHelpers.RunTestAsync( inputCode, expectedGeneratedFileName, expectedOutput, - buildPropertiesDictionary: buildPropertiesDict); + buildPropertiesDictionary: buildPropertiesDict, + languageVersion: languageVersion); } } } diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs index 39c337fc4..f4d482c29 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs @@ -1,11 +1,9 @@ // 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.Reflection; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; -using Microsoft.CodeAnalysis.Testing; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -48,7 +46,7 @@ public IntegratedTriggersAndBindingsTests() } [Fact] - public async Task FunctionWhereOutputBindingIsInTheReturnType() + public async Task FunctionsWhereOutputBindingIsInTheReturnType() { // test generating function metadata for a simple HttpTrigger string inputCode = """ @@ -67,6 +65,13 @@ public static MyOutputType Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", { throw new NotImplementedException(); } + + [Function("OutputTypeNoHttpProp")] + public static MyOutputTypeNoHttpProp Test([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + FunctionContext context) + { + throw new NotImplementedException(); + } } public class MyOutputType @@ -76,6 +81,12 @@ public class MyOutputType public HttpResponseData HttpResponse { get; set; } } + + public class MyOutputTypeNoHttpProp + { + [QueueOutput("functionstesting2", Connection = "AzureWebJobsStorage")] + public string Name { get; set; } + } } """; @@ -95,14 +106,20 @@ public class MyOutputType namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); - Function0RawBindings.Add(@"{""name"":""Name"",""type"":""Queue"",""direction"":""Out"",""queueName"":""functionstesting2"",""connection"":""AzureWebJobsStorage""}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function0RawBindings.Add(@"{""name"":""Name"",""type"":""queue"",""direction"":""Out"",""queueName"":""functionstesting2"",""connection"":""AzureWebJobsStorage""}"); Function0RawBindings.Add(@"{""name"":""HttpResponse"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -114,11 +131,27 @@ public Task> GetFunctionMetadataAsync(string d ScriptFile = "TestProject.dll" }; metadataList.Add(Function0); + var Function1RawBindings = new List(); + Function1RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function1RawBindings.Add(@"{""name"":""Name"",""type"":""queue"",""direction"":""Out"",""queueName"":""functionstesting2"",""connection"":""AzureWebJobsStorage""}"); + + var Function1 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "OutputTypeNoHttpProp", + EntryPoint = "FunctionApp.HttpTriggerWithMultipleOutputBindings.Test", + RawBindings = Function1RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function1); return Task.FromResult(metadataList.ToImmutableArray()); } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -209,15 +242,21 @@ public class Book namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); - Function0RawBindings.Add(@"{""name"":""myBlob"",""type"":""Blob"",""direction"":""In"",""properties"":{""supportsDeferredBinding"":""True""},""blobPath"":""test-samples/sample1.txt"",""connection"":""AzureWebJobsStorage"",""dataType"":""String""}"); - Function0RawBindings.Add(@"{""name"":""Book"",""type"":""Queue"",""direction"":""Out"",""queueName"":""functionstesting2"",""connection"":""AzureWebJobsStorage""}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function0RawBindings.Add(@"{""name"":""myBlob"",""type"":""blob"",""direction"":""In"",""properties"":{""supportsDeferredBinding"":""True""},""blobPath"":""test-samples/sample1.txt"",""connection"":""AzureWebJobsStorage"",""dataType"":""String""}"); + Function0RawBindings.Add(@"{""name"":""Book"",""type"":""queue"",""direction"":""Out"",""queueName"":""functionstesting2"",""connection"":""AzureWebJobsStorage""}"); Function0RawBindings.Add(@"{""name"":""HttpResponse"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -234,6 +273,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -311,13 +353,19 @@ public FakeAttribute(string name) namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get""]}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get""]}"); Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -334,6 +382,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -398,13 +449,19 @@ public Task RunTimer([TimerTrigger("0 0 0 * * *", RunOnStartup = false)] object namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""timer"",""type"":""TimerTrigger"",""direction"":""In"",""schedule"":""0 0 0 * * *"",""runOnStartup"":""False""}"); + Function0RawBindings.Add(@"{""name"":""timer"",""type"":""timerTrigger"",""direction"":""In"",""schedule"":""0 0 0 * * *"",""runOnStartup"":""False""}"); var Function0 = new DefaultFunctionMetadata { @@ -420,6 +477,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -484,13 +544,19 @@ public Task Http([HttpTrigger(AuthorizationLevel.Admin, "get", namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""myReq"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Admin"",""methods"":[""get"",""Post""],""route"":""/api2""}"); + Function0RawBindings.Add(@"{""name"":""myReq"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Admin"",""methods"":[""get"",""Post""],""route"":""/api2""}"); Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -507,6 +573,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -580,13 +649,19 @@ public async Task RunAsync2([HttpTrigger(AuthorizationLevel.An namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -599,7 +674,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function0); var Function1RawBindings = new List(); - Function1RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function1RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); Function1RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function1 = new DefaultFunctionMetadata @@ -612,7 +687,7 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function1); var Function2RawBindings = new List(); - Function2RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function2RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); Function2RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function2 = new DefaultFunctionMetadata @@ -629,6 +704,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/NestedTypesTest.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/NestedTypesTest.cs index c85e0928a..c3d846bb0 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/NestedTypesTest.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/NestedTypesTest.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.CodeAnalysis.CSharp; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -35,8 +36,14 @@ public NestedTypesTests() }; } - [Fact] - public async Task NestedNamespace() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task NestedNamespace(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -78,13 +85,19 @@ public static HttpResponseData Run(HttpRequestData req) namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""User"",""methods"":[""get""]}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""User"",""methods"":[""get""]}"); Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -101,6 +114,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -123,10 +139,18 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async Task NestedClass() + + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task NestedClass(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -165,13 +189,19 @@ public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.User, "get")] namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""User"",""methods"":[""get""]}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""User"",""methods"":[""get""]}"); Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); var Function0 = new DefaultFunctionMetadata @@ -188,6 +218,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -210,7 +243,8 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } } } diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/RetryOptionsTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/RetryOptionsTests.cs index f8b122c85..be11deab7 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/RetryOptionsTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/RetryOptionsTests.cs @@ -5,6 +5,7 @@ using Microsoft.Azure.Functions.Worker.Sdk.Generators; using System.Threading.Tasks; using Xunit; +using Microsoft.CodeAnalysis.CSharp; namespace Microsoft.Azure.Functions.SdkGeneratorTests { @@ -40,8 +41,14 @@ public RetryOptionsTests() }; } - [Fact] - public async Task FixedDelayRetryPopulated_Success() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task FixedDelayRetryPopulated_Success(LanguageVersion languageVersion) { // test generating function metadata for a simple HttpTrigger string inputCode = """ @@ -79,13 +86,19 @@ public static void Run([TimerTrigger("0 */5 * * * *")] TimerInfo timerInfo, namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""timerInfo"",""type"":""TimerTrigger"",""direction"":""In"",""schedule"":""0 */5 * * * *""}"); + Function0RawBindings.Add(@"{""name"":""timerInfo"",""type"":""timerTrigger"",""direction"":""In"",""schedule"":""0 */5 * * * *""}"); var Function0 = new DefaultFunctionMetadata { @@ -105,7 +118,10 @@ public Task> GetFunctionMetadataAsync(string d return Task.FromResult(metadataList.ToImmutableArray()); } } - + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -128,11 +144,18 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async Task ExponentialBackoffRetryPopulated_Success() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async Task ExponentialBackoffRetryPopulated_Success(LanguageVersion languageVersion) { // test generating function metadata for a simple HttpTrigger string inputCode = """ @@ -169,13 +192,19 @@ public static void Run([TimerTrigger("0 */5 * * * *")] TimerInfo timerInfo, namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""timerInfo"",""type"":""TimerTrigger"",""direction"":""In"",""schedule"":""0 */5 * * * *""}"); + Function0RawBindings.Add(@"{""name"":""timerInfo"",""type"":""timerTrigger"",""direction"":""In"",""schedule"":""0 */5 * * * *""}"); var Function0 = new DefaultFunctionMetadata { @@ -196,7 +225,10 @@ public Task> GetFunctionMetadataAsync(string d return Task.FromResult(metadataList.ToImmutableArray()); } } - + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -219,7 +251,8 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } } } diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs index e5028230b..e29b7ce18 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs @@ -1,10 +1,9 @@ // 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.Reflection; using Microsoft.Azure.Functions.Worker.Sdk.Generators; -using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.CSharp; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -42,8 +41,14 @@ public StorageBindingTests() }; } - [Fact] - public async void TestQueueTriggerAndOutput() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void TestQueueTriggerAndOutput(LanguageVersion languageVersion) { string inputCode = """ using System.Collections.Generic; @@ -82,14 +87,20 @@ public string QueueTriggerAndOutputFunction([QueueTrigger("test-input-dotnet-iso namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""$return"",""type"":""Queue"",""direction"":""Out"",""queueName"":""test-output-dotnet-isolated""}"); - Function0RawBindings.Add(@"{""name"":""message"",""type"":""QueueTrigger"",""direction"":""In"",""queueName"":""test-input-dotnet-isolated"",""dataType"":""String""}"); + Function0RawBindings.Add(@"{""name"":""$return"",""type"":""queue"",""direction"":""Out"",""queueName"":""test-output-dotnet-isolated""}"); + Function0RawBindings.Add(@"{""name"":""message"",""type"":""queueTrigger"",""direction"":""In"",""queueName"":""test-input-dotnet-isolated"",""dataType"":""String""}"); var Function0 = new DefaultFunctionMetadata { @@ -105,6 +116,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -127,11 +141,18 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); } - [Fact] - public async void TestBlobAndQueueInputsAndOutputs() + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void TestBlobAndQueueInputsAndOutputs(LanguageVersion languageVersion) { string inputCode = """ using System; @@ -188,14 +209,20 @@ public object BlobsToQueue( namespace TestProject { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider { + /// public Task> GetFunctionMetadataAsync(string directory) { var metadataList = new List(); var Function0RawBindings = new List(); - Function0RawBindings.Add(@"{""name"":""$return"",""type"":""Blob"",""direction"":""Out"",""blobPath"":""container1/hello.txt"",""connection"":""MyOtherConnection""}"); - Function0RawBindings.Add(@"{""name"":""queuePayload"",""type"":""QueueTrigger"",""direction"":""In"",""queueName"":""queueName"",""connection"":""MyConnection"",""dataType"":""String""}"); + Function0RawBindings.Add(@"{""name"":""$return"",""type"":""blob"",""direction"":""Out"",""blobPath"":""container1/hello.txt"",""connection"":""MyOtherConnection""}"); + Function0RawBindings.Add(@"{""name"":""queuePayload"",""type"":""queueTrigger"",""direction"":""In"",""queueName"":""queueName"",""connection"":""MyConnection"",""dataType"":""String""}"); var Function0 = new DefaultFunctionMetadata { @@ -207,8 +234,8 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function0); var Function1RawBindings = new List(); - Function1RawBindings.Add(@"{""name"":""$return"",""type"":""Queue"",""direction"":""Out"",""queueName"":""queue2""}"); - Function1RawBindings.Add(@"{""name"":""blob"",""type"":""BlobTrigger"",""direction"":""In"",""properties"":{""supportsDeferredBinding"":""True""},""path"":""container2/%file%"",""source"":""EventGrid"",""dataType"":""String""}"); + Function1RawBindings.Add(@"{""name"":""$return"",""type"":""queue"",""direction"":""Out"",""queueName"":""queue2""}"); + Function1RawBindings.Add(@"{""name"":""blob"",""type"":""blobTrigger"",""direction"":""In"",""properties"":{""supportsDeferredBinding"":""True""},""path"":""container2/%file%"",""source"":""EventGrid"",""dataType"":""String""}"); var Function1 = new DefaultFunctionMetadata { @@ -220,8 +247,8 @@ public Task> GetFunctionMetadataAsync(string d }; metadataList.Add(Function1); var Function2RawBindings = new List(); - Function2RawBindings.Add(@"{""name"":""$return"",""type"":""Queue"",""direction"":""Out"",""queueName"":""queue2""}"); - Function2RawBindings.Add(@"{""name"":""blobs"",""type"":""Blob"",""direction"":""In"",""properties"":{""supportsDeferredBinding"":""True""},""blobPath"":""container2""}"); + Function2RawBindings.Add(@"{""name"":""$return"",""type"":""queue"",""direction"":""Out"",""queueName"":""queue2""}"); + Function2RawBindings.Add(@"{""name"":""blobs"",""type"":""blob"",""direction"":""In"",""properties"":{""supportsDeferredBinding"":""True""},""blobPath"":""container2""}"); var Function2 = new DefaultFunctionMetadata { @@ -237,6 +264,9 @@ public Task> GetFunctionMetadataAsync(string d } } + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// public static class WorkerHostBuilderFunctionMetadataProviderExtension { /// @@ -259,7 +289,113 @@ await TestHelpers.RunTestAsync( _referencedExtensionAssemblies, inputCode, expectedGeneratedFileName, - expectedOutput); + expectedOutput, + languageVersion: languageVersion); + } + + [Theory] + [InlineData(LanguageVersion.CSharp7_3)] + [InlineData(LanguageVersion.CSharp8)] + [InlineData(LanguageVersion.CSharp9)] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + [InlineData(LanguageVersion.Latest)] + public async void TestQueueOutputWithHttpTrigger(LanguageVersion languageVersion) + { + string inputCode = """ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Text.Json.Serialization; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Http; + + namespace FunctionApp + { + public class HttpTriggerQueueOutput + { + [Function("HttpWithQueueOutput")] + [QueueOutput("myqueue", Connection = "Con")] + public string Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + throw new NotImplementedException(); + } + } + } + """; + + string expectedGeneratedFileName = $"GeneratedFunctionMetadataProvider.g.cs"; + string expectedOutput = """ + // + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Text.Json; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + + namespace TestProject + { + /// + /// Custom implementation that returns function metadata definitions for the current worker."/> + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider + { + /// + public Task> GetFunctionMetadataAsync(string directory) + { + var metadataList = new List(); + var Function0RawBindings = new List(); + Function0RawBindings.Add(@"{""name"":""$return"",""type"":""queue"",""direction"":""Out"",""queueName"":""myqueue"",""connection"":""Con""}"); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""httpTrigger"",""direction"":""In"",""authLevel"":""Function"",""methods"":[""get"",""post""]}"); + + var Function0 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "HttpWithQueueOutput", + EntryPoint = "FunctionApp.HttpTriggerQueueOutput.Run", + RawBindings = Function0RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function0); + + return Task.FromResult(metadataList.ToImmutableArray()); + } + } + + /// + /// Extension methods to enable registration of the custom implementation generated for the current worker. + /// + public static class WorkerHostBuilderFunctionMetadataProviderExtension + { + /// + /// Adds the GeneratedFunctionMetadataProvider to the service collection. + /// During initialization, the worker will return generated function metadata instead of relying on the Azure Functions host for function indexing. + /// + public static IHostBuilder ConfigureGeneratedFunctionMetadataProvider(this IHostBuilder builder) + { + builder.ConfigureServices(s => + { + s.AddSingleton(); + }); + return builder; + } + } + } + """; + + await TestHelpers.RunTestAsync( + _referencedExtensionAssemblies, + inputCode, + expectedGeneratedFileName, + expectedOutput, + languageVersion: languageVersion); } } } diff --git a/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj b/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj index 838a3f7ea..8feb33d9a 100644 --- a/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj +++ b/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj @@ -36,6 +36,7 @@ + diff --git a/test/Sdk.Generator.Tests/TestHelpers.cs b/test/Sdk.Generator.Tests/TestHelpers.cs index c1d015ebc..4b0aae6f6 100644 --- a/test/Sdk.Generator.Tests/TestHelpers.cs +++ b/test/Sdk.Generator.Tests/TestHelpers.cs @@ -15,21 +15,30 @@ using Microsoft.Azure.Functions.Worker.Core; using System.Reflection; using System.Collections.Generic; +using System.Linq; +using System.Runtime.Versioning; namespace Microsoft.Azure.Functions.SdkGeneratorTests { static class TestHelpers { + // Default language version is the lowest version we support.(C# 7.3 which is default for .NET Framework) + // See full matrix here: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/configure-language-version + private const LanguageVersion _defaultLanguageVersion = LanguageVersion.CSharp7_3; + public static Task RunTestAsync( IEnumerable extensionAssemblyReferences, string inputSource, string? expectedFileName, string? expectedOutputSource, List? expectedDiagnosticResults = null, - IDictionary? buildPropertiesDictionary = null) where TSourceGenerator : ISourceGenerator, new() + IDictionary? buildPropertiesDictionary = null, + string? generatedCodeNamespace = null, + LanguageVersion? languageVersion = null) where TSourceGenerator : ISourceGenerator, new() { CSharpSourceGeneratorVerifier.Test test = new() { + LanguageVersion = languageVersion ?? _defaultLanguageVersion, TestState = { Sources = { inputSource }, @@ -49,7 +58,7 @@ public static Task RunTestAsync( var config = $@"is_global = true build_property.FunctionsEnableExecutorSourceGen = {true} build_property.FunctionsEnableMetadataSourceGen = {true} - build_property.RootNamespace = TestProject"; + build_property.FunctionsGeneratedCodeNamespace = {generatedCodeNamespace ?? "TestProject"}"; // Add test specific MSBuild properties. if (buildPropertiesDictionary is not null) @@ -61,6 +70,7 @@ public static Task RunTestAsync( } } + test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", config)); foreach (var item in extensionAssemblyReferences) @@ -86,11 +96,16 @@ public class Test : CSharpSourceGeneratorTest { public Test() { - // See https://www.nuget.org/packages/Microsoft.NETCore.App.Ref/6.0.0 + var targetFrameworkAttribute = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(TargetFrameworkAttribute), false) + .SingleOrDefault() as TargetFrameworkAttribute; + + string targetFramework = targetFrameworkAttribute!.FrameworkName; + var tfm = ConvertFrameworkMonikerToTfm(targetFramework); + this.ReferenceAssemblies = new ReferenceAssemblies( - targetFramework: "net6.0", - referenceAssemblyPackage: new PackageIdentity("Microsoft.NETCore.App.Ref", "6.0.0"), - referenceAssemblyPath: Path.Combine("ref", "net6.0")); + targetFramework: tfm, + referenceAssemblyPackage: new PackageIdentity("Microsoft.NETCore.App.Ref", Environment.Version.ToString()), + referenceAssemblyPath: Path.Combine("ref", tfm)); } public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.CSharp9; @@ -118,6 +133,29 @@ protected override ParseOptions CreateParseOptions() { return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(this.LanguageVersion); } + + /// + /// Example input: .NETCoreApp,Version=v7.0 + /// Example output: net7.0 + /// + private static string ConvertFrameworkMonikerToTfm(string frameworkMoniker) + { + var parts = frameworkMoniker.Split(','); + var identifier = parts[0]; + var version = parts[1].Split('=')[1]; + + switch (identifier) + { + case ".NETCoreApp": + return $"net{version.Substring(1)}"; + case ".NETFramework": + return $"net{version.Replace(".", "")}"; + case ".NETStandard": + return $"netstandard{version.Substring(1)}"; + default: + throw new NotSupportedException($"Unknown framework identifier: {identifier}"); + } + } } } } diff --git a/test/Worker.Extensions.Tests/Blob/ByteArrayTests.cs b/test/Worker.Extensions.Tests/Blob/ByteArrayTests.cs index 2d6073f44..73f0debe3 100644 --- a/test/Worker.Extensions.Tests/Blob/ByteArrayTests.cs +++ b/test/Worker.Extensions.Tests/Blob/ByteArrayTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -251,14 +251,10 @@ public async Task ConvertAsync_ByteArrayCollection_WithFilePath_ValidContent_Ret var jsonString = JsonConvert.SerializeObject(new List { Encoding.UTF8.GetBytes("Item1"), Encoding.UTF8.GetBytes("Item2") }); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonString)); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List{ BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); @@ -288,14 +284,10 @@ public async Task ConvertAsync_ByteArrayCollection_WithFilePath_InvalidContent_R var context = new TestConverterContext(typeof(byte[][]), grpcModelBindingData); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes("[1,2]")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List{ BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); diff --git a/test/Worker.Extensions.Tests/Blob/POCOTests.cs b/test/Worker.Extensions.Tests/Blob/POCOTests.cs index caf9383e3..8a20c5b22 100644 --- a/test/Worker.Extensions.Tests/Blob/POCOTests.cs +++ b/test/Worker.Extensions.Tests/Blob/POCOTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -57,14 +57,10 @@ public async Task ConvertAsync_POCO_WithFilePath_ReturnsSuccess() var expectedBook = new Book() { Name = "MyBook" }; var testStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"Name\":\"MyBook\"}")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(testStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(testStream); var mockContainer = new Mock(); mockContainer.Setup(m => m.GetBlobClient(It.IsAny())).Returns(mockBlobClient.Object); @@ -91,14 +87,10 @@ public async Task ConvertAsync_POCO_FilePathWithoutFileExtension_ReturnsSuccess( var expectedBook = new Book() { Name = "MyBook" }; var testStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"Name\":\"MyBook\"}")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(testStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(testStream); var mockContainer = new Mock(); mockContainer.Setup(m => m.GetBlobClient(It.IsAny())).Returns(mockBlobClient.Object); @@ -144,14 +136,10 @@ public async Task ConvertAsync_POCO_InvalidJson_ReturnsFailed() var expectedBook = new Book() { Name = "MyBook" }; var testStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"Name:\"MyBook\"}")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(testStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(testStream); var mockContainer = new Mock(); mockContainer.Setup(m => m.GetBlobClient(It.IsAny())).Returns(mockBlobClient.Object); @@ -177,15 +165,10 @@ public async Task ConvertAsync_POCOCollection_List_WithContainerPath_ReturnsSucc var expectedBookList = new List() { new Book() { Name = "MyBook" } }; var testStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"Name\":\"MyBook\"}")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(testStream); - - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(testStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List { BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); @@ -216,15 +199,10 @@ public async Task ConvertAsync_POCOCollection_Array_WithContainerPath_ReturnsSuc var expectedBookList = new Book[] { new Book() { Name = "MyBook" } }; var testStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"Name\":\"MyBook\"}")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(testStream); - - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(testStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List { BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); @@ -255,15 +233,10 @@ public async Task ConvertAsync_POCOCollection_InvalidJson_ReturnsFailed() var expectedBookList = new Book[] { new Book() { Name = "MyBook" } }; var testStream = new MemoryStream(Encoding.UTF8.GetBytes("i should fail")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(testStream); - - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(testStream); var blobItemMockResponse = new Mock(); var expectedOutput = Page.FromValues(new List { BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, blobItemMockResponse.Object); @@ -294,14 +267,10 @@ public async Task ConvertAsync_POCOCollection_WithFilePath_ValidContent_ReturnsS var jsonString = JsonConvert.SerializeObject(new List { new { Name = "MyBook" }, new { Name = "MySecondBook" }}); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonString)); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List{ BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); @@ -332,14 +301,10 @@ public async Task ConvertAsync_POCOCollection_WithFilePath_InvalidContent_Return var context = new TestConverterContext(typeof(Book[]), grpcModelBindingData); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes("[1,2]")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List{ BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); diff --git a/test/Worker.Extensions.Tests/Blob/StreamTests.cs b/test/Worker.Extensions.Tests/Blob/StreamTests.cs index 0fc8323e7..d7713f368 100644 --- a/test/Worker.Extensions.Tests/Blob/StreamTests.cs +++ b/test/Worker.Extensions.Tests/Blob/StreamTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -54,14 +54,10 @@ public async Task ConvertAsync_Stream_WithFilePath_ReturnsSuccess() var context = new TestConverterContext(typeof(Stream), grpcModelBindingData); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes("MyBlobStream")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockContainer = new Mock(); mockContainer.Setup(m => m.GetBlobClient(It.IsAny())).Returns(mockBlobClient.Object); @@ -85,14 +81,10 @@ public async Task ConvertAsync_Stream_FilePathWithoutFileExtension_ReturnsSucces var context = new TestConverterContext(typeof(Stream), grpcModelBindingData); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes("MyBlobStream")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockContainer = new Mock(); mockContainer.Setup(m => m.GetBlobClient(It.IsAny())).Returns(mockBlobClient.Object); @@ -136,15 +128,10 @@ public async Task ConvertAsync_StreamCollection_List_WithContainerPath_ReturnsSu var context = new TestConverterContext(typeof(IEnumerable), grpcModelBindingData); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes("MyBlobStream")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List{ BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); @@ -174,15 +161,10 @@ public async Task ConvertAsync_StreamCollection_Array_WithContainerPath_ReturnsS var context = new TestConverterContext(typeof(Stream[]), grpcModelBindingData); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes("MyBlobStream")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List{ BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); @@ -211,15 +193,10 @@ public async Task ConvertAsync_StreamCollection_WithSubdirectoryPath_ReturnsSucc var context = new TestConverterContext(typeof(Stream[]), grpcModelBindingData); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes("MyBlobStream")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List{ BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); @@ -248,14 +225,10 @@ public async Task ConvertAsync_StreamCollection_WithFilePath_Throws_ReturnsFaile var context = new TestConverterContext(typeof(Stream[]), grpcModelBindingData); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes("[{\"name\": \"Stream1\"}]")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List{ BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); diff --git a/test/Worker.Extensions.Tests/Blob/StringTests.cs b/test/Worker.Extensions.Tests/Blob/StringTests.cs index bda0bb2e6..cafe5be8c 100644 --- a/test/Worker.Extensions.Tests/Blob/StringTests.cs +++ b/test/Worker.Extensions.Tests/Blob/StringTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -199,14 +199,10 @@ public async Task ConvertAsync_StringCollection_WithFilePath_ValidContent_Return var jsonString = JsonConvert.SerializeObject(new List { "1", "2" }); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonString)); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List { BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); @@ -237,14 +233,10 @@ public async Task ConvertAsync_StringCollection_WithFilePath_InvalidContent_Thro var context = new TestConverterContext(typeof(string[]), grpcModelBindingData); var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes("[1,2]")); - var blobDownloadResult = BlobsModelFactory.BlobDownloadStreamingResult(expectedStream); - var mockResponse = new Mock>(); - mockResponse.SetupGet(r => r.Value).Returns(blobDownloadResult); - var mockBlobClient = new Mock(); mockBlobClient - .Setup(m => m.DownloadStreamingAsync(It.IsAny(), It.IsAny(), It.IsAny(), null, default)) - .ReturnsAsync(mockResponse.Object); + .Setup(m => m.OpenReadAsync(0, default, default, default)) + .ReturnsAsync(expectedStream); var mockBlobItemResponse = new Mock(); var expectedOutput = Page.FromValues(new List { BlobsModelFactory.BlobItem("MyBlob") }, continuationToken: null, mockBlobItemResponse.Object); diff --git a/test/Worker.Extensions.Tests/ServiceBus/ServiceBusMessageActionsTests.cs b/test/Worker.Extensions.Tests/ServiceBus/ServiceBusMessageActionsTests.cs new file mode 100644 index 000000000..04096f40a --- /dev/null +++ b/test/Worker.Extensions.Tests/ServiceBus/ServiceBusMessageActionsTests.cs @@ -0,0 +1,133 @@ +// 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 System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Microsoft.Azure.ServiceBus.Grpc; +using Xunit; + +namespace Microsoft.Azure.Functions.Worker.Extensions.Tests +{ + public class ServiceBusMessageActionsTests + { + [Fact] + public async Task CanCompleteMessage() + { + var message = ServiceBusModelFactory.ServiceBusReceivedMessage(lockTokenGuid: Guid.NewGuid()); + var messageActions = new ServiceBusMessageActions(new MockSettlementClient(message.LockToken)); + await messageActions.CompleteMessageAsync(message); + } + + [Fact] + public async Task CanAbandonMessage() + { + var message = ServiceBusModelFactory.ServiceBusReceivedMessage(lockTokenGuid: Guid.NewGuid()); + var properties = new Dictionary() + { + { "int", 1 }, + { "string", "foo"}, + { "timespan", TimeSpan.FromSeconds(1) }, + { "datetime", DateTime.UtcNow }, + { "datetimeoffset", DateTimeOffset.UtcNow }, + { "guid", Guid.NewGuid() } + }; + var messageActions = new ServiceBusMessageActions(new MockSettlementClient(message.LockToken, properties)); + await messageActions.AbandonMessageAsync(message, properties); + } + + [Fact] + public async Task CanDeadLetterMessage() + { + var message = ServiceBusModelFactory.ServiceBusReceivedMessage(lockTokenGuid: Guid.NewGuid()); + var properties = new Dictionary() + { + { "int", 1 }, + { "string", "foo"}, + { "timespan", TimeSpan.FromSeconds(1) }, + { "datetime", DateTime.UtcNow }, + { "datetimeoffset", DateTimeOffset.UtcNow }, + { "guid", Guid.NewGuid() } + }; + var messageActions = new ServiceBusMessageActions(new MockSettlementClient(message.LockToken, properties)); + await messageActions.DeadLetterMessageAsync(message, properties); + } + + [Fact] + public async Task CanDeferMessage() + { + var message = ServiceBusModelFactory.ServiceBusReceivedMessage(lockTokenGuid: Guid.NewGuid()); + var properties = new Dictionary() + { + { "int", 1 }, + { "string", "foo"}, + { "timespan", TimeSpan.FromSeconds(1) }, + { "datetime", DateTime.UtcNow }, + { "datetimeoffset", DateTimeOffset.UtcNow }, + { "guid", Guid.NewGuid() } + }; + var messageActions = new ServiceBusMessageActions(new MockSettlementClient(message.LockToken, properties)); + await messageActions.DeferMessageAsync(message, properties); + } + + [Fact] + public async Task PassingNullMessageThrows() + { + var messageActions = new ServiceBusMessageActions(new MockSettlementClient(null)); + await Assert.ThrowsAsync(async () => await messageActions.CompleteMessageAsync(null)); + await Assert.ThrowsAsync(async () => await messageActions.AbandonMessageAsync(null)); + await Assert.ThrowsAsync(async () => await messageActions.DeadLetterMessageAsync(null)); + await Assert.ThrowsAsync(async () => await messageActions.DeferMessageAsync(null)); + } + + private class MockSettlementClient : Settlement.SettlementClient + { + private readonly string _lockToken; + private readonly ByteString _propertiesToModify; + public MockSettlementClient(string lockToken, IDictionary propertiesToModify = default) : base() + { + _lockToken = lockToken; + if (propertiesToModify != null) + { + _propertiesToModify = ServiceBusMessageActions.ConvertToByteString(propertiesToModify); + } + } + + public override AsyncUnaryCall CompleteAsync(CompleteRequest request, Metadata headers = null, DateTime? deadline = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + Assert.Equal(_lockToken, request.Locktoken); + return new AsyncUnaryCall(Task.FromResult(new Empty()), Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { }); + } + + public override AsyncUnaryCall AbandonAsync(AbandonRequest request, Metadata headers = null, DateTime? deadline = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + Assert.Equal(_lockToken, request.Locktoken); + Assert.Equal(_propertiesToModify, request.PropertiesToModify); + return new AsyncUnaryCall(Task.FromResult(new Empty()), Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { }); + } + + public override AsyncUnaryCall DeadletterAsync(DeadletterRequest request, Metadata headers = null, DateTime? deadline = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + Assert.Equal(_lockToken, request.Locktoken); + Assert.Equal(_propertiesToModify, request.PropertiesToModify); + return new AsyncUnaryCall(Task.FromResult(new Empty()), Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { }); + } + + public override AsyncUnaryCall DeferAsync(DeferRequest request, Metadata headers = null, DateTime? deadline = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + Assert.Equal(_lockToken, request.Locktoken); + Assert.Equal(_propertiesToModify, request.PropertiesToModify); + return new AsyncUnaryCall(Task.FromResult(new Empty()), Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { }); + } + } + } +} \ No newline at end of file diff --git a/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/AspNetCoreHttpRequestDataTests.cs b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/AspNetCoreHttpRequestDataTests.cs new file mode 100644 index 000000000..908eb4b0e --- /dev/null +++ b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/AspNetCoreHttpRequestDataTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore; +using Microsoft.Azure.Functions.Worker.Tests; + +namespace Worker.Extensions.Http.AspNetCore.Tests +{ + public class AspNetCoreHttpRequestDataTests + { + [Fact] + public void RequestData_ExposesRequestProperties() + { + var uri = "http://localhost:808/test/123?query=test"; + var request = CreateRequest(uri, "POST", "Hello World"); + + var testIdentity = new ClaimsIdentity(); + var testUser = new ClaimsPrincipal(testIdentity); + request.HttpContext.User = testUser; + + var requestData = new AspNetCoreHttpRequestData(request, new TestFunctionContext()); + + Assert.Equal(uri, requestData.Url.AbsoluteUri); + Assert.Same(request.Body, requestData.Body); + Assert.Same(testIdentity, requestData.Identities.Single()); + Assert.Equal(request.Method, requestData.Method); + } + + [Fact] + public void RequestData_ExposesRequestCookies() + { + var uri = "http://localhost:808/test/123?query=test"; + var request = CreateRequest(uri, "POST", "Hello World"); + request.Cookies = new CookiesCollection + { + { "cookie1", "value1" }, + { "cookie2", "value2" } + }; + + var requestData = new AspNetCoreHttpRequestData(request, new TestFunctionContext()); + + Assert.Collection(requestData.Cookies, + c => Assert.Equal("cookie1:value1", $"{c.Name}:{c.Value}"), + c => Assert.Equal("cookie2:value2", $"{c.Name}:{c.Value}")); + } + + private static HttpRequest CreateRequest(string uri, string method, string body) + { + var request = new DefaultHttpContext().Request; + var uriBuilder = new UriBuilder(uri); + + request.Scheme = uriBuilder.Scheme; + request.Host = new HostString(uriBuilder.Host, uriBuilder.Port); + request.Path = uriBuilder.Path; + request.Method = method; + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); + request.QueryString = new QueryString(uriBuilder.Query); + + return request; + } + } +} diff --git a/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/CookiesCollection.cs b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/CookiesCollection.cs new file mode 100644 index 000000000..c022665a8 --- /dev/null +++ b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/CookiesCollection.cs @@ -0,0 +1,61 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http; + +namespace Worker.Extensions.Http.AspNetCore.Tests +{ + internal class CookiesCollection : IRequestCookieCollection, IDictionary + { + private readonly Dictionary _cookies = new(); + + public string? this[string key] => _cookies[key]; + + string IDictionary.this[string key] + { + get => _cookies[key]; + set => _cookies[key] = value; + } + + public int Count => _cookies.Count; + + public ICollection Keys => _cookies.Keys; + + public ICollection Values => _cookies.Values; + + public bool IsReadOnly => ((ICollection>)_cookies).IsReadOnly; + + public void Add(string key, string value) => _cookies.Add(key, value); + + public void Add(KeyValuePair item) => _cookies.Add(item.Key, item.Value); + + public void Clear()=> _cookies.Clear(); + + public bool Contains(KeyValuePair item) => _cookies.Contains(item); + + public bool ContainsKey(string key) => _cookies.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((ICollection>)_cookies).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + => _cookies.GetEnumerator(); + + public bool Remove(string key) + { + return ((IDictionary)_cookies).Remove(key); + } + + public bool Remove(KeyValuePair item) => _cookies.Remove(item.Key); + + public bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + => _cookies.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() + => _cookies.GetEnumerator(); + } +} diff --git a/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/FunctionsHostBuilderExtensionsTests.cs b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/FunctionsHostBuilderExtensionsTests.cs new file mode 100644 index 000000000..bc5556666 --- /dev/null +++ b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/FunctionsHostBuilderExtensionsTests.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore; +using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.AspNetMiddleware; +using Microsoft.Azure.Functions.Worker.Middleware; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Worker.Extensions.Http.AspNetCore.Tests +{ + public class FunctionsHostBuilderExtensionsTests + { + [Fact] + public void ConfigureFunctionsWebApplication_ShouldConfigureFunctionsWebApplicationWithoutAction() + { + var underTest = new HostBuilder(); + underTest.ConfigureFunctionsWebApplication(); + var host = underTest.Build(); + VerifyRegistrationOfAspNetCoreIntegrationServices(host); + } + + [Fact] + public void ConfigureFunctionsWebApplication_ShouldConfigureFunctionsWebApplicationWithActionForBuilder() + { + var underTest = new HostBuilder(); + underTest.ConfigureFunctionsWebApplication(builder => builder.UseMiddleware()); + var host = underTest.Build(); + VerifyRegistrationOfCustomMiddleware(host); + VerifyRegistrationOfAspNetCoreIntegrationServices(host); + } + + [Fact] + public void ConfigureFunctionsWebApplication_ShouldConfigureFunctionsWebApplicationWithActionForContext() + { + const string expectedConfigValue = "test_config_value"; + const string configKey = "test_config_key"; + + var underTest = new HostBuilder(); + underTest.ConfigureHostConfiguration(builder => + { + builder.Add(new MemoryConfigurationSource { InitialData = new Dictionary { { configKey, expectedConfigValue } } }); + }); + string? actualConfigValue = null; + + underTest.ConfigureFunctionsWebApplication((context, builder) => + { + actualConfigValue = context.Configuration.GetValue(configKey); + builder.UseMiddleware(); + }); + var host = underTest.Build(); + + Assert.Equal(expectedConfigValue, actualConfigValue); + VerifyRegistrationOfCustomMiddleware(host); + VerifyRegistrationOfAspNetCoreIntegrationServices(host); + } + + private static void VerifyRegistrationOfCustomMiddleware(IHost host) + { + Assert.NotNull(host.Services.GetService()); + } + + private static void VerifyRegistrationOfAspNetCoreIntegrationServices(IHost host) + { + Assert.NotNull(host.Services.GetService()); + Assert.NotNull(host.Services.GetService()); + var httpCoordinator = host.Services.GetService(); + Assert.NotNull(httpCoordinator); + Assert.IsType(httpCoordinator); + } + + private class TestMiddleware : IFunctionsWorkerMiddleware + { + public Task Invoke(FunctionContext context, FunctionExecutionDelegate next) + { + return Task.CompletedTask; + } + } + } +} diff --git a/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/GlobalUsings.cs b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/GlobalUsings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/RegistrationExpectedInAspNetIntegrationTests.cs b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/RegistrationExpectedInAspNetIntegrationTests.cs new file mode 100644 index 000000000..675b2ccb4 --- /dev/null +++ b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/RegistrationExpectedInAspNetIntegrationTests.cs @@ -0,0 +1,420 @@ +using AnalyzerTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest; +using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier; +using CodeFixTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixTest; +using CodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier; +using Microsoft.CodeAnalysis.Testing; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Tests +{ + public class RegistrationExpectedInAspNetIntegrationTests + { + private const string ExpectedRegistrationMethod = "ConfigureFunctionsWebApplication"; + + [Fact] + public async Task AspNetIntegration_MissingRegistration_Diagnostics_Expected() + { + string testCode = @" + namespace AspNetIntegration + { + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + class Program + { + static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + + host.Run(); + } + } + }"; + + var test = new AnalyzerTest + { + ReferenceAssemblies = LoadRequiredDependencyAssemblies(), + TestCode = testCode + }; + + test.ExpectedDiagnostics.Add(Verifier.Diagnostic() + .WithSeverity(DiagnosticSeverity.Error) + .WithSpan(16, 34, 16, 66) + .WithArguments(ExpectedRegistrationMethod)); + + await test.RunAsync(); + } + + + [Fact] + public async Task AspNetIntegration_CommentedRegistration_Diagnostics_NotExpected() + { + string testCode = @" + namespace AspNetIntegration + { + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + class Program + { + static void Main(string[] args) + { + var host = new HostBuilder() + //.ConfigureFunctionsWorkerDefaults() + //.ConfigureFunctionsWebApplication() + .Build(); + + host.Run(); + } + } + }"; + + var test = new AnalyzerTest + { + ReferenceAssemblies = LoadRequiredDependencyAssemblies(), + TestCode = testCode + }; + + // test.ExpectedDiagnostics is an empty collection because ConfigureFunctionsWorkerDefaults() is not present + + await test.RunAsync(); + } + + [Fact] + public async Task AspNetIntegration_EmptyMain_Diagnostics_NotExpected() + { + string testCode = @" + namespace AspNetIntegration + { + class Program + { + static void Main(string[] args) + { + } + } + }"; + + var test = new AnalyzerTest + { + ReferenceAssemblies = LoadRequiredDependencyAssemblies(), + TestCode = testCode + }; + + // test.ExpectedDiagnostics is an empty collection because ConfigureFunctionsWorkerDefaults() is not present + + await test.RunAsync(); + } + + [Fact] + public async Task AspNetIntegrationWithTrigger_MissingRegistration_Diagnostics_Expected() + { + string testCode = @" + namespace AspNetIntegration + { + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + class Program + { + static void Main(string[] args) + { + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + + host.Run(); + } + } + } + + namespace AspNetIntegration + { + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Http; + + public class FunctionHttpTrigger + { + [Function(nameof(FunctionHttpTrigger))] + public void Run([HttpTrigger(AuthorizationLevel.Anonymous, ""post"")] HttpRequestData req) + { + } + } + }"; + + var test = new AnalyzerTest + { + ReferenceAssemblies = LoadRequiredDependencyAssemblies(), + TestCode = testCode + }; + + test.ExpectedDiagnostics.Add(Verifier.Diagnostic() + .WithSeverity(DiagnosticSeverity.Error) + .WithSpan(16, 34, 16, 66) + .WithArguments(ExpectedRegistrationMethod)); + + await test.RunAsync(); + } + + [Fact] + public async Task AspNetIntegration_WithRegistration_Diagnostics_NotExpected() + { + string testCode = @" + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + namespace AspNetIntegration + { + class Program + { + static void Main(string[] args) + { + + // + var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .Build(); + + host.Run(); + // + } + + public static void Method1() + { + } + + private static void Method2() + { + } + } + }"; + + var test = new AnalyzerTest + { + ReferenceAssemblies = LoadRequiredDependencyAssemblies(), + TestCode = testCode + }; + + // test.ExpectedDiagnostics is an empty collection. + + await test.RunAsync(); + } + + [Fact] + public async Task AspNetIntegration_WithMiddleWare_WithRegistration_Diagnostics_NotExpected() + { + string testCode = @" + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + namespace AspNetIntegration + { + class Program + { + static void Main(string[] args) + { + #if ENABLE_MIDDLEWARE + var host = new HostBuilder() + .ConfigureFunctionsWebApplication(builder => + { + // can still register middleware and use this extension method the same way + // .ConfigureFunctionsWorkerDefaults() is used + builder.UseWhen((context)=> + { + // We want to use this middleware only for http trigger invocations. + return context.FunctionDefinition.InputBindings.Values + .First(a => a.Type.EndsWith(""Trigger"")).Type == ""httpTrigger""; + }); + }) + .Build(); + host.Run(); + #else + // + var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .Build(); + + host.Run(); + // + #endif + } + } + }"; + + var test = new AnalyzerTest + { + ReferenceAssemblies = LoadRequiredDependencyAssemblies(), + TestCode = testCode + }; + + // test.ExpectedDiagnostics is an empty collection. + + await test.RunAsync(); + } + + [Fact] + public async Task AspNetIntegration_WithIncorrectRegistration_Diagnostics_Expected() + { + string testCode = @" + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + namespace AspNetIntegration + { + class Program + { + static void Main(string[] args) + { + + // + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + + host.Run(); + // + } + + public static void Method1() + { + } + + private static void Method2() + { + } + } + }"; + + var test = new AnalyzerTest + { + ReferenceAssemblies = LoadRequiredDependencyAssemblies(), + TestCode = testCode + }; + + test.ExpectedDiagnostics.Add(Verifier.Diagnostic() + .WithSeverity(DiagnosticSeverity.Error) + .WithSpan(18, 34, 18, 66) + .WithArguments(ExpectedRegistrationMethod)); + + await test.RunAsync(); + } + + [Fact] + public async Task AspNetIntegration_WithIncorrectRegistration_Diagnostics_Expected_CodeFixWorks() + { + string inputCode = @" + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + namespace AspNetIntegration + { + class Program + { + static void Main(string[] args) + { + // + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + host.Run(); + // + } + public static void Method1() + { + } + private static void Method2() + { + } + } + }"; + + string expectedCode = @" + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + namespace AspNetIntegration + { + class Program + { + static void Main(string[] args) + { + // + var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .Build(); + host.Run(); + // + } + public static void Method1() + { + } + private static void Method2() + { + } + } + }"; + + + var expectedDiagnosticResult = CodeFixVerifier + .Diagnostic("AZFW0014") + .WithSeverity(DiagnosticSeverity.Error) + .WithSpan(16, 34, 16, 66) + .WithArguments(ExpectedRegistrationMethod); + + var test = new CodeFixTest + { + ReferenceAssemblies = LoadRequiredDependencyAssemblies(), + TestCode = inputCode, + FixedCode = expectedCode + }; + + test.ExpectedDiagnostics.AddRange(new[] { expectedDiagnosticResult }); + await test.RunAsync(); + } + + private static ReferenceAssemblies LoadRequiredDependencyAssemblies() + { + var referenceAssemblies = ReferenceAssemblies.Net.Net60.WithPackages(ImmutableArray.Create( + new PackageIdentity("Microsoft.Azure.Functions.Worker", "1.19.0"), + new PackageIdentity("Microsoft.Azure.Functions.Worker.Sdk", "1.15.1"), + new PackageIdentity("Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs", "6.0.0"), + new PackageIdentity("Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore", "1.0.0"), + new PackageIdentity("Microsoft.Azure.Functions.Worker.Extensions.Abstractions", "5.0.0"), + new PackageIdentity("Microsoft.Extensions.Hosting.Abstractions", "6.0.0"))); + + return referenceAssemblies; + } + } +} diff --git a/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/Worker.Extensions.Http.AspNetCore.Tests.csproj b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/Worker.Extensions.Http.AspNetCore.Tests.csproj new file mode 100644 index 000000000..b73a0b62a --- /dev/null +++ b/test/extensions/Worker.Extensions.Http.AspNetCore.Tests/Worker.Extensions.Http.AspNetCore.Tests.csproj @@ -0,0 +1,35 @@ + + + + net7.0 + enable + enable + Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Tests + Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Tests + ASP.NET Core extensions for .NET isolated functions + false + true + True + ..\..\..\key.snk + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + \ No newline at end of file