Guides.NET

ASP.NET Core with PostgreSQL and EF Core

Create a .NET API project with ASP.NET Core Minimal APIs, PostgreSQL, EF Core, ASP.NET Identity, SignalR, Serilog, and xUnit using Better Fullstack.

Updated August 16, 2026

aspnet-corepostgresef-coredotnet

Use this stack when you want a .NET HTTP API with relational persistence, structured logging, and a test project wired up from the first commit.

npm create better-fullstack@latest my-dotnet-api -- \  --ecosystem dotnet \  --dotnet-web-framework aspnet-minimal \  --dotnet-orm ef-core \  --dotnet-auth aspnet-identity \  --dotnet-api minimal-api \  --dotnet-testing xunit \  --dotnet-realtime signalr \  --dotnet-observability serilog \  --dotnet-validation fluentvalidation \  --dotnet-deploy docker \  --database postgres

What this creates

  • An ASP.NET Core application using Minimal APIs.
  • PostgreSQL through the Npgsql EF Core provider.
  • ASP.NET Core Identity registered against the application DbContext.
  • A SignalR hub for server-pushed updates.
  • Serilog console logging.
  • FluentValidation rules and an xUnit test project.

Generated shape

The scaffold is a single ASP.NET Core project plus a sibling test project. The project name and namespace come from the directory name with every character outside A-Za-z0-9_ replaced by an underscore, so my-dotnet-api produces my_dotnet_api.

txt
my-dotnet-api/
├── my_dotnet_api.csproj
├── my_dotnet_api.http
├── Program.cs
├── appsettings.json
├── appsettings.Development.json
├── Dockerfile
├── Validators/
│   └── SampleInputValidator.cs
└── my_dotnet_api.Tests/
    ├── my_dotnet_api.Tests.csproj
    └── ApiTests.cs

Everything lives in Program.cs: endpoint mapping, service registration, the TodoItem model, and AppDbContext. Split it into folders once the API grows past the generated sample.

Prerequisites

  • .NET SDK 10 or newer.
  • A PostgreSQL instance reachable from the machine running the app.
  • Docker if you keep the generated Dockerfile in your deploy path.

Run and verify

bash
cd my-dotnet-api
dotnet restore
dotnet run

The generated appsettings.json ships a local development connection string. Override it for anything else through ConnectionStrings__DefaultConnection rather than editing the committed file.

bash
export ConnectionStrings__DefaultConnection="Host=localhost;Port=5432;Database=my_dotnet_api;Username=postgres;Password=postgres"

Open the URL printed by ASP.NET Core, then use the generated .http file to exercise the endpoints.

No solution file is generated, and the test SDK lives only in the test project, so a bare dotnet test from the project root builds the API and runs nothing. Name the test project explicitly:

bash
dotnet build
dotnet test my_dotnet_api.Tests/my_dotnet_api.Tests.csproj

Create the schema before calling the data endpoints

The generated Program.cs registers AppDbContext but never calls Migrate() or EnsureCreated(), so /api/todos hits an empty database until you create the schema yourself. Microsoft.EntityFrameworkCore.Design is already referenced, so the EF tooling works without adding packages:

bash
dotnet tool install --global dotnet-ef
dotnet ef migrations add InitialCreate
dotnet ef database update

Run migrations against production before routing traffic. Treat this as the first change you make to a fresh scaffold.

Representative snippets

The persisted model and context are plain EF Core.

csharp
public sealed class TodoItem
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public bool Complete { get; set; }
}

public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
    public DbSet<TodoItem> Todos => Set<TodoItem>();
}

Minimal API endpoints take the context by injection.

csharp
app.MapGet("/api/todos", async (AppDbContext db) =>
{
    return Results.Ok(await db.Todos.OrderBy(todo => todo.Id).ToListAsync());
});

app.MapPost("/api/todos", async (TodoInput input, AppDbContext db) =>
{
    var todo = new TodoItem { Title = input.Title, Complete = false };
    db.Todos.Add(todo);
    await db.SaveChangesAsync();
    return Results.Created($"/api/todos/{todo.Id}", todo);
});

The xUnit project boots the real application through WebApplicationFactory, so tests exercise the actual pipeline rather than a mock.

csharp
public sealed class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task RootEndpointReturnsOk()
    {
        using var client = _factory.CreateClient();
        using var response = await client.GetAsync("/");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}

When to choose it

Choose .NET when you want a compiled, statically typed backend with first-party database, identity, logging, and realtime libraries maintained on one release cadence.

Choose Minimal APIs over --dotnet-web-framework aspnet-mvc when the service is API-only. Choose MVC when you want controllers, model binding conventions, and view support.

Compatibility notes

  • --dotnet-orm ef-core selects the provider from --database: Npgsql for postgres, Microsoft.EntityFrameworkCore.Sqlite for sqlite.

  • --dotnet-auth aspnet-identity calls AddIdentityCore<IdentityUser>().AddEntityFrameworkStores<AppDbContext>(), but the generated AppDbContext derives from DbContext, not IdentityDbContext. Change the base class and generate a migration before resolving the user store.

  • --dotnet-validation fluentvalidation generates SampleInputValidator but does not register it, and the csproj references only the core FluentValidation package. Assembly scanning needs the DI extensions package first:

    bash
    dotnet add package FluentValidation.DependencyInjectionExtensions

    Then builder.Services.AddValidatorsFromAssemblyContaining<SampleInputValidator>() resolves. Without that package the call does not compile, whatever the generated file's own comment suggests.

  • Deployment targets for .NET come from --dotnet-deploy, not the TypeScript --web-deploy and --server-deploy flags.

Deployment notes

The generated Dockerfile builds and publishes the app. The scaffold reads PORT and binds all interfaces when ASPNETCORE_URLS is unset, so container platforms that inject a port work without changes.

Before deploying, set the production connection string, apply migrations, and confirm Serilog's sink configuration matches where you collect logs.

Troubleshooting

SymptomCheck
relation "Todos" does not existCreate and apply an EF migration; nothing creates the schema at startup.
App binds an unexpected portPORT is only honored when ASPNETCORE_URLS is unset.
Identity user store fails to resolveAppDbContext needs to derive from IdentityDbContext and have a migration.
Validators never runAdd FluentValidation.DependencyInjectionExtensions, then register them with AddValidatorsFromAssemblyContaining.
Connection string changes are ignoredReal environment variables win over the local .env the app loads at startup.

Tradeoffs

A single Program.cs keeps the scaffold readable but stops scaling once you add more than a handful of endpoints. Plan the split into endpoint groups and a separate data project early.

Next steps

GitHub Sponsors