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);