Refactor deferred binding attributes (#1723)

This commit is contained in:
Lilian Kasem 2023-07-07 15:34:55 -07:00 коммит произвёл Lilian Kasem
Родитель ce7b2a4b7d
Коммит 424fc1d3ae
18 изменённых файлов: 137 добавлений и 112 удалений

Просмотреть файл

@ -39,7 +39,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators
internal const string FixedDelayRetryAttribute = "Microsoft.Azure.Functions.Worker.FixedDelayRetryAttribute";
internal const string ExponentialBackoffRetryAttribute = "Microsoft.Azure.Functions.Worker.ExponentialBackoffRetryAttribute";
internal const string InputConverterAttributeType = "Microsoft.Azure.Functions.Worker.Converters.InputConverterAttribute";
internal const string SupportedConverterTypeAttributeType = "Microsoft.Azure.Functions.Worker.Converters.SupportedConverterTypeAttribute";
internal const string SupportedTargetTypeAttributeType = "Microsoft.Azure.Functions.Worker.Converters.SupportedTargetTypeAttribute";
internal const string SupportsDeferredBindingAttributeType = "Microsoft.Azure.Functions.Worker.Extensions.Abstractions.SupportsDeferredBindingAttribute";
// System types

Просмотреть файл

@ -380,7 +380,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators
if (converterAdvertisesDeferredBindingSupport)
{
bool converterAdvertisesTypes = converterAdvertisedAttributes.Any(
a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _knownFunctionMetadataTypes.SupportedConverterTypeAttributeType));
a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _knownFunctionMetadataTypes.SupportedTargetTypeAttributeType));
if (!converterAdvertisesTypes)
{
@ -399,7 +399,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators
{
foreach (AttributeData attribute in converterAdvertisedAttributes)
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _knownFunctionMetadataTypes.SupportedConverterTypeAttributeType))
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _knownFunctionMetadataTypes.SupportedTargetTypeAttributeType))
{
foreach (var element in attribute.ConstructorArguments)
{

Просмотреть файл

@ -21,7 +21,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators
private readonly Lazy<INamedTypeSymbol?> _fixedDelayRetryAttribute;
private readonly Lazy<INamedTypeSymbol?> _exponentialBackoffRetryAttribute;
private readonly Lazy<INamedTypeSymbol?> _inputConverterAttributeType;
private readonly Lazy<INamedTypeSymbol?> _supportedConverterTypeAttributeType;
private readonly Lazy<INamedTypeSymbol?> _supportedTargetTypeAttributeType;
private readonly Lazy<INamedTypeSymbol?> _supportsDeferredBindingAttributeType;
internal KnownFunctionMetadataTypes(Compilation compilation)
@ -38,7 +38,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators
_fixedDelayRetryAttribute = new Lazy<INamedTypeSymbol?>(() => compilation.GetTypeByMetadataName(Constants.Types.FixedDelayRetryAttribute));
_exponentialBackoffRetryAttribute = new Lazy<INamedTypeSymbol?>(() => compilation.GetTypeByMetadataName(Constants.Types.ExponentialBackoffRetryAttribute));
_inputConverterAttributeType = new Lazy<INamedTypeSymbol?>(() => compilation.GetTypeByMetadataName(Constants.Types.InputConverterAttributeType));
_supportedConverterTypeAttributeType = new Lazy<INamedTypeSymbol?>(() => compilation.GetTypeByMetadataName(Constants.Types.SupportedConverterTypeAttributeType));
_supportedTargetTypeAttributeType = new Lazy<INamedTypeSymbol?>(() => compilation.GetTypeByMetadataName(Constants.Types.SupportedTargetTypeAttributeType));
_supportsDeferredBindingAttributeType = new Lazy<INamedTypeSymbol?>(() => compilation.GetTypeByMetadataName(Constants.Types.SupportsDeferredBindingAttributeType));
}
@ -66,7 +66,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk.Generators
public INamedTypeSymbol? InputConverterAttributeType { get => _inputConverterAttributeType.Value; }
public INamedTypeSymbol? SupportedConverterTypeAttributeType { get => _supportedConverterTypeAttributeType.Value; }
public INamedTypeSymbol? SupportedTargetTypeAttributeType { get => _supportedTargetTypeAttributeType.Value; }
public INamedTypeSymbol? SupportsDeferredBindingAttributeType { get => _supportsDeferredBindingAttributeType.Value; }
}

