
I've been messing around in the .NET ecosystem again, jumping back in with Azure Functions (similar to AWS Lambda) to get my blog onto 99% static hosting. I immediately ran into the API changes between v1 and v2 (in beta at the time).
Updated October 2026: This post is now history. Version 4.x is the only supported Azure Functions runtime—support for 1.x ended on 14 September 2026 and 2.x and 3.x went out of support at the end of 2022. The in-process model the v2 sample uses also loses support on 10 November 2026 in favour of the isolated worker model. I've left the original v1/v2 comparison below for anyone migrating old code and added a v4 isolated worker version of the sample at the end.
These changes are because v1 was based around .NET 4.6 using WebAPI 2 while v2 is based on ASP.NET Core which uses MVC 6. There are some guides around conversion, but none in the context of Azure Functions.
I'll illustrate with a PageViewCount sample that uses Table Storage to retrieve and update a simple page count.
v1 (.NET 4.61 / WebAPI 2)
[FunctionName("PageView")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")]HttpRequestMessage req, TraceWriter log) {
var page = req.MessageUri.ParseQueryString()["page"];
if (String.IsNullOrEmpty(page))
return req.CreateErrorResponse(HttpStatusCode.BadRequest, "'page' parameter missing.");
var table = Helpers.GetTableReference("PageViewCounts");
var pageView = await table.RetrieveAsync<PageViewCount>("damieng.com", page)
?? new PageViewCount(page) { ViewCount = 0 };
var operation = pageView.ViewCount == 0
? TableOperation.Insert(pageView)
: TableOperation.Replace(pageView);
pageView.ViewCount++;
await table.ExecuteAsync(operation);
return req.CreateResponse(HttpStatusCode.OK, new { viewCount = pageView.ViewCount });
}
v2 (ASP.NET Core / MVC 6)
[FunctionName("PageView")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")]HttpRequest req, TraceWriter log) {
var page = req.Query["page"];
if (String.IsNullOrEmpty(page))
return new BadRequestObjectResult("'page' parameter missing.");
var table = Helpers.GetTableReference("PageViewCounts");
var pageView = await table.RetrieveAsync<PageViewCount>("damieng.com", page)
?? new PageViewCount(page) { ViewCount = 0 };
var operation = pageView.ViewCount == 0
? TableOperation.Insert(pageView)
: TableOperation.Replace(pageView);
pageView.ViewCount++;
await table.ExecuteAsync(operation);
return new OkObjectResult(new { viewCount = pageView.ViewCount });
}
Differences
The main differences are that:
- Return types are
IActionResult/ObjectResultobjects rather than extension methods againstHttpRequestMessage(easier to mock/create custom ones) - Input is the
HttpRequestobject rather thanHttpRequestMessage(easier to get query parameters)
The error Can not create abstract class when executing your function means you are trying to use the wrong tech for that environment.
Helpers
Both classes above utilise a small helper class to take care of Table Storage which doesn't have the nicest to use API. A data-context like wrapper that ensures the right types go to the right table might be an even better option.
static class Helpers {
public static CloudStorageAccount GetCloudStorageAccount() {
var connection = ConfigurationManager.AppSettings["DamienGTableStorage"];
return connection == null ? CloudStorageAccount.DevelopmentStorageAccount : CloudStorageAccount.Parse(connection);
}
public static CloudTable GetTableReference(string name) {
return GetCloudStorageAccount().CreateCloudTableClient().GetTableReference(name);
}
public static async Task<T> RetrieveAsync<T>(this CloudTable cloudTable, string partitionKey, string rowKey)
where T:TableEntity {
var tableResult = await cloudTable.ExecuteAsync(TableOperation.Retrieve<T>(partitionKey, rowKey));
return (T)tableResult.Result;
}
}
To compile
If you want to compile this, or Google led you here looking for code to do a simple page counter, here's the missing TableEntity class;
public class PageViewCount : TableEntity
{
public PageViewCount(string pageName)
{
PartitionKey = "damieng.com";
RowKey = pageName;
}
public PageViewCount() { }
public int ViewCount { get; set; }
}
Note that the CloudStorageAccount/CloudTable types used here come from the old WindowsAzure.Storage and Microsoft.Azure.Cosmos.Table packages which are both deprecated on NuGet. New code should use Azure.Data.Tables as in the v4 sample below.
Today: v4 and the isolated worker model
If you're here in 2026 or later you want version 4.x of the runtime using the isolated worker model, where your functions run in their own .NET process rather than inside the Functions host. Microsoft's v1 to v4 migration guide covers the details but the key renames for this sample are:
| Version 1.x | v4 isolated worker |
|---|---|
[FunctionName] attribute | [Function] attribute |
TraceWriter | ILogger<T> (via dependency injection) |
HttpRequestMessage | HttpRequest (with ASP.NET Core integration) or HttpRequestData |
HttpResponseMessage | IActionResult (with ASP.NET Core integration) or HttpResponseData |
The good news is that with ASP.NET Core integration (the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore package) you keep the HttpRequest and IActionResult style from the v2 sample. If you're stuck on .NET Framework 4.8 the isolated worker still runs it, but you'll use HttpRequestData/HttpResponseData instead.
Your Program.cs sets up the host and registers the table client:
using Azure.Data.Tables;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();
builder.Services.AddSingleton(new TableServiceClient(builder.Configuration["DamienGTableStorage"]));
builder.Build().Run();
And the function itself becomes a regular class with its dependencies injected, using the generic TableEntity from Azure.Data.Tables so we no longer need our own entity class or helpers:
using Azure.Data.Tables;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
public class PageView(TableServiceClient tableService, ILogger<PageView> logger)
{
[Function("PageView")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
{
string? page = req.Query["page"];
if (string.IsNullOrEmpty(page))
return new BadRequestObjectResult("'page' parameter missing.");
var table = tableService.GetTableClient("PageViewCounts");
var existing = await table.GetEntityIfExistsAsync<TableEntity>("damieng.com", page);
var pageView = existing.HasValue ? existing.Value! : new TableEntity("damieng.com", page);
var viewCount = (pageView.GetInt32("ViewCount") ?? 0) + 1;
pageView["ViewCount"] = viewCount;
await table.UpsertEntityAsync(pageView, TableUpdateMode.Replace);
logger.LogInformation("Page {Page} now has {ViewCount} views", page, viewCount);
return new OkObjectResult(new { viewCount });
}
}
[)amien
0 responses