Skip to content

Commit

Permalink
Implement bypass deferred binding (#1462)
Browse files Browse the repository at this point in the history
  • Loading branch information
surgupta-msft authored and liliankasem committed Jul 11, 2023
1 parent 420ccd6 commit f3bccb3
Show file tree
Hide file tree
Showing 18 changed files with 544 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
namespace Microsoft.Azure.Functions.Worker.Extensions.Abstractions
{
/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class SupportsDeferredBindingAttribute : Attribute
Expand Down
2 changes: 1 addition & 1 deletion release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@

### Microsoft.Azure.Functions.Worker.Grpc <version>

- bumping grpc packages (#1719)
- <entry>
2 changes: 2 additions & 0 deletions sdk/Sdk/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
77 changes: 73 additions & 4 deletions sdk/Sdk/FunctionMetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.IO;
using System.Linq;
using Mono.Cecil;
using Mono.Collections.Generic;

namespace Microsoft.Azure.Functions.Worker.Sdk
{
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();

Expand All @@ -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<CustomAttribute> 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)
Expand Down
2 changes: 1 addition & 1 deletion sdk/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

### Microsoft.Azure.Functions.Worker.Sdk <version> (meta package)

- <entry>
- Implementation for bypass deferred binding (#1462/#1495)

### Microsoft.Azure.Functions.Worker.Sdk.Analyzers <version> (delete if not updated)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,8 +22,8 @@ internal class DefaultFunctionInputBindingFeature : IFunctionInputBindingFeature

public DefaultFunctionInputBindingFeature(IConverterContextFactory converterContextFactory)
{
_converterContextFactory = converterContextFactory ??
throw new ArgumentNullException(nameof(converterContextFactory));
_converterContextFactory = converterContextFactory
?? throw new ArgumentNullException(nameof(converterContextFactory));
}

public async ValueTask<FunctionInputBindingResult> BindFunctionInputAsync(FunctionContext context)
Expand All @@ -38,7 +39,7 @@ public async ValueTask<FunctionInputBindingResult> BindFunctionInputAsync(Functi
// Return the cached value if BindFunctionInputAsync is called a second time during invocation.
return _inputBindingResult!;
}

IFunctionBindingsFeature functionBindings = context.GetBindings();
var inputBindingCache = context.InstanceServices.GetService<IBindingCache<ConversionResult>>();
var inputConversionFeature = context.Features.Get<IInputConversionFeature>();
Expand Down Expand Up @@ -69,18 +70,29 @@ public async ValueTask<FunctionInputBindingResult> BindFunctionInputAsync(Functi
}
else
{
IReadOnlyDictionary<string, object> properties = ImmutableDictionary<string, object>.Empty;
var properties = new Dictionary<string, object>();

// 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<string, object>()
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<string, object>.Empty);

bindingResult = await inputConversionFeature.ConvertAsync(converterContext);
inputBindingCache[cacheKey] = bindingResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,17 +48,39 @@ public async ValueTask<ConversionResult> 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<IInputConverter, List<Type>>? 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();
Expand Down Expand Up @@ -111,6 +134,35 @@ private async ValueTask<ConversionResult> AwaitAndReturnConversionTaskResult(Val
return null;
}


/// <summary>
/// Gets all converters advertised by the Binding Attribute along with supported types by each converter
/// </summary>
private Dictionary<IInputConverter, List<Type>>? GetExplicitConverterTypes(ConverterContext context)
{
var result = new Dictionary<IInputConverter, List<Type>>();

if (context.Properties.TryGetValue(PropertyBagKeys.BindingAttributeSupportedConverters, out var converterTypes))
{
if (converterTypes is Dictionary<Type, List<Type>> 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;
}

/// <summary>
/// Checks a type has an "InputConverter" attribute decoration present
/// and if present, return the assembly qualified name of the "ConverterType" property.
Expand All @@ -133,5 +185,35 @@ private async ValueTask<ConversionResult> AwaitAndReturnConversionTaskResult(Val
}, targetType);
}

/// <summary>
/// Returns boolean value indicating whether fallback to registered converters allowed by the binding attribute
/// </summary>
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;
}

/// <summary>
/// Returns boolean value indicating whether Target type is supported by the converter
/// </summary>
private bool IsTargetTypeSupportedByConverter(List<Type> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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
{
/// <summary>
/// An attribute that specifies if Converter fallback is allowed
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class AllowConverterFallbackAttribute : Attribute
{
/// <summary>
/// Gets the value of whether Converter fallback is allowed.
/// </summary>
public bool AllowConverterFallback { get; }

/// <summary>
/// Creates a new instance of <see cref="AllowConverterFallbackAttribute"/>
/// </summary>
/// <param name="allowConverterFallback">The value to indicate if converter fallback is allowed.</param>
public AllowConverterFallbackAttribute(bool allowConverterFallback)
{
AllowConverterFallback = allowConverterFallback;
}
}
}

0 comments on commit f3bccb3

Please sign in to comment.