Просмотреть файл

@ -17,7 +17,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk
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";
internal const string SupportedTargetTypeAttributeType = "Microsoft.Azure.Functions.Worker.Converters.SupportedTargetTypeAttribute";
// System types
internal const string IEnumerableType = "System.Collections.IEnumerable";

Просмотреть файл

@ -837,11 +837,11 @@ namespace Microsoft.Azure.Functions.Worker.Sdk
if (converterAdvertisesDeferredBindingSupport)
{
bool converterAdvertisesTypes = typeReferenceCustomAttributes.Any(a => string.Equals(a.AttributeType.FullName, Constants.SupportedConverterTypeAttributeType, StringComparison.Ordinal));
bool converterAdvertisesTypes = typeReferenceCustomAttributes.Any(a => string.Equals(a.AttributeType.FullName, Constants.SupportedTargetTypeAttributeType, 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
// If a converter advertises deferred binding but does not explicitly advertise any types then DeferredBinding will be supported for all the types
return true;
}
@ -857,7 +857,7 @@ namespace Microsoft.Azure.Functions.Worker.Sdk
// Parse attributes advertised by converter
foreach (CustomAttribute attribute in customAttributes)
{
if (string.Equals(attribute.AttributeType.FullName, Constants.SupportedConverterTypeAttributeType, StringComparison.Ordinal))
if (string.Equals(attribute.AttributeType.FullName, Constants.SupportedTargetTypeAttributeType, StringComparison.Ordinal))
{
foreach (CustomAttributeArgument element in attribute.ConstructorArguments)
{

Просмотреть файл

@ -9,8 +9,8 @@ namespace Microsoft.Azure.Functions.Worker.Core
public abstract class CollectionModelBindingData
{
/// <summary>
/// Gets the array of ModelBindingData
/// Gets a ModelBindingData array
/// </summary>
public abstract ModelBindingData[] ModelBindingDataArray { get; }
public abstract ModelBindingData[] ModelBindingData { get; }
}
}

Просмотреть файл

@ -72,25 +72,11 @@ namespace Microsoft.Azure.Functions.Worker.Context.Features
{
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.Add(PropertyBagKeys.ConverterType, converterTypeAssemblyFullName);
}
AddFunctionParameterPropertyIfPresent(properties, param, PropertyBagKeys.ConverterType);
AddFunctionParameterPropertyIfPresent(properties, param, PropertyBagKeys.ConverterFallbackBehavior);
AddFunctionParameterPropertyIfPresent(properties, param, PropertyBagKeys.BindingAttributeSupportedConverters);
// 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))
{
properties.Add(PropertyBagKeys.BindingAttributeSupportedConverters, converters);
}
var converterContext = _converterContextFactory.Create(param.Type, source, context, properties.Count() != 0
var converterContext = _converterContextFactory.Create(param.Type, source, context, properties.Count() != 0
? properties.ToImmutableDictionary()
: ImmutableDictionary<string, object>.Empty);
@ -129,6 +115,14 @@ namespace Microsoft.Azure.Functions.Worker.Context.Features
}
}
private void AddFunctionParameterPropertyIfPresent(IDictionary<string, object> properties, FunctionParameter param, string key)
{
if (param.Properties.TryGetValue(key, out object val))
{
properties.Add(key, val);
}
}
public void Dispose()
{
if (_disposed)

Просмотреть файл

@ -191,11 +191,11 @@ namespace Microsoft.Azure.Functions.Worker.Context.Features
/// </summary>
private bool IsConverterFallbackAllowed(ConverterContext context)
{
if (context.Properties.TryGetValue(PropertyBagKeys.AllowConverterFallback, out var result))
if (context.Properties.TryGetValue(PropertyBagKeys.ConverterFallbackBehavior, out var result))
{
if (result is not null && result is bool res)
if (result is not null && result is ConverterFallbackBehavior fallbackBehavior)
{
return res;
return fallbackBehavior != ConverterFallbackBehavior.Disallow;
}
}

Просмотреть файл

@ -1,28 +0,0 @@
// 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,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
{
/// <summary>
/// Specifies the fallback behavior for a converter.
/// The default behavior is <see cref="ConverterFallbackBehavior.Allow"/>.
/// </summary>
public enum ConverterFallbackBehavior
{
/// <summary>
/// Allows fallback to built-in converters. This is the default behavior.
/// </summary>
Allow = 0,
/// <summary>
/// Disallows fallback to built-in converters.
/// </summary>
Disallow = 1,
/// <summary>
/// Specifies the default fallback behavior as <see cref="ConverterFallbackBehavior.Allow"/>
/// </summary>
Default = Allow
}
}

Просмотреть файл

@ -0,0 +1,31 @@
// 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 or disallowed.
/// Converter fallback refers to the ability to use built-in converters when custom converters
/// cannot handle a given request.
/// The default converter fallback behavior is <see cref="ConverterFallbackBehavior.Allow"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class ConverterFallbackBehaviorAttribute : Attribute
{
/// <summary>
/// Gets the value of the converter fallback behavior.
/// </summary>
public ConverterFallbackBehavior Behavior { get; }
/// <summary>
/// Creates a new instance of <see cref="ConverterFallbackBehaviorAttribute"/>
/// </summary>
/// <param name="fallbackBehavior">The value to indicate if converter fallback is allowed or disallowed.</param>
public ConverterFallbackBehaviorAttribute(ConverterFallbackBehavior fallbackBehavior)
{
Behavior = fallbackBehavior;
}
}
}

Просмотреть файл

@ -6,7 +6,7 @@ namespace Microsoft.Azure.Functions.Worker.Converters
internal static class PropertyBagKeys
{
internal const string ConverterType = "converterType";
internal const string AllowConverterFallback = "allowConverterFallback";
internal const string ConverterFallbackBehavior = "converterFallbackBehavior";
internal const string BindingAttributeSupportedConverters = "bindingAttributeSupportedConverters";
}
}

Просмотреть файл

@ -1,29 +0,0 @@
// 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 can specify a type supported by function input conversion.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class SupportedConverterTypeAttribute : Attribute
{
/// <summary>
/// Gets the input converter type.
/// </summary>
public Type Type { get; }
/// <summary>
/// Creates a new instance of <see cref="SupportedConverterTypeAttribute"/>
/// </summary>
/// <param name="type">Input converter type.</param>
/// <exception cref="ArgumentNullException">Thrown when type is null</exception>
public SupportedConverterTypeAttribute(Type type)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
}
}
}

Просмотреть файл

@ -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
{
/// <summary>
/// An attribute that can specify a target type supported by function input conversion.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class SupportedTargetTypeAttribute : Attribute
{
/// <summary>
/// Gets the input converter supported target type.
/// </summary>
public Type TargetType { get; }
/// <summary>
/// Creates a new instance of <see cref="SupportedTargetTypeAttribute"/>
/// </summary>
/// <param name="targetType">Input converter target type.</param>
/// <exception cref="ArgumentNullException">Thrown when type is null</exception>
public SupportedTargetTypeAttribute(Type targetType)
{
TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType));
}
}
}

