Azure Functions 3 and [FromBody] modelbinding
Asked Answered
V

3

6

I am creating a post endpoint using Azure Functions version 3. In Asp.net it is very convenient to get the post object using the [FromBody] tag and the magic will happen with modelbinding. Is there a way to use the FromBody tag in Azure Functions v3?

Vociferance answered 30/1, 2020 at 11:38 Comment(0)
R
7

Yes you can do that,

 public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "post")][FromBody] User user, ILogger log, ExecutionContext context)

Here is an Example

Rausch answered 30/1, 2020 at 11:51 Comment(5)
Thanks for your answer. Just to add that it looks like the [FromBody] parameter needs to be before parameters without the tag.Vociferance
It makes sense to note that this binding has a disadvantage: it does not error in case of invalid JSON. It just instantiates the type with all default values and provedes it to a code.Faro
[FromBody] attribute is completely irrelevant. Values will be bound anyway. Moreover if you send same parameters using query parameters as in body, it'll bind values from query parameters and there is no way how to fix it since [FromQuery] is irrelevant as well.Neigh
@Neigh Can you elaborate -- any relevant docs? I have inherited a function app that has some [FromBody] attributes. I was surprised to see that the functions work fine without the attribute and came across your comment when searching this subject.Tenuto
@HolisticDeveloper unfortunately I couldn't find any documentation, I had to exercise binding and perform experiments to get to this conclusion :/Neigh
C
1

Microsoft.Azure.Functions.Worker version 1.7.0-preview1 makes custom input conversion possible. The below will convert HttpRequestData.Body to a POCO via converting the stream to a byte array then passing the byte array back into the normal input converter process (where it is convertered by the built-in JsonPocoConverter. It relies on reflection as the services required to delegate the conversion after converting the stream to a byte array are internal, so it may break at some point.

Converter:

internal class FromHttpRequestDataBodyConverter : IInputConverter
{
    public async ValueTask<ConversionResult> ConvertAsync(ConverterContext context)
    {
        if (context.Source is null
            || context.Source is not HttpRequestData req
            || context.TargetType.IsAssignableFrom(typeof(HttpRequestData)))
        {
            return ConversionResult.Unhandled();
        }
        
        var newContext = new MyConverterContext(
            context,
            await ReadStream(req.Body));

        return await ConvertAsync(newContext);
    }

    private static async Task<ReadOnlyMemory<byte>> ReadStream(Stream source)
    {
        var byteArray = new byte[source.Length];

        using (var memStream = new MemoryStream(byteArray))
        {
            await source.CopyToAsync(memStream);
        }

        return byteArray.AsMemory();
    }

    private static ValueTask<ConversionResult> ConvertAsync(MyConverterContext context)
    {
        // find the IInputConversionFeature service
        var feature = context.FunctionContext
            .Features
            .First(f => f.Key == InputConvertionFeatureType)
            .Value;

        // run the default conversion
        return (ValueTask<ConversionResult>)(ConvertAsyncMethodInfo.Invoke(feature, new[] { context })!);
    }

    #region Reflection Helpers

    private static Assembly? _afWorkerCoreAssembly = null;
    private static Assembly AFWorkerCoreAssembly => _afWorkerCoreAssembly
        ??= AssemblyLoadContext.Default
            .LoadFromAssemblyName(
                Assembly.GetExecutingAssembly()
                    .GetReferencedAssemblies()
                    .Single(an => an.Name == "Microsoft.Azure.Functions.Worker.Core"))
        ?? throw new InvalidOperationException();

    private static Type? _inputConversionFeatureType = null;
    private static Type InputConvertionFeatureType => _inputConversionFeatureType
        ??= AFWorkerCoreAssembly
            .GetType("Microsoft.Azure.Functions.Worker.Context.Features.IInputConversionFeature", true)
        ?? throw new InvalidOperationException();

    private static MethodInfo? _convertAsyncMethodInfo = null;
    private static MethodInfo ConvertAsyncMethodInfo => _convertAsyncMethodInfo
        ??= InputConvertionFeatureType.GetMethod("ConvertAsync")
        ?? throw new InvalidOperationException();

    #endregion
}

Concrete ConverterContext class:

internal sealed class MyConverterContext : ConverterContext
{
    public MyConverterContext(Type targetType, object? source, FunctionContext context, IReadOnlyDictionary<string, object> properties)
    {
        TargetType = targetType ?? throw new ArgumentNullException(nameof(context));
        Source = source;
        FunctionContext = context ?? throw new ArgumentNullException(nameof(context));
        Properties = properties ?? throw new ArgumentNullException(nameof(properties));
    }

    public MyConverterContext(ConverterContext context, object? source = null)
    {
        TargetType = context.TargetType;
        Source = source ?? context.Source;
        FunctionContext = context.FunctionContext;
        Properties = context.Properties;
    }

    public override Type TargetType { get; }

    public override object? Source { get; }

    public override FunctionContext FunctionContext { get; }

    public override IReadOnlyDictionary<string, object> Properties { get; }
}

Service configuration:

public class Program
{
    public static void Main()
    {
        var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults()
            .ConfigureServices(services =>
            {
                services.Configure<WorkerOptions>((workerOptions) =>
                {
                    workerOptions.InputConverters.Register<Converters.FromHttpRequestDataBodyConverter>();
                });
            })
            .Build();

        host.Run();
    }
}
Counterpoise answered 2/1, 2022 at 8:3 Comment(0)
E
0

Here's the write-up as of 2024: https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2&pivots=programming-language-csharp#payload

TL;DR , you need their 'special' version of the FromBody guy

using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute;

Elevated answered 9/10 at 9:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.