Introduction to Isolated Azure Functions (.Net 5)

One of the bigger constraints of Azure Function Apps was the tight coupling between the function code and the Azure function runtime. The In-Process Azure Functions ran as a class library in the same process as the host service and was tightly coupled. This forced the function code to use the same version of .Net as the host service. This would also mean you cannot use .Net 5 to develop your function apps.

This restriction was overcome with Isolated Function App, which runs out of process in Azure Functions. The Isolated Function App runs as a console application and could now use .Net 5 for implementation. Additionally, it had fewer conflics due to different versions of same assembly as it runs on a separate process. The other benefits also included the ability to control the function app in a better way, including the application start, configuration and support for dependency injection. One of the biggest limitations of the out-of-process isolated Azure functions is the lack of support for Durable Functions.

Project Template

Let us now proceed to create our Isolated Function.

Isolated Function could be created by choosing the Azure Function Template from Visual Studio.

You need to make sure that you select “.Net 5.0” as the framework. This would generate the Isolated Function template for you.The Isolated Function project is a console application and the template comprises of following files.

  • host.json
  • local.setttings.json
  • C# Project File
  • Program.cs

Start Up and Configuration

Since as a developer you have access to the start up of the Isolated functions, you are responsible for creating, configuring and starting the Host instance. You do so using the HostBuilder class. If you look at the default code generated you could find the following code.

public class Program
{
    public static void Main()
    {
        var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults()
            .Build();

        host.Run();
    }
}

The code above creates an instance of HostBuilder, uses the ConfigureFunctionWorkerDefaults extension method to create teh default configurations and uses the Build extension to initialize the configuration.

It then uses the IHost.Run() method to start the Host Instance.

As mentioned earlier, since you are building your own host instance, you have ability to configure it with the services and middleware you would prefer.

ConfigureFunctionsWorkerDefaults

The ConfigureFunctionWorkerDefaults method adds the default settings which is required to run the function app as __ out of process_. The default configuration includes

  • Default set of converters
  • Default JsonSerializerOptions
  • Integrate Azure Logging
  • Output binding Middlewareand features
  • Function execution middleware
  • Default gRPC support

You could use an override of the ConfigureFunctionWorkerDefaults to custom configure some of the existing services/configuration. For example, the default JsonSerializerOptions configured by the ConfigureFunctionWorkerDefaults is as following.


// Code from IServiceCollection.AddFunctionsWorkerDefaults extension method
services.Configure<JsonSerializerOptions>(options =>
{
    options.PropertyNameCaseInsensitive = true;
});

This configures the default Json configuration to ignore the casing on property names. You could further configure it to ignore the trailing commas in Json (default is false).

var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults(builder=>
            {
                builder.Services.Configure<JsonSerializerOptions>(options =>
                {
                    options.AllowTrailingCommas = true;
                });

            })
            .Build();


Middleware

The isolated functions also allows you to configure your custom middlewares to the invocation pipelines. The ConfigureFunctionsWorkerDefault method again comes in handy for the purpose.

var host = new HostBuilder()
                .ConfigureFunctionsWorkerDefaults(builder=>
                {
                    builder.Services.Configure<JsonSerializerOptions>(options =>
                    {
                        options.AllowTrailingCommas = true;
                    });

                    builder.UseMiddleware<ExceptionMiddleWare>();
                })
                .Build();

The ExceptionMiddleWare in the above is a custom middleware that can inject custom logic into the invocation pipeline.

public class ExceptionMiddleWare : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        try
        {
            await next(context);
        }
        catch(Exception ex)
        {
            var logger = context.GetLogger(context.FunctionDefinition.Name);
            logger.LogError("Unexpected Error in {0}: {1}",context.FunctionDefinition.Name,ex.Message);
        }
    }

}

In the above code, the logic injected by the Middleware is called after the function exection, but you could also inject logic before the function is called.

Dependency Injection and Services

You can register your services during the start up using the IHostBuilder.ConfigureServices extension method. For example,

var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults(builder=>
            {
                builder.Services.Configure<JsonSerializerOptions>(options =>
                {
                    options.AllowTrailingCommas = true;
                });

                builder.UseMiddleware<ExceptionMiddleWare>();
            })
            .ConfigureServices(serviceCollection =>
            {
                serviceCollection.AddSingleton<IMockDataService>(new MockDataService());
            })
            .Build();

For the sake of this example, I am using a sample Service which emulate by Data Service.

public interface IMockDataService
{
    UserDto CreateUser(string userName);
}

public class MockDataService : IMockDataService
{
    private readonly Random _randomGenerator = new Random();
    public UserDto CreateUser(string userName)
    {
        return new UserDto
        {
            Name = "Anu Viswan",
            Id = _randomGenerator.Next()
        };
    }
}

I could now inject the dependency in my function code.

Functions

Since the Isolated Functions are running out of process, the way we have the Http Request in the Http Trigger functions are difference from the in-process version. Since we do not have access to the original Http Request and response, you would need to use the HttpRequestData and HttpResponseData object instead to access the request and response. While these provides all the Headers,URL, Body of the incoming request, it is not the original request, bur rather a representation of the same. A sample Http Trigger function might look like the following.

[Function(nameof(SayHello))]
public static async Task<HttpResponseData> SayHello(
    [HttpTrigger(AuthorizationLevel.Anonymous,"post")] HttpRequestData req,
    FunctionContext executionContext)
{
    // Use the FunctionContext.GetLogger to fetch instance of ILogger
    var logger = executionContext.GetLogger(nameof(StaticFunctions));
    logger.LogInformation("Started execution of function");

    var data = await req.ReadFromJsonAsync<UserDto>();

    // Should use HttpResponseData as response
    var response = req.CreateResponse(HttpStatusCode.OK);

    await response.WriteStringAsync($"Hello {data.Name}, Welcome to Isolated functions demo.");
    return response;
}

The bindings too are serverly restricted as there is no longer deep integrtion with the Azure function runtime. This would also mean you are restricted from using the ICollection<T> or CloudBlockBlob. Instead your need to relay POCOS or strings (or arrays). The Logger also needs to be fetched using the FunctionContext.GetLogger method.

Earlier,we registered own DataService. We could use Dependency Injection to inject the same and use in our function. For example,

private readonly IMockDataService _mockDataService;
public InstanceFunctions(IMockDataService mockDataService)
{
    _mockDataService = mockDataService;
}

[Function(nameof(CreateUser))]
public async Task<HttpResponseData> CreateUser(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
    FunctionContext executionContext)
{
    var queryStrings = System.Web.HttpUtility.ParseQueryString(req.Url.PathAndQuery);
    var userName = queryStrings["user"];

    // Use the FunctionContext.GetLogger to fetch instance of ILogger
    var logger = executionContext.GetLogger(nameof(StaticFunctions));
    logger.LogInformation("Started execution of function");

    var data = _mockDataService.CreateUser(userName);

    // Should use HttpResponseData as response
    var response = req.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync<UserDto>(data);

    return response;
}

The above method uses Constructor Injection to inject our dependency and later uses the same in the CreateUser method.

Summary

In the this post, we have a peak in the world of Isolated Functions and how to create them. We understood some of the differences between the in-process functions and the new out-of-process isolated functions. We also understood how we have more control over the start up process, which in-turns helps use to configure the function app with the desired services and middlewares. One of the key draw backs of the Isolated function at the moment is the lack of support of Durable Functions.

The complete list of differences is outlined here by Microsoft : Difference with .Net Library Functions.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s