diff --git a/extensions/Worker.Extensions.Abstractions/src/SupportsDeferredBindingAttribute.cs b/extensions/Worker.Extensions.Abstractions/src/SupportsDeferredBindingAttribute.cs index b88551c43..52c054eaa 100644 --- a/extensions/Worker.Extensions.Abstractions/src/SupportsDeferredBindingAttribute.cs +++ b/extensions/Worker.Extensions.Abstractions/src/SupportsDeferredBindingAttribute.cs @@ -6,8 +6,8 @@ namespace Microsoft.Azure.Functions.Worker.Extensions.Abstractions { /// - /// Specifies if a binding attribute supports deferred binding when generating function metadata. - /// This is to be used on input, output or trigger attributes that support deferred binding. + /// Specifies if a converter supports deferred binding when generating function metadata. + /// This is to be used on converters that support deferred binding. /// [AttributeUsage(AttributeTargets.Class)] public class SupportsDeferredBindingAttribute : Attribute diff --git a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBConverter.cs b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBConverter.cs index a50823f1c..c5ca4b1d7 100644 --- a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBConverter.cs +++ b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBConverter.cs @@ -12,12 +12,14 @@ using Microsoft.Azure.Functions.Worker.Extensions.CosmosDB; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; namespace Microsoft.Azure.Functions.Worker { /// /// Converter to bind Cosmos DB type parameters. /// + [SupportsDeferredBinding] internal class CosmosDBConverter : IInputConverter { private readonly IOptionsSnapshot _cosmosOptions; diff --git a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBInputAttribute.cs b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBInputAttribute.cs index 30e35a713..bc6aca573 100644 --- a/extensions/Worker.Extensions.CosmosDB/src/CosmosDBInputAttribute.cs +++ b/extensions/Worker.Extensions.CosmosDB/src/CosmosDBInputAttribute.cs @@ -2,11 +2,13 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; +using Microsoft.Azure.Functions.Worker.Converters; using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; namespace Microsoft.Azure.Functions.Worker { - [SupportsDeferredBinding] + [AllowConverterFallback(false)] + [InputConverter(typeof(CosmosDBConverter))] public sealed class CosmosDBInputAttribute : InputBindingAttribute { /// diff --git a/extensions/Worker.Extensions.CosmosDB/src/CosmosExtensionStartup.cs b/extensions/Worker.Extensions.CosmosDB/src/CosmosExtensionStartup.cs index d67838cf7..5036e3b01 100644 --- a/extensions/Worker.Extensions.CosmosDB/src/CosmosExtensionStartup.cs +++ b/extensions/Worker.Extensions.CosmosDB/src/CosmosExtensionStartup.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; @@ -24,11 +24,6 @@ public override void Configure(IFunctionsWorkerApplicationBuilder applicationBui applicationBuilder.Services.AddAzureClientsCore(); // Adds AzureComponentFactory applicationBuilder.Services.AddOptions(); applicationBuilder.Services.AddSingleton, CosmosDBBindingOptionsSetup>(); - - applicationBuilder.Services.Configure((workerOption) => - { - workerOption.InputConverters.RegisterAt(0); - }); } } } diff --git a/extensions/Worker.Extensions.Storage.Blobs/src/BlobInputAttribute.cs b/extensions/Worker.Extensions.Storage.Blobs/src/BlobInputAttribute.cs index 78ab31abc..89e4ca58c 100644 --- a/extensions/Worker.Extensions.Storage.Blobs/src/BlobInputAttribute.cs +++ b/extensions/Worker.Extensions.Storage.Blobs/src/BlobInputAttribute.cs @@ -1,11 +1,13 @@ // 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.Converters; using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; namespace Microsoft.Azure.Functions.Worker { - [SupportsDeferredBinding] + [AllowConverterFallback(false)] + [InputConverter(typeof(BlobStorageConverter))] public sealed class BlobInputAttribute : InputBindingAttribute { private readonly string _blobPath; diff --git a/extensions/Worker.Extensions.Storage.Blobs/src/BlobStorageConverter.cs b/extensions/Worker.Extensions.Storage.Blobs/src/BlobStorageConverter.cs index 280ccc22e..b0c4be9f0 100644 --- a/extensions/Worker.Extensions.Storage.Blobs/src/BlobStorageConverter.cs +++ b/extensions/Worker.Extensions.Storage.Blobs/src/BlobStorageConverter.cs @@ -15,12 +15,14 @@ using Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; namespace Microsoft.Azure.Functions.Worker { /// /// Converter to bind Blob Storage type parameters. /// + [SupportsDeferredBinding] internal class BlobStorageConverter : IInputConverter { private readonly IOptions _workerOptions; diff --git a/extensions/Worker.Extensions.Storage.Blobs/src/BlobTriggerAttribute.cs b/extensions/Worker.Extensions.Storage.Blobs/src/BlobTriggerAttribute.cs index 59ac9fbc8..6313dfe48 100644 --- a/extensions/Worker.Extensions.Storage.Blobs/src/BlobTriggerAttribute.cs +++ b/extensions/Worker.Extensions.Storage.Blobs/src/BlobTriggerAttribute.cs @@ -1,11 +1,13 @@ // 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.Converters; using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; namespace Microsoft.Azure.Functions.Worker { - [SupportsDeferredBinding] + [AllowConverterFallback(true)] + [InputConverter(typeof(BlobStorageConverter))] public sealed class BlobTriggerAttribute : TriggerBindingAttribute { private readonly string _blobPath; diff --git a/extensions/Worker.Extensions.Storage.Blobs/src/StorageExtensionStartup.cs b/extensions/Worker.Extensions.Storage.Blobs/src/StorageExtensionStartup.cs index a7bee100d..9f8b10bb1 100644 --- a/extensions/Worker.Extensions.Storage.Blobs/src/StorageExtensionStartup.cs +++ b/extensions/Worker.Extensions.Storage.Blobs/src/StorageExtensionStartup.cs @@ -24,11 +24,6 @@ public override void Configure(IFunctionsWorkerApplicationBuilder applicationBui applicationBuilder.Services.AddAzureClientsCore(); // Adds AzureComponentFactory applicationBuilder.Services.AddOptions(); applicationBuilder.Services.AddSingleton, BlobStorageBindingOptionsSetup>(); - - applicationBuilder.Services.Configure((workerOption) => - { - workerOption.InputConverters.RegisterAt(0); - }); } } } diff --git a/release_notes.md b/release_notes.md index cacacc889..46d9b33b6 100644 --- a/release_notes.md +++ b/release_notes.md @@ -2,3 +2,5 @@ + +- Implementation for bypass deferred binding (#1462/#1495) \ No newline at end of file diff --git a/samples/WorkerBindingSamples/NuGet.Config b/samples/WorkerBindingSamples/NuGet.Config new file mode 100644 index 000000000..3f0e00340 --- /dev/null +++ b/samples/WorkerBindingSamples/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/WorkerBindingSamples/WorkerBindingSamples.csproj b/samples/WorkerBindingSamples/WorkerBindingSamples.csproj index 66fe3d12a..3342d74a2 100644 --- a/samples/WorkerBindingSamples/WorkerBindingSamples.csproj +++ b/samples/WorkerBindingSamples/WorkerBindingSamples.csproj @@ -1,29 +1,30 @@  - - net7.0 - v4 - Exe - enable - enable - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - Never - - + + net7.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + \ No newline at end of file diff --git a/sdk/Sdk/Constants.cs b/sdk/Sdk/Constants.cs index bf6a24df1..3c72b7449 100644 --- a/sdk/Sdk/Constants.cs +++ b/sdk/Sdk/Constants.cs @@ -16,6 +16,8 @@ internal static class Constants internal const string ExponentialBackoffRetryAttributeType = "Microsoft.Azure.Functions.Worker.ExponentialBackoffRetryAttribute"; internal const string BindingPropertyNameAttributeType = "Microsoft.Azure.Functions.Worker.Extensions.Abstractions.BindingPropertyNameAttribute"; internal const string SupportsDeferredBindingAttributeType = "Microsoft.Azure.Functions.Worker.Extensions.Abstractions.SupportsDeferredBindingAttribute"; + internal const string InputConverterAttributeType = "Microsoft.Azure.Functions.Worker.Converters.InputConverterAttribute"; + internal const string SupportedConverterTypeAttributeType = "Microsoft.Azure.Functions.Worker.Converters.SupportedConverterTypeAttribute"; // System types internal const string IEnumerableType = "System.Collections.IEnumerable"; diff --git a/sdk/Sdk/FunctionMetadataGenerator.cs b/sdk/Sdk/FunctionMetadataGenerator.cs index f05f57968..3b1730daa 100644 --- a/sdk/Sdk/FunctionMetadataGenerator.cs +++ b/sdk/Sdk/FunctionMetadataGenerator.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using Mono.Cecil; +using Mono.Collections.Generic; namespace Microsoft.Azure.Functions.Worker.Sdk { @@ -487,7 +488,7 @@ private static ExpandoObject BuildBindingMetadataFromAttribute(CustomAttribute a // For extensions that support deferred binding, set the supportsDeferredBinding property so parameters are bound by the worker // Only use deferred binding for input and trigger bindings, output is out not currently supported - if (SupportsDeferredBinding(attribute) && direction != Constants.OutputBindingDirection) + if (SupportsDeferredBinding(attribute, parameterType) && direction != Constants.OutputBindingDirection) { bindingProperties.Add(Constants.SupportsDeferredBindingProperty, Boolean.TrueString); } @@ -795,7 +796,8 @@ private static string GetBindingDirection(CustomAttribute attribute) return Constants.InputBindingDirection; } - private static bool SupportsDeferredBinding(CustomAttribute attribute) + // Input and Trigger Binding Attribute will be checked for supporting deferred binding + private static bool SupportsDeferredBinding(CustomAttribute attribute, TypeReference bindingType) { var typeDefinition = attribute?.AttributeType?.Resolve(); @@ -804,8 +806,75 @@ private static bool SupportsDeferredBinding(CustomAttribute attribute) return false; } - return typeDefinition.CustomAttributes - .Any(a => string.Equals(a.AttributeType.FullName, Constants.SupportsDeferredBindingAttributeType, StringComparison.Ordinal)); + // checking attributes advertised by the binding attribute + foreach (CustomAttribute bindingAttribute in typeDefinition.CustomAttributes) + { + if (string.Equals(bindingAttribute.AttributeType.FullName, Constants.InputConverterAttributeType, StringComparison.Ordinal)) + { + // InputConverterAttribute will have supported converter type + foreach (var customAttribute in bindingAttribute.ConstructorArguments) + { + if (DoesConverterSupportDeferredBinding(customAttribute, bindingType)) + { + return true; + } + } + } + } + + return false; + } + + private static bool DoesConverterSupportDeferredBinding(CustomAttributeArgument customAttribute, TypeReference bindingType) + { + var typeReferenceValue = customAttribute.Value as TypeReference; + var typeReferenceCustomAttributes = typeReferenceValue?.Resolve().CustomAttributes; + + // Attributes advertised by converter + if (typeReferenceCustomAttributes is not null) + { + bool converterAdvertisesDeferredBindingSupport = typeReferenceCustomAttributes.Any(a => string.Equals(a.AttributeType.FullName, Constants.SupportsDeferredBindingAttributeType, StringComparison.Ordinal)); + + if (converterAdvertisesDeferredBindingSupport) + { + bool converterAdvertisesTypes = typeReferenceCustomAttributes.Any(a => string.Equals(a.AttributeType.FullName, Constants.SupportedConverterTypeAttributeType, StringComparison.Ordinal)); + + if (!converterAdvertisesTypes) + { + // If a converter advertises deferred binding but does not explictly advertise any types then DeferredBinding will be supported for all the types + return true; + } + + return DoesConverterSupportTargetType(typeReferenceCustomAttributes, bindingType); + } + } + + return false; + } + + private static bool DoesConverterSupportTargetType(Collection customAttributes, TypeReference bindingType) + { + // Parse attributes advertised by converter + foreach (CustomAttribute attribute in customAttributes) + { + if (string.Equals(attribute.AttributeType.FullName, Constants.SupportedConverterTypeAttributeType, StringComparison.Ordinal)) + { + foreach (CustomAttributeArgument element in attribute.ConstructorArguments) + { + if (string.Equals(element.Type.FullName, typeof(Type).FullName, StringComparison.Ordinal)) + { + var supportedType = element.Value as TypeReference; + + if (supportedType is not null && string.Equals(supportedType.FullName, bindingType.FullName, StringComparison.Ordinal)) + { + return true; + } + } + } + } + } + + return false; } private static bool IsOutputBindingType(CustomAttribute attribute) diff --git a/src/DotNetWorker.Core/Context/Features/DefaultFunctionInputBindingFeature.cs b/src/DotNetWorker.Core/Context/Features/DefaultFunctionInputBindingFeature.cs index ad5880659..4e10ff6ca 100644 --- a/src/DotNetWorker.Core/Context/Features/DefaultFunctionInputBindingFeature.cs +++ b/src/DotNetWorker.Core/Context/Features/DefaultFunctionInputBindingFeature.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Converters; @@ -69,18 +70,29 @@ public async ValueTask BindFunctionInputAsync(Functi } else { - IReadOnlyDictionary properties = ImmutableDictionary.Empty; + var properties = new Dictionary(); // Pass info about specific input converter type defined for this parameter, if present. if (param.Properties.TryGetValue(PropertyBagKeys.ConverterType, out var converterTypeAssemblyFullName)) { - properties = new Dictionary() + properties.Add(PropertyBagKeys.ConverterType, converterTypeAssemblyFullName); + } + + // Pass info about the flag to allow fallback to default converters defined for this parameter, if present. + if (param.Properties.TryGetValue(PropertyBagKeys.AllowConverterFallback, out var flag)) + { + properties.Add(PropertyBagKeys.AllowConverterFallback, flag); + } + + // Pass info about input converter types defined for this parameter, if present. + if (param.Properties.TryGetValue(PropertyBagKeys.BindingAttributeSupportedConverters, out var converters)) { - { PropertyBagKeys.ConverterType, converterTypeAssemblyFullName } - }; + properties.Add(PropertyBagKeys.BindingAttributeSupportedConverters, converters); } - var converterContext = _converterContextFactory.Create(param.Type, source, context, properties); + var converterContext = _converterContextFactory.Create(param.Type, source, context, properties.Count() != 0 + ? properties.ToImmutableDictionary() + : ImmutableDictionary.Empty); bindingResult = await inputConversionFeature.ConvertAsync(converterContext); inputBindingCache[cacheKey] = bindingResult; diff --git a/src/DotNetWorker.Core/Context/Features/DefaultInputConversionFeature.cs b/src/DotNetWorker.Core/Context/Features/DefaultInputConversionFeature.cs index a1f561bc2..d3d4940e2 100644 --- a/src/DotNetWorker.Core/Context/Features/DefaultInputConversionFeature.cs +++ b/src/DotNetWorker.Core/Context/Features/DefaultInputConversionFeature.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Converters; @@ -47,17 +48,39 @@ public async ValueTask ConvertAsync(ConverterContext converter } } - // Use the registered converters. The first converter which can handle the conversion wins. - foreach (var converter in _inputConverterProvider.RegisteredInputConverters) - { - var conversionResult = await ConvertAsyncUsingConverter(converter, converterContext); + // Get all converters advertised by the Binding Attribute along with supported types by each converter + Dictionary>? advertisedConverterTypes = GetExplicitConverterTypes(converterContext); - if (conversionResult.Status != ConversionStatus.Unhandled) + if (advertisedConverterTypes is not null) + { + foreach (var converterType in advertisedConverterTypes) { - return conversionResult; + if (IsTargetTypeSupportedByConverter(converterType.Value, converterContext.TargetType)) + { + var conversionResult = await ConvertAsyncUsingConverter(converterType.Key, converterContext); + + if (conversionResult.Status != ConversionStatus.Unhandled) + { + return conversionResult; + } + } } + } - // If "Status" is Unhandled, we move on to the next converter and try to convert with that. + if (IsConverterFallbackAllowed(converterContext)) + { + // Use the registered converters. The first converter which can handle the conversion wins. + foreach (var converter in _inputConverterProvider.RegisteredInputConverters) + { + var conversionResult = await ConvertAsyncUsingConverter(converter, converterContext); + + if (conversionResult.Status != ConversionStatus.Unhandled) + { + return conversionResult; + } + + // If "Status" is Unhandled, we move on to the next converter and try to convert with that. + } } return ConversionResult.Unhandled(); @@ -111,6 +134,35 @@ private async ValueTask AwaitAndReturnConversionTaskResult(Val return null; } + + /// + /// Gets all converters advertised by the Binding Attribute along with supported types by each converter + /// + private Dictionary>? GetExplicitConverterTypes(ConverterContext context) + { + var result = new Dictionary>(); + + if (context.Properties.TryGetValue(PropertyBagKeys.BindingAttributeSupportedConverters, out var converterTypes)) + { + if (converterTypes is Dictionary> converters) + { + var interfaceType = typeof(IInputConverter); + + foreach (var (converterType, supportedTypes) in converters) + { + if (converterType is not null && interfaceType.IsAssignableFrom(converterType)) + { + result.Add(_inputConverterProvider.GetOrCreateConverterInstance(converterType), supportedTypes); + } + } + + return result; + } + } + + return null; + } + /// /// Checks a type has an "InputConverter" attribute decoration present /// and if present, return the assembly qualified name of the "ConverterType" property. @@ -133,5 +185,35 @@ private async ValueTask AwaitAndReturnConversionTaskResult(Val }, targetType); } + + /// + /// Returns boolean value indicating whether fallback to registered converters allowed by the binding attribute + /// + private bool IsConverterFallbackAllowed(ConverterContext context) + { + if (context.Properties.TryGetValue(PropertyBagKeys.AllowConverterFallback, out var result)) + { + if (result is not null && result is bool res) + { + return res; + } + } + + return true; + } + + /// + /// Returns boolean value indicating whether Target type is supported by the converter + /// + private bool IsTargetTypeSupportedByConverter(List supportedTypes, Type targetType) + { + if (supportedTypes is null or { Count: 0 }) + { + return true; + } + + // If types are explicitly advertised by the converter then we should send only those types for conversion. + return supportedTypes.Any(a => a.AssemblyQualifiedName == targetType.AssemblyQualifiedName); + } } } diff --git a/src/DotNetWorker.Core/Converters/Converter/AllowConverterFallbackAttribute.cs b/src/DotNetWorker.Core/Converters/Converter/AllowConverterFallbackAttribute.cs new file mode 100644 index 000000000..5feb6804d --- /dev/null +++ b/src/DotNetWorker.Core/Converters/Converter/AllowConverterFallbackAttribute.cs @@ -0,0 +1,29 @@ +// 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.Converters +{ + /// + /// An attribute that specifies if Converter fallback is allowed + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class AllowConverterFallbackAttribute : Attribute + { + /// + /// Gets the value of whether Converter fallback is allowed. + /// + public bool AllowConverterFallback { get; } + + /// + /// Creates a new instance of + /// + /// The value to indicate if converter fallback is allowed. + public AllowConverterFallbackAttribute(bool allowConverterFallback) + { + AllowConverterFallback = allowConverterFallback; + } + + } +} diff --git a/src/DotNetWorker.Core/Converters/Converter/DefaultInputConverterProvider.cs b/src/DotNetWorker.Core/Converters/Converter/DefaultInputConverterProvider.cs index ccfee9f0b..5545ea352 100644 --- a/src/DotNetWorker.Core/Converters/Converter/DefaultInputConverterProvider.cs +++ b/src/DotNetWorker.Core/Converters/Converter/DefaultInputConverterProvider.cs @@ -50,12 +50,30 @@ public IEnumerable RegisteredInputConverters { yield return _converterCache.GetOrAdd(converterType.AssemblyQualifiedName!, (key) => { - return (IInputConverter)ActivatorUtilities.CreateInstance(_serviceProvider, converterType); + return GetOrCreateConverterInstance(converterType); }); } } } + /// + /// Gets an instance of the converter for the type requested. + /// + /// The type for which we are requesting an IInputConverter instance. + /// Throws when the converterType param is null. + /// IConverter instance of the requested type. + public IInputConverter GetOrCreateConverterInstance(Type converterType) + { + if (converterType is null) + { + throw new ArgumentNullException($"Could not create an instance of {(nameof(converterType))}."); + } + + EnsureTypeCanBeAssigned(converterType); + + return (IInputConverter)ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, converterType); + } + /// /// Gets an instance of the converter for the type requested. /// @@ -80,9 +98,7 @@ public IInputConverter GetOrCreateConverterInstance(string converterTypeName) throw new InvalidOperationException($"Could not create an instance of {converterTypeAssemblyQualifiedName}."); } - EnsureTypeCanBeAssigned(converterType); - - return (IInputConverter)ActivatorUtilities.CreateInstance(_serviceProvider, converterType); + return GetOrCreateConverterInstance(converterType); }, converterTypeName); } diff --git a/src/DotNetWorker.Core/Converters/Converter/IInputConverterProvider.cs b/src/DotNetWorker.Core/Converters/Converter/IInputConverterProvider.cs index b7ddc77f2..d8b4820b7 100644 --- a/src/DotNetWorker.Core/Converters/Converter/IInputConverterProvider.cs +++ b/src/DotNetWorker.Core/Converters/Converter/IInputConverterProvider.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; namespace Microsoft.Azure.Functions.Worker.Converters @@ -16,6 +17,13 @@ internal interface IInputConverterProvider /// IEnumerable RegisteredInputConverters { get; } + /// + /// Gets an instance of the converter for the type requested. + /// + /// The type for which we are requesting an IInputConverter instance. + /// IInputConverter instance of the requested type. + IInputConverter GetOrCreateConverterInstance(Type converterType); + /// /// Gets an instance of the converter for the type requested. /// diff --git a/src/DotNetWorker.Core/Converters/Converter/InputConverterAttribute.cs b/src/DotNetWorker.Core/Converters/Converter/InputConverterAttribute.cs index 09e6e7905..4cdad86c8 100644 --- a/src/DotNetWorker.Core/Converters/Converter/InputConverterAttribute.cs +++ b/src/DotNetWorker.Core/Converters/Converter/InputConverterAttribute.cs @@ -13,7 +13,8 @@ namespace Microsoft.Azure.Functions.Worker.Converters AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Enum | - AttributeTargets.Struct)] + AttributeTargets.Struct, + AllowMultiple = true)] public sealed class InputConverterAttribute : Attribute { /// diff --git a/src/DotNetWorker.Core/Converters/Converter/PropertyBagKeys.cs b/src/DotNetWorker.Core/Converters/Converter/PropertyBagKeys.cs index 77f14f6ea..ce4606e07 100644 --- a/src/DotNetWorker.Core/Converters/Converter/PropertyBagKeys.cs +++ b/src/DotNetWorker.Core/Converters/Converter/PropertyBagKeys.cs @@ -5,6 +5,8 @@ namespace Microsoft.Azure.Functions.Worker.Converters { internal static class PropertyBagKeys { - internal const string ConverterType ="converterType"; + internal const string ConverterType = "converterType"; + internal const string AllowConverterFallback = "allowConverterFallback"; + internal const string BindingAttributeSupportedConverters = "bindingAttributeSupportedConverters"; } } diff --git a/src/DotNetWorker.Core/Converters/Converter/SupportedConverterType.cs b/src/DotNetWorker.Core/Converters/Converter/SupportedConverterType.cs new file mode 100644 index 000000000..5fa2b2987 --- /dev/null +++ b/src/DotNetWorker.Core/Converters/Converter/SupportedConverterType.cs @@ -0,0 +1,29 @@ +// 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.Converters +{ + /// + /// An attribute that can specify a type supported by function input conversion. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class SupportedConverterTypeAttribute : Attribute + { + /// + /// Gets the input converter type. + /// + public Type Type { get; } + + /// + /// Creates a new instance of + /// + /// Input converter type. + /// Thrown when type is null + public SupportedConverterTypeAttribute(Type type) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + } + } +} diff --git a/src/DotNetWorker.Grpc/Definition/GrpcFunctionDefinition.cs b/src/DotNetWorker.Grpc/Definition/GrpcFunctionDefinition.cs index 2b9d2335e..1abf2f3e3 100644 --- a/src/DotNetWorker.Grpc/Definition/GrpcFunctionDefinition.cs +++ b/src/DotNetWorker.Grpc/Definition/GrpcFunctionDefinition.cs @@ -10,6 +10,11 @@ using Microsoft.Azure.Functions.Worker.Grpc.Messages; using Microsoft.Azure.Functions.Worker.Invocation; using Microsoft.Azure.Functions.Worker.Converters; +using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; +using Microsoft.Azure.Functions.Worker.Context.Features; +using Microsoft.Azure.Functions.Worker.Core; +using System.Text; +using System.Xml.Linq; namespace Microsoft.Azure.Functions.Worker.Definition { @@ -79,8 +84,79 @@ private ImmutableDictionary GetAdditionalPropertiesDictionary(Pa { PropertyBagKeys.ConverterType, inputConverterAttribute.ConverterType.AssemblyQualifiedName! } }.ToImmutableDictionary(); } + else + { + var inputAttribute = parameterInfo?.GetCustomAttribute(); + var triggerAttribute = parameterInfo?.GetCustomAttribute(); + + return GetBindingAttributePropertiesDictionary(inputAttribute) ?? + GetBindingAttributePropertiesDictionary(triggerAttribute) ?? + ImmutableDictionary.Empty; + } + } + + private ImmutableDictionary? GetBindingAttributePropertiesDictionary(BindingAttribute? bindingAttribute) + { + if (bindingAttribute is null) + { + return null; + } + + var output = new Dictionary(); + bool isInputConverterAttributeAdvertised = false; + + // ConverterTypesDictionary will be "object" part of the return value - ImmutableDictionary + // The dictionary has key of type IInputConverter and value as List of Types supported by the converter. + var converterTypesDictionary = new Dictionary>(); + + + Type type = bindingAttribute.GetType(); + var attributes = type.GetCustomAttributes(); + + if (attributes.Any()) + { + isInputConverterAttributeAdvertised = true; + + foreach (var attribute in attributes) + { + Type converter = attribute.ConverterType; + List supportedTypes = GetTypesSupportedByConverter(converter); + converterTypesDictionary.Add(converter, supportedTypes); + } + } + + output.Add(PropertyBagKeys.BindingAttributeSupportedConverters, converterTypesDictionary); + + if (isInputConverterAttributeAdvertised) + { + output[PropertyBagKeys.AllowConverterFallback] = type.GetCustomAttribute()?.AllowConverterFallback ?? true; + } + + return output.ToImmutableDictionary(); + } + + private List GetTypesSupportedByConverter(Type converter) + { + var types = new List(); + + foreach (CustomAttributeData converterAttribute in converter.CustomAttributes) + { + if (converterAttribute.AttributeType == typeof(SupportedConverterTypeAttribute)) + { + foreach (CustomAttributeTypedArgument supportedType in converterAttribute.ConstructorArguments) + { + if (supportedType is { ArgumentType: not null, Value: not null } && supportedType.ArgumentType == typeof(Type)) + { + if (supportedType.Value is Type type) + { + types.Add(type); + } + } + } + } + } - return ImmutableDictionary.Empty; + return types; } } } diff --git a/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj b/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj index 4028ebcc6..36b8eec72 100644 --- a/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj +++ b/src/DotNetWorker.Grpc/DotNetWorker.Grpc.csproj @@ -45,11 +45,8 @@ - - + + @@ -57,6 +54,7 @@ + diff --git a/src/DotNetWorker.Grpc/Http/GrpcWorkerDiagnostics.cs b/src/DotNetWorker.Grpc/Http/GrpcWorkerDiagnostics.cs index 9cb9256c2..1466ea435 100644 --- a/src/DotNetWorker.Grpc/Http/GrpcWorkerDiagnostics.cs +++ b/src/DotNetWorker.Grpc/Http/GrpcWorkerDiagnostics.cs @@ -65,7 +65,7 @@ public void OnFunctionLoaded(FunctionDefinition definition) EventId = nameof(OnFunctionLoaded), Level = RpcLog.Types.Level.Debug, LogCategory = RpcLog.Types.RpcLogCategory.System, - Message = JsonSerializer.Serialize(definition, SerializerOptions) + Message = definition.Name } }; } diff --git a/test/DotNetWorkerTests/DotNetWorkerTests.csproj b/test/DotNetWorkerTests/DotNetWorkerTests.csproj index 1d3b37876..4fb9cfa48 100644 --- a/test/DotNetWorkerTests/DotNetWorkerTests.csproj +++ b/test/DotNetWorkerTests/DotNetWorkerTests.csproj @@ -23,6 +23,7 @@ + diff --git a/test/DotNetWorkerTests/Features/DefaultInputConversionFeatureTests.cs b/test/DotNetWorkerTests/Features/DefaultInputConversionFeatureTests.cs index efb89fa06..a5ae075ca 100644 --- a/test/DotNetWorkerTests/Features/DefaultInputConversionFeatureTests.cs +++ b/test/DotNetWorkerTests/Features/DefaultInputConversionFeatureTests.cs @@ -4,11 +4,14 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Text.Json; using System.Threading.Tasks; using Azure.Core.Serialization; using Microsoft.Azure.Functions.Worker.Context.Features; using Microsoft.Azure.Functions.Worker.Converters; +using Microsoft.Azure.Functions.Worker.Core; +using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; using Xunit; namespace Microsoft.Azure.Functions.Worker.Tests.Features @@ -104,6 +107,169 @@ public async Task Convert_Using_Converter_From_InputConverterAttribute_Of_Target Assert.Equal("16-converted customer", customer.Name); } + [Fact] + public async Task Convert_Using_Advertised_Converter_Specified_In_ConverterContext_Properties_Works() + { + IReadOnlyDictionary properties = new Dictionary() + { + { PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary>() { { + typeof(MySimpleSyncInputConverter), + new List() { typeof(string), typeof(string[])} } } }, + { PropertyBagKeys.AllowConverterFallback, false } + }; + + var converterContext = CreateConverterContext(typeof(string), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties); + + var actual = await _defaultInputConversionFeature.ConvertAsync(converterContext); + + Assert.Equal(ConversionStatus.Succeeded, actual.Status); + Assert.Equal("0c67c078-7213-4e91-ad41-f8747c865f3d-converted value", actual.Value); + TestUtility.AssertIsTypeAndConvert(actual.Value); + } + + [Fact] + public async Task Convert_Using_Advertised_Converter_Specified_In_ConverterContext_Properties_Unhandled() + { + IReadOnlyDictionary properties = new Dictionary() + { + { PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary>() { { + typeof(MySimpleSyncInputConverter), + new List() { typeof(string), typeof(Stream), typeof(IEnumerable), typeof(Stream[]) } } } }, + { PropertyBagKeys.AllowConverterFallback, false } + }; + + var converterContext = CreateConverterContext(typeof(object), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties); + + var actual = await _defaultInputConversionFeature.ConvertAsync(converterContext); + + Assert.Equal(ConversionStatus.Unhandled, actual.Status); + } + + [Fact] + public async Task Convert_Using_Advertised_Converter_Specified_In_ConverterContext_ArrayType_Unhandled() + { + IReadOnlyDictionary properties = new Dictionary() + { + { PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary>() { { + typeof(MySimpleSyncInputConverter), + new List() { typeof(string), typeof(Stream), typeof(IEnumerable), typeof(Stream[]) } } } }, + { PropertyBagKeys.AllowConverterFallback, false } + }; + + var converterContext = CreateConverterContext(typeof(string[]), new string[] { "val1", "val2" }, properties); + + var actual = await _defaultInputConversionFeature.ConvertAsync(converterContext); + + Assert.Equal(ConversionStatus.Unhandled, actual.Status); + } + + [Fact] + public async Task Convert_Using_Advertised_Converter_Specified_In_ConverterContext_Properties_Poco_Unhandled() + { + // Explicitly specify a converter to be used via ConverterContext.Properties. + IReadOnlyDictionary properties = new Dictionary() + { + { PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary>() { { + typeof(MySimpleSyncInputConverter), + new List() { typeof(string), typeof(Stream) } } } }, + { PropertyBagKeys.AllowConverterFallback, false } + }; + + var converterContext = CreateConverterContext(typeof(Poco), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties); + + var actual = await _defaultInputConversionFeature.ConvertAsync(converterContext); + + Assert.Equal(ConversionStatus.Unhandled, actual.Status); + } + + [Fact] + public async Task Convert_Using_Advertised_Converter_Specified_In_ConverterContext_Properties_Poco_Works() + { + // No explicit type advertised and converter supports deferred binding so all the types will be supported + IReadOnlyDictionary properties = new Dictionary() + { + { PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary>() { { + typeof(MySimpleSyncInputConverter2), new List() } } }, + { PropertyBagKeys.AllowConverterFallback, false } + }; + + var converterContext = CreateConverterContext(typeof(Poco), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties); + + var actual = await _defaultInputConversionFeature.ConvertAsync(converterContext); + + Assert.Equal(ConversionStatus.Succeeded, actual.Status); + } + + + [Fact] + public async Task Convert_Using_Advertised_Converter_Specified_In_ConverterContext_Properties_Poco_Succeeded() + { + // No explicit type advertised and converter supports deferred binding so all the types will be supported + IReadOnlyDictionary properties = new Dictionary() + { + { PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary>() { { + typeof(MySimpleSyncInputConverter2), new List() } } }, + { PropertyBagKeys.AllowConverterFallback, false } + }; + var converterContext = CreateConverterContext(typeof(Poco[]), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties); + + var actual = await _defaultInputConversionFeature.ConvertAsync(converterContext); + + Assert.Equal(ConversionStatus.Succeeded, actual.Status); + } + + [Fact] + public async Task Convert_Using_Advertised_Converter_Specified_In_ConverterContext_Properties_StringCollection_Works() + { + // No explicit type advertised and converter supports deferred binding so all the types will be supported + IReadOnlyDictionary properties = new Dictionary() + { + { PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary>() { { + typeof(MySimpleSyncInputConverter2), null } } }, + { PropertyBagKeys.AllowConverterFallback, false } + }; + var converterContext = CreateConverterContext(typeof(IEnumerable), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties); + + var actual = await _defaultInputConversionFeature.ConvertAsync(converterContext); + + Assert.Equal(ConversionStatus.Succeeded, actual.Status); + } + + [Fact] + public async Task Convert_Using_Advertised_Converter_Specified_In_ConverterContext_Properties_FallbackEnabled_Works() + { + // Explicitly specify a converter to be used via ConverterContext.Properties. + IReadOnlyDictionary properties = new Dictionary() + { + { PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary>() { { + typeof(MySimpleSyncInputConverter), new List() { } } } }, + { PropertyBagKeys.AllowConverterFallback, true } + }; + var converterContext = CreateConverterContext(typeof(string), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties); + + var actual = await _defaultInputConversionFeature.ConvertAsync(converterContext); + + Assert.Equal(ConversionStatus.Succeeded, actual.Status); + Assert.Equal("0c67c078-7213-4e91-ad41-f8747c865f3d-converted value", actual.Value); + TestUtility.AssertIsTypeAndConvert(actual.Value); + } + + [Fact] + public async Task Convert_Using_Advertised_Converter_Specified_In_ConverterContext_Properties_DisableFallback_Unhandled() + { + // Explicitly specify a converter to be used via ConverterContext.Properties. + IReadOnlyDictionary properties = new Dictionary() + { + { PropertyBagKeys.BindingAttributeSupportedConverters, new Tuple>(false, new List() { }) }, + { PropertyBagKeys.AllowConverterFallback, false } + }; + var converterContext = CreateConverterContext(typeof(string), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties); + + var actual = await _defaultInputConversionFeature.ConvertAsync(converterContext); + + Assert.Equal(ConversionStatus.Unhandled, actual.Status); + } + [InputConverter(typeof(MyCustomerAsyncInputConverter))] internal record Customer(string Id, string Name); @@ -118,6 +284,9 @@ public async ValueTask ConvertAsync(ConverterContext context) } } + [SupportsDeferredBinding] + [SupportedConverterType(typeof(string))] + [SupportedConverterType(typeof(Stream))] internal class MySimpleSyncInputConverter : IInputConverter { public ValueTask ConvertAsync(ConverterContext context) @@ -128,6 +297,21 @@ public ValueTask ConvertAsync(ConverterContext context) } } + [SupportsDeferredBinding] + internal class MySimpleSyncInputConverter2 : IInputConverter + { + public ValueTask ConvertAsync(ConverterContext context) + { + var result = ConversionResult.Success(value: context.Source + "-converted value"); + + return new ValueTask(result); + } + } + + internal class Poco + { + } + private DefaultConverterContext CreateConverterContext(Type targetType, object source, IReadOnlyDictionary properties = null) { var definition = new TestFunctionDefinition(); diff --git a/test/DotNetWorkerTests/GrpcFunctionDefinitionTests.cs b/test/DotNetWorkerTests/GrpcFunctionDefinitionTests.cs index 2edffc281..34dc65348 100644 --- a/test/DotNetWorkerTests/GrpcFunctionDefinitionTests.cs +++ b/test/DotNetWorkerTests/GrpcFunctionDefinitionTests.cs @@ -1,9 +1,14 @@ // 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.Linq; using System.Threading; using Microsoft.Azure.Functions.Tests; +using Microsoft.Azure.Functions.Worker.Converters; +using Microsoft.Azure.Functions.Worker.Core; using Microsoft.Azure.Functions.Worker.Grpc.Messages; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Azure.Functions.Worker.Invocation; @@ -74,6 +79,74 @@ public void Creates() }); } + [Fact] + public void GrpcFunctionDefinition_BlobInput_Creates() + { + using var testVariables = new TestScopedEnvironmentVariable("FUNCTIONS_WORKER_DIRECTORY", "."); + + var bindingInfoProvider = new DefaultOutputBindingsInfoProvider(); + var methodInfoLocator = new DefaultMethodInfoLocator(); + + string fullPathToThisAssembly = GetType().Assembly.Location; + var functionLoadRequest = new FunctionLoadRequest + { + FunctionId = "abc", + Metadata = new RpcFunctionMetadata + { + EntryPoint = $"Microsoft.Azure.Functions.Worker.Tests.{nameof(GrpcFunctionDefinitionTests)}+{nameof(MyBlobFunctionClass)}.{nameof(MyBlobFunctionClass.Run)}", + ScriptFile = Path.GetFileName(fullPathToThisAssembly), + Name = "myfunction" + } + }; + + // We base this on the request exclusively, not the binding attributes. + functionLoadRequest.Metadata.Bindings.Add("req", new BindingInfo { Type = "HttpTrigger", Direction = Direction.In }); + functionLoadRequest.Metadata.Bindings.Add("$return", new BindingInfo { Type = "Http", Direction = Direction.Out }); + + FunctionDefinition definition = functionLoadRequest.ToFunctionDefinition(methodInfoLocator); + + Assert.Equal(functionLoadRequest.FunctionId, definition.Id); + Assert.Equal(functionLoadRequest.Metadata.EntryPoint, definition.EntryPoint); + Assert.Equal(functionLoadRequest.Metadata.Name, definition.Name); + Assert.Equal(fullPathToThisAssembly, definition.PathToAssembly); + + // Parameters + Assert.Collection(definition.Parameters, + p => + { + Assert.Equal("req", p.Name); + Assert.Equal(typeof(HttpRequestData), p.Type); + }, + q => + { + Assert.Equal("myBlob", q.Name); + Assert.Equal(typeof(string), q.Type); + Assert.Contains(PropertyBagKeys.AllowConverterFallback, q.Properties.Keys); + Assert.Contains(PropertyBagKeys.BindingAttributeSupportedConverters, q.Properties.Keys); + Assert.True(true, q.Properties[PropertyBagKeys.AllowConverterFallback].ToString()); + Assert.Contains(new Dictionary>().ToString(), q.Properties[PropertyBagKeys.BindingAttributeSupportedConverters].ToString()); + }); + + // InputBindings + Assert.Collection(definition.InputBindings, + p => + { + Assert.Equal("req", p.Key); + Assert.Equal(BindingDirection.In, p.Value.Direction); + Assert.Equal("HttpTrigger", p.Value.Type); + }); + + // OutputBindings + Assert.Collection(definition.OutputBindings, + p => + { + Assert.Equal("$return", p.Key); + Assert.Equal(BindingDirection.Out, p.Value.Direction); + Assert.Equal("Http", p.Value.Type); + }); + } + + private class MyFunctionClass { public HttpResponseData Run(HttpRequestData req) @@ -89,5 +162,16 @@ public HttpResponseData Run(HttpRequestData req, CancellationToken cancellationT return req.CreateResponse(); } } + + private class MyBlobFunctionClass + { + public HttpResponseData Run( + [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req, + [BlobInput("input-container/{id}.txt")] string myBlob) + { + return req.CreateResponse(); + } + } + } } diff --git a/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs b/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs index 9832840d5..8c27bf816 100644 --- a/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs +++ b/test/FunctionMetadataGeneratorTests/FunctionMetadataGeneratorTests.cs @@ -6,9 +6,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Dynamic; +using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using Azure.Storage.Blobs; using Microsoft.Azure.Functions.Tests; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; @@ -244,28 +246,61 @@ void ValidateQueueOutput(ExpandoObject b) } [Fact] - public void StorageFunction_SDKTypeBindings() + public void BlobStorageFunctions_SDKTypeBindings() { var generator = new FunctionMetadataGenerator(); var module = ModuleDefinition.ReadModule(_thisAssembly.Location); - var typeDef = TestUtility.GetTypeDefinition(typeof(SDKTypeBindings)); + var typeDef = TestUtility.GetTypeDefinition(typeof(SDKTypeBindings_BlobStorage)); var functions = generator.GenerateFunctionMetadata(typeDef); var extensions = generator.Extensions; - Assert.Single(functions); + Assert.Equal(5, functions.Count()); - var blobToBlob = functions.Single(p => p.Name == "BlobToBlobFunction"); + var blobStringToBlobStringFunction = functions.Single(p => p.Name == "BlobStringToBlobStringFunction"); - ValidateFunction(blobToBlob, "BlobToBlobFunction", GetEntryPoint(nameof(SDKTypeBindings), nameof(SDKTypeBindings.BlobToBlob)), + ValidateFunction(blobStringToBlobStringFunction, "BlobStringToBlobStringFunction", GetEntryPoint(nameof(SDKTypeBindings_BlobStorage), nameof(SDKTypeBindings_BlobStorage.BlobStringToBlobStringFunction)), b => ValidateBlobTrigger(b), b => ValidateBlobInput(b), b => ValidateBlobOutput(b)); + AssertDictionary(extensions, new Dictionary { { "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs", "5.1.0-beta.1" }, }); + var blobClientToBlobStringFunction = functions.Single(p => p.Name == "BlobClientToBlobStringFunction"); + + ValidateFunction(blobClientToBlobStringFunction, "BlobClientToBlobStringFunction", GetEntryPoint(nameof(SDKTypeBindings_BlobStorage), nameof(SDKTypeBindings_BlobStorage.BlobClientToBlobStreamFunction)), + b => ValidateBlobTrigger(b), + b => ValidateBlobInput(b), + b => ValidateBlobOutput(b)); + + + var blobUnsupportedTypeToBlobStringFunction = functions.Single(p => p.Name == "BlobUnsupportedTypeToBlobStringFunction"); + + ValidateFunction(blobUnsupportedTypeToBlobStringFunction, "BlobUnsupportedTypeToBlobStringFunction", GetEntryPoint(nameof(SDKTypeBindings_BlobStorage), nameof(SDKTypeBindings_BlobStorage.BlobUnsupportedTypeToBlobClientFunction)), + b => ValidateBlobTrigger(b), + b => ValidateBlobInput(b), + b => ValidateBlobOutput(b)); + + + var blobPocoToBlobUnsupportedType = functions.Single(p => p.Name == "BlobPocoToBlobUnsupportedType"); + + ValidateFunction(blobPocoToBlobUnsupportedType, "BlobPocoToBlobUnsupportedType", GetEntryPoint(nameof(SDKTypeBindings_BlobStorage), nameof(SDKTypeBindings_BlobStorage.BlobPocoToBlobUnsupportedType)), + b => ValidateBlobTrigger(b), + b => ValidateBlobInput(b), + b => ValidateBlobOutput(b)); + + + var blobByteArrayToBlobByteArrayFunction = functions.Single(p => p.Name == "BlobByteArrayToBlobByteArrayFunction"); + + ValidateFunction(blobByteArrayToBlobByteArrayFunction, "BlobByteArrayToBlobByteArrayFunction", GetEntryPoint(nameof(SDKTypeBindings_BlobStorage), nameof(SDKTypeBindings_BlobStorage.BlobByteArrayToBlobByteArrayFunction)), + b => ValidateBlobTrigger(b), + b => ValidateBlobInput(b), + b => ValidateBlobOutput(b)); + + void ValidateBlobTrigger(ExpandoObject b) { AssertExpandoObject(b, new Dictionary @@ -305,6 +340,105 @@ void ValidateBlobOutput(ExpandoObject b) } } + [Fact] + public void BlobCollectionFunctions_SDKTypeBindings() + { + var generator = new FunctionMetadataGenerator(); + var module = ModuleDefinition.ReadModule(_thisAssembly.Location); + var typeDef = TestUtility.GetTypeDefinition(typeof(SDKTypeBindings_BlobCollection)); + var functions = generator.GenerateFunctionMetadata(typeDef); + var extensions = generator.Extensions; + + Assert.Equal(4, functions.Count()); + + AssertDictionary(extensions, new Dictionary + { + { "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs", "5.1.0-beta.1" }, + }); + + var blobStringToBlobClientEnumerable = functions.Single(p => p.Name == "BlobStringToBlobClientEnumerable"); + + ValidateFunction(blobStringToBlobClientEnumerable, "BlobStringToBlobClientEnumerable", GetEntryPoint(nameof(SDKTypeBindings_BlobCollection), nameof(SDKTypeBindings_BlobCollection.BlobStringToBlobClientEnumerable)), + b => ValidateBlobTrigger(b), + b => ValidateBlobInputForEnumerable(b), + b => ValidateBlobOutput(b)); + + void ValidateBlobInputForEnumerable(ExpandoObject b) + { + AssertExpandoObject(b, new Dictionary + { + { "Name", "blobinput" }, + { "Type", "blob" }, + { "Direction", "In" }, + { "blobPath", "container2" }, + { "Cardinality", "Many" }, + { "Properties", new Dictionary( ) { { "SupportsDeferredBinding", "True" } } } + }); + } + + var blobStringToBlobStringArray = functions.Single(p => p.Name == "BlobStringToBlobStringArray"); + + ValidateFunction(blobStringToBlobStringArray, "BlobStringToBlobStringArray", GetEntryPoint(nameof(SDKTypeBindings_BlobCollection), nameof(SDKTypeBindings_BlobCollection.BlobStringToBlobStringArray)), + b => ValidateBlobTrigger(b), + b => ValidateBlobInputForStringArray(b), + b => ValidateBlobOutput(b)); + + void ValidateBlobInputForStringArray(ExpandoObject b) + { + AssertExpandoObject(b, new Dictionary + { + { "Name", "blobinput" }, + { "Type", "blob" }, + { "Direction", "In" }, + { "blobPath", "container2" }, + { "Cardinality", "Many" }, + { "DataType", "String" }, + { "Properties", new Dictionary( ) { { "SupportsDeferredBinding", "True" } } } + }); + } + + var blobStringToBlobPocoEnumerable = functions.Single(p => p.Name == "BlobStringToBlobPocoEnumerable"); + + ValidateFunction(blobStringToBlobPocoEnumerable, "BlobStringToBlobPocoEnumerable", GetEntryPoint(nameof(SDKTypeBindings_BlobCollection), nameof(SDKTypeBindings_BlobCollection.BlobStringToBlobPocoEnumerable)), + b => ValidateBlobTrigger(b), + b => ValidateBlobInputForEnumerable(b), + b => ValidateBlobOutput(b)); + + + var blobStringToBlobPocoArray = functions.Single(p => p.Name == "BlobStringToBlobPocoArray"); + + ValidateFunction(blobStringToBlobPocoArray, "BlobStringToBlobPocoArray", GetEntryPoint(nameof(SDKTypeBindings_BlobCollection), nameof(SDKTypeBindings_BlobCollection.BlobStringToBlobPocoArray)), + b => ValidateBlobTrigger(b), + b => ValidateBlobInputForEnumerable(b), + b => ValidateBlobOutput(b)); + + + void ValidateBlobTrigger(ExpandoObject b) + { + AssertExpandoObject(b, new Dictionary + { + { "Name", "blob" }, + { "Type", "blobTrigger" }, + { "Direction", "In" }, + { "path", "container2/%file%" }, + { "Properties", new Dictionary( ) { { "SupportsDeferredBinding" , "True"} } } + }); + } + + void ValidateBlobOutput(ExpandoObject b) + { + AssertExpandoObject(b, new Dictionary + { + { "Name", "$return" }, + { "Type", "blob" }, + { "Direction", "Out" }, + { "blobPath", "container1/hello.txt" }, + { "Connection", "MyOtherConnection" }, + { "Properties", new Dictionary() } + }); + } + } + [Fact] public void TimerFunction() { @@ -811,16 +945,93 @@ public object BlobToBlobs( } } - private class SDKTypeBindings + private class SDKTypeBindings_BlobStorage { - [Function("BlobToBlobFunction")] + [Function("BlobStringToBlobStringFunction")] [BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")] - public object BlobToBlob( + public object BlobStringToBlobStringFunction( [BlobTrigger("container2/%file%")] string blob, [BlobInput("container2/%file%")] string blobinput) { throw new NotImplementedException(); } + + + [Function("BlobClientToBlobStringFunction")] + [BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")] + public object BlobClientToBlobStreamFunction( + [BlobTrigger("container2/%file%")] BlobClient blob, + [BlobInput("container2/%file%")] Stream blobinput) + { + throw new NotImplementedException(); + } + + [Function("BlobByteArrayToBlobByteArrayFunction")] + [BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")] + public object BlobByteArrayToBlobByteArrayFunction( + [BlobTrigger("container2/%file%")] byte[] blob, + [BlobInput("container2/%file%")] byte[] blobinput) + { + throw new NotImplementedException(); + } + + [Function("BlobUnsupportedTypeToBlobStringFunction")] + [BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")] + public object BlobUnsupportedTypeToBlobClientFunction( + [BlobTrigger("container2/%file%")] BinaryData blob, + [BlobInput("container2/%file%")] BlobClient blobinput) + { + throw new NotImplementedException(); + } + + [Function("BlobPocoToBlobUnsupportedType")] + [BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")] + public object BlobPocoToBlobUnsupportedType( + [BlobTrigger("container2/%file%")] Poco blob, + [BlobInput("container2/%file%")] BinaryData blobinput) + { + throw new NotImplementedException(); + } + } + + private class SDKTypeBindings_BlobCollection + { + [Function("BlobStringToBlobStringArray")] + [BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")] + public object BlobStringToBlobStringArray( + [BlobTrigger("container2/%file%")] string blob, + [BlobInput("container2", IsBatched = true)] string[] blobinput) + { + throw new NotImplementedException(); + } + + + [Function("BlobStringToBlobClientEnumerable")] + [BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")] + public object BlobStringToBlobClientEnumerable( + [BlobTrigger("container2/%file%")] string blob, + [BlobInput("container2", IsBatched = true)] IEnumerable blobinput) + { + throw new NotImplementedException(); + } + + [Function("BlobStringToBlobPocoEnumerable")] + [BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")] + public object BlobStringToBlobPocoEnumerable( + [BlobTrigger("container2/%file%")] string blob, + [BlobInput("container2", IsBatched = true)] IEnumerable blobinput) + { + throw new NotImplementedException(); + } + + [Function("BlobStringToBlobPocoArray")] + [BlobOutput("container1/hello.txt", Connection = "MyOtherConnection")] + public object BlobStringToBlobPocoArray( + [BlobTrigger("container2/%file%")] string blob, + [BlobInput("container2", IsBatched = true)] Poco[] blobinput) + { + throw new NotImplementedException(); + } } private class ExternalType_Return diff --git a/test/SdkE2ETests/Contents/WorkerBindingSamplesOutput/functions.metadata b/test/SdkE2ETests/Contents/WorkerBindingSamplesOutput/functions.metadata new file mode 100644 index 000000000..2d151241b --- /dev/null +++ b/test/SdkE2ETests/Contents/WorkerBindingSamplesOutput/functions.metadata @@ -0,0 +1,792 @@ +[ + { + "name": "BlobInputClientFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobInputBindingSamples.BlobInputClientFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "client", + "direction": "In", + "type": "blob", + "blobPath": "input-container/sample1.txt", + "cardinality": "One", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "BlobInputStreamFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobInputBindingSamples.BlobInputStreamFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "stream", + "direction": "In", + "type": "blob", + "blobPath": "input-container/sample1.txt", + "cardinality": "One", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "BlobInputByteArrayFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobInputBindingSamples.BlobInputByteArrayFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "data", + "direction": "In", + "type": "blob", + "blobPath": "input-container/sample1.txt", + "cardinality": "One", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "BlobInputStringFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobInputBindingSamples.BlobInputStringFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "data", + "direction": "In", + "type": "blob", + "blobPath": "input-container/{filename}", + "cardinality": "One", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "BlobInputBookFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobInputBindingSamples.BlobInputBookFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "data", + "direction": "In", + "type": "blob", + "blobPath": "input-container/book.json", + "cardinality": "One", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "BlobInputCollectionFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobInputBindingSamples.BlobInputCollectionFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "blobs", + "direction": "In", + "type": "blob", + "blobPath": "input-container", + "cardinality": "Many", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "BlobInputStringArrayFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobInputBindingSamples.BlobInputStringArrayFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "blobContent", + "direction": "In", + "type": "blob", + "blobPath": "input-container", + "cardinality": "Many", + "dataType": "String", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "BlobInputBookArrayFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobInputBindingSamples.BlobInputBookArrayFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "books", + "direction": "In", + "type": "blob", + "blobPath": "input-container", + "cardinality": "Many", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "BlobClientFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobTriggerBindingSamples.BlobClientFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "client", + "direction": "In", + "type": "blobTrigger", + "path": "client-trigger/{name}", + "properties": { + "supportsDeferredBinding": "True" + } + } + ] + }, + { + "name": "BlobStreamFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobTriggerBindingSamples.BlobStreamFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "stream", + "direction": "In", + "type": "blobTrigger", + "path": "stream-trigger/{name}", + "properties": { + "supportsDeferredBinding": "True" + } + } + ] + }, + { + "name": "BlobByteArrayFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobTriggerBindingSamples.BlobByteArrayFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "data", + "direction": "In", + "type": "blobTrigger", + "path": "byte-trigger", + "properties": { + "supportsDeferredBinding": "True" + } + } + ] + }, + { + "name": "BlobStringFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobTriggerBindingSamples.BlobStringFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "data", + "direction": "In", + "type": "blobTrigger", + "path": "string-trigger", + "properties": { + "supportsDeferredBinding": "True" + } + } + ] + }, + { + "name": "BlobBookFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.BlobTriggerBindingSamples.BlobBookFunction", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "data", + "direction": "In", + "type": "blobTrigger", + "path": "book-trigger", + "properties": { + "supportsDeferredBinding": "True" + } + } + ] + }, + { + "name": "ExpressionFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.ExpressionFunction.Run", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "book", + "direction": "In", + "type": "queueTrigger", + "queueName": "expression-trigger", + "properties": {} + }, + { + "name": "myBlob", + "direction": "In", + "type": "blob", + "blobPath": "input-container/{id}.txt", + "cardinality": "One", + "properties": { + "supportsDeferredBinding": "True" + } + } + ] + }, + { + "name": "DocsByUsingCosmosClient", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosInputBindingFunctions.DocsByUsingCosmosClient", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "client", + "direction": "In", + "type": "cosmosDB", + "databaseName": "", + "containerName": "", + "connection": "CosmosDBConnection", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "DocsByUsingDatabaseClient", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosInputBindingFunctions.DocsByUsingDatabaseClient", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "database", + "direction": "In", + "type": "cosmosDB", + "databaseName": "ToDoItems", + "containerName": "", + "connection": "CosmosDBConnection", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "DocsByUsingContainerClient", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosInputBindingFunctions.DocsByUsingContainerClient", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "container", + "direction": "In", + "type": "cosmosDB", + "databaseName": "ToDoItems", + "containerName": "Items", + "connection": "CosmosDBConnection", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "DocByIdFromQueryString", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosInputBindingFunctions.DocByIdFromQueryString", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "toDoItem", + "direction": "In", + "type": "cosmosDB", + "databaseName": "ToDoItems", + "containerName": "Items", + "connection": "CosmosDBConnection", + "id": "{Query.id}", + "partitionKey": "{Query.partitionKey}", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "DocByIdFromRouteData", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosInputBindingFunctions.DocByIdFromRouteData", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "route": "todoitems/{partitionKey}/{id}", + "properties": {} + }, + { + "name": "toDoItem", + "direction": "In", + "type": "cosmosDB", + "databaseName": "ToDoItems", + "containerName": "Items", + "connection": "CosmosDBConnection", + "id": "{id}", + "partitionKey": "{partitionKey}", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "DocByIdFromRouteDataUsingSqlQuery", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosInputBindingFunctions.DocByIdFromRouteDataUsingSqlQuery", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "route": "todoitems2/{id}", + "properties": {} + }, + { + "name": "toDoItems", + "direction": "In", + "type": "cosmosDB", + "databaseName": "ToDoItems", + "containerName": "Items", + "connection": "CosmosDBConnection", + "sqlQuery": "SELECT * FROM ToDoItems t where t.id = {id}", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "DocByIdFromQueryStringUsingSqlQuery", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosInputBindingFunctions.DocByIdFromQueryStringUsingSqlQuery", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "toDoItems", + "direction": "In", + "type": "cosmosDB", + "databaseName": "ToDoItems", + "containerName": "Items", + "connection": "CosmosDBConnection", + "sqlQuery": "SELECT * FROM ToDoItems t where t.id = {id}", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "DocsBySqlQuery", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosInputBindingFunctions.DocsBySqlQuery", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "req", + "direction": "In", + "type": "httpTrigger", + "authLevel": "Function", + "methods": [ + "get", + "post" + ], + "properties": {} + }, + { + "name": "toDoItems", + "direction": "In", + "type": "cosmosDB", + "databaseName": "ToDoItems", + "containerName": "Items", + "connection": "CosmosDBConnection", + "sqlQuery": "SELECT * FROM ToDoItems t WHERE CONTAINS(t.description, 'cat')", + "properties": { + "supportsDeferredBinding": "True" + } + }, + { + "name": "$return", + "type": "http", + "direction": "Out" + } + ] + }, + { + "name": "DocByIdFromJSON", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosInputBindingFunctions.DocByIdFromJSON", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "toDoItemLookup", + "direction": "In", + "type": "queueTrigger", + "queueName": "todoqueueforlookup", + "properties": {} + }, + { + "name": "toDoItem", + "direction": "In", + "type": "cosmosDB", + "databaseName": "ToDoItems", + "containerName": "Items", + "connection": "CosmosDBConnection", + "id": "{ToDoItemId}", + "partitionKey": "{ToDoItemPartitionKeyValue}", + "properties": { + "supportsDeferredBinding": "True" + } + } + ] + }, + { + "name": "CosmosTriggerFunction", + "scriptFile": "WorkerBindingSamples.dll", + "entryPoint": "SampleApp.CosmosTriggerFunction.Run", + "language": "dotnet-isolated", + "properties": { + "IsCodeless": false + }, + "bindings": [ + { + "name": "todoItems", + "direction": "In", + "type": "cosmosDBTrigger", + "databaseName": "ToDoItems", + "containerName": "TriggerItems", + "connection": "CosmosDBConnection", + "createLeaseContainerIfNotExists": true, + "properties": {} + } + ] + } +] \ No newline at end of file diff --git a/test/SdkE2ETests/PublishTests.cs b/test/SdkE2ETests/PublishTests.cs index 4b456bf41..75ec544cd 100644 --- a/test/SdkE2ETests/PublishTests.cs +++ b/test/SdkE2ETests/PublishTests.cs @@ -75,9 +75,72 @@ private async Task RunPublishTest(string outputDir, string additionalParams = nu Assert.True(JToken.DeepEquals(extensionsJsonContents, expected), $"Actual: {extensionsJsonContents}{Environment.NewLine}Expected: {expected}"); // Verify functions.metadata - TestUtility.ValidateFunctionsMetadata(functionsMetadataPath, "functions.metadata"); + TestUtility.ValidateFunctionsMetadata(functionsMetadataPath, "Microsoft.Azure.Functions.SdkE2ETests.Contents.functions.metadata"); } + + [Fact] + public async Task Publish_SdkTypeBindings() + { + string outputDir = await TestUtility.InitializeTestAsync(_testOutputHelper, nameof(Publish_SdkTypeBindings)); + await RunPublishTestForSdkTypeBindings(outputDir); + } + + [Fact] + public async Task Publish_Rid_SdkTypeBindings() + { + string outputDir = await TestUtility.InitializeTestAsync(_testOutputHelper, nameof(Publish_Rid_SdkTypeBindings)); + await RunPublishTestForSdkTypeBindings(outputDir, "-r win-x86"); + } + + private async Task RunPublishTestForSdkTypeBindings(string outputDir, string additionalParams = null) + { + // Name of the csproj + string projectFileDirectory = Path.Combine(TestUtility.SamplesRoot, "WorkerBindingSamples", "WorkerBindingSamples.csproj"); + + await TestUtility.RestoreAndPublishProjectAsync(projectFileDirectory, outputDir, additionalParams, _testOutputHelper); + + // Make sure files are in /.azurefunctions + string azureFunctionsDir = Path.Combine(outputDir, ".azurefunctions"); + Assert.True(Directory.Exists(azureFunctionsDir)); + var files = Directory.EnumerateFiles(azureFunctionsDir); + + // Verify files are present + string metadataLoaderPath = Path.Combine(azureFunctionsDir, "Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll"); + string extensionsJsonPath = Path.Combine(outputDir, "extensions.json"); + string functionsMetadataPath = Path.Combine(outputDir, "functions.metadata"); + Assert.True(File.Exists(metadataLoaderPath)); + Assert.True(File.Exists(extensionsJsonPath)); + Assert.True(File.Exists(functionsMetadataPath)); + + // Verify extensions.json + JObject jObjects = JObject.Parse(File.ReadAllText(extensionsJsonPath)); + JObject extensionsJsonContents = jObjects; + JToken expected = JObject.FromObject(new + { + extensions = new[] + { + new Extension("CosmosDB", + "Microsoft.Azure.WebJobs.Extensions.CosmosDB.CosmosDBWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.CosmosDB, Version=4.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", + @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.CosmosDB.dll"), + new Extension("Startup", + "Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.Startup, Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader, Version=1.0.0.0, Culture=neutral, PublicKeyToken=551316b6919f366c", + @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll"), + new Extension("AzureStorageBlobs", + "Microsoft.Azure.WebJobs.Extensions.Storage.AzureStorageBlobsWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.Storage.Blobs, Version=5.1.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8", + @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs.dll"), + new Extension("AzureStorageQueues", + "Microsoft.Azure.WebJobs.Extensions.Storage.AzureStorageQueuesWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.Storage.Queues, Version=5.0.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8", + @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Storage.Queues.dll") + } + }); + Assert.True(JToken.DeepEquals(extensionsJsonContents, expected), $"Actual: {extensionsJsonContents}{Environment.NewLine}Expected: {expected}"); + + // Verify functions.metadata + TestUtility.ValidateFunctionsMetadata(functionsMetadataPath, "Microsoft.Azure.Functions.SdkE2ETests.Contents.WorkerBindingSamplesOutput.functions.metadata"); + } + + private class Extension { public Extension(string name, string typeName, string hintPath) diff --git a/test/SdkE2ETests/SdkE2ETests.csproj b/test/SdkE2ETests/SdkE2ETests.csproj index e370cb0ca..452d4db20 100644 --- a/test/SdkE2ETests/SdkE2ETests.csproj +++ b/test/SdkE2ETests/SdkE2ETests.csproj @@ -10,10 +10,12 @@ + + diff --git a/test/SdkE2ETests/TestUtility.cs b/test/SdkE2ETests/TestUtility.cs index b86a94ff6..ba4473206 100644 --- a/test/SdkE2ETests/TestUtility.cs +++ b/test/SdkE2ETests/TestUtility.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; +using Xunit.Sdk; namespace Microsoft.Azure.Functions.SdkE2ETests { @@ -67,7 +68,7 @@ public static void ValidateFunctionsMetadata(string actualFilePath, string embed JToken functionsMetadataContents = JToken.Parse(File.ReadAllText(actualFilePath)); var assembly = Assembly.GetExecutingAssembly(); string resourceName = assembly.GetManifestResourceNames() - .Single(str => str.EndsWith("functions.metadata")); + .Single(str => str.EndsWith(embeddedResourceName)); using (Stream stream = assembly.GetManifestResourceStream(resourceName)) { using (StreamReader reader = new StreamReader(stream)) @@ -84,6 +85,10 @@ public static void ValidateFunctionsMetadata(string actualFilePath, string embed public static async Task RestoreAndPublishProjectAsync(string fullPathToProjFile, string outputDir, string additionalParams, ITestOutputHelper outputHelper) { + await PackWorkerSdk(outputHelper); + + await UpdateNugetPackagesForApp(fullPathToProjFile, outputHelper); + // Name of the csproj string projectNameToTest = Path.GetFileName(fullPathToProjFile); string projectFileDirectory = Path.GetDirectoryName(fullPathToProjFile);