Просмотреть файл

@ -111,7 +111,6 @@ namespace Microsoft.Azure.Functions.Worker.Definition
// The dictionary has key of type IInputConverter and value as List of Types supported by the converter.
var converterTypesDictionary = new Dictionary<Type, List<Type>>();
Type type = bindingAttribute.GetType();
var attributes = type.GetCustomAttributes<InputConverterAttribute>();
@ -131,7 +130,7 @@ namespace Microsoft.Azure.Functions.Worker.Definition
if (isInputConverterAttributeAdvertised)
{
output[PropertyBagKeys.AllowConverterFallback] = type.GetCustomAttribute<AllowConverterFallbackAttribute>()?.AllowConverterFallback ?? true;
output[PropertyBagKeys.ConverterFallbackBehavior] = type.GetCustomAttribute<ConverterFallbackBehaviorAttribute>()?.Behavior ?? ConverterFallbackBehavior.Default;
}
return output.ToImmutableDictionary();
@ -143,7 +142,7 @@ namespace Microsoft.Azure.Functions.Worker.Definition
foreach (CustomAttributeData converterAttribute in converter.CustomAttributes)
{
if (converterAttribute.AttributeType == typeof(SupportedConverterTypeAttribute))
if (converterAttribute.AttributeType == typeof(SupportedTargetTypeAttribute))
{
foreach (CustomAttributeTypedArgument supportedType in converterAttribute.ConstructorArguments)
{

Просмотреть файл

@ -8,12 +8,12 @@ namespace Microsoft.Azure.Functions.Worker.Grpc.Messages
{
internal partial class GrpcCollectionModelBindingData : Microsoft.Azure.Functions.Worker.Core.CollectionModelBindingData
{
public GrpcCollectionModelBindingData(CollectionModelBindingData modelBindingDataArray)
public GrpcCollectionModelBindingData(CollectionModelBindingData collectionModelBindingData)
{
ModelBindingDataArray = modelBindingDataArray.ModelBindingData
ModelBindingData = collectionModelBindingData.ModelBindingData
.Select(p => new GrpcModelBindingData(p)).ToArray();
}
public override Core.ModelBindingData[] ModelBindingDataArray { get; }
public override Core.ModelBindingData[] ModelBindingData { get; }
}
}

Просмотреть файл

@ -115,7 +115,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
{ PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary<Type, List<Type>>() { {
typeof(MySimpleSyncInputConverter),
new List<Type>() { typeof(string), typeof(string[])} } } },
{ PropertyBagKeys.AllowConverterFallback, false }
{ PropertyBagKeys.ConverterFallbackBehavior, ConverterFallbackBehavior.Disallow }
};
var converterContext = CreateConverterContext(typeof(string), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties);
@ -135,7 +135,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
{ PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary<Type, List<Type>>() { {
typeof(MySimpleSyncInputConverter),
new List<Type>() { typeof(string), typeof(Stream), typeof(IEnumerable<string>), typeof(Stream[]) } } } },
{ PropertyBagKeys.AllowConverterFallback, false }
{ PropertyBagKeys.ConverterFallbackBehavior, ConverterFallbackBehavior.Disallow }
};
var converterContext = CreateConverterContext(typeof(object), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties);
@ -153,7 +153,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
{ PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary<Type, List<Type>>() { {
typeof(MySimpleSyncInputConverter),
new List<Type>() { typeof(string), typeof(Stream), typeof(IEnumerable<string>), typeof(Stream[]) } } } },
{ PropertyBagKeys.AllowConverterFallback, false }
{ PropertyBagKeys.ConverterFallbackBehavior, ConverterFallbackBehavior.Disallow }
};
var converterContext = CreateConverterContext(typeof(string[]), new string[] { "val1", "val2" }, properties);
@ -172,7 +172,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
{ PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary<Type, List<Type>>() { {
typeof(MySimpleSyncInputConverter),
new List<Type>() { typeof(string), typeof(Stream) } } } },
{ PropertyBagKeys.AllowConverterFallback, false }
{ PropertyBagKeys.ConverterFallbackBehavior, ConverterFallbackBehavior.Disallow }
};
var converterContext = CreateConverterContext(typeof(Poco), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties);
@ -190,7 +190,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
{
{ PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary<Type, List<Type>>() { {
typeof(MySimpleSyncInputConverter2), new List<Type>() } } },
{ PropertyBagKeys.AllowConverterFallback, false }
{ PropertyBagKeys.ConverterFallbackBehavior, ConverterFallbackBehavior.Disallow }
};
var converterContext = CreateConverterContext(typeof(Poco), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties);
@ -209,7 +209,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
{
{ PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary<Type, List<Type>>() { {
typeof(MySimpleSyncInputConverter2), new List<Type>() } } },
{ PropertyBagKeys.AllowConverterFallback, false }
{ PropertyBagKeys.ConverterFallbackBehavior, ConverterFallbackBehavior.Disallow }
};
var converterContext = CreateConverterContext(typeof(Poco[]), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties);
@ -226,7 +226,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
{
{ PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary<Type, List<Type>>() { {
typeof(MySimpleSyncInputConverter2), null } } },
{ PropertyBagKeys.AllowConverterFallback, false }
{ PropertyBagKeys.ConverterFallbackBehavior, ConverterFallbackBehavior.Disallow }
};
var converterContext = CreateConverterContext(typeof(IEnumerable<string>), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties);
@ -243,7 +243,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
{
{ PropertyBagKeys.BindingAttributeSupportedConverters, new Dictionary<Type, List <Type>>() { {
typeof(MySimpleSyncInputConverter), new List<Type>() { } } } },
{ PropertyBagKeys.AllowConverterFallback, true }
{ PropertyBagKeys.ConverterFallbackBehavior, ConverterFallbackBehavior.Allow }
};
var converterContext = CreateConverterContext(typeof(string), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties);
@ -261,7 +261,7 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
IReadOnlyDictionary<string, object> properties = new Dictionary<string, object>()
{
{ PropertyBagKeys.BindingAttributeSupportedConverters, new Tuple<bool, List<Type>>(false, new List<Type>() { }) },
{ PropertyBagKeys.AllowConverterFallback, false }
{ PropertyBagKeys.ConverterFallbackBehavior, ConverterFallbackBehavior.Disallow }
};
var converterContext = CreateConverterContext(typeof(string), "0c67c078-7213-4e91-ad41-f8747c865f3d", properties);
@ -285,8 +285,8 @@ namespace Microsoft.Azure.Functions.Worker.Tests.Features
}
[SupportsDeferredBinding]
[SupportedConverterType(typeof(string))]
[SupportedConverterType(typeof(Stream))]
[SupportedTargetType(typeof(string))]
[SupportedTargetType(typeof(Stream))]
internal class MySimpleSyncInputConverter : IInputConverter
{
public ValueTask<ConversionResult> ConvertAsync(ConverterContext context)

Просмотреть файл

@ -132,8 +132,8 @@ namespace Microsoft.Azure.Functions.Worker.Tests
Assert.True(bindingData.GetType() == typeof(GrpcCollectionModelBindingData));
var grpcCollectionModelBindingData = (GrpcCollectionModelBindingData)bindingData;
Assert.True(grpcCollectionModelBindingData.ModelBindingDataArray.Count() == 1);
var grpcModelBindingData = grpcCollectionModelBindingData.ModelBindingDataArray[0];
Assert.True(grpcCollectionModelBindingData.ModelBindingData.Count() == 1);
var grpcModelBindingData = grpcCollectionModelBindingData.ModelBindingData[0];
Assert.True(grpcModelBindingData.Version == "1.1.1");
Assert.True(grpcModelBindingData.Content.GetType() == typeof(BinaryData));
}
@ -177,8 +177,8 @@ namespace Microsoft.Azure.Functions.Worker.Tests
Assert.True(bindingData.GetType() == typeof(GrpcCollectionModelBindingData));
var grpcCollectionModelBindingData = (GrpcCollectionModelBindingData)bindingData;
Assert.True(grpcCollectionModelBindingData.ModelBindingDataArray.Count() == 1);
var grpcModelBindingData = grpcCollectionModelBindingData.ModelBindingDataArray[0];
Assert.True(grpcCollectionModelBindingData.ModelBindingData.Count() == 1);
var grpcModelBindingData = grpcCollectionModelBindingData.ModelBindingData[0];
Assert.True(grpcModelBindingData.Version == "1.1.1");
Assert.True(grpcModelBindingData.Content.GetType() == typeof(BinaryData));
}