Create a complete Azure Function project in .NET 6 and AF v4

Manuel Pinto
11 min readDec 8, 2021

--

.NET 6 is finally here! OpenApi(Swagger)/Multiple Functions/Simplified DI/Testing/… all of it must feature in your next Azure function project!

I started working with functions in .NET5 using the basic templates and some of its features required some work or were lacking official support (OpenAPI). I have documented the journey in this article. Thankfully, Microsoft is upgrading its serverless option (AZ functions) and .NET 6 gives some long awaited features like OpenAPI support. This guided aims to help you set up an azure function project with all the features needed for your modern function app using a practical example. Feel free to jump to any particular section, each is written with the required context so they can be looked into individually.

  • Application overview
  • Create (out-of-process) function using VSCode
    * Create function project using template
    * Parse parameters from the URL and modify the route
    * Add services via dependency injection
    * Refactor folder structure
    * Test the function
  • Add support for OpenApi (Swagger)
    *
    Install dependencies
    * Add OpenApi attributes to the function code
    * Test Swagger page
    * Import OpenApi endpoints
  • Add multiple functions to the same project
  • Add unit tests
    * Create testing project using xUnit
    * Test example
    * Run the tests in the command line
    * Run the tests visually in VSCode

Application overview

In order to test the different capabilities of a function we will create a practical example that will query an external API to get and return some data. The final code used is available in GitHub. The data in the example will be stock related information where the Open and Close price for a given stock symbol is returned. There are multiple sources of stock data, we will use the free-tier API of finhub where the data is available through a single call as illustrated in the following diagram for the Open case.

The function will then be responsible to call the external API, collect and transform the data so that only the Open stock price is returned to the client. As simple as this application looks there are non trivial topics involved like asynchronous calls or the unit testing of the result mapping logic.

Create (out-of-process) function using VSCode

Use out-of-process mode

Functions comes in two different flavours: in process(dotnet) and out-of-process (dotnet-isolated).
Taken from Microsoft documentation, there are advantages in choosing out-of-process:

Fewer conflicts: because the functions run in a separate process, assemblies used in your app won’t conflict with different version of the same assemblies used by the host process.

Full control of the process: you control the start-up of the app and can control the configurations used and the middleware started.

Dependency injection: because you have full control of the process, you can use current .NET behaviours for dependency injection and incorporating middleware into your function app.

Using this recommended approach, let us create a function using the default template for out-of-process. Throughout the guide, we will be using VisualStudio Code as editor since it has all the required functionality to manage, run and even deploy functions.

Install Azure Functions SDK and VS Code extension

This extension contains the required dependencies for generating an azure function project. If you are interested in a command line approach please see this guide.

Create function project using template

Move to the Azure Function extension tab and click on the little thunder to start the template project generation.

Accept the prompt and move on to the wizard.

At this stage the wizard will ask you for the different settings of your function. These will be the chosen settings for this guide.

  • Language: C#
    Use whatever language you would like to work with.
  • .NET Runtime: .NET 6 isolated
    At time of writing, the .NET 6 is the latest available.
  • Template: HttpTrigger
    Azure functions can have multiple triggers (see this). We will be using the HttpTrigger template that triggers the execution when the function specific endpoint is hit. At time of writing, OpenAPI template is only available for in-process function type although MS claims it will be soon extend the support to out-of-process.
  • Function Name: GetOpenStockPriceForSymbol
    As example let us create a function that returns the Open price for a given stock symbol.
  • Namespace: “Example.Function
    Something meaningful to your function space.
  • Access Rights: Anonymous
    Access authentication is something that you should pay special attention in order to develop a secure function. For the sake of simplicity, the Anonymous type will accept every request made to the public endpoint.

Hit F5 and to make sure that the application has everything required to run. If interested in knowing what commands are being used in the background to run the function, have a look at .vscode/tasks.json.

Go ahead, open the browser to the provided function URL.

Pretty basic function, right? Let us dive in the function code.

The function is only writing the “Welcome to Azure functions” string to the response and logging the message “C# HTTP trigger function processed a request.” to the console.

  • HttpTrigger attribute

This defines the context in which the function is expected to be triggered. In our case there is no authorisation (AuthorizationLevel.Anonymous). A GET endpoint using the default route equal to the function name.

  • Logger injection

