TL;DR:See this example.
自定义绑定也可用。虽然 [FromQuery] 不是原生的 Functions 的有效绑定,因为它与 AspNetCore 一样,但我们可以自己制作。
首先,让我们创建一个用于函数绑定的属性:
[Binding]
[AttributeUsage(AttributeTargets.Parameter)]
public class FromQueryAttribute : Attribute
{
}
[Binding] 属性在函数中与参数绑定一起使用时是必需的。
在 Function App 启动时解决绑定:
- 查找所有函数(用
[FunctionName] 修饰的任何方法)
- 解决方法签名中使用的参数绑定
当绑定用属性修饰时,将使用特定的绑定规则来解析绑定的数据输出。可以使用IExtensionConfigProvider配置此绑定规则:
[Extension("FromQuery")]
public class FromQueryConfigProvider : IExtensionConfigProvider
{
private readonly IBindingProvider _BindingProvider;
public FromQueryConfigProvider(FromQueryBindingProvider bindingProvider)
{
_BindingProvider = bindingProvider;
}
public void Initialize(ExtensionConfigContext context)
{
context.AddBindingRule<FromQueryAttribute>().Bind(_BindingProvider);
}
}
Initialize 在启动期间被调用一次。我们使用它为我们创建的FromQueryAttribute 声明一个绑定规则,并使用Bind 方法为其声明一个特定的绑定提供程序。
[Extension] 属性是必需的,因为 WebJobs 使用它来区分此特定扩展与其他扩展。
IBindingProvider 包含有关函数及其方法签名的一些信息;在我们的例子中,我们需要参数信息,它与绑定提供者的上下文一起提供,BindingProviderContext:
public class FromQueryBindingProvider : IBindingProvider
{
private readonly FromQueryBinding _Binding;
public FromQueryBindingProvider(FromQueryBinding binding)
{
_Binding = binding;
}
public Task<IBinding> TryCreateAsync(BindingProviderContext context)
{
_Binding.Parameter = context.Parameter;
return Task.FromResult(_Binding as IBinding);
}
}
TryCreateAsync 方法在启动时也会被调用一次,用于声明调用 Function 时使用的具体绑定。
绑定本身必须实现IBinding,并且每次函数调用使用一次。 IBinding 保存与当前调用相关的信息。 BindAsync 方法在每次 Function 调用时调用一次,它的 BindingContext 是当前调用的所有绑定数据可用的地方。可用数据因触发器类型而异。对于HttpTrigger,它将始终包含一个 http 请求、标头字典和查询字典。但是,查询字典的解析没有考虑可能的数组值:包含key=1&key=2 的查询字符串将被解析为key => 2(总是选择最后一个),而不是我们希望的var => [1,2],并且是我们正在做我们正在做的事情的唯一原因。
public class FromQueryBinding : IBinding
{
private readonly FromQueryValueProvider _ValueProvider;
public bool FromAttribute { get; }
public ParameterInfo? Parameter { get; set; }
public FromQueryBinding(FromQueryValueProvider valueProvider)
{
_ValueProvider = valueProvider;
}
public Task<IValueProvider> BindAsync(object value, ValueBindingContext context)
{
throw new NotImplementedException();
}
public Task<IValueProvider> BindAsync(BindingContext context)
{
if (Parameter is null)
throw new ArgumentNullException(nameof(Parameter));
_ValueProvider.Type = Parameter.ParameterType;
_ValueProvider.ParameterName = Parameter.Name;
return Task.FromResult(_ValueProvider as IValueProvider);
}
public ParameterDescriptor ToParameterDescriptor()
{
return new ParameterDescriptor();
}
}
您可能会注意到,除了为值提供者设置一些属性(接下来会解释)之外,我们实际上并没有使用绑定上下文中的任何内容。这是因为我们实际上需要来自BindingContext 的请求。但是,与其试图猜测BindingContext.BindingData 中的哪些条目实际上是我们的请求,我们可以直接使用HttpContextAccessor 来解决这个问题。
价值提供者是魔法发生的地方;我们在其中构建绑定的实际输出。由于我们需要查询字符串,但没有从BindingContext 中找到查询字符串,我们只需将IHttpContextAccessor 注入我们的值提供程序,然后从HttpContext 中获取查询字符串。
public class FromQueryValueProvider : IValueProvider
{
private readonly IHttpContextAccessor _HttpContextAccessor;
private readonly IEnumerable<IStringValueConverter> _Converters;
public Type? Type { get; set; }
public string? ParameterName { get; set; }
public FromQueryValueProvider(
IHttpContextAccessor httpContextAccessor,
IEnumerable<IStringValueConverter> converters)
{
_HttpContextAccessor = httpContextAccessor;
_Converters = converters;
}
public Task<object> GetValueAsync()
{
if (Type is null)
{
throw new ArgumentNullException(nameof(Type));
}
if (string.IsNullOrWhiteSpace(ParameterName))
{
throw new ArgumentNullException(nameof(ParameterName));
}
if (_HttpContextAccessor.HttpContext is null)
{
throw new ArgumentNullException(nameof(_HttpContextAccessor.HttpContext));
}
StringValues stringValues = _HttpContextAccessor.HttpContext.Request.Query.ContainsKey(ParameterName)
? _HttpContextAccessor.HttpContext.Request.Query[ParameterName]
: new StringValues();
Type resolvedType = ResolveType(Type);
object[] convertedValues = typeof(string).IsAssignableFrom(resolvedType)
? stringValues.ToArray()
: ConvertValues(stringValues, resolvedType);
if (typeof(Array).IsAssignableFrom(Type))
{
object array = Array.CreateInstance(resolvedType, convertedValues.Length)!;
for (int i = 0; i < ((Array)array).Length; i++)
{
((Array)array).SetValue(convertedValues[i], i);
}
return Task.FromResult(array);
}
if (typeof(IEnumerable).IsAssignableFrom(Type))
{
Type genericTypeDefinition = Type.GetGenericTypeDefinition()!;
Type genericList = genericTypeDefinition.MakeGenericType(resolvedType)!;
object initializedGenericList = Activator.CreateInstance(genericList)!;
for (int i = 0; i < convertedValues.Length; i++)
{
((IList)initializedGenericList).Add(convertedValues[i]);
}
return Task.FromResult(initializedGenericList);
}
object convertedValue = Activator.CreateInstance(resolvedType, convertedValues.Single())!;
return Task.FromResult(convertedValue);
}
private object[] ConvertValues(StringValues stringValues, Type resolvedType)
{
IStringValueConverter converter =_Converters.First(converter => converter.Type == resolvedType);
return stringValues
.ToList()
.Select(value => converter.Convert(value))
.ToArray();
}
private Type ResolveType(Type type)
{
if (typeof(Array).IsAssignableFrom(type))
{
return type.GetElementType()!;
}
if (typeof(IEnumerable).IsAssignableFrom(type) && type.IsGenericType)
{
return type.GetGenericArguments().Single();
}
return type;
}
public string ToInvokeString()
{
return string.Empty;
}
}
在值提供者中,我们只是尝试找到匹配的查询键和相关值,然后通过一些反射创建绑定的输出。我不是反射专家,所以这可能是您想要修改您的需求的地方。
我尝试使用的转换器使用Guid 和int:
public interface IStringValueConverter
{
Type? Type { get; }
object Convert(string value);
}
public class IntConverter : IStringValueConverter
{
public Type? Type => typeof(int);
public object Convert(string value)
{
return int.Parse(value);
}
}
public class GuidConverter : IStringValueConverter
{
public Type? Type => typeof(Guid);
public object Convert(string value)
{
return Guid.Parse(value);
}
}
当然,所有这些都需要在启动期间同时向 WebJobs 和 Functions 注册:
[assembly: FunctionsStartup(typeof(FunctionApp1.FunctionStartup))]
[assembly: WebJobsStartup(typeof(FunctionApp1.WebJobsStartup))]
namespace QueryParameterFunction
{
public class ConverterOptions
{
public IEnumerable<IStringValueConverter> Converters { get; set; } = new List<IStringValueConverter>();
}
public static class FromQueryExntesions
{
public static IWebJobsBuilder AddFromQueryExtension(this IWebJobsBuilder builder)
{
builder.AddExtension<FromQueryConfigProvider>();
builder.Services
.AddTransient<IStringValueConverter, GuidConverter>()
.AddTransient<IStringValueConverter, IntConverter>()
.AddTransient<FromQueryBinding>()
.AddTransient<FromQueryBindingProvider>()
.AddTransient<FromQueryValueProvider>();
return builder;
}
}
public class WebJobsStartup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
builder.AddFromQueryExtension();
}
}
public class FunctionStartup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
ConfigureServices(builder.Services);
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IRepository<Product>, ProductsRepository>();
services.AddAutoMapper(typeof(FunctionStartup));
}
}
}
在行动:
[FunctionName("GetProducts")]
public async Task<IActionResult> GetProducts(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "products")] HttpRequest request,
[FromQuery] List<Guid> productIds)
{
ICollection<Product> products = await _ProductsRepository.GetMany(productIds.ToArray());
List<ProductModel> productModels = products.Select(product => _Mapper.Map<ProductModel>(product)).ToList();
return new OkObjectResult(new ProductsModel
{
Products = productModels,
});
}
由于这些绑定的生命周期,或者更确切地说,启动期间绑定设置的生命周期,如果我们希望能够多次使用[FromQuery],我们实际上需要一些工厂来提供绑定和值提供者。 See this example for clarification.