Exploring Neon as a Serverless Postgres Alternative for .NET Applications on Azure - Part 1 (Simple ASP.NET Core on App Service)

Neon, a serverless Postgres platform, was recently brought to my attention. It comes with the promise of some interesting features like scale-to-zero, on-demand autoscaling, point-in-time restore and time travel with up to 30 days retention, instant read replicas, and probably the most unique, branching (which allows you to quickly branch your schema and data to create isolated development, test, or other purpose environments).

That's a lot of stuff to play with, but before I jumped in, I wanted to see how Neon integrated with my tech stack of choice - .NET, Azure, and GitHub. I decided to start with something simple - an ASP.NET Core application hosted on Azure App Service. Since I like to build things from the ground up, my first step was infrastructure.

Deploying the Infrastructure

Neon has three deployment options that might be of interest to me:

  • Create a Neon project hosted in the AWS region
  • Create a Neon project hosted in the Azure region
  • Deploy a Neon organization as an Azure Native ISV Service

The first two options are fully managed by Neon, you grab the connection details from the Neon console and start hacking. But I wanted to explore the third option because it provides the tightest integration from an Azure perspective (including SSO and unified billing), and that's what the organizations I work with are usually looking for.

As a strong IaC advocate, I also didn't want to deploy Neon through the Azure Portal or CLI, I wanted to use Bicep. Thanks to the native Azure integration, the Neon organization comes with its own resource type - Neon.Postgres/organizations.

resource neonOrganization 'Neon.Postgres/organizations@2024-08-01-preview' = {
  name: 'neon-net-applications-on-azure'
  location: location
  properties: {
    companyDetails: { }
    marketplaceDetails: {
      subscriptionId: subscription().id
      offerDetails: {
        publisherId: 'neon1722366567200'
        offerId: 'neon_serverless_postgres_azure_prod'
        planId: 'neon_serverless_postgres_azure_prod_free'
        termUnit: 'P1M'
        termId: 'gmz7xq9ge3py'
      }
    }
    partnerOrganizationProperties: {
      organizationName: 'net-applications-on-azure'
    }
    userDetails: {
      upn: 'userId@domainName'
    }
  }
}

You may ask, how did I get the values for the properties? Well, they are not documented (yet, I'm assuming) and I had to resort to reverse engineering (inspecting the template of the manually deployed resource).

The next step is to create a project. Everything in Neon (branches, databases, etc.) lives inside a project. Neon projects don't have an Azure resource representation, so I had to change the tool. I could create a project through the UI, but since I still wanted to have repeatability (something I could later reuse in a GitHub Actions workflow later), I decided to use Neon CLI. I still had to visit the UI (thanks to SSO I could just click on a link available on the resource Overview blade) to get myself an API key.

export NEON_API_KEY=<neon_api_key>

neon projects create \
    --name simple-asp-net-core-on-app-service \
    --region-id azure-westus3 \
    --output json

The output includes connection details for the created database. I don't want to manage secrets manually if I don't have to, so I decided to quickly create a key vault and put them there.

The last missing elements of my intended infrastructure were a managed identity, a container registry, an app service plan, and an app service. Below is the final diagram.

Infrastructure for simple ASP.NET Core application on Azure App Service (managed identity, key vault, container registry, app service plan, and app service) and using Neon deployed as Azure Native ISV Service

Seeding the Database

With all the infrastructure in place, I could start building my ASP.NET Core application. I wanted something simple, something that just displayed data, as my goal here was to see if Neon could be a drop-in replacement for Posgres from a code perspective. But to display data, you have to have data. So I needed to seed the database. I decided to take the simplest approach I could think of - using the built-in seeding capabilities of the Entity Framework Core. I had two options to choose from: UseSeeding/UseAsyncSeeding method or model managed data. I chose the latter and I quickly created a database context with two sets and a bunch of entities to add.

public class StarWarsDbContext : DbContext
{
    public DbSet<Character> Characters { get; private set; }

    public DbSet<Planet> Planets { get; private set; }

    public StarWarsDbContext(DbContextOptions<StarWarsDbContext> options)
    : base(options)
    { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Planet>(builder =>
        {
            builder.Property(x => x.Name).IsRequired();
            builder.HasData(
                new Planet { Id = 1, Name = "Tatooine", ... },
                ...
            );
        });

        modelBuilder.Entity<Character>(builder =>
        {
            builder.Property(x => x.Name).IsRequired();
            builder.HasData(
                new Character { Id = 1, Name = "Luke Skywalker", ... },
                ...
            );
        });
    }
}

Now I needed to register this database context as a service in my application. For the provider I used Npgsql.EntityFrameworkCore.PostgreSQLin the spirit of the drop-in replacement approach. With the connection string in the key vault and a managed identity in place to authenticate against that key vault, I could use DefaultAzureCredential and SecretClient to configure the provider and restrict my application settings to the key vault URI (yes, I choose this option even in demos).

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<StarWarsDbContext>(options =>
{
    var keyVaultSecrettClient = new SecretClient(
        new Uri(builder.Configuration["KEY_VAULT_URI"]),
        new DefaultAzureCredential()
    );
    options.UseNpgsql(keyVaultSecrettClient.GetSecret("neon-connection-string").Value.Value);
});

...

The last thing to do here was to trigger the model creation code. This only needs to happen once, and as this is a demo, it can be ugly. I used an old trick of getting the database context as part of the startup and calling EnsureCreated (please don't do this in serious applications).

...

var app = builder.Build();

using (var serviceScope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
    var context = serviceScope.ServiceProvider.GetRequiredService<StarWarsDbContext>();
    context.Database.EnsureCreated();
}

...

All the pieces are now in place. It's time to wrap things up.

Completing and Deploying the Application

For the application to be complete, it needed some UI. Since I didn't have an idea for anything special, I did something I used to do a lot in the past - a jqGrid based table that was quickly set up with the help of my old project Lib.AspNetCore.Mvc.JqGrid (I've basically copied some stuff from its demo).

As you've probably guessed from the infrastructure, my intention from the start was to deploy this application as a container. So my last step was to add a docker file, build, push, and voilà. I was able to navigate to the application, browse and sort the data.

Everything worked as expected and I consider my first experiment with Neon a success 😊.

Thoughts

They say to never publish part one if you don't have part two ready. I don't have part two ready, but I really think I'll write one, I just don't know when 😉. That's because Neon really got me interested.

In this post I wanted to check out the basics and set the stage for digging deeper. While writing it, I've also created a repository where you can find ready-to-deploy infrastructure and application. I've created GitHub Actions workflows for deploying a Neon organization, creating a Neon project, and deploying the solution itself. All you need to do to play with it is clone and provide credentials 🙂.