The logger responsible for outputting messages to the console is being created in the function constructor by Injecting the ILoggerFactory and creating an instance for type GetOpenStockPriceForSymbol.

private readonly ILogger _logger;public GetOpenStockPriceForSymbol(ILoggerFactory loggerFactory)
{
_logger= loggerFactory.CreateLogger<GetOpenStockPriceForSymbol>();
}

Let us see how we can easily modify the function so it includes more complex functionality.

Parse parameters from the URL and modify the route

If not provided (i.e = null) route template will assume the function name. In this case and to illustrate how the route template can be modified and URL parameters can be parsed to the function code, let us use the following route attribute.

Route = "stock-price/symbol/{symbol:alpha}/open"

This overrides the api endpoint to /api/stock-price/symbol/{symbol:alpha}/open and by using brackets {} in the route we can map it to variables like the symbol string defined as parameter of the Run method. The alpha keyword after the variable name states that the endpoint is expecting an alphanumeric sequence of characters to be parsed to the string type.

Add Services via Dependency injection

Let us create the following code structure with multiple dependencies.

The first obvious dependency we will need in the function is the interface IStockDataProvider which itself is dependent on the FinhubHttpClient and FinhubDataMapper. There is also an HttpHelper used only within the Function.

Enable constructor Dependency Injection. (see MS documentation on DI)

In Program.cs, register the Interfaces and the HttpClient.

.ConfigureServices(s =>                
{
s.AddScoped<IFinhubDataMapper, FinhubDataMapper>();
s.AddScoped<IStockDataProvider, FinhubProvider>();
s.AddScoped<IHttpHelper, HttpHelper>();
s.AddHttpClient<FinhubHttpClient>();
})

Refactor folder structure

Let us refactor the file structure by moving the code within a /src folder with shared content in /Function.Domain/ and Functions in /Functions/ folder. This will help us separate Functions from shared code and Unit tests in different projects

└── src
├── Function.Domain
│ ├── Models
│ │ ├── FinhubStockData.cs
│ │ └── StockData.cs
│ ├── Providers
│ │ ├── FinhubProvider.cs
│ │ └── IStockDataProvider.cs
│ └── Services
│ │ ├── FinhubDataMapper.cs
│ │ ├── HttpClients
│ │ │ └── FinhubHttpClient.cs
│ │ └── IFinhubDataMapper.cs
│ └── Helpers
│ ├── HttpHelper.cs
│ └── IHttpHelper.cs
├── Functions
│ └── GetOpenStockPriceForSymbol.cs
├── Program.cs
├── azure-function-example-csharp.csproj
├── host.json
└── local.settings.json

VSCode may complain about not finding the project file as these have moved. Please adjust the path in .vscode/tasks.json, ex:

"options": {
"cwd": "src/"
},

Test the function

Its time to give it a try! If you are following this example using finhub, make sure to add your account token to the local.settings.json

"Values": {
(...)
"finhub_api_baseUrl": "https://finnhub.io/api/v1/",
"finhub_api_token": "<your_secret_key>"
}

these will be used by the FinhubHttpClient.cs

Lets try with Apple stock (symbol = AAPL)

…and there it is!

Add support for OpenAPI (Swagger)

One of the main features of AF4 is the official extension support for OpenAPI spec. It needs a bit of tweaking in order to make it work out-of-process. We will cover the required steps.

Install dependencies

Add OpenApi package
Install Nuget Package Microsoft.Azure.Functions.Worker.Extensions.OpenApi

Modify Program.cs
Import the newly installed package and add/update the host builder with configuration methods

.ConfigureFunctionsWorkerDefaults(worker => worker.UseNewtonsoftJson())
.ConfigureOpenApi()

So that the final code resembles the following:

Add OpenApi attributes to the Function code

The final bit is to add the OpenApi definition of our function and that is achieved through multiple attributes. See OpenApi spec for a more detailed description on the meaning and usage of each field.

  • OpenApiOperation
    Provide a unique identifier to the function endpoint in operationId. Group operations with tags.
[OpenApiOperation(
operationId: "GetOpenStockDataForSymbol",
tags: new[] { "stock-price/symbol"})
]
  • OpenApiParameter

