CtrlK
BlogDocsLog inGet started
Tessl Logo

aspnetcore-project-starter

Scaffold a production-ready ASP.NET Core 9 API with C# 13, Controllers and Minimal APIs, EF Core 9, Identity authentication, middleware pipeline, dependency injection, and xUnit tests.

77

Quality

72%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./dotnet/aspnetcore-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

ASP.NET Core Project Starter

Scaffold a production-ready ASP.NET Core 9 API with C# 13, Controllers and Minimal APIs, EF Core 9, Identity authentication, middleware pipeline, dependency injection, and xUnit tests.

Prerequisites

  • .NET 9 SDK
  • PostgreSQL (or SQL Server)
  • Entity Framework CLI tools (dotnet tool install -g dotnet-ef)

Scaffold Command

# Web API with controllers
dotnet new webapi -n <ProjectName> --use-controllers
cd <ProjectName>

# Add packages
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Swashbuckle.AspNetCore

# Create test project
dotnet new xunit -n <ProjectName>.Tests
dotnet add <ProjectName>.Tests reference <ProjectName>
dotnet add <ProjectName>.Tests package Microsoft.AspNetCore.Mvc.Testing
dotnet add <ProjectName>.Tests package FluentAssertions
dotnet add <ProjectName>.Tests package Moq

# Create solution
dotnet new sln
dotnet sln add <ProjectName>
dotnet sln add <ProjectName>.Tests

Project Structure

<ProjectName>/
  Program.cs                      # Application entry point — service registration, middleware
  appsettings.json                # Base configuration (commit this — use placeholders for secrets)
  appsettings.Development.json    # Dev overrides (connection strings, debug settings)
  .env.example                    # Optional — document required env vars for non-.NET teams
  Controllers/
    UsersController.cs            # REST controller
    AuthController.cs             # Authentication endpoints
  MinimalApis/
    HealthEndpoints.cs            # Minimal API route groups
  Models/
    User.cs                       # Entity
    Dtos/
      CreateUserRequest.cs        # Request DTO
      UserResponse.cs             # Response DTO
  Data/
    AppDbContext.cs                # EF Core DbContext
    Migrations/                   # EF Core migrations
  Services/
    IUserService.cs               # Interface
    UserService.cs                # Implementation
  Middleware/
    ExceptionHandlerMiddleware.cs # Global error handling
    RequestTimingMiddleware.cs    # Logging middleware
  Extensions/
    ServiceCollectionExtensions.cs # DI registration helpers
  Validators/
    CreateUserRequestValidator.cs  # FluentValidation (optional)
<ProjectName>.Tests/
  Controllers/
    UsersControllerTests.cs
  Services/
    UserServiceTests.cs
  IntegrationTests/
    UsersApiTests.cs              # WebApplicationFactory tests

Key Conventions

  • Program.cs is the single entry point — service registration then middleware pipeline, in order
  • Dependency injection everywhere — register in DI, inject via constructor
  • Controllers for complex CRUD, Minimal APIs for simple endpoints — mix freely
  • DTOs for request/response shapes, never expose entities directly
  • EF Core DbContext registered as scoped service
  • appsettings.json + environment-specific overrides + user secrets for local dev
  • Middleware ordering matters: exception handling first, then auth, then routing
  • Use ILogger<T> for structured logging — never Console.WriteLine
  • C# 13 features: primary constructors, collection expressions, params spans

Essential Patterns

Application Entry Point — Program.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Database
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

// Identity
builder.Services.AddIdentity<User, IdentityRole<Guid>>()
    .AddEntityFrameworkStores<AppDbContext>()
    .AddDefaultTokenProviders();

// JWT Authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = 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(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
    };
});

builder.Services.AddAuthorization();

// Services
builder.Services.AddScoped<IUserService, UserService>();

// Controllers + Swagger
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Middleware pipeline — order matters
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseMiddleware<ExceptionHandlerMiddleware>();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.MapHealthEndpoints();

app.Run();

// Make Program accessible for WebApplicationFactory in tests
public partial class Program { }

Controller — Controllers/UsersController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class UsersController(IUserService userService, ILogger<UsersController> logger)
    : ControllerBase
{
    [HttpGet]
    public async Task<ActionResult<PagedResult<UserResponse>>> GetAll(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20)
    {
        var result = await userService.GetAllAsync(page, pageSize);
        return Ok(result);
    }

    [HttpGet("{id:guid}")]
    public async Task<ActionResult<UserResponse>> GetById(Guid id)
    {
        var user = await userService.GetByIdAsync(id);
        if (user is null)
            return NotFound(new { error = $"User {id} not found" });

        return Ok(user);
    }

    [HttpPost]
    [AllowAnonymous]
    public async Task<ActionResult<UserResponse>> Create([FromBody] CreateUserRequest request)
    {
        var user = await userService.CreateAsync(request);
        return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
    }

    [HttpPut("{id:guid}")]
    public async Task<ActionResult<UserResponse>> Update(Guid id, [FromBody] UpdateUserRequest request)
    {
        var user = await userService.UpdateAsync(id, request);
        if (user is null)
            return NotFound(new { error = $"User {id} not found" });

        return Ok(user);
    }

    [HttpDelete("{id:guid}")]
    public async Task<IActionResult> Delete(Guid id)
    {
        var deleted = await userService.DeleteAsync(id);
        if (!deleted)
            return NotFound(new { error = $"User {id} not found" });

        return NoContent();
    }
}

Minimal API Route Group — MinimalApis/HealthEndpoints.cs

public static class HealthEndpoints
{
    public static void MapHealthEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/api/health")
            .WithTags("Health");

        group.MapGet("/", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }))
            .WithName("HealthCheck");

        group.MapGet("/ready", async (AppDbContext db) =>
        {
            try
            {
                await db.Database.CanConnectAsync();
                return Results.Ok(new { status = "ready" });
            }
            catch
            {
                return Results.StatusCode(503);
            }
        }).WithName("ReadinessCheck");
    }
}

Entity — Models/User.cs

using Microsoft.AspNetCore.Identity;

public class User : IdentityUser<Guid>
{
    public required string Name { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

DTOs — Models/Dtos/

// CreateUserRequest.cs
public record CreateUserRequest(string Email, string Name, string Password);

// UpdateUserRequest.cs
public record UpdateUserRequest(string? Email, string? Name);

// UserResponse.cs
public record UserResponse(Guid Id, string Email, string Name, DateTime CreatedAt);

// PagedResult.cs
public record PagedResult<T>(IReadOnlyList<T> Data, int Page, int PageSize, int Total);

DbContext — Data/AppDbContext.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

public class AppDbContext(DbContextOptions<AppDbContext> options)
    : IdentityDbContext<User, IdentityRole<Guid>, Guid>(options)
{
    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<User>(entity =>
        {
            entity.Property(u => u.Name).IsRequired().HasMaxLength(100);
            entity.HasIndex(u => u.Email).IsUnique();
        });
    }

    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        foreach (var entry in ChangeTracker.Entries<User>()
            .Where(e => e.State == EntityState.Modified))
        {
            entry.Entity.UpdatedAt = DateTime.UtcNow;
        }

        return base.SaveChangesAsync(cancellationToken);
    }
}

Service — Services/UserService.cs

public interface IUserService
{
    Task<PagedResult<UserResponse>> GetAllAsync(int page, int pageSize);
    Task<UserResponse?> GetByIdAsync(Guid id);
    Task<UserResponse> CreateAsync(CreateUserRequest request);
    Task<UserResponse?> UpdateAsync(Guid id, UpdateUserRequest request);
    Task<bool> DeleteAsync(Guid id);
}

public class UserService(AppDbContext db, UserManager<User> userManager) : IUserService
{
    public async Task<PagedResult<UserResponse>> GetAllAsync(int page, int pageSize)
    {
        pageSize = Math.Min(pageSize, 100);
        var total = await db.Users.CountAsync();
        var users = await db.Users
            .OrderByDescending(u => u.CreatedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(u => new UserResponse(u.Id, u.Email!, u.Name, u.CreatedAt))
            .ToListAsync();

        return new PagedResult<UserResponse>(users, page, pageSize, total);
    }

    public async Task<UserResponse?> GetByIdAsync(Guid id)
    {
        var user = await db.Users.FindAsync(id);
        return user is null ? null : new UserResponse(user.Id, user.Email!, user.Name, user.CreatedAt);
    }

    public async Task<UserResponse> CreateAsync(CreateUserRequest request)
    {
        var user = new User { Email = request.Email, UserName = request.Email, Name = request.Name };
        var result = await userManager.CreateAsync(user, request.Password);

        if (!result.Succeeded)
        {
            var errors = string.Join(", ", result.Errors.Select(e => e.Description));
            throw new InvalidOperationException($"Failed to create user: {errors}");
        }

        return new UserResponse(user.Id, user.Email, user.Name, user.CreatedAt);
    }

    public async Task<UserResponse?> UpdateAsync(Guid id, UpdateUserRequest request)
    {
        var user = await db.Users.FindAsync(id);
        if (user is null) return null;

        if (request.Email is not null) user.Email = request.Email;
        if (request.Name is not null) user.Name = request.Name;

        await db.SaveChangesAsync();
        return new UserResponse(user.Id, user.Email!, user.Name, user.CreatedAt);
    }

    public async Task<bool> DeleteAsync(Guid id)
    {
        var user = await db.Users.FindAsync(id);
        if (user is null) return false;

        db.Users.Remove(user);
        await db.SaveChangesAsync();
        return true;
    }
}

Error Handling Middleware — Middleware/ExceptionHandlerMiddleware.cs

public class ExceptionHandlerMiddleware(RequestDelegate next, ILogger<ExceptionHandlerMiddleware> logger)
{
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (InvalidOperationException ex)
        {
            logger.LogWarning(ex, "Validation error");
            context.Response.StatusCode = 400;
            await context.Response.WriteAsJsonAsync(new { error = ex.Message });
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Unhandled exception");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
        }
    }
}

Configuration — appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Database=myapp;Username=postgres;Password=postgres"
  },
  "Jwt": {
    "Key": "your-256-bit-secret-key-change-in-production",
    "Issuer": "myapp",
    "Audience": "myapp",
    "ExpiryMinutes": 60
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Warning"
    }
  }
}

Integration Test — Tests/IntegrationTests/UsersApiTests.cs

using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using FluentAssertions;

public class UsersApiTests(WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client = factory.CreateClient();

    [Fact]
    public async Task CreateUser_ReturnsCreated()
    {
        var request = new { Email = "test@example.com", Name = "Test User", Password = "P@ssword123" };

        var response = await _client.PostAsJsonAsync("/api/users", request);

        response.StatusCode.Should().Be(HttpStatusCode.Created);
        var user = await response.Content.ReadFromJsonAsync<UserResponse>();
        user!.Email.Should().Be("test@example.com");
    }

    [Fact]
    public async Task GetUser_NotFound_Returns404()
    {
        var response = await _client.GetAsync($"/api/users/{Guid.NewGuid()}");
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }
}

First Steps After Scaffold

  1. Update appsettings.Development.json with your local PostgreSQL connection string and JWT key
  2. Initialize user secrets for local dev: dotnet user-secrets init && dotnet user-secrets set "Jwt:Key" "your-256-bit-secret"
  3. Run dotnet restore to install all NuGet packages
  4. Create the initial migration and apply it: dotnet ef migrations add InitialCreate && dotnet ef database update
  5. Start the dev server: dotnet watch run
  6. Verify the Swagger UI: open https://localhost:5001/swagger in a browser

Common Commands

# Development server (hot reload)
dotnet watch run

# Run
dotnet run

# Build
dotnet build

# Publish release
dotnet publish -c Release -o ./publish

# Tests
dotnet test
dotnet test --filter "FullyQualifiedName~UsersApiTests"

# EF Core migrations
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet ef database update 0        # revert all
dotnet ef migrations remove        # remove last unapplied migration

# User secrets (development)
dotnet user-secrets init
dotnet user-secrets set "Jwt:Key" "your-secret-key"

# Scaffold controller from model
dotnet aspnet-codegenerator controller -name PostsController -m Post -dc AppDbContext --relativeFolderPath Controllers -api

Integration Notes

  • Auth: ASP.NET Identity handles user management, password hashing, roles. JWT Bearer for API auth. Use [Authorize] and [AllowAnonymous] attributes on controllers/actions.
  • Database: EF Core 9 with PostgreSQL via Npgsql. Use migrations for schema changes. SaveChangesAsync with ChangeTracker for audit fields.
  • Validation: Use Data Annotations ([Required], [StringLength]) on DTOs, or FluentValidation (dotnet add package FluentValidation.AspNetCore) for complex rules.
  • Caching: IMemoryCache for in-process, IDistributedCache with Redis (Microsoft.Extensions.Caching.StackExchangeRedis) for distributed.
  • Background Jobs: IHostedService or BackgroundService for simple tasks. Hangfire or Quartz.NET for scheduled/persistent jobs.
  • Testing: WebApplicationFactory<Program> for integration tests that spin up the full pipeline in-memory. Use test-specific appsettings.Testing.json and an in-memory or test database.
  • Docker: Multi-stage build — mcr.microsoft.com/dotnet/sdk:9.0 for build, mcr.microsoft.com/dotnet/aspnet:9.0 for runtime.
  • Health Checks: Use built-in builder.Services.AddHealthChecks().AddNpgSql(...) and app.MapHealthChecks("/health") for production readiness probes.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.