Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ private bool TryGetBindings(IMethodSymbol method, out IList<IDictionary<string,
hasHttpTrigger = false;
validatedRetryOptions = null;

if (!TryGetMethodOutputBinding(method, out bool hasOutputBinding, out GeneratorRetryOptions? retryOptions, out IList<IDictionary<string, object>>? methodOutputBindings)
if (!TryGetMethodOutputBinding(method, out bool hasMethodOutputBinding, out GeneratorRetryOptions? retryOptions, out IList<IDictionary<string, object>>? methodOutputBindings)
Comment thread
satvu marked this conversation as resolved.
|| !TryGetParameterInputAndTriggerBindings(method, out bool supportsRetryOptions, out hasHttpTrigger, out IList<IDictionary<string, object>>? parameterInputAndTriggerBindings)
|| !TryGetReturnTypeBindings(method, hasHttpTrigger, hasOutputBinding, out IList<IDictionary<string, object>>? returnTypeBindings))
|| !TryGetReturnTypeBindings(method, hasHttpTrigger, hasMethodOutputBinding, out IList<IDictionary<string, object>>? returnTypeBindings))
{
bindings = null;
return false;
Expand Down Expand Up @@ -129,12 +129,12 @@ private bool TryGetBindings(IMethodSymbol method, out IList<IDictionary<string,
/// <summary>
/// Checks for and returns any OutputBinding attributes associated with the method.
/// </summary>
private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasOutputBinding, out GeneratorRetryOptions? retryOptions, out IList<IDictionary<string, object>>? bindingsList)
private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasMethodOutputBinding, out GeneratorRetryOptions? retryOptions, out IList<IDictionary<string, object>>? 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)
Expand All @@ -149,16 +149,16 @@ 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;
return false;
}

outputBindingAttribute = attribute;
hasOutputBinding = true;
hasMethodOutputBinding = true;
}
}

Expand Down Expand Up @@ -400,7 +400,7 @@ private bool DoesConverterSupportTargetType(List<AttributeData> converterAdverti
/// <summary>
/// Checks for and returns any bindings found in the Return Type of the method
/// </summary>
private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, bool hasOutputBinding, out IList<IDictionary<string, object>>? bindingsList)
private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, bool hasMethodOutputBinding, out IList<IDictionary<string, object>>? bindingsList)
{
ITypeSymbol? returnTypeSymbol = method.ReturnType;
bindingsList = new List<IDictionary<string, object>>();
Expand Down Expand Up @@ -440,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;
Expand All @@ -451,10 +451,11 @@ private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger,
return true;
}

private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool hasHttpTrigger, bool hasOutputBinding, out IList<IDictionary<string, object>>? bindingsList)
private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool hasHttpTrigger, bool hasMethodOutputBinding, out IList<IDictionary<string, object>>? bindingsList)
{
var members = returnTypeSymbol.GetMembers();
var foundHttpOutput = false;
var returnTypeHasOutputBindings = false;
bindingsList = new List<IDictionary<string, object>>(); // 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))
Expand Down Expand Up @@ -503,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;
Expand Down
1 change: 1 addition & 0 deletions sdk/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
### Microsoft.Azure.Functions.Worker.Sdk 1.16.3 (meta package)

- Update worker.config generation to accurate worker executable name (#1053)
- Bug fix for scenarios with `$return` output binding and `HttpTrigger` breaking output-binding rules (#2098)
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public IntegratedTriggersAndBindingsTests()
}

[Fact]
public async Task FunctionWhereOutputBindingIsInTheReturnType()
public async Task FunctionsWhereOutputBindingIsInTheReturnType()
{
// test generating function metadata for a simple HttpTrigger
string inputCode = """
Expand All @@ -67,6 +67,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
Expand All @@ -76,6 +83,12 @@ public class MyOutputType

public HttpResponseData HttpResponse { get; set; }
}

public class MyOutputTypeNoHttpProp
{
[QueueOutput("functionstesting2", Connection = "AzureWebJobsStorage")]
public string Name { get; set; }
}
}
""";

Expand Down Expand Up @@ -119,6 +132,19 @@ public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string d
ScriptFile = "TestProject.dll"
};
metadataList.Add(Function0);
var Function1RawBindings = new List<string>();
Comment thread
satvu marked this conversation as resolved.
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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// 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 Xunit;

namespace Microsoft.Azure.Functions.SdkGeneratorTests
Expand Down Expand Up @@ -277,6 +275,103 @@ await TestHelpers.RunTestAsync<FunctionMetadataProviderGenerator>(
expectedGeneratedFileName,
expectedOutput);
}

[Fact]
public async void TestQueueOutputWithHttpTrigger()
{
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 = """
// <auto-generated/>
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
{
/// <summary>
/// Custom <see cref="IFunctionMetadataProvider"/> implementation that returns function metadata definitions for the current worker."/>
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider
{
/// <inheritdoc/>
public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string directory)
{
var metadataList = new List<IFunctionMetadata>();
var Function0RawBindings = new List<string>();
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());
}
}

/// <summary>
/// Extension methods to enable registration of the custom <see cref="IFunctionMetadataProvider"/> implementation generated for the current worker.
/// </summary>
public static class WorkerHostBuilderFunctionMetadataProviderExtension
{
///<summary>
/// 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.
///</summary>
public static IHostBuilder ConfigureGeneratedFunctionMetadataProvider(this IHostBuilder builder)
{
builder.ConfigureServices(s =>
{
s.AddSingleton<IFunctionMetadataProvider, GeneratedFunctionMetadataProvider>();
});
return builder;
}
}
}
""";

await TestHelpers.RunTestAsync<FunctionMetadataProviderGenerator>(
_referencedExtensionAssemblies,
inputCode,
expectedGeneratedFileName,
expectedOutput);
}
}
}
}