2
votes

I'm trying to create an Azure Function in F# that has an HTTP trigger and that retrieves data from Cosmos DB. The documentation only provides examples in C# for the CosmosDB binding and translating the code results in the error Cannot bind parameter 'toDoItems' to type IEnumerable`1

This C# example is able to log the id's of the items in Cosmos:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;

namespace Customer
{
    public class ToDoItem
    {
        public string Id { get; set; }
        public string PartitionKey { get; set; }
        public string Description { get; set; }
    }

    public static class DocByIdFromQueryString
    {
        [FunctionName("Triggered")]
        public static IActionResult Run(
            [HttpTrigger(
                AuthorizationLevel.Anonymous,
                "get",
                Route = null
            )] HttpRequest req,
            [CosmosDB(
                databaseName: "customer-db",
                collectionName: "todo-collection",
                ConnectionStringSetting = "CosmosDBConnection",
                SqlQuery = "select * from c"
            )] IEnumerable<ToDoItem> toDoItems, ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            foreach (ToDoItem toDoItem in toDoItems)
            {
                log.LogInformation(toDoItem.Id);
            }
            return new OkResult();
        }
    }
}

This is the F# translation from the working C# code. Both examples have the same dependency versions and use the same database (and connection string).

namespace Customer

open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Mvc
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.Extensions.Logging


module CosmosBindings =
    type ToDoItem = {
        Id : string
        PartitionKey : string
        Description : string
    }

    [<FunctionName("HttpTrigger")>]
    let Run([<HttpTrigger(
                AuthorizationLevel.Anonymous,
                "get",
                Route = null
            )>] req: HttpRequest,
            [<CosmosDB(
                databaseName = "customer-db",
                collectionName = "todo-collection",
                ConnectionStringSetting = "CosmosDBConnection",
                SqlQuery = "select * from c"
            )>] toDoItems: ToDoItem seq) (log: ILogger) =
                log.LogInformation "F# HTTP trigger function processed a request."

                toDoItems
                |> Seq.iter (fun item -> log.LogInformation item.Id)

                OkObjectResult("Hello")

I've tried specifying bindings in an function.json file, but no matter what is added there the file in bin/HttpTrigger/function.json only contains the HttpTrigger (both C# and F#):

{
  "generatedBy": "Microsoft.NET.Sdk.Functions-3.0.1",
  "configurationSource": "attributes",
  "bindings": [
    {
      "type": "httpTrigger",
      "methods": [
        "get"
      ],
      "authLevel": "anonymous",
      "name": "req"
    }
  ],
  "disabled": false,
  "scriptFile": "../bin/Customer.dll",
  "entryPoint": "Customer.CosmosBindings.Run"
}

The dependencies and function version are the same for both C# and F#:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="3.0.3" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.1" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="GetCustomer.fs" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

The errors from running the F# example:

[2/19/2020 4:01:52 PM] Starting JobHost
[2/19/2020 4:01:52 PM] Starting Host (HostId=havardandaestensenvippsnosmacboo, InstanceId=faf11b1c-b81d-49d2-8876-e4ef8785dc9f, Version=3.0.13107, ProcessId=30665, AppDomainId=1, InDebugMode=False, InDiagnosticMode=False, FunctionsExtensionVersion=(null))
[2/19/2020 4:01:52 PM] Loading functions metadata
[2/19/2020 4:01:52 PM] 1 functions loaded
[2/19/2020 4:01:52 PM] Generating 1 job function(s)
[2/19/2020 4:01:52 PM] Microsoft.Azure.WebJobs.Host: Error indexing method 'HttpTrigger'. Microsoft.Azure.WebJobs.Host: Cannot bind parameter 'toDoItems' to type IEnumerable`1. Make sure the parameter Type is supported by the binding. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
[2/19/2020 4:01:52 PM] Error indexing method 'HttpTrigger'
[2/19/2020 4:01:52 PM] Microsoft.Azure.WebJobs.Host: Error indexing method 'HttpTrigger'. Microsoft.Azure.WebJobs.Host: Cannot bind parameter 'toDoItems' to type IEnumerable`1. Make sure the parameter Type is supported by the binding. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
[2/19/2020 4:01:52 PM] Function 'HttpTrigger' failed indexing and will be disabled.
[2/19/2020 4:01:52 PM] No job functions found. Try making your job classes and methods public. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
[2/19/2020 4:01:52 PM] Initializing function HTTP routes
[2/19/2020 4:01:52 PM] Mapped function route 'api/HttpTrigger' [get] to 'HttpTrigger'
[2/19/2020 4:01:52 PM]
[2/19/2020 4:01:52 PM] Host initialized (199ms)
[2/19/2020 4:01:52 PM] Host started (206ms)
[2/19/2020 4:01:52 PM] Job host started
[2/19/2020 4:01:52 PM] The 'HttpTrigger' function is in error: Microsoft.Azure.WebJobs.Host: Error indexing method 'HttpTrigger'. Microsoft.Azure.WebJobs.Host: Cannot bind parameter 'toDoItems' to type IEnumerable`1. Make sure the parameter Type is supported by the binding. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).

Removing the type from the F# code and just returning toDoItems from CosmosDB also results in an error.

[2/19/2020 4:22:33 PM] Generating 1 job function(s)
[2/19/2020 4:22:33 PM] No job functions found. Try making your job classes and methods public. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
[2/19/2020 4:22:33 PM] Initializing function HTTP routes
2
Do you mean that the C# code works as expected but not the F# translation? - Matias Quaranta
Yes! Added clarification. - Håvard Anda Estensen

2 Answers

0
votes

There isn't much documentation about F# and Bindings but based on this: https://docs.microsoft.com/azure/azure-functions/functions-reference-fsharp#binding-to-arguments, it looks like output variables use byref<> and for input variables, the types need to be decorated with [<CLIMutable>].

In this case, being an IEnumerable in C# I'm not sure if Seq.cast is needed instead of seq.

0
votes

It turns out there's a bug in the F# template that is used to generate a function. There's been an open issue for almost a year that you have to use Include rather than Update for host.json and local.settings.json. This is the correct .fsproj file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="3.0.3" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.1" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="GetCustomer.fs" />
  </ItemGroup>
  <ItemGroup>
    <Content Include="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
    <Content Include="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </Content>
  </ItemGroup>
</Project>

I'm puzzled why you have to specify the return type of the output variable. I believe it's perfectly valid F# to not do it. And the error message does not help in debugging:

[3/3/2020 11:57:09 AM] An unhandled host error has occurred.
[3/3/2020 11:57:09 AM] Microsoft.Azure.WebJobs.Host: 'HttpTrigger' can't be invoked from Azure WebJobs SDK. Is it missing Azure WebJobs SDK attributes?.

For the record, toDoItems: ToDoItem seq and toDoItems: IEnumerable<ToDoItem> both produces the same result.