Skip to main content

Migrating my Azure Function App project from .NET 6 to .NET 8

ยท 8 min read
Mongezi Kasha
Full-stack Software Engineer

On November 12, 2024, Microsoft announced the end of support for .NET 6. I'm pretty sure they announced it earlier and I just ignored it. However, when the deadline came I decided to go on a journey to migrate from .NET 6 to .NET 8.

The question you might be asking is, "Why 8? Why not 7, 9, or 10?" I'm the one that asked that question but let's pretend you did. ๐Ÿค”

Why .NET 8? The Million Dollar Question ๐Ÿ’ฐโ€‹

According to my research, .NET 8 has long-term support (LTS) which will be supported for about the next 3 years (including security patches and reliability fixes). .NET 7 is already out of support as well as of May 14, 2024. .NET 9 is still in preview stages and will also be end of support sooner than .NET 8. .NET 10 is still to be officially released this year in November.

With all the above brief information, I think we can all agree .NET 8 is the "latest" tested stable version of .NET currently available and there's also a huge community of people running on .NET 8 that can offer support.

Yeah, because we learn from each other... ๐Ÿค

.NET 6 to 8 Migration

My Function App: A Brief Overview ๐Ÿ“ฑโ€‹

I built a web app for my church using Blazor front-end, Function App as a proxy, and SQL Server for data. The app captures people's details - basically an onboarding app. But for this topic, I will focus on a specific functionality. There's a feature for document generation using a C# library called QuestPDF and downloading a document from the UI and storing the document as Base64 in the SQL database.

I have endpoints for generating, storing, and persisting the documents.

Below is some of the code I have on my project and the changes I had to make.

The Code Journey: Before and After ๐Ÿ”„โ€‹

.NET 6 Implementationโ€‹

๐Ÿ”ง MyFunctionApp.csproj

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
<PackageReference Include="QuestPDF" Version="2024.3.0" />
</ItemGroup>

</Project>

๐Ÿ“„ Models/DocumentRequest.cs

namespace MyFunctionApp.Models
{
public class DocumentRequest
{
public string Title { get; set; }
public string Content { get; set; }
}
}

๐Ÿ“„ Services/DocumentService.cs

using Microsoft.Data.SqlClient;

namespace MyFunctionApp.Services
{
public class DocumentService
{
private readonly string _connectionString;

public DocumentService()
{
_connectionString = Environment.GetEnvironmentVariable("SqlConnectionString");
}

public async Task SaveDocumentAsync(string fileName, byte[] fileBytes)
{
using var conn = new SqlConnection(_connectionString);
await conn.OpenAsync();

var cmd = new SqlCommand("INSERT INTO Documents (FileName, Content) VALUES (@FileName, @Content)", conn);
cmd.Parameters.AddWithValue("@FileName", fileName);
cmd.Parameters.AddWithValue("@Content", fileBytes);
await cmd.ExecuteNonQueryAsync();
}
}
}

๐Ÿ“„ Functions/GenerateDocumentFunction.cs

using System.IO;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using MyFunctionApp.Models;
using MyFunctionApp.Services;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using System.Text.Json;

namespace MyFunctionApp.Functions
{
public static class GenerateDocumentFunction
{
[FunctionName("GenerateDocument")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
ILogger log)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var data = JsonSerializer.Deserialize<DocumentRequest>(requestBody);

if (data == null)
return new BadRequestObjectResult("Invalid input.");

byte[] pdfBytes = GeneratePdf(data.Title, data.Content);
string fileName = $"{data.Title}_{DateTime.UtcNow:yyyyMMddHHmmss}.pdf";

var documentService = new DocumentService();
await documentService.SaveDocumentAsync(fileName, pdfBytes);

return new OkObjectResult("PDF generated and saved to DB.");
}

private static byte[] GeneratePdf(string title, string content)
{
return Document.Create(container =>
{
container.Page(page =>
{
page.Margin(40);
page.Header().Text(title).FontSize(24).Bold().AlignCenter();
page.Content().Text(content).FontSize(14).LineHeight(1.5f);
page.Footer().AlignCenter().Text(x =>
{
x.CurrentPageNumber();
x.Span(" / ");
x.TotalPages();
});
});
}).GeneratePdf();
}
}
}

โœ… local.settings.json

{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"SqlConnectionString": "Server=localhost;Database=MyDb;User Id=sa;Password=YourPassword123;"
}
}

.NET 8 Implementationโ€‹

๐Ÿ”ง MyFunctionApp.csproj

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.18.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.18.0" OutputItemType="Analyzer" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
<PackageReference Include="QuestPDF" Version="2024.3.0" />
</ItemGroup>

</Project>

๐Ÿ“„ Models/DocumentRequest.cs

namespace FunctionApp.Models;

public class DocumentRequest
{
public string Title { get; set; }
public string Content { get; set; }
}

๐Ÿ“„ Services/DocumentService.cs

using Microsoft.Data.SqlClient;

namespace FunctionApp.Services;

public class DocumentService
{
private readonly string _connectionString;

public DocumentService()
{
_connectionString = Environment.GetEnvironmentVariable("SqlConnectionString");
}

public async Task SaveDocumentAsync(string fileName, byte[] content)
{
using var conn = new SqlConnection(_connectionString);
await conn.OpenAsync();

var cmd = new SqlCommand("INSERT INTO Documents (FileName, Content) VALUES (@FileName, @Content)", conn);
cmd.Parameters.AddWithValue("@FileName", fileName);
cmd.Parameters.AddWithValue("@Content", content);
await cmd.ExecuteNonQueryAsync();
}
}

๐Ÿ“„ Functions/GenerateDocumentFunction.cs

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using FunctionApp.Models;
using FunctionApp.Services;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using System.Net;
using System.Text.Json;

namespace FunctionApp.Functions;

public class GenerateDocumentFunction
{
private readonly ILogger _logger;

public GenerateDocumentFunction(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<GenerateDocumentFunction>();
}

[Function("GenerateDocument")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
var data = await JsonSerializer.DeserializeAsync<DocumentRequest>(req.Body);

if (data == null || string.IsNullOrWhiteSpace(data.Title))
{
var badResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badResponse.WriteStringAsync("Invalid input.");
return badResponse;
}

byte[] pdfBytes = GeneratePdf(data.Title, data.Content);
string fileName = $"{data.Title}_{DateTime.UtcNow:yyyyMMddHHmmss}.pdf";

var documentService = new DocumentService();
await documentService.SaveDocumentAsync(fileName, pdfBytes);

var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteStringAsync("PDF generated and saved to DB.");
return response;
}

private static byte[] GeneratePdf(string title, string content)
{
return Document.Create(container =>
{
container.Page(page =>
{
page.Margin(40);
page.Header().Text(title).FontSize(24).Bold().AlignCenter();
page.Content().Text(content).FontSize(14).LineHeight(1.5f);
page.Footer().AlignCenter().Text(x =>
{
x.CurrentPageNumber();
x.Span(" / ");
x.TotalPages();
});
});
}).GeneratePdf();
}
}

โœ… local.settings.json

{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"SqlConnectionString": "Server=localhost;Database=MyDb;User Id=sa;Password=YourPassword123;"
}
}

๐Ÿ“„ Program.cs (New in .NET 8)

using Microsoft.Extensions.Hosting;
using Microsoft.Azure.Functions.Worker;

var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.Build();

host.Run();

Breaking Down the Changes ๐Ÿง โ€‹

From the above, we might have taken note of some code changes that had to happen. Let me just take you through the changes and why:

1. Runtime Model Changeโ€‹

.NET 6 (In-Process).NET 8 (Isolated Worker)
Runs inside the same process as the Azure Functions hostRuns in a separate process (decoupled from host)
Uses Microsoft.NET.Sdk.FunctionsUses Microsoft.Azure.Functions.Worker.Sdk
Accesses HttpRequest / HttpResponse directlyUses HttpRequestData / HttpResponseData wrappers

The reason for the above is .NET 8 moved to the isolated worker model as default to enable better separation of concerns, version independence, and more control over DI and startup logic. I have explained what dependency injection is in my previous post - you can go ahead and read about it if you want. Here is the link: Understanding Dependency Injection in C#.

2. Project File (.csproj) Differencesโ€‹

There are obviously some differences in the csproj file between .NET 6 and 8. Here's why:

  • Isolated model uses Worker SDK instead of the Functions SDK
  • Adds Microsoft.Azure.Functions.Worker and explicitly references the HTTP extension
  • Output is an executable, hence OutputType is set to Exe

3. Startup & Dependency Injectionโ€‹

.NET 6.NET 8 (Isolated)
DI done via Startup.cs and IFunctionsHostBuilderDI handled in Program.cs using Host.CreateDefaultBuilder
Uses builder.Services.AddXyz() in ConfigureUses builder.ConfigureFunctionsWorkerDefaults() with Services.AddXyz()

The above is because .NET 8 follows a modern generic host pattern, same as ASP.NET Core.

4. Function Bindings & Method Signaturesโ€‹

Aspect.NET 6.NET 8 (Isolated)
Attribute[FunctionName("Xyz")][Function("Xyz")]
Request TypeHttpRequest from Microsoft.AspNetCore.HttpHttpRequestData from Microsoft.Azure.Functions.Worker.Http
Response TypeIActionResult / HttpResponseMessageHttpResponseData
Body Deserializationawait req.ReadFromJsonAsync<T>()JsonSerializer.DeserializeAsync<T>(req.Body)

Why: In isolated model, you don't have access to HttpRequest, you use HttpRequestData instead. Deserialization is manual unless you build abstractions.

5. Loggingโ€‹

.NET 6.NET 8 (Isolated)
Uses ILogger<T> injected into functionUses ILogger<T> via constructor injection or DI
Logging behavior tied closely to host runtimeMore flexible and can integrate with any logging framework (Isolation)

6. PDF Generation Flowโ€‹

On this one, the logic is pretty much the same, but I just wanted to highlight how I access the request and respond back to the client.

7. Output Type & Entry Pointโ€‹

.NET 6.NET 8
No Program.cs โ€” Azure handles entryYou must define Program.cs with Host.CreateDefaultBuilder()

Summary of Major Code Differencesโ€‹

Category.NET 6 (In-Process).NET 8 (Isolated Worker)Why Change?
SDKFunctions SDKWorker SDKIsolated = decoupled runtime
Runtime BindingASP.NET HttpRequestHttpRequestDataNew pipeline
DIStartup.csProgram.csGeneric Host
LoggingBuilt into Functions runtimeCustomizable, injectedMore flexible
Response TypeIActionResultHttpResponseDataStreamlined & customizable
PDF HandlingSameSameLogic unchanged
SQL AccessSame via SqlConnectionSameReusable logic
Function Attribute[FunctionName][Function]SDK syntax

Conclusion ๐ŸŽ‰โ€‹

Just like that, we have upgraded our function to .NET 8! The migration process wasn't too painful, but it required understanding the shift from in-process to isolated worker model. The benefits include better separation of concerns, improved testability, and future-proofing with LTS support.

If you're still running on .NET 6, I highly recommend making the jump to .NET 8. Your future self will thank you! ๐Ÿ˜Š

Please subscribe to my YouTube channel and watch me write terrible code - you'll feel better about your coding skills! YouTube Channel

References ๐Ÿ“šโ€‹