Skip to main content
DamienG
Code editor beside a diagram of connected services and databases

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:

  1. Return types are IActionResult/ObjectResult objects rather than extension methods against HttpRequestMessage (easier to mock/create custom ones)
  2. Input is the HttpRequest object rather than HttpRequestMessage (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.xv4 isolated worker
[FunctionName] attribute[Function] attribute
TraceWriterILogger<T> (via dependency injection)
HttpRequestMessageHttpRequest (with ASP.NET Core integration) or HttpRequestData
HttpResponseMessageIActionResult (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

  1. Information is only used to show your comment. See my Privacy Policy.