Azure Functions offer two distinct hosting models;
Out of these, in-process is the default model that you will see used the majority of the time. In most use cases, this model is 'good enough', and does everything a developer needs it to do.
However, a couple of major downsides with the in in-process model are that;
You may want to upgrade to reduce any potential technical debt by always being on the newest stable .NET version, or you may be reliant on bug fixes, new language features (our use case was that a client needed a fix to large EF migrations that is available in .NET 8) or functionality provided by a certain middleware.
Migrating an in-process Azure Function to use the isolated worker model has a rather steep initial learning curve. Because of this, and to help ease the process for anyone doing it in the future, we have written this guide with a few tips and tricks we learnt along the way of doing it.
NOTE - This guide is a quite HTTP function-centric, but feel free to pick and choose what you need. There is a brief note at the end about function bindings for none-HTTP functions.
If you are migrating to the isolated worker model, with the purpose of updating the language the function will use, you need to;
There are some Nuget package updates to be made. Some of the core functionality exists in different packages with the isolated model.
NOTE - While updating the named packages below, it is likely worthwhile to take the time to update your other referenced packages to the newest available, and to ones targeting the .NET version you are upgrading to.
The in-process model doesn't need a Program.cs file as it runs in the same process as the Functions host. However, isolated does as the entry point into your program.
Add a Program.cs file to your Azure Function project. The minimum skeleton you will likely need is;
public class Program { static async Task Main(string[] args) { var host = new HostBuilder() .ConfigureFunctionsWorkerDefaults() .ConfigureOpenApi() .ConfigureServices(services => { // Add DI here (likely move // from Startup.cs) }) .Build(); await host.RunAsync(); } }
Counter to what was talked about with the Program.cs file, isolated worker model Azure Functions do not use a Startup.cs file.
Instead, the dependancy injection work that would previously have been done in there should move into the ConfigureServices method of Program.cs and the Startup.cs should then be deleted
Configuring EF has changed. Swap from;
services.AddDbContext<DataContext>( options => options.UseSqlServer( connectionString));
To the simpler;
services.AddSqlServer<DataContext>( connectionString)
As a rule of thumb, we should remove the following
using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http;
Add;
using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Extensions .OpenApi.Extensions; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Hosting;
Obviously remove any of the above that are not being used (VS/Rider tooling will show the unused usings), and add others as and where needed (again, lean on the tooling to help).
We need to set out project output type to be an executable (when in process it piggybacks of the host function as mentioned before, so this wasn't needed).
In the main .csproj file, add;
<PropertyGroup> <OutputType>Exe</OutputType> </PropertyGroup>
In your local.settings.json file, or wherever you are keeping your appsettings, set the following;
FUNCTIONS_WORKER_RUNTIME dotnet-isolated
FunctionName to Function on functions
(All the following to be added to the method rather then to any other property as they may have been previously)
Please see https://www.ais.com/creating-self-documenting-azure-functions-with-c-and-openapi-update/ for a comprehensive guide to setting these properties
UPDATE 2024-03-18 - The information in the following sections with regards to Request and Response changes, are correct when using the isolated worker built-in HTTP model.
However reddit user u/RiverRoll has steered me to the documention showing that you can actually continue using the 'HttpRequest', 'HttpResponse' and IActionResult methods, by doing the following to references;
Additionally swap to ConfigureFunctionsWebApplication from ConfigureFunctionsWorkerDefaults in Program.cs.
With these changes made you can swap the following two sections.
With the change to the isolated worked model, the way we recieve request data has changed.
The HttpRequest class is no longer used to recieve request data, instead we use the similar HttpRequestData. Update your function definitions to swap from one to the other.
Request.Query is now a NameValueCollection rather then a IQueryCollection. You interact with it in the same fashion however.
By the way, the namespace for NameValueCollection is System.Collection.Specialized rather then Microsoft.AspNetCore.Http for IQueryCollection
If you want to retrieve the body straight into JSON, previously you may have done the following;
request.ReadFromJsonAsync<T>()
This method no longer works, so instead switch to;
JsonSerializer.DeserializeAsync<T>(request.Body)
Likewise, with the change to the isolated worked model, the way we send response data has changed.
Anywhere you are interacting with an HttpResponse class, the equivilent is now HttpResponseData.
With the isolated worker model, you can't return an IActionResult (or more technically - you can but the response will not be what you expected).
Instead you will need to do, something along the lines of the following;
var response = request.CreateResponse( HttpStatusCode.OK); response.WriteString(JsonSerializer.Serialize( data, new JsonSerializerOptions { WriteIndented = true, ReferenceHandler = ReferenceHandler.IgnoreCycles })); response.Headers.Add("Content-Type", "application/json"); retun response;
We suggest encapsulating this in some type of helper method. I have one I have named 'OkResponse', so can change from old IActionResult types quickly.
With IActionResult / OkObjectResult, complex objects are serialised automatically. However with HttpResponseData, it isn't.
Because of this you will need to do something along the lines of the following to build up a string;
JsonSerializer.Serialize( data, new JsonSerializerOptions { WriteIndented = true, ReferenceHandler = ReferenceHandler.IgnoreCycles }));
Nothing is set by default when using HttpResponseData. So you will need to set the content type explicitly, with code like below;
response.Headers.Add("Content-Type", "application/json");
There are changes to how logging works. You can no longer recieve an additional ILogger parameter to your function.
Instead, an ILogger<T> will need to be injected into your function constructors.
To mock the http request and response data, the following classes should come in useful (thanks to Vincent Bitter from StackOverflow where this is adapted from).
public class MockHttpRequestData :HttpRequestData { public MockHttpRequestData( FunctionContext functionContext, Uri url, Stream body = null) : base(functionContext) { Url = url; Body = body ?? new MemoryStream(); } prviate Stream _Body = new MemoryStream(); public override Stream Body { get { return _Body; } } public void SetBody(Stream data) { _Body = data; } public override HttpHeadersCollection Headers { get; } = new(); public override IReadOnlyCollection Cookies { get; } public override Uri Url { get; } public override IEnumerable Identities { get; } public override string Method { get; } public NameValueCollection _Query = new(); public override NameValueCollection Query { get { return _Query; } } public void SetQuery( NameValueCollection data) { _Query = data; } public override HttpResponseData CreateResponse() { return new MockHttpRequestData( FunctionContext); } }
and;
public class MockHttpResponseData :HttpResponseData { public MockHttpResponseData( FunctionContext functionContext) : base(functionContext) { } public override HttpStatusCode StatusCode { get; set; } public override HttpHeadersCollection Headers { get; set; } = new(); public override Stream Body { get; set; } = new MemoryStream(); public override HttpCookies Cookies { get; } }
Change any DefaultHttpContext to use MockHttpRequestData.
Anywhere passing in httpContext.Request change to your httpRequestData instance.
From;
_httpContext = new DefaultHttpContext { Request = { Body = stream, ContentLength = stream.Length, ContentType = "application/json" } }
To;
var body = new MemoryStream( Encoding.ASCII.GetBytes("{}")); var context = new Mock<FunctionContext>(); _httpRequestData = new MockHttpRequestData( context.Object, new Uri("https://example.org"), body ); _httpRequestData.Headers.Add("content-length", stream.Length.ToString()); _httpRequestData.Headers.Add("content-type", "application/json"); _httpRequestData.SetBody(stream);
We now don't need any standalone Swagger methods. Its 'automagically' done for you if correct packages are included, and attributes added.
If your program writes any data to the local file system, you will not be able to write to the current directory. Instead you can write to the a temporary directory.
Change any references of Environment.CurrentDirectory to Path.GetTempPath()
Microsoft have a good page showing how to change the other bindings for none-http methods at https://learn.microsoft.com/en-us/azure/azure-functions/migrate-dotnet-to-isolated-model?tabs=net8