Introduction
As a .NET Solutions Architect with 13 years of experience, I’ve seen how ASP.NET Core accelerates web application development by combining performance, modularity, and cross-platform support. In this tutorial we will specifically focus on ASP.NET Core, the modern, cross-platform framework for building web applications on .NET 8. You’ll learn how to set up a development environment for .NET 8, create a first application, and implement common features such as authentication, data access with Entity Framework Core, and deployment patterns.
The goal is practical: by the end you should be able to scaffold an app, wire up EF Core (with a small CRUD example), secure endpoints with policy-based/JWT authentication, and troubleshoot common runtime issues in a .NET 8 environment.
1. Setting Up Your Development Environment
Installing Required Tools (targeting .NET 8)
For this tutorial target .NET 8. Install the .NET SDK and a development IDE:
- Download .NET SDK: dotnet.microsoft.com
- Visual Studio or VS Code: visualstudio.microsoft.com (Community) or code.visualstudio.com
Verify the SDK installation on your machine:
dotnet --version
This should return a .NET 8.x version number when the SDK is installed correctly.
Configuring Your IDE
In Visual Studio, enable the .NET workloads for ASP.NET Core (Tools > Get Tools and Features). For VS Code, install the C# extension and the OmniSharp tooling. Recommended extensions:
- C# (Microsoft) — language support and debugging
- Docker — container support
- GitLens — Git integration and history
Helpful development commands:
git init
git add .
git commit -m "Initial commit"
# Use dotnet watch during development to auto-restart on file changes
dotnet watch run
dotnet watch run is especially useful during development: it rebuilds and restarts the app when source files change, saving context switches and speeding up feedback loops.
2. Understanding the Basics of ASP.NET Framework
Core Concepts
ASP.NET Core is a cross-platform, high-performance framework for building web apps and APIs on the unified .NET platform. Key concepts to know:
- Middleware pipeline — request/response handlers configured in sequence.
- Dependency Injection — built-in DI container for registering services.
- Routing — endpoint routing for controllers, Razor Pages, or minimal APIs.
- Hosting model — minimal hosting (Program.cs) since .NET 6 and onwards.
Example: a minimal Program.cs in .NET 8 that registers services and middleware:
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
// Middleware pipeline
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.Run();
2.1 ASP.NET vs ASP.NET Core
It's important to distinguish the legacy ASP.NET (Framework) from ASP.NET Core:
- ASP.NET (Framework) targets Windows and the .NET Framework (4.x). It is mature and used in many legacy enterprise apps.
- ASP.NET Core is cross-platform, modular, and runs on the modern .NET (now .NET 8). It includes built-in dependency injection, improved performance, and a unified runtime for web and cloud workloads.
When we say "ASP.NET" in this guide, we mean ASP.NET Core on .NET 8, unless explicitly noted otherwise.
3. Creating Your First ASP.NET Application
Scaffold a new MVC project
After installing the .NET 8 SDK, create a new MVC app and run it locally:
dotnet new mvc -n BookstoreApp
cd BookstoreApp
dotnet run
# or use dotnet watch for faster edit-run cycles
dotnet watch run
The app will start a local server (typically serving HTTPS on a localhost port). Open the URL shown in the console to confirm the site runs.
| Command | Description |
|---|---|
| dotnet new mvc -n BookstoreApp | Create a new MVC project |
| dotnet run | Start the development server |
| dotnet watch run | Auto-rebuild and restart on code changes |
4. Exploring ASP.NET Features: MVC and Web API
MVC Architecture
MVC separates concerns so teams can work independently on UI (Views), user interactions (Controllers), and data/business logic (Models). Controllers expose actions that return views or JSON for API endpoints.
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
Web API and Minimal APIs
ASP.NET Core supports both controller-based APIs and minimal APIs (lightweight endpoints in Program.cs). Use minimal APIs for small microservices and controller-based APIs for feature-rich services.
5. EF Core CRUD Example (Bookstore)
This small, complete example demonstrates using Entity Framework Core 8.x with SQL Server to perform basic CRUD operations for a Book entity. It includes project packages, a DbContext, a migration command sequence, and sample endpoints (minimal API) to add and list books.
Packages (example versions)
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.0.0
dotnet tool install --global dotnet-ef --version 8.0.0
Note: use a specific version that matches your SDK if you need deterministic builds. The example below assumes EF Core 8.x.
Model and DbContext
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public DateTime PublishedOn { get; set; }
}
public class BookContext : DbContext
{
public BookContext(DbContextOptions options) : base(options) { }
public DbSet Books { get; set; }
}
Program.cs (minimal API CRUD)
var builder = WebApplication.CreateBuilder(args);
// Connection string should be stored in secrets or environment variables in real apps
builder.Services.AddDbContext(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
app.MapGet("/books", async (BookContext db) =>
{
return await db.Books.ToListAsync();
});
app.MapGet("/books/{id}", async (int id, BookContext db) =>
{
var book = await db.Books.FindAsync(id);
return book is not null ? Results.Ok(book) : Results.NotFound();
});
app.MapPost("/books", async (Book book, BookContext db) =>
{
db.Books.Add(book);
await db.SaveChangesAsync();
return Results.Created($"/books/{book.Id}", book);
});
app.MapPut("/books/{id}", async (int id, Book incoming, BookContext db) =>
{
var book = await db.Books.FindAsync(id);
if (book is null) return Results.NotFound();
book.Title = incoming.Title;
book.Author = incoming.Author;
book.PublishedOn = incoming.PublishedOn;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/books/{id}", async (int id, BookContext db) =>
{
var book = await db.Books.FindAsync(id);
if (book is null) return Results.NotFound();
db.Books.Remove(book);
await db.SaveChangesAsync();
return Results.NoContent();
});
app.Run();
Migrations and database update
From the project root, create the initial migration and apply it to the database:
dotnet ef migrations add InitialCreate
dotnet ef database update
Troubleshooting tips:
- If migrations fail, ensure the startup project is set correctly or use
--projectand--startup-projectoptions withdotnet ef. - Keep connection strings out of source control: use
dotnet user-secretsfor local dev or environment variables in CI/CD. - Use cancellation tokens and batching for high-throughput inserts; ensure proper indexed columns for query performance.
6. Securing Endpoints (Authorize + JWT)
This section shows how to protect endpoints using the [Authorize] attribute and a simple policy-based role check with JWT authentication. Example uses Microsoft.AspNetCore.Authentication.JwtBearer (8.x) for bearer token validation.
Add the package
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.0
appsettings.json example
Store issuer, audience, and a base64-encoded symmetric key in configuration (use secrets/vault in real apps). Example appsettings.json snippet:
{
"Jwt": {
"Issuer": "https://your-issuer.example",
"Audience": "your-audience",
"Key": ""
},
"ConnectionStrings": {
"Default": "Server=(localdb)\\mssqllocaldb;Database=BookstoreDb;Trusted_Connection=True;"
}
}
Note: generate a secure random key (32+ bytes) and base64-encode it. For local development, prefer dotnet user-secrets. In production use a managed key vault.
How to generate a secure key
One quick way to produce a base64-encoded 32-byte key is using OpenSSL:
openssl rand -base64 32
Store the resulting string with dotnet user-secrets during local development, for example:
dotnet user-secrets set "Jwt:Key" ""
For CI/CD and production, inject the key via environment variables or a managed vault (Azure Key Vault, AWS Secrets Manager, etc.).
Program.cs configuration
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(builder.Configuration["Jwt:Key"]))
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Generating a JWT token for testing
For initial testing you can add a simple token-issuing endpoint (only for dev/local use). This example issues a short-lived JWT based on configuration and a basic username/role check. The LoginRequest record used by the endpoint is defined below for clarity:
record LoginRequest(string Username, string Password);
app.MapPost("/token", (LoginRequest req, IConfiguration config) =>
{
// WARNING: Replace this stub with a proper credential check (DB/Identity) in real apps
if (req.Username != "alice" || req.Password != "P@ssw0rd")
return Results.Unauthorized();
var claims = new[] { new Claim(ClaimTypes.Name, req.Username), new Claim(ClaimTypes.Role, "Admin") };
var key = new SymmetricSecurityKey(Convert.FromBase64String(config["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: config["Jwt:Issuer"],
audience: config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: creds);
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
return Results.Ok(new { access_token = tokenString });
});
Use Postman or Insomnia to POST { "username": "alice", "password": "P@ssw0rd" } to /token to receive a bearer token for testing. Then add an Authorization header: Authorization: Bearer when calling protected endpoints.
Securing controllers/actions
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly BookContext _db;
public BooksController(BookContext db) => _db = db;
// Anyone authenticated
[HttpGet]
[Authorize]
public async Task Get() => Ok(await _db.Books.ToListAsync());
// Admin-only
[HttpPost]
[Authorize(Policy = "AdminOnly")]
public async Task Create(Book book)
{
_db.Books.Add(book);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(Get), new { id = book.Id }, book);
}
}
Security insights:
- Never store symmetric keys in plaintext in source control. Use user secrets for development and a managed key vault (cloud provider) in production.
- Prefer short-lived tokens and refresh tokens. Use HTTPS to protect tokens in transit and set secure cookie flags if using cookie auth.
- Validate roles and scopes in policies rather than using role checks scattered across code to make authorization central and auditable.
7. Best Practices and Resources for Continued Learning
Embracing Best Practices
- Dependency Injection — register services with appropriate lifetimes (scoped for DbContext, singleton for stateless services).
- Configuration — use appsettings.json, environment variables, and a secrets manager; keep secrets out of source control.
- Logging — use Microsoft.Extensions.Logging; consider Serilog for structured logs and push critical logs to a central store.
- EF Core Migrations — manage schema changes via migrations and include them in your CI process.
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped();
}
Security & Troubleshooting
Security and operational readiness are crucial:
- Always enforce HTTPS and consider HSTS in production.
- Use parameterized queries or EF Core to avoid SQL injection.
- Protect secrets with a secrets manager (development) and a vault in production (cloud provider vaults).
- For authentication: use ASP.NET Core Identity or JWT-based tokens (Microsoft.AspNetCore.Authentication.JwtBearer 8.x).
Troubleshooting tips:
- Port conflicts: check the running process with
lsof -i :{port}(macOS/Linux) or Task Manager (Windows). - EF Core migrations: ensure the correct design-time services and run
dotnet ef migrations add InitialCreatethendotnet ef database update. - Check application logs (console/Serilog) and use
ASPNETCORE_ENVIRONMENTto reproduce environment-specific issues.
Recommended Resources
- Official documentation: learn.microsoft.com
- Pluralsight courses: pluralsight.com
- Udemy: udemy.com
- Community support: stackoverflow.com
8. Next Steps & Project Ideas
After you complete this tutorial, pick one of these guided projects to reinforce learning and build a portfolio piece:
- Build a multi-user Bookstore app: implement Identity (registration/login), role-based access for admin tasks (seed an Admin user), and full CRUD with EF Core and migrations.
- Create a microservice with minimal APIs: implement a product catalog service with Redis caching and health checks.
- Integrate with an external OAuth provider: add Google or Microsoft login to demonstrate external auth flows.
- CI/CD pipeline: create GitHub Actions or Azure DevOps pipeline to run tests, build a Docker image, push to a registry, and deploy to Azure App Service or a Kubernetes cluster.
- Add automated tests: unit-test controllers/services with xUnit and integration-test EF Core using the SQLite in-memory provider.
Practical tips:
- Start small and iterate: add one feature at a time (authentication, then roles, then CI/CD).
- Use feature branches and code reviews to keep code quality high.
- Measure performance before and after optimizations with tools like
dotnet-traceor tracing in Application Insights.
8.1 Deployment Strategies
This dedicated section covers common deployment strategies, step-by-step examples (Docker + GitHub Actions), and Azure App Service deployment patterns you can use for .NET 8 apps. It includes security recommendations and troubleshooting tips for production rollouts.
Containerize with Docker (recommended for portability)
Use a multi-stage Dockerfile to produce a small runtime image for ASP.NET Core on .NET 8. Example Dockerfile (multi-stage):
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . ./
RUN dotnet publish -c Release -o /app/publish
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish ./
ENV ASPNETCORE_URLS=http://+:80
EXPOSE 80
ENTRYPOINT ["dotnet", "YourApp.dll"]
Build and run locally:
docker build -t bookstoreapp:1.0 .
docker run -p 8080:80 --env ASPNETCORE_ENVIRONMENT=Production bookstoreapp:1.0
CI/CD example: GitHub Actions (build, test, push image)
Minimal GitHub Actions workflow that builds the Docker image, runs tests, and pushes to a container registry (Docker Hub or Azure Container Registry). The snippet below uses the runner ubuntu-latest and builds via Docker Buildx.
name: CI
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build
run: dotnet build -c Release --no-restore
- name: Test
run: dotnet test --no-build -c Release
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ secrets.REGISTRY }}/bookstoreapp:${{ github.sha }}
To deploy to Azure App Service from a container, you can use a subsequent deployment job that logs into the registry and calls the appropriate deploy action or uses the Azure CLI.
Deploy to Azure App Service (container or code-based)
Two common approaches for Azure:
- Deploy a container image to Azure Web App for Containers. Use App Service configuration to set environment variables and connection strings.
- Use the App Service build pipeline to publish your app (zip deploy). The
dotnet publishartifacts are pushed to App Service.
Example Azure CLI commands (container-based):
# Create a resource group
az group create --name myResourceGroup --location eastus
# Create an App Service plan
az appservice plan create --name myPlan --resource-group myResourceGroup --sku B1 --is-linux
# Create a Web App using a container image
az webapp create --resource-group myResourceGroup --plan myPlan --name my-unique-app-name --deployment-container-image-name myregistry.azurecr.io/bookstoreapp:latest
# Configure app settings (connection strings, secrets via environment variables)
az webapp config appsettings set --resource-group myResourceGroup --name my-unique-app-name --settings "ConnectionStrings__Default=..." "ASPNETCORE_ENVIRONMENT=Production"
Security and operational recommendations:
- Use a managed secrets store (Azure Key Vault) and grant the app a managed identity instead of embedding secrets into app settings.
- Enable HTTPS only and HSTS in production. Use TLS termination at the front door or load balancer.
- Use health checks (
app.UseHealthChecks) and container readiness probes to let the orchestrator know when your app is ready. - Enable Application Insights for telemetry and distributed tracing; connect traces to logs for production troubleshooting.
Kubernetes and higher-scale deployments
For larger deployments use Kubernetes (AKS, EKS, GKE) with Helm charts or Kustomize. Important patterns:
- Use readiness and liveness probes.
- Externalize configuration via ConfigMaps/Secrets and a secret store integration (e.g., CSI driver for secrets).
- Use resource requests/limits and autoscaling (HPA) based on CPU or custom metrics.
Deployment troubleshooting
- If your app starts locally but fails in the target environment, check environment-specific configuration differences and connection strings first.
- Inspect container logs (
docker logsor cloud provider logs) and the app's stdout/stderr; enable structured logging to capture context. - For Azure: check the Web App's Diagnose and solve problems blade or container logs and the App Service log stream.
9. Key Takeaways
- ASP.NET Core on .NET 8 is cross-platform and suitable for modern web and cloud scenarios.
- Entity Framework Core (EF Core) simplifies data access and migrations for relational databases; follow migration best practices for CI/CD.
- MVC and minimal APIs serve different needs—choose based on complexity and team workflows.
- Built-in dependency injection, middleware, and configuration systems make applications more maintainable.
- Prioritize security practices (HTTPS, secrets management, input validation) before production deployment.
10. Frequently Asked Questions
- What is the best way to set up an ASP.NET Core development environment?
- Install the .NET 8 SDK from the official site and use Visual Studio 2022 or Visual Studio Code. Both IDEs support developing with .NET 8. Add Docker if you plan to containerize your app.
- How can I improve performance in my ASP.NET applications?
- Use caching (in-memory or distributed like Redis), asynchronous programming (async/await), and minimize allocations. Profile using tools like dotnet-trace and Application Insights in production.
- Do I need to know C# to work with ASP.NET Core?
- Yes. ASP.NET Core is C#-first; familiarity with C#, LINQ, and async programming is essential.
- What are the differences between ASP.NET MVC and ASP.NET Core?
- ASP.NET Core is a modern, modular rewrite of older ASP.NET MVC for the .NET platform—it’s cross-platform, includes built-in DI, and offers improved performance and a simplified hosting model.
- How can I deploy my ASP.NET Core application to Azure?
- Deploy a container image to Azure App Service, or build a CI/CD pipeline with GitHub Actions or Azure DevOps to publish artifacts. Ensure app settings and secrets are configured using managed identities or a secrets vault.
Conclusion
ASP.NET Core on .NET 8 provides a modern, performant foundation for web applications. Focus on solid architecture (DI, separation of concerns), secure practices (HTTPS, secrets management), and automated migrations/deployments to move from development to production reliably. Start with a small API or MVC app, add EF Core for persistence, and iterate—this hands-on approach builds the knowledge you need for real-world projects.