【问题标题】:Azure Function C# Pass List<string> as an argument over Http Trigger (GET)Azure Function C# Pass List<string> 作为 Http Trigger (GET) 上的参数
【发布时间】:2021-01-30 22:29:43
【问题描述】:

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

我的功能

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

TestRequest 在哪里:

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

有没有办法将其作为 GET 查询调用?

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

都失败了

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

【问题讨论】:

  • 有什么适合你的答案吗?

标签: azure-functions


【解决方案1】:

TL;DR:See this example.

自定义绑定也可用。虽然 [FromQuery] 不是原生的 Functions 的有效绑定,因为它与 AspNetCore 一样,但我们可以自己制作。

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

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

[Binding] 属性在函数中与参数绑定一起使用时是必需的。

在 Function App 启动时解决绑定:

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

当绑定用属性修饰时,将使用特定的绑定规则来解析绑定的数据输出。可以使用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&amp;key=2 的查询字符串将被解析为key =&gt; 2(总是选择最后一个),而不是我们希望的var =&gt; [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;
    }
}

在值提供者中,我们只是尝试找到匹配的查询键和相关值,然后通过一些反射创建绑定的输出。我不是反射专家,所以这可能是您想要修改您的需求的地方。 我尝试使用的转换器使用Guidint

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.

【讨论】:

  • 非常感谢您提供如此详尽的回答
  • @MarkusFoss 没问题。如果您需要一些更高级的示例,可以查看 GitHub 上的 WebJobs SDK 存储库。特别是,Microsoft.Azure.WebJobs.Extensions.Storage 项目的源代码显示了如何实现 blob、队列等的绑定 (github.com/Azure/azure-webjobs-sdk/tree/dev/src/…)。所有这些自定义绑定的一个脚注是,其中很多还没有“准备好供公众使用”,并且一些核心功能可能会在未来的 SDK 版本中发生变化。
  • @MarkusFoss 我做了一个更好的例子,它能够解析类型。它支持数组和IEnumerables,如果你对它感兴趣的话。
【解决方案2】:

如果您只想以您描述的方式发送它,可以这样做:

public static class Function1
{
    [FunctionName("Function1")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        string inputString = req.Query["Fields"];
        var listStrings = inputString.Split(',').ToList();

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        inputString = inputString ?? data?.Fields;

        string responseMessage = string.IsNullOrEmpty(inputString)
            ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
            : $"Result is - {inputString}. This HTTP triggered function executed successfully.";

        return new OkObjectResult(responseMessage);
    }
}

azure function C#类库开发的方法签名只能包括这些:

  • 用于日志记录的 ILogger 或 TraceWriter(仅限 v1 版本)
  • 用于正常关闭的 CancellationToken 参数
  • 使用属性修饰标记输入和输出绑定
  • 绑定表达式参数以获取触发器元数据

请查看此链接以查看更多详细信息supported bindings

【讨论】:

    猜你喜欢
    • 2019-11-06
    • 1970-01-01
    • 2020-08-26
    • 1970-01-01
    • 1970-01-01
    • 2012-11-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多