Azure Function C#传递列表< string>作为Http触发器(GET)的参数 [英] Azure Function C# Pass List<string> as an argument over Http Trigger (GET)

查看:52
本文介绍了Azure Function C#传递列表< string>作为Http触发器(GET)的参数的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想通过GET将字符串集合发送到Azure Function C#后端.

I want to send a collection of strings over GET to a Azure Function C# backend.

我的功能

    [FunctionName("GetColl")]
    public async Task<string> TestColl(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "blah")]
        TestRequest request)
    {

TestRequest的位置:

Where TestRequest has:

 {
    public List<string> Fields { get;set; }
 }

有什么方法可以将它作为GET查询来调用?

Is there any way to invoke this as a GET query?

 http://localhost:7101/api/blah?Fields="xxx"&Fields="yyy"
 http://localhost:7101/api/blah?Fields=x,y

均失败

我可以将其设为POST方法,但是我想将其保留为我们服务中的GET公开REST端点

I could of cause make it a POST method, however I want to keep it as a GET exposed REST endpoint in our services

推荐答案

TL; DR:请参阅这个例子.

自定义绑定也可用.尽管 [FromQuery] 本身不是与AspNetCore一样对Function的有效绑定,但我们可以自己创建.

Custom bindings are also available. While [FromQuery] is not a valid binding for Functions natively as it is with AspNetCore, we could just make our own.

首先,让我们创建一个将用于函数绑定的属性:

First of, let's create an attribute that we will use for our Function's bindings:

[Binding]
[AttributeUsage(AttributeTargets.Parameter)]
public class FromQueryAttribute : Attribute
{
}

当我们将 [Binding] 属性与Function中的参数绑定一起使用时,是必需的.

The [Binding] attribute is required when we use it with parameter binding in Functions.

在功能应用程序启动时解析绑定:

Bindings are resolved upon startup of the Function App:

  1. 查找所有函数(任何用 [FunctionName] 装饰的方法)
  2. 解决方法签名中使用的参数的绑定

使用属性装饰绑定时,将使用特定的绑定规则来解析绑定的数据输出.可以使用 IExtensionConfigProvider 配置该绑定规则:

When bindings are decorated with an attribute, a specific binding rule will be used to resolve the data output for the binding. This binding rule can be configured using 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使用该属性来区分此特定扩展名与其他扩展名.

The Initialize is invoked once during startup. We use it to declare a binding rule for the FromQueryAttribute we created, and declare a specific binding provider for it with the Bind method. The [Extension] attribute is required as it is used by WebJobs to distinguish this particular extension from others.

IBindingProvider 包含一些有关Function及其方法签名的信息;在我们的例子中,我们需要参数信息,该信息随绑定提供程序的上下文 BindingProviderContext :

The IBindingProvider holds some information about the Function and its method signature; in our case, we need the parameter information, which is supplied with the binding provider's context, 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时要使用的特定绑定.

The TryCreateAsync method is also invoked once during startup, and is used to declare what specific binding to use when the Function is invoked.

绑定本身必须实现 IBinding ,并且每次Function调用都使用一次. IBinding 保存与当前调用有关的信息. BindAsync 方法在每个Function调用中被调用一次,其 BindingContext 是可用于当前调用的所有绑定数据的位置.可用数据因触发器类型而异.对于 HttpTrigger ,它将始终包含http请求,标头字典和查询字典.但是,解析查询字典时不会考虑可能的数组值:包含 key = 1& key = 2 的查询字符串将解析为 key =>.2 (始终选择后入),而不是 var =>[1,2] ,正如我们所希望的那样,这是我们正在做自己正在做的事情的唯一原因.

The binding itself must implement IBinding, and is used once per Function invocation. The IBinding holds information related to the current invocation. The BindAsync method is invoked once per Function invocation, and its BindingContext is where all the binding data for the current invocation is available from. The available data varies from trigger type to trigger type. For a HttpTrigger, it will always contain a http request, header dictionary and query dictionary. However, the query dictionary is resolved without taking possible array values into account: a query string containing key=1&key=2 will be resolved to key => 2 (last in is always picked), not var => [1,2], as we were hoping, and is the sole reason we're doing what we're doing.

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 .

You probably notice that we don't actually use anything from the binding context here, other than setting a few properties for a value provider (explained next). This is due to the fact that we actually need the request from the BindingContext. But instead of trying to guess which of the entries in the BindingContext.BindingData in fact is our request, we can instead just use the HttpContextAccessor for this.

神奇的地方就是价值提供者;在这里我们为绑定建立实际的输出.由于我们需要查询字符串,而没有从 BindingContext 中找到查询字符串,因此我们只需将 IHttpContextAccessor 注入到我们的值提供程序中,然后从<代替code> HttpContext .

The value provider is where the magic happens; where we build the actual output for the binding. Since we need the query string, and didn't find the query string from the BindingContext, we simply inject IHttpContextAccessor into our value provider, and fetch the query string from the HttpContext instead.

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 :

In the value provider we simply try to find the matching query key and associated values, and then with some reflection create the output for the binding. I'm not reflection expert, so this is probably where you want to tinker for your needs. The converters I tried with was using Guid and 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中注册:

All of this of course needs to be registered with both WebJobs and Functions during startup:

[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] <,则实际上需要一些工厂用于绑定和值提供程序/code>不止一次.请参见此示例以进行澄清.

Because of the lifecycle of these bindings, or rather, the lifecycle of the binding setup during startup, we actually need some factories for the binding and value provider, if we want to be able to use [FromQuery] more than once. See this example for clarification.

这篇关于Azure Function C#传递列表&lt; string&gt;作为Http触发器(GET)的参数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