The dotnet isolated model of azure functions has a binding pipeline which is responsible for populating your function inputs. While the built-in mechanism satisfies the majority of the binding use cases, there may be instances where you want to customize this pipeline.

Let’s get familiarized with a few terminologies before we move forward

Input conversion

The process of using the raw input data to populate all the function inputs appropriately.

Input converter

A small component responsible for converting the raw input to a specific type of function input type. The dotnet isolated package ships with a default set of converters and the Input conversion pipeline uses these converters to successfully bind all the function parameters.

Extending the input conversion pipeline

If the built-in converters are not handling your use cases, you may write your own input converter and register that with your function app. Let’s take a look at an example.

Imagine you have POCO like below to represent customer information.

public class Customer
{
    public string Name { get; set; }

    public string Gender { get; set; }

    public string Culture { get; set; }
}

and you want to use that type as a parameter of your function and you want to populate this from using some minimal information from your http request. For example, assume your http request includes the unique id of the customer in the route (ex: “/api/customers/123”) and you want to use that id value to pull the customer details from another source (ex: a database /XML file/ a REST API etc..)

[Function("Update")]
public HttpResponseData Update(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get",Route ="customers/{id}")] HttpRequestData req,
    Customer customer)
{
    _logger.LogInformation($"Processing request for {customer.Id}");

    //do something with the customer instance.

    var response = req.CreateResponse(HttpStatusCode.OK);
    response.WriteString($"Processed {customer.Id} {customer.Name}");

    return response;
}

To make this happen, we will extend the input conversion pipeline with a custom converter.

Writing a custom input converter

Create a class and have it implement the IInputConverter interface. The InputConverter interface has a ConvertAsync method which we are going to implement with our custom logic.

public class CustomerConverter : IInputConverter
{
    public ValueTask<ConversionResult> ConvertAsync(ConverterContext context)
    {
        // we are going to replace this with actual implementation shortly.
        throw new NotImplementedException();
    }
}

The ConvertAsync method receives an instance of ConverterContext, from which we can get information about the raw input data and the target type (the parameter type) we are going to bind to. In this specific example, we need the Id value from the request URL/route. We could access that from the BindingContext on the FunctionContext.

public async ValueTask<ConversionResult> ConvertAsync(ConverterContext context)
{
    if (context.FunctionContext.BindingContext.BindingData.TryGetValue("id", out var idObj))
    {
        var id = Convert.ToInt32(idObj);
        try
        {
            var customer = await GetCustomerFromRestAPIAsync(id);
            if (customer is not null)
            {
                return ConversionResult.Success(customer);
            }
        }
        catch(Exception ex)
        {
            return ConversionResult.Failed(ex);
        }
    }

    return ConversionResult.Unhandled();
}

Here we are reading the Id route param value and using that to populate a Customer instance. The GetCustomerFromRESTAPIAsync method is simply calling a REST API to get the customer details.

private async Task<Customer?> GetCustomerFromRestAPIAsync(int id)
{
    var url = $"https://anapioficeandfire.com/api/characters/{id}";

    return await httpClient.GetFromJsonAsync<Customer?>(url);
}
  • httpClient is a static instance of HttpClient.
  • The above method is kept as minimal and simple. Modify as needed to make it more robust as needed.

You can see that we used 3 helper methods in our ConvertAsync code. Let’s take a look at what they do.

ConversionResult.Success

If the conversion is successful we return the result of ConversionResult.Success helper method, which returns an instance of ConversionResult with the Status property value set to Succeeded and Value property value set to the populated customer object.

ConversionResult.Failed

If there is was exception during the conversion, we use the ConversionResult.Failed helper which returns a ConversionResult instance with the Error property populated. The Status property value will be set to Failed in this case.

ConversionResult.Unhandled

Finally the ConversionResult.Unhandled method can be used when you want to tell the calling pipeline that this converter is not able to convert this specific input. Remember this converter will be called while trying to populate every function input (although there is a way to optimize this which you will see below). For example, the date time converter returns Unhandled when the target type is not a DateTime type or the source is not string.

https://github.com/Azure/azure-functions-dotnet-worker/blob/7d659ced053af41d58274ea4a36d2b5db2138750/src/DotNetWorker.Core/Converters/DateTimeConverter.cs#L16-L19

Register the custom converter in the pipeline.

The custom converter can be hooked up to the function app in any of the following 3 approaches.

1) App bootstrapping time

Add the new converter to WorkerOptions.InputConverters while bootstrapping the function app.

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(workerOptions =>
    {
        workerOptions.InputConverters.Register<CustomerConverter>();
    })
    .Build();

await host.RunAsync();

The above approach will add the custom converter as the last entry in the InputConverters collection. The binding pipeline calls each converter sequentially in the order they are registered. So in this case the CustomerConverter will be called only after all the built-in converters are called and all of them returned an Unhandled result.

If you want the custom converter to be used earlier, you can use the RegisterAt method.

// Adds MyCustomConverter to the set of available (built-in) converters, but at index 3. 
workerOptions.InputConverters.RegisterAt<CustomerConverter>(3);

2) Type level

Decorate the type used as the parameter of the function with InputConverter attribute where you will define the custom converter type to be used.

[InputConverter(typeof(BookConverter))]
public class Book
{
    public string Name { set; get; }

    public string Isbn { set; get; }
}

and

[Function("GetBook")]
public HttpResponseData Update(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get",Route ="books/{id}")] HttpRequestData req,
    Book book)
{
    _logger.LogInformation($"Processing request for {book.Name}");

    //do something with the Book instance.

    var response = req.CreateResponse(HttpStatusCode.OK);
    response.WriteString($"Processed {book.Name}");

    return response;
}

The above example assumes that you have written a BookConverter similar to the CustomerConverter above which populates a Book instance.

3) Function parameter level

Decorate the function parameter using the InputConverter attribute

[Function("GetBook")]
public HttpResponseData Update(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "books/{id}")] HttpRequestData req,
    [InputConverter(typeof(BookConverter))] Book book)
{
    _logger.LogInformation($"Processing request for {book.Name}");

    //do something with the Book instance.

    var response = req.CreateResponse(HttpStatusCode.OK);
    response.WriteString($"Processed {book.Name}");

    return response;
}

In this case, you do not need to decorate your book POCO with the InputConverter attribute.

public class Book
{
    public string Name { set; get; }

    public string Isbn { set; get; }
}

The last 2 approaches are more optimized than the first one since those will directly pick the explicitly specified converter for the parameter instead of going through all the registered converters.

Dependency Injection

Yes, dependency injection work with your custom input converters. You can inject dependencies to your converter class constructor.

public class BookConverter : IInputConverter
{
    private readonly ILogger<BookConverter> _logger;

    public BookConverter(ILogger<BookConverter> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
        
    public ValueTask<ConversionResult> ConvertAsync(ConverterContext context)
    {
        _logger.LogInformation($"BookConverter is the best!");
        throw new NotImplementedException();
    }
}

Take a look at this repo for a complete working sample.

Give it a try. If you are running into issues, please open a new issue in the dotnet worker repo.

Cheers