简体   繁体   中英

Azure Function with HTTP trigger fails to bind model from JSON payload (.NET 5 out-of-proc )

I am running an isolated process function that is failing to bind the input model from the JSON payload.

This is my function:

[Function("AddChip")]
public async Task<HttpResponseData> AddChip([HttpTrigger(AuthorizationLevel.Anonymous,
 "post", Route = "chip")] Chip chip, HttpRequestData req, FunctionContext executionContext)
{
            
    var logger = executionContext.GetLogger("Function1");
    logger.LogInformation("C# HTTP trigger function processed a request.");

    var result = await _chipService.UpsertChipAsync(chip);

    var response = req.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync(result);
    return response;
}

And this is my Chip class:

public class Chip
{
    public string Id {  get; set; }
    public double Balance { get; set; }
    public DateTime Created { get; set; }
    public DateTime Updated { get; set; }
}

But when I call it from Insomnia making sure the Content-Type application/json header is set with this body:

{
    "Id": "1",
    "Balance": 0,
    "Created": "2001-01-01T00:00:00",
    "Updated": "2001-01-01T00:00:00"
}

I get an error message saying that the chip parameter could not be converted:

Microsoft.Azure.Functions.Worker.Diagnostics.Exceptions.FunctionInputConverterException: Error converting 1 input parameters for Function 'AddChip': Cannot convert input parameter 'chip' to type 'Models.Chip' from type 'Microsoft.Azure.Functions.Worker.GrpcHttpRequestData'.

I have tried unsuccessfully:

  • Removing the HttpRequestData parameter,
  • Different classes and payloads,
  • Different AuthorizationLevel

It seems related to gRPC somehow as the error references GrpcHttpRequestData .

Different sources like Microsoft documentation or this Rick van den Bosch post or even other Stack Overflow answers like Azure Functions model binding imply this should work, but it does not.

Azure Functions .NET 5 uses the new out-of-proc model, which is currently very limited in terms of model binding.

In the case of HTTP requests, your method gets a HttpRequestData . Any mapping of request body or URI parameters must be done by your code.

As Stephen mentioned in the other answer, there is no fully fledged model binding capability today in the .net 5 azure function(out of process model). Your request payload is available in the Body property of the HttpRequestData instance and you may de-serialize it inside your function.

The below example uses JsonSerializer from System.Text.Json namespace.

static JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    PropertyNameCaseInsensitive = true
};

[Function("AddChip")]
public async Task<HttpResponseData> AddChip(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "chip")] HttpRequestData req,
    FunctionContext executionContext)
{
    Chip chip = null;

    if (req.Body is not null)
    {
        chip = await JsonSerializer.DeserializeAsync<Chip>(req.Body, SerializerOptions);
    }

    var logger = executionContext.GetLogger("Function1");
    logger.LogInformation("C# HTTP trigger function processed a request for." + chip?.Id);

    var result = await _chipService.UpsertChipAsync(chip);

    var response = req.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync(result);
    return response;
}

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();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM