diff --git a/extensions/Worker.Extensions.Http.AspNetCore/release_notes.md b/extensions/Worker.Extensions.Http.AspNetCore/release_notes.md index 463011e9c..8d4c519f5 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/release_notes.md +++ b/extensions/Worker.Extensions.Http.AspNetCore/release_notes.md @@ -9,4 +9,4 @@ - Improvements to context coordination/synchronization handling and observability - Failure to receive any of the expected context synchronization calls will now result in a `TimeoutException` thrown with the appropriate exception information. Previously this would block indefinitely and failures here were difficult to diagnose. - Debug logs are now emitted in the context coordination calls, improving observability. - +- Introduces fix to properly handle multiple output binding scenarios (#2322). diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs index 43ccedd3e..72b80c43a 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.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.Concurrent; using System.Linq; using System.Threading.Tasks; @@ -9,14 +10,17 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Routing; using Microsoft.Azure.Functions.Worker.Extensions.Http.Converters; +using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Infrastructure; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Azure.Functions.Worker.Middleware; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore { internal class FunctionsHttpProxyingMiddleware : IFunctionsWorkerMiddleware { private const string HttpTrigger = "httpTrigger"; + private const string HttpBindingType = "http"; private readonly IHttpCoordinator _coordinator; private readonly ConcurrentDictionary _isHttpTrigger = new(); @@ -47,40 +51,67 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next await next(context); - var invocationResult = context.GetInvocationResult(); - - if (invocationResult?.Value is IActionResult actionResult) + var responseHandled = await TryHandleHttpResult(context.GetInvocationResult().Value, context, httpContext, true) + || await TryHandleOutputBindingsHttpResult(context, httpContext); + + if (!responseHandled) { - ActionContext actionContext = new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor()); - - await actionResult.ExecuteResultAsync(actionContext); + var logger = context.InstanceServices.GetRequiredService(); + logger.NoHttpResponseReturned(context.FunctionDefinition.Name, context.InvocationId); } - else if (invocationResult?.Value is AspNetCoreHttpResponseData) + + // Allow ASP.NET Core middleware to continue + _coordinator.CompleteFunctionInvocation(invocationId); + } + + private static async Task TryHandleHttpResult(object? result, FunctionContext context, HttpContext httpContext, bool isInvocationResult = false) + { + switch (result) { - // 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; + case IActionResult actionResult: + ActionContext actionContext = new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor()); + await actionResult.ExecuteResultAsync(actionContext); + break; + case AspNetCoreHttpRequestData when isInvocationResult: + // 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. + context.GetInvocationResult().Value = null; + break; + case IResult iResult: + await iResult.ExecuteAsync(httpContext); + break; + default: + return false; } - // allows asp.net middleware to continue - _coordinator.CompleteFunctionInvocation(invocationId); + return true; + } + + private static Task TryHandleOutputBindingsHttpResult(FunctionContext context, HttpContext httpContext) + { + var httpOutputBinding = context.GetOutputBindings() + .FirstOrDefault(a => string.Equals(a.BindingType, HttpBindingType, StringComparison.OrdinalIgnoreCase)); + + return httpOutputBinding is null + ? Task.FromResult(false) + : TryHandleHttpResult(httpOutputBinding.Value, context, httpContext); } private static void AddHttpContextToFunctionContext(FunctionContext funcContext, HttpContext httpContext) { funcContext.Items.Add(Constants.HttpContextKey, httpContext); - // add asp net version of httprequestdata feature + // Add ASP.NET Core integration version of IHttpRequestDataFeature funcContext.Features.Set(AspNetCoreHttpRequestDataFeature.Instance); } private static bool IsHttpTriggerFunction(FunctionContext funcContext) { return funcContext.FunctionDefinition.InputBindings - .Any(p => p.Value.Type.Equals(HttpTrigger, System.StringComparison.OrdinalIgnoreCase)); + .Any(p => p.Value.Type.Equals(HttpTrigger, StringComparison.OrdinalIgnoreCase)); } } } diff --git a/extensions/Worker.Extensions.Http.AspNetCore/src/Infrastructure/ExtensionTrace.General.cs b/extensions/Worker.Extensions.Http.AspNetCore/src/Infrastructure/ExtensionTrace.General.cs index 3fe64eada..c4349390a 100644 --- a/extensions/Worker.Extensions.Http.AspNetCore/src/Infrastructure/ExtensionTrace.General.cs +++ b/extensions/Worker.Extensions.Http.AspNetCore/src/Infrastructure/ExtensionTrace.General.cs @@ -18,6 +18,11 @@ public void FunctionContextSet(string invocationId) GeneralLog.FunctionContextSet(_defaultLogger, invocationId); } + public void NoHttpResponseReturned(string functionName, string invocationId) + { + GeneralLog.NoHttpResponseReturned(_defaultLogger, functionName, invocationId); + } + private static partial class GeneralLog { [LoggerMessage(1, LogLevel.Debug, @"HttpContext set for invocation ""{InvocationId}"", Request id ""{RequestId}"".", EventName = nameof(HttpContextSet))] @@ -25,6 +30,9 @@ private static partial class GeneralLog [LoggerMessage(2, LogLevel.Debug, @"FunctionContext set for invocation ""{InvocationId}"".", EventName = nameof(FunctionContextSet))] public static partial void FunctionContextSet(ILogger logger, string invocationId); + + [LoggerMessage(3, LogLevel.Trace, @"No HTTP response returned from function '{FunctionName}', invocation {InvocationId}.", EventName = nameof(NoHttpResponseReturned))] + public static partial void NoHttpResponseReturned(ILogger logger, string functionName, string invocationId); } } } diff --git a/extensions/Worker.Extensions.Http/release_notes.md b/extensions/Worker.Extensions.Http/release_notes.md index 8f0f26717..53dc80d72 100644 --- a/extensions/Worker.Extensions.Http/release_notes.md +++ b/extensions/Worker.Extensions.Http/release_notes.md @@ -4,7 +4,8 @@ - My change description (#PR/#issue) --> -### Microsoft.Azure.Functions.Worker.Extensions.Http +### Microsoft.Azure.Functions.Worker.Extensions.Http 3.2.0 - Added ability to bind a POCO parameter to the request body using `FromBodyAttribute` - Special thanks to @njqdev for the contributions and collaboration on this feature +- Introduces `HttpResultAttribute`, which should be used to label the parameter associated with the HTTP result in multiple output binding scenarios (#2322). diff --git a/extensions/Worker.Extensions.Http/src/HttpResultAttribute.cs b/extensions/Worker.Extensions.Http/src/HttpResultAttribute.cs new file mode 100644 index 000000000..bc823b2de --- /dev/null +++ b/extensions/Worker.Extensions.Http/src/HttpResultAttribute.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Azure.Functions.Worker +{ + /// + /// Attribute used to mark an HTTP Response on an HTTP Trigger function with multiple output bindings. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class HttpResultAttribute : Attribute + { + } +} diff --git a/extensions/Worker.Extensions.Http/src/Worker.Extensions.Http.csproj b/extensions/Worker.Extensions.Http/src/Worker.Extensions.Http.csproj index c134d92c8..7aa205d47 100644 --- a/extensions/Worker.Extensions.Http/src/Worker.Extensions.Http.csproj +++ b/extensions/Worker.Extensions.Http/src/Worker.Extensions.Http.csproj @@ -6,7 +6,7 @@ HTTP extensions for .NET isolated functions - 3.1.0 + 3.2.0 diff --git a/sdk/Sdk.Generators/Constants.cs b/sdk/Sdk.Generators/Constants.cs index 9a4fc9482..7799f557e 100644 --- a/sdk/Sdk.Generators/Constants.cs +++ b/sdk/Sdk.Generators/Constants.cs @@ -41,7 +41,8 @@ internal static class Types internal const string BindingPropertyNameAttribute = "Microsoft.Azure.Functions.Worker.Extensions.Abstractions.BindingPropertyNameAttribute"; internal const string DefaultValue = "Microsoft.Azure.Functions.Worker.Extensions.Abstractions.DefaultValueAttribute"; - internal const string HttpResponse = "Microsoft.Azure.Functions.Worker.Http.HttpResponseData"; + internal const string HttpResponseData = "Microsoft.Azure.Functions.Worker.Http.HttpResponseData"; + internal const string HttpResultAttribute = "Microsoft.Azure.Functions.Worker.HttpResultAttribute"; internal const string HttpTriggerBinding = "Microsoft.Azure.Functions.Worker.HttpTriggerAttribute"; internal const string BindingCapabilitiesAttribute = "Microsoft.Azure.Functions.Worker.Extensions.Abstractions.BindingCapabilitiesAttribute"; diff --git a/sdk/Sdk.Generators/DiagnosticDescriptors.cs b/sdk/Sdk.Generators/DiagnosticDescriptors.cs index 9aaca2a6b..464890002 100644 --- a/sdk/Sdk.Generators/DiagnosticDescriptors.cs +++ b/sdk/Sdk.Generators/DiagnosticDescriptors.cs @@ -45,7 +45,7 @@ private static DiagnosticDescriptor Create(string id, string title, string messa public static DiagnosticDescriptor MultipleHttpResponseTypes { get; } = Create(id: "AZFW0007", title: "Symbol could not be found in user compilation.", - messageFormat: "Found multiple public properties of type HttpResponseData defined in return type '{0}'. Only one HTTP response binding type is supported in your return type definition.", + messageFormat: "Found multiple HTTP Response types (properties with HttpResultAttribute or properties of type HttpResponseData) defined in return type '{0}'. Only one HTTP response binding type is supported in your return type definition.", category: "FunctionMetadataGeneration", severity: DiagnosticSeverity.Error); diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs index 56e2f3f94..cc011110c 100644 --- a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs @@ -164,7 +164,7 @@ private bool TryGetMethodOutputBinding(IMethodSymbol method, out bool hasMethodO if (outputBindingAttribute != null) { - if (!TryCreateBindingDict(outputBindingAttribute, Constants.FunctionMetadataBindingProps.ReturnBindingName, Location.None, out IDictionary? bindingDict)) + if (!TryCreateBindingDictionary(outputBindingAttribute, Constants.FunctionMetadataBindingProps.ReturnBindingName, Location.None, out IDictionary? bindings)) { bindingsList = null; return false; @@ -172,7 +172,7 @@ private bool TryGetMethodOutputBinding(IMethodSymbol method, out bool hasMethodO bindingsList = new List>(capacity: 1) { - bindingDict! + bindings! }; return true; @@ -288,22 +288,22 @@ private bool TryGetParameterInputAndTriggerBindings(IMethodSymbol method, out bo string bindingName = parameter.Name; - if (!TryCreateBindingDict(attribute, bindingName, Location.None, out IDictionary? bindingDict, supportsDeferredBinding)) + if (!TryCreateBindingDictionary(attribute, bindingName, Location.None, out IDictionary? bindings, supportsDeferredBinding)) { - bindingsList = null; + bindings = null; return false; } // If cardinality is supported and validated, but was not found in named args, constructor args, or default value attributes // default to Cardinality: One to stay in sync with legacy generator. - if (cardinalityValidated && !bindingDict!.Keys.Contains("cardinality")) + if (cardinalityValidated && !bindings!.Keys.Contains("cardinality")) { - bindingDict!.Add("cardinality", "One"); + bindings!.Add("cardinality", "One"); } if (dataType is not DataType.Undefined) { - bindingDict!.Add("dataType", Enum.GetName(typeof(DataType), dataType)); + bindings!.Add("dataType", Enum.GetName(typeof(DataType), dataType)); } // check for binding capabilities @@ -318,7 +318,7 @@ private bool TryGetParameterInputAndTriggerBindings(IMethodSymbol method, out bo } } - bindingsList.Add(bindingDict!); + bindingsList.Add(bindings!); } } } @@ -434,7 +434,7 @@ private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, } } - if (SymbolEqualityComparer.Default.Equals(returnTypeSymbol, _knownFunctionMetadataTypes.HttpResponse)) // If return type is HttpResponseData + if (SymbolEqualityComparer.Default.Equals(returnTypeSymbol, _knownFunctionMetadataTypes.HttpResponseData)) { bindingsList.Add(GetHttpReturnBinding(Constants.FunctionMetadataBindingProps.ReturnBindingName)); } @@ -467,8 +467,9 @@ private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool return false; } - // Check if this attribute is an HttpResponseData type attribute - if (prop is IPropertySymbol property && SymbolEqualityComparer.Default.Equals(property.Type, _knownFunctionMetadataTypes.HttpResponse)) + // Check for HttpResponseData type for legacy apps + if (prop is IPropertySymbol property + && (SymbolEqualityComparer.Default.Equals(property.Type, _knownFunctionMetadataTypes.HttpResponseData))) { if (foundHttpOutput) { @@ -479,34 +480,46 @@ private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool foundHttpOutput = true; bindingsList.Add(GetHttpReturnBinding(prop.Name)); + continue; } - else if (prop.GetAttributes().Length > 0) // check if this property has any attributes + + var propAttributes = prop.GetAttributes(); + + if (propAttributes.Length > 0) { - var foundPropertyOutputAttr = false; + var bindingAttributes = propAttributes.Where(p => p.AttributeClass!.IsOrDerivedFrom(_knownFunctionMetadataTypes.BindingAttribute)); + + if (bindingAttributes.Count() > 1) + { + _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, Location.None, new object[] { "Property", prop.Name.ToString() })); + bindingsList = null; + return false; + } - foreach (var attr in prop.GetAttributes()) // now loop through and check if any of the attributes are Binding attributes + // Check if this property has an HttpResultAttribute on it + if (HasHttpResultAttribute(prop)) { - if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass?.BaseType, _knownFunctionMetadataTypes.OutputBindingAttribute)) + if (foundHttpOutput) { - // validate that there's only one binding attribute per property - if (foundPropertyOutputAttr) - { - _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, Location.None, new object[] { "Property", prop.Name.ToString() })); - bindingsList = null; - return false; - } + _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleHttpResponseTypes, Location.None, new object[] { returnTypeSymbol.Name })); + bindingsList = null; + return false; + } - if (!TryCreateBindingDict(attr, prop.Name, prop.Locations.FirstOrDefault(), out IDictionary? bindingDict)) - { - bindingsList = null; - return false; - } + foundHttpOutput = true; + bindingsList.Add(GetHttpReturnBinding(prop.Name)); + } + else + { + if (!TryCreateBindingDictionary(bindingAttributes.FirstOrDefault(), prop.Name, prop.Locations.FirstOrDefault(), out IDictionary? bindings)) + { + bindingsList = null; + return false; + } - bindingsList.Add(bindingDict!); + bindingsList.Add(bindings!); - returnTypeHasOutputBindings = true; - foundPropertyOutputAttr = true; - } + returnTypeHasOutputBindings = true; } } } @@ -519,6 +532,21 @@ private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool return true; } + private bool HasHttpResultAttribute(ISymbol prop) + { + var attributes = prop.GetAttributes(); + foreach (var attribute in attributes) + { + if (attribute.AttributeClass is not null && + attribute.AttributeClass.IsOrDerivedFrom(_knownFunctionMetadataTypes.HttpResultAttribute)) + { + return true; + } + } + + return false; + } + private IDictionary GetHttpReturnBinding(string returnBindingName) { var httpBinding = new Dictionary @@ -531,7 +559,7 @@ private IDictionary GetHttpReturnBinding(string returnBindingNam return httpBinding; } - private bool TryCreateBindingDict(AttributeData bindingAttrData, string bindingName, Location? bindingLocation, out IDictionary? bindings, bool supportsDeferredBinding = false) + private bool TryCreateBindingDictionary(AttributeData bindingAttrData, string bindingName, Location? bindingLocation, out IDictionary? bindings, bool supportsDeferredBinding = false) { // Get binding info as a dictionary with keys as the property name and value as the property value if (!TryGetAttributeProperties(bindingAttrData, bindingLocation, out IDictionary? attributeProperties)) diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/KnownFunctionMetadataTypes.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/KnownFunctionMetadataTypes.cs index 1dc2684bd..b7b413fcb 100644 --- a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/KnownFunctionMetadataTypes.cs +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/KnownFunctionMetadataTypes.cs @@ -14,7 +14,7 @@ internal readonly struct KnownFunctionMetadataTypes private readonly Lazy _functionName; private readonly Lazy _bindingPropertyNameAttribute; private readonly Lazy _defaultValue; - private readonly Lazy _httpResponse; + private readonly Lazy _httpResponseData; private readonly Lazy _httpTriggerBinding; private readonly Lazy _retryAttribute; private readonly Lazy _bindingCapabilitiesAttribute; @@ -23,6 +23,7 @@ internal readonly struct KnownFunctionMetadataTypes private readonly Lazy _inputConverterAttributeType; private readonly Lazy _supportedTargetTypeAttributeType; private readonly Lazy _supportsDeferredBindingAttributeType; + private readonly Lazy _httpResultAttribute; internal KnownFunctionMetadataTypes(Compilation compilation) { @@ -31,7 +32,7 @@ internal KnownFunctionMetadataTypes(Compilation compilation) _functionName = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.FunctionName)); _bindingPropertyNameAttribute = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.BindingPropertyNameAttribute)); _defaultValue = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.DefaultValue)); - _httpResponse = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.HttpResponse)); + _httpResponseData = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.HttpResponseData)); _httpTriggerBinding = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.HttpTriggerBinding)); _retryAttribute = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.RetryAttribute)); _bindingCapabilitiesAttribute = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.BindingCapabilitiesAttribute)); @@ -40,6 +41,7 @@ internal KnownFunctionMetadataTypes(Compilation compilation) _inputConverterAttributeType = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.InputConverterAttributeType)); _supportedTargetTypeAttributeType = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.SupportedTargetTypeAttributeType)); _supportsDeferredBindingAttributeType = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.SupportsDeferredBindingAttributeType)); + _httpResultAttribute = new Lazy(() => compilation.GetTypeByMetadataName(Constants.Types.HttpResultAttribute)); } public INamedTypeSymbol? BindingAttribute { get => _bindingAttribute.Value; } @@ -52,7 +54,9 @@ internal KnownFunctionMetadataTypes(Compilation compilation) public INamedTypeSymbol? DefaultValue { get => _defaultValue.Value; } - public INamedTypeSymbol? HttpResponse { get => _httpResponse.Value; } + public INamedTypeSymbol? HttpResponseData { get => _httpResponseData.Value; } + + public INamedTypeSymbol? HttpResultAttribute { get => _httpResultAttribute.Value; } public INamedTypeSymbol? HttpTriggerBinding { get => _httpTriggerBinding.Value; } diff --git a/sdk/Sdk.Generators/Sdk.Generators.csproj b/sdk/Sdk.Generators/Sdk.Generators.csproj index 62d08e767..3f0393f74 100644 --- a/sdk/Sdk.Generators/Sdk.Generators.csproj +++ b/sdk/Sdk.Generators/Sdk.Generators.csproj @@ -9,8 +9,7 @@ Microsoft.Azure.Functions.Worker.Sdk.Generators false true - 2 - 1 + 3 true diff --git a/sdk/release_notes.md b/sdk/release_notes.md index 68327a1fc..419c67a57 100644 --- a/sdk/release_notes.md +++ b/sdk/release_notes.md @@ -8,3 +8,19 @@ - Re-add SDK refactors that were reverted in #2313 (#2347) - Address assembly scanning regression (#2347) +- Updating to use `Microsoft.NET.Sdk.Functions.Generators` 1.2.2 (#2247) + +### Microsoft.Azure.Functions.Worker.Sdk.Generators 1.3.0 + +- Introduces handling for `HttpResultAttribute`, which is used on HTTP response properties in [multiple output-binding scenarios](https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide?tabs=windows#multiple-output-bindings). Example: + +```csharp +public class MyOutputType +{ + [QueueOutput("myQueue")] + public string Name { get; set; } + + [HttpResult] + public IActionResult HttpResponse { get; set; } +} +``` diff --git a/test/DotNetWorkerTests/AspNetCore/FunctionsHttpProxyingMiddlewareTests.cs b/test/DotNetWorkerTests/AspNetCore/FunctionsHttpProxyingMiddlewareTests.cs index 157a77448..39c6b6ad7 100644 --- a/test/DotNetWorkerTests/AspNetCore/FunctionsHttpProxyingMiddlewareTests.cs +++ b/test/DotNetWorkerTests/AspNetCore/FunctionsHttpProxyingMiddlewareTests.cs @@ -5,8 +5,12 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker.Context.Features; using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore; +using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.Infrastructure; using Microsoft.Azure.Functions.Worker.Middleware; +using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; @@ -17,7 +21,7 @@ public class FunctionsHttpProxyingMiddlewareTests [Fact] public async Task Middleware_AddsHttpContextToFunctionContext_Success() { - var test = SetupInputs("httpTrigger"); + var test = SetupTest("httpTrigger"); var mockDelegate = new Mock(); var funcMiddleware = new FunctionsHttpProxyingMiddleware(test.MockCoordinator.Object); @@ -35,7 +39,7 @@ public async Task Middleware_AddsHttpContextToFunctionContext_Success() [Fact] public async Task Middleware_NoOpsOnNonHttpTriggers() { - var test = SetupInputs("someTrigger"); + var test = SetupTest("someTrigger"); var mockDelegate = new Mock(); var funcMiddleware = new FunctionsHttpProxyingMiddleware(test.MockCoordinator.Object); @@ -47,18 +51,125 @@ public async Task Middleware_NoOpsOnNonHttpTriggers() test.MockCoordinator.Verify(p => p.CompleteFunctionInvocation(It.IsAny()), Times.Never()); } - private static (FunctionContext FunctionContext, HttpContext HttpContext, Mock MockCoordinator) SetupInputs(string triggerType) + [Fact] + public async Task SimpleHttpTrigger_ActionResultHandled() + { + var test = SetupTest("httpTrigger"); + var mockDelegate = new Mock(); + + // In a simple HTTP trigger function, there is only one input (the trigger), and the HTTP response + // The HTTP response will be in the InvocationResult + var mockActionResult = GetMockActionResult(); + var bindingFeatures = test.FunctionContext.Features.Get(); + bindingFeatures.InvocationResult = mockActionResult.Object; + + var funcMiddleware = new FunctionsHttpProxyingMiddleware(test.MockCoordinator.Object); + await funcMiddleware.Invoke(test.FunctionContext, mockDelegate.Object); + + mockActionResult.Verify(); + test.MockCoordinator.Verify(p => p.CompleteFunctionInvocation(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task SimpleHttpTrigger_IResultHandled() + { + var test = SetupTest("httpTrigger"); + var mockDelegate = new Mock(); + + // In a simple HTTP trigger function, there is only one input (the trigger), and the HTTP response + // The HTTP response will be in the InvocationResult + var mockResult = GetMockIResult(); + var bindingFeatures = test.FunctionContext.Features.Get(); + bindingFeatures.InvocationResult = mockResult.Object; + + var funcMiddleware = new FunctionsHttpProxyingMiddleware(test.MockCoordinator.Object); + await funcMiddleware.Invoke(test.FunctionContext, mockDelegate.Object); + + mockResult.Verify(); + test.MockCoordinator.Verify(p => p.CompleteFunctionInvocation(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task MultipleOutput_IActionResultIsHandled() + { + var test = SetupTest("httpTrigger", GetMultiOutputTypeOutputBindings()); + var mockDelegate = new Mock(); + + // In a multi-output HTTP trigger function, the HTTP response is stored in the output bindings, and InvocationResult is empty + var mockActionResult = GetMockActionResult(); + var bindingFeatures = test.FunctionContext.Features.Get(); + bindingFeatures.OutputBindingData.Add("result", mockActionResult.Object); + + var funcMiddleware = new FunctionsHttpProxyingMiddleware(test.MockCoordinator.Object); + await funcMiddleware.Invoke(test.FunctionContext, mockDelegate.Object); + + mockActionResult.Verify(); + } + + [Fact] + public async Task MultipleOutput_IResultIsHandled() + { + var test = SetupTest("httpTrigger", GetMultiOutputTypeOutputBindings()); + var mockDelegate = new Mock(); + + // In a multi-output HTTP trigger function, the HTTP response is stored in the output bindings, and InvocationResult is empty + var mockResult = GetMockIResult(); + var bindingFeatures = test.FunctionContext.Features.Get(); + bindingFeatures.OutputBindingData.Add("result", mockResult.Object); + + var funcMiddleware = new FunctionsHttpProxyingMiddleware(test.MockCoordinator.Object); + await funcMiddleware.Invoke(test.FunctionContext, mockDelegate.Object); + + mockResult.Verify(); + } + + [Fact] + public async Task Multiple_OutputAspNetHttpRequestData_Completes() + { + var test = SetupTest("httpTrigger", GetMultiOutputTypeOutputBindings()); + var mockDelegate = new Mock(); + + SetUpAspNetCoreHttpRequestDataBindingInfo(test.FunctionContext, false); + + var funcMiddleware = new FunctionsHttpProxyingMiddleware(test.MockCoordinator.Object); + await funcMiddleware.Invoke(test.FunctionContext, mockDelegate.Object); + + test.MockCoordinator.Verify(p => p.CompleteFunctionInvocation(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvocationResultNull_WhenResultIsTypeAspNetCoreHttpRequestData() + { + var test = SetupTest("httpTrigger", GetMultiOutputTypeOutputBindings()); + var mockDelegate = new Mock(); + + SetUpAspNetCoreHttpRequestDataBindingInfo(test.FunctionContext, true); + + var funcMiddleware = new FunctionsHttpProxyingMiddleware(test.MockCoordinator.Object); + await funcMiddleware.Invoke(test.FunctionContext, mockDelegate.Object); + + Assert.Null(test.FunctionContext.GetInvocationResult().Value); + test.MockCoordinator.Verify(p => p.CompleteFunctionInvocation(It.IsAny()), Times.Once()); + } + + private static (FunctionContext FunctionContext, HttpContext HttpContext, Mock MockCoordinator) SetupTest(string triggerType, IDictionary outputBindings = null) { var inputBindings = new Dictionary() { { "test", new TestBindingMetadata("test", triggerType, BindingDirection.In ) } }; - var functionDef = new TestFunctionDefinition(inputBindings: inputBindings); + var functionDefinition = new TestFunctionDefinition(inputBindings: inputBindings, outputBindings: outputBindings); + + var serviceProvider = new ServiceCollection() + .AddSingleton() + .AddLogging() + .BuildServiceProvider(); - var functionContext = new TestFunctionContext(functionDef, new TestFunctionInvocation(), CancellationToken.None) + var functionContext = new TestFunctionContext(functionDefinition, new TestFunctionInvocation(), CancellationToken.None) { - Items = new Dictionary() + Items = new Dictionary(), + InstanceServices = serviceProvider }; var httpContext = new DefaultHttpContext(); @@ -74,5 +185,68 @@ private static (FunctionContext FunctionContext, HttpContext HttpContext, Mock + /// A dictionary of binding metadata representing the output bindings in a Multi-Ouptut HTTP trigger function + /// with a POCO that has a queue output and an http result. + /// + /// Dictionary with entries of (name, bindingMetadata). + private Dictionary GetMultiOutputTypeOutputBindings() + { + var outputBindings = new Dictionary() + { + { "name", new TestBindingMetadata("name", "queue", BindingDirection.Out) }, + { "result", new TestBindingMetadata("result", "http", BindingDirection.Out)} + }; + + return outputBindings; + } + + /// + /// Sets up an AspNetCoreHttpRequestData object using a mock HTTP request and stores it in the appropriate location in a FunctionContext instance. + /// + /// The function context used for testing. + /// True if the object is expected to be in the invocation result, false if the object is expected in the output bindings (which is the case for + /// multi-output scenarios). + private void SetUpAspNetCoreHttpRequestDataBindingInfo(FunctionContext functionContext, bool isInvocationResult) + { + var mockRequest = new Mock(); + mockRequest.SetupGet(req => req.Path).Returns("/test"); + mockRequest.SetupGet(req => req.QueryString).Returns(new QueryString("?param=value")); + mockRequest.SetupGet(req => req.Scheme).Returns("http"); + mockRequest.SetupGet(req => req.Host).Returns(new HostString("localhost")); + + var bindingFeatures = functionContext.Features.Get(); + var aspNetCoreHttpRequestData = new AspNetCoreHttpRequestData(mockRequest.Object, functionContext); + + if (isInvocationResult) + { + bindingFeatures.InvocationResult = aspNetCoreHttpRequestData; + } + else + { + bindingFeatures.OutputBindingData.Add("result", aspNetCoreHttpRequestData); + } + } + + private Mock GetMockActionResult() + { + var mockActionResult = new Mock(); + mockActionResult.Setup(p => p.ExecuteResultAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + return mockActionResult; + } + + private Mock GetMockIResult() + { + var mockResult = new Mock(); + mockResult.Setup(p => p.ExecuteAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + return mockResult; + } } } diff --git a/test/Sdk.Generator.Tests/FunctionExecutor/DependentAssemblyTest.cs b/test/Sdk.Generator.Tests/FunctionExecutor/DependentAssemblyTest.cs index 9e42f4edc..ac2f31cec 100644 --- a/test/Sdk.Generator.Tests/FunctionExecutor/DependentAssemblyTest.cs +++ b/test/Sdk.Generator.Tests/FunctionExecutor/DependentAssemblyTest.cs @@ -4,6 +4,8 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -18,10 +20,10 @@ 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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; var dependentAssembly = Assembly.LoadFrom("DependentAssemblyWithFunctions.dll"); _referencedAssemblies = new[] diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs index 8c0cbafa5..a59321d0d 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/AutoConfigureStartupTypeTests.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -21,10 +23,10 @@ public AutoConfigureStartupTypeTests() // 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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; _referencedExtensionAssemblies = new[] { diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.NetFx.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.NetFx.cs index b2cd05c50..9b2e87c00 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.NetFx.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.NetFx.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -21,10 +23,10 @@ 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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; var dependentAssembly = Assembly.LoadFrom("DependentAssemblyWithFunctions.NetStandard.dll"); _referencedExtensionAssemblies = new[] diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs index b162560e2..7de6e6864 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs @@ -4,6 +4,8 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -19,10 +21,10 @@ public DependentAssemblyTest() // 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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; var dependentAssembly = Assembly.LoadFrom("DependentAssemblyWithFunctions.dll"); _referencedExtensionAssemblies = new[] diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs index 9ac5e4f2a..4b803ba90 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs @@ -6,6 +6,9 @@ using Microsoft.Azure.Functions.Worker.Sdk.Generators; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -23,11 +26,11 @@ public DiagnosticResultTests() var storageExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Storage.dll"); var blobExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs.dll"); var queueExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues.dll"); - var loggerExtension = Assembly.LoadFrom("Microsoft.Extensions.Logging.Abstractions.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 loggerExtension = typeof(NullLogger).Assembly; + var hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; referencedExtensionAssemblies = new[] { diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs index e7834b15a..8c4264741 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs @@ -7,6 +7,8 @@ using Microsoft.Azure.Functions.Worker.Sdk.Generators; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -23,11 +25,10 @@ public EventHubsBindingsTests() var abstractionsExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Abstractions.dll"); var httpExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Http.dll"); var eventHubsExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.EventHubs.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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; _referencedExtensionAssemblies = new[] { abstractionsExtension, diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs index 3aac634e1..26d1a39f4 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/HttpTriggerTests.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -18,13 +20,12 @@ public class HttpTriggerTests public HttpTriggerTests() { - // 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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; _referencedExtensionAssemblies = new[] { diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs index 72549b1a3..c512c7554 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/IntegratedTriggersAndBindingsTests.cs @@ -3,7 +3,13 @@ using System.Reflection; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -23,11 +29,14 @@ public IntegratedTriggersAndBindingsTests() var timerExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Timer.dll"); var blobExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs.dll"); var queueExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues.dll"); - var loggerExtension = Assembly.LoadFrom("Microsoft.Extensions.Logging.Abstractions.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 loggerExtension = typeof(NullLogger).Assembly; + var hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; + var actionResult = typeof(IActionResult).Assembly; + var aspnetHtpp = typeof(HttpContextAccessor).Assembly; + var httpRequest = typeof(HttpRequest).Assembly; _referencedExtensionAssemblies = new[] { @@ -41,7 +50,10 @@ public IntegratedTriggersAndBindingsTests() hostingExtension, hostingAbExtension, diExtension, - diAbExtension + diAbExtension, + actionResult, + aspnetHtpp, + httpRequest }; } @@ -177,6 +189,147 @@ await TestHelpers.RunTestAsync( expectedOutput); } + [Theory] + [InlineData(LanguageVersion.Latest)] + public async void FunctionsMultipleOutputBindingWithActionResult(LanguageVersion languageVersion) + { + string inputCode = """ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Net; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Http; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + + namespace FunctionApp + { + public static class FunctionsMultipleOutputBindingWithActionResult + { + [Function(nameof(FunctionsMultipleOutputBindingWithActionResult))] + public static MyOutputType Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, + FunctionContext context) + { + throw new NotImplementedException(); + } + + [Function("OutputTypeHttpHasTwoAttributes")] + public static MyOutputType2 Test([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, + FunctionContext context) + { + throw new NotImplementedException(); + } + } + + public class MyOutputType + { + [QueueOutput("functionstesting2", Connection = "AzureWebJobsStorage")] + public string Name { get; set; } + + [HttpResult] + public IActionResult HttpResponse { get; set; } + } + + public class MyOutputType2 + { + [QueueOutput("functionstesting2", Connection = "AzureWebJobsStorage")] + public string Name { get; set; } + + [SuppressMessage("Microsoft.Naming", "Foo", Justification = "Bar")] + [HttpResult] + public IActionResult HttpResponse { get; set; } + } + } + """; + + + 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"":""Name"",""type"":""queue"",""direction"":""Out"",""queueName"":""functionstesting2"",""connection"":""AzureWebJobsStorage""}"); + Function0RawBindings.Add(@"{""name"":""HttpResponse"",""type"":""http"",""direction"":""Out""}"); + + var Function0 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "FunctionsMultipleOutputBindingWithActionResult", + EntryPoint = "FunctionApp.FunctionsMultipleOutputBindingWithActionResult.Run", + RawBindings = Function0RawBindings, + 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""}"); + Function1RawBindings.Add(@"{""name"":""HttpResponse"",""type"":""http"",""direction"":""Out""}"); + + var Function1 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "OutputTypeHttpHasTwoAttributes", + EntryPoint = "FunctionApp.FunctionsMultipleOutputBindingWithActionResult.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 + { + /// + /// 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); + } + [Fact] public async Task FunctionWithStringDataTypeInputBinding() { diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/KafkaTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/KafkaTests.cs index e8a35e556..fa0052b08 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/KafkaTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/KafkaTests.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -20,10 +22,10 @@ public KafkaTests() { // 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 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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; var kafkaExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Kafka.dll"); _referencedExtensionAssemblies = new[] diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/NestedTypesTest.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/NestedTypesTest.cs index c3d846bb0..adbcbff6a 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/NestedTypesTest.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/NestedTypesTest.cs @@ -5,6 +5,8 @@ using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -20,10 +22,10 @@ public NestedTypesTests() // 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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; _referencedExtensionAssemblies = new[] { diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/RetryOptionsTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/RetryOptionsTests.cs index be11deab7..7800fe95e 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/RetryOptionsTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/RetryOptionsTests.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Xunit; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Microsoft.Azure.Functions.SdkGeneratorTests { @@ -20,10 +22,10 @@ public RetryOptionsTests() // 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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; var cosmosDBExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.CosmosDB.dll"); var timerExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Timer.dll"); diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/ServiceBustTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/ServiceBustTests.cs index b33e81f82..d08eb4aec 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/ServiceBustTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/ServiceBustTests.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk.Generators; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -21,10 +23,10 @@ public ServiceBusTests() // 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 sbExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.ServiceBus.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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; var blobExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs.dll"); var azSb = Assembly.LoadFrom("Azure.Messaging.ServiceBus.dll"); diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs index e29b7ce18..b632d0c2d 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/StorageBindingTests.cs @@ -4,6 +4,8 @@ using System.Reflection; using Microsoft.Azure.Functions.Worker.Sdk.Generators; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Xunit; namespace Microsoft.Azure.Functions.SdkGeneratorTests @@ -21,10 +23,10 @@ public StorageBindingTests() var httpExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Http.dll"); var storageExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Storage.dll"); var queueExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues.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 hostingExtension = typeof(HostBuilder).Assembly; + var diExtension = typeof(DefaultServiceProviderFactory).Assembly; + var hostingAbExtension = typeof(IHost).Assembly; + var diAbExtension = typeof(IServiceCollection).Assembly; var blobExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs.dll"); _referencedExtensionAssemblies = new[] diff --git a/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj b/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj index 26bf261ee..2b350f7c6 100644 --- a/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj +++ b/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj @@ -12,6 +12,7 @@ + @@ -23,7 +24,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -44,6 +45,7 @@ +