This is more of a reference than a how to guide. It goes through the steps of setting up a new ASP.NET Core API with a database, but there's not much information on why you would do things a certain way.
Setup
dotnet new web -o MyApp
If you want controller's you can add them in the endpoints section.
Database and Models
Models
directory with a model class for your resource.For example, if you were creating a Todo
list application, you might name your model file Todo.cs
and it would look like this:
// /Models/Todo.cs
namespace MyApp.Models;
public class Todo
{
public int Id { get; set; }
public string Title { get; set; }
public bool Completed { get; set; }
public DateTime CreatedAt { get; set; }
}
DatabaseContext.cs
file in your Models
directory to manage your database
connection.using Microsoft.EntityFrameworkCore;
namespace MyApp.Models;
public class DatabaseContext : DbContext
{
public DatabaseContext(DbContextOptions<DatabaseContext> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Todo>()
.Property(e => e.CreatedAt)
.HasDefaultValueSql("now()");
}
}
Npgsql.EntityFrameworkCore.PostgreSQL
package:dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
Program.cs
file:using Microsoft.EntityFrameworkCore;
using MyApp.Models;
//...
var connectionString = "Host=localhost;Database=yourDBName;Username=yourUsername;Password=yourPassword";
services.AddDbContext<DatabaseContext>(
opt =>
{
opt.UseNpgsql(connectionString);
if (builder.Environment.IsDevelopment())
{
opt
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
}
);
Optionally, you could also add the Diagnostics
package to get some nice error pages when things go wrong. I haven't seen this work yet, but the docs suggest using it so 🤷♀️
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
if (builder.Environment.IsDevelopment())
{
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
}
Environment Variables
This isn't really document well anywhere and seems to go against how the .NET community likes to store configuration values. However, here's my argument for using normal environment variables:
- In production, we're supposed to use a secure secret manager like AWS Secrets Manager or Azure Key Vault. These services are designed to store secrets and are much more secure than environment variables, or configuration files.
- In development, we're not really concerned with security as much, so it doesn't really matter how we store our secrets as long as they are "fake" secrets. Like the root password to my local MySQL database isn't really sensitive information in any way.
- Environment variables are commonly used, so most devs are familiar with them. If we're sharing code with other devs, and developing on multiple machines, it's easier to just use environment variables for secrets and configuration to avoid updating source control tracked files for each user's confiruations.
dotnet add package DotNetEnv
.env
file in the root of your project and add your environment
variables:DATABASE_CONNECTION_STRING="your database connection string"
DotNetEnv.Env.Load();
var connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING");
Migrations
dotnet-ef
tool installed on your machine:dotnet tool install --global dotnet-ef
Microsoft.EntityFrameworkCore.Design
package:dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet ef migrations add InitialCreate
dotnet ef database update
CRUD Endpoints
Once you have your database setup, you can start creating CRUD endpionts. This is the fun part!
app.MapGet("/api/todoitems", async (DatabaseContext db) =>
await db.Todos.ToListAsync());
app.MapGet("/api/todoitems/complete", async (DatabaseContext db) =>
await db.Todos.Where(t => t.Completed).ToListAsync());
app.MapGet("/api/todoitems/{id}", async (int id, DatabaseContext db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/api/todoitems", async (Todo todo, DatabaseContext db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/api/todoitems/{todo.Id}", todo);
});
app.MapPut("/api/todoitems/{id}", async (int id, Todo inputTodo, DatabaseContext db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.Completed = inputTodo.Completed;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/api/todoitems/{id}", async (int id, DatabaseContext db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.Ok(todo);
}
return Results.NotFound();
});
You can test the API using a tool like Postman, but I suggest adding swagger.
Swagger
ASP.NET APIs provide built-in support for generating information about endpoints via the Microsoft.AspNetCore.OpenApi
package.
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/openapi?view=aspnetcore-7.0
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet add package Swashbuckle.AspNetCore
using Microsoft.AspNetCore.OpenApi;
// ...
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
Now you can test your app and see the swagger UI at http://localhost:5000/swagger/
React
This react app will be deployed as part of your .NET app in production. They will exist on the same server, with the .NET app serving the react app as its frontend.
yarn create vite my-app-frontend
// https://vitejs.dev/config/
export default defineConfig({
server: {
proxy: {
"/api": "http://localhost:5053",
},
},
plugins: [react()],
})
Make GET, POST, PUT, and DELETE requests to the API to CRUD your resource. Example:
async function createTodo(name) {
const result = await fetch("/api/todos", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name }),
}).then((res) => res.json())
}
yarn build
dist
folder from your react app into your .NET project and rename
dist
to wwwroot
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapFallbackToFile("index.html");
The dotnet app should now serve up your frontend app when you make a request that isn't handled by the API.