Reputation: 445
I want to send a collection of strings over GET to a Azure Function C# backend.
My function
[FunctionName("GetColl")]
public async Task<string> TestColl(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "blah")]
TestRequest request)
{
Where TestRequest has:
{
public List<string> Fields { get;set; }
}
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
Both fail
I could of cause make it a POST method, however I want to keep it as a GET exposed REST endpoint in our services
Upvotes: 3
Views: 4632
Reputation: 421
If you would like just to send it in a way you describe it can be done like this:
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);
}
}
Method signatures developed by the azure function C # class library can only include these:
Please take a look at this link to see more details supported bindings
Upvotes: 4
Reputation: 1391
TL;DR: See this example.
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
{
}
The [Binding]
attribute is required when we use it with parameter binding in Functions.
Bindings are resolved upon startup of the Function App:
[FunctionName]
)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);
}
}
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.
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);
}
}
The TryCreateAsync
method is also invoked once during startup, and is used to declare what specific binding to use when the Function is invoked.
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();
}
}
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.
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;
}
}
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);
}
}
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));
}
}
}
In action:
[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,
});
}
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.
Upvotes: 2