We can run our C# Azure Functions in an isolated process, decoupling the version of .NET that we use in our functions from the runtime version.⚡
Before this, we would have to develop Functions that had a class library and host that were tightly integrated with each other. That meant that we had to run our .NET in-process on the same version as the Azure Functions Runtime (.NET Core 3.x on Azure Functions Runtime v3). With out-of-process Functions, we can use .NET 5 with v3 of the Azure Functions runtime.
With the ability to run our Functions out-of-process, we can benefit in the following ways.
To create a Function that runs out-of-process from the Functions runtime, we'll need the following:
Once you have those installed, create a new Azure Function project in Visual Studio. When you create your project, make sure that you select .NET 5 (Isolated) for your project, like so:
For this demo, I'm going to create a simple function that triggers on a HTTP POST request and inserts an item into an Azure Cosmos DB container. Not exactly changing the world I know, but the purpose here is to show you how Isolated Functions work compared to Azure Functions built as a C# Class Library Function.
Essentially a .NET isolated function project is Console App that targets .NET 5.0. Your solution explorer should look something like this:
I've added a couple of files here, but these files get generated for you:
Through the Program.cs file, we have access to the start-up of our Function application. Instead of having to create a separate Startup class to do this, we now have direct access to the host instance, enabling us to set any configurations and dependencies on the host directly.
Here's an example:
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.IO;
namespace DemoIsolatedFunction
{
public class Program
{
public static void Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureAppConfiguration(config => config
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("local.settings.json")
.AddEnvironmentVariables())
.ConfigureServices(services =>
{
services.AddSingleton(sp =>
{
IConfiguration configuration = sp.GetService<IConfiguration>();
return new CosmosClient(configuration["CosmosDBConnectionString"]);
});
})
.Build();
host.Run();
}
}
}
Here, we are creating our host instance using a new HostBuilder object that will return an IHost instance that runs asynchronously to start our Function.
The ConfigureFunctionsWorkerDefaults method is used to add the settings required to run our Function app out-of-process. This does a couple of things, such as providing integration with Azure Functions logging and providing default gRPC support.
The ConfigureAppConfiguration method is being used here to add the configuration we need for our Function App. Here, I'm using it to use my local.settings.json file for local debugging.
The ConfigureServices method allows us to inject the services we need in our application. Here, I'm using it to inject a Singleton instance of my Cosmos Client.
We're now ready to start writing our Function code. Here, I'm simply injecting the services I need in my function and, on a POST request, inserting a Todo item into my Cosmos DB container:
using DemoIsolatedFunction.Models;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
namespace DemoIsolatedFunction.Functions
{
public class InsertTodo
{
private readonly IConfiguration _configuration;
private readonly CosmosClient _cosmosClient;
private readonly Container _todoContainer;
public InsertTodo(
IConfiguration configuration,
CosmosClient cosmosClient)
{
_configuration = configuration;
_cosmosClient = cosmosClient;
_todoContainer = _cosmosClient.GetContainer(_configuration["DatabaseName"], _configuration["ContainerName"]);
}
[Function("InsertTodo")]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "Todo")] HttpRequestData req,
FunctionContext executionContext)
{
HttpResponseData response;
var logger = executionContext.GetLogger("InsertTodo");
logger.LogInformation("C# HTTP trigger function processed a request.");
try
{
var request = await new StreamReader(req.Body).ReadToEndAsync();
var todo = JsonConvert.DeserializeObject<TodoItem>(request);
todo.Id = Guid.NewGuid().ToString();
await _todoContainer.CreateItemAsync(
todo,
new PartitionKey(todo.Id));
response = req.CreateResponse(HttpStatusCode.OK);
}
catch (Exception ex)
{
logger.LogError($"Exception thrown: {ex.Message}");
response = req.CreateResponse(HttpStatusCode.InternalServerError);
}
return response;
}
}
}
This function uses an HTTP Trigger to write a record to Cosmos DB. HTTP Triggers in isolated functions are different from older versions of the runtime as we must use HttpRequestData and HttpResponseData to access the request and response data.
In out-of-process functions, we don't have access to the original HTTP request and response objects. What happens in out-of-process functions is that the incoming HTTP Request Message gets translated to a HttpRequestData object. From here, the data is provided from the request.
Here's our post request:
This request provides data to our Body attribute in the HttpRequestData object.
We can write to logs in .NET isolated functions by using an ILogger instance. Isolated functions pass through a FunctionContext object that provides information about a function execution. Here, we can call the GetLogger() method passing in our function name like so:
var logger = executionContext.GetLogger("InsertTodo");
logger.LogInformation("C# HTTP trigger function processed a request.");
There's still a bit of work to do on .NET Isolated Functions. With .NET 6 dropping sometime in November, that version will probably be the LTS version, and you can start working with .NET 6 with Azure Functions v4 (It is in early preview, so expect bugs).
Personally, I'm super excited about .NET Isolated Functions! 🙌 With the increase in .NET version cadence, having the .NET version decoupled from the Azure Functions runtime version will provide .NET devs 👩💻👨💻 far more flexibility when it comes to using the latest features in .NET, rather than being constrained by limitations imposed by the runtime.
If you want to read more about how the .NET isolated process works, check out this article.
If you prefer to get your hands dirty with the code, follow this tutorial.
Hopefully, you found this article useful! As always, if you have any questions, feel free to comment below or ask me on Twitter!
Happy Coding! 💻👨💻👩💻
Previously published on dev.to.