Provide information about the input parameter. In the example, a parameter (symbol) of type string is expected to be sent in the endpoint path.

[OpenApiParameter(
name: "symbol",
In = ParameterLocation.Path,
Required = true,
Type = typeof(string),
Description = "Symbol to get stock data from")
]
  • OpenApiResponseWithBody

Provide the response format, possible return codes etc. For simplicity let us just expect 200-OK Http status code.

[OpenApiResponseWithBody(
statusCode: HttpStatusCode.OK,
contentType: "text/plain",
bodyType: typeof(string),
Description = "OK response")
]

Test Swagger page

Hit run on your function. Notice that there are four new functions.

The OpenApi Swagger is available in function named RenderSwaggerUI.

The function endpoints can now be called from within the Swagger page. Let us give it a try.

It correctly outputs the open stock price for Apple (symbol: AAPL).

Import OpenApi endpoints

The main reason for supporting OpenAPI is so that we don’t have to specify the endpoint definition when creating a client. Definitions can now be easily imported with any OpenAPI supported tool. Let’s try it with Visual Studio.

Right click solution > Add Connected Service > Add Service reference > OpenAPI

Provide the location of the definition .json.

Once the code is generated, the endpoints can be called like methods of an HttpClient.

If the endpoint definition changes so will the method definition once the connected services are in sync. This enforces your code to always be up to date with your function’s definition reducing the likelihood of interoperability problems between your services.

Support multiple functions

Now that we have separated shared code in the Domain folder, and Functions in another, adding a new function is as simple as creating a new class in the /Functions folder. Let us create a function for getting the Close price.

This function will be very similar to to the previous one, make sure to modify the Route parameter as functions need to have a unique route and the Close price maps to the PreviousClose value in the StockData model.

Starting the project will now expose the two new functions.

Hit the new endpoint

the previously close price is returned!

Add Unit tests

Our API wouldn’t be complete without the base of the testing pyramid, Unit tests! Remember that these should ideally be done before the implementation of the Interfaces (see TDD).

Create testing project using xUnit

The tests are stored in a different project and due to the way we’ve been structuring our app this is something that is really easy to do. .NET provides templates for the most used testing frameworks. For this example we will create a unit test project using xUnit. (see “$dotnet new -l” for a list of included templates)

Create folder for project ex: {function-root}/tests/unit-tests navigate to the folder and run the command

/tests/unit-tests % dotnet new xunit

This will create the .csproj with the required xUnit dependencies and a sample test class called UnitTest1.cs.
The classes that we will be interested in unit testing are the ones for shared code in the Function.Domain namespace sitting in /src/Function.Domain. This code has its own dependencies part of another project (marketsim-func-finhub.csproj) so in order to access these classes we need to add a reference to the main function project. This can easily be done by navigating to the folder where the unit test project sits and add reference via the command

tests/unit-tests % dotnet add reference ../../src/azure-function-example-csharp.csproj

Examine the unit-tests.csproj and confirm that the reference was added

<ItemGroup>
<ProjectReference Include=”..\..\src\azure-function-example-csharp.csproj” />
</ItemGroup>

Test example

We now have the tools to start unit testing our application. This example does not have very complex logic units but for the sake of simplicity let us focus on the mapper class (FinhubDataMapper) responsible for mapping the results from the Finhub API to a more human readable naming to be returned by the Function.

To test this logic we just need to

  • Arrange the FinhubStockData model
  • Act by inputting this model in the system under testing (FinhubDataMapper.MapToStockData)
  • Assert that the output model is correctly mapped

Run the tests using command line

Tests can be run in the command line with the command

tests/unit-tests % dotnet test

This should run and return the test results.

Run the tests visually in VSCode

If you want to trigger/debug specific tests, and have a more visually appealing way to check the status of your tests there are some VSCode add-ons that can help us achieve this. .NetCore test Explorer is a basic but capable one.

Once installed, change the test discovery path to the test folder (.vscode/settings.json) as by default it will look for projects in the root folder.

“dotnet-test-explorer.testProjectPath”:”**/*-tests.@(csproj|vbproj|fsproj)”

Once discovery is complete, the tests should now be available in the ‘Testing’ tab.

— — —

That is all hope it was helpful, happy coding! ;)

--

--