dotnet/aspnetcore-project-starter/SKILL.md
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.
npx skillsauth add achreftlili/deep-dev-skills aspnetcore-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
dotnet tool install -g dotnet-ef)# 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
<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
Program.cs is the single entry point — service registration then middleware pipeline, in orderappsettings.json + environment-specific overrides + user secrets for local devILogger<T> for structured logging — never Console.WriteLineparams spansProgram.csusing 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 { }
Controllers/UsersController.csusing 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();
}
}
MinimalApis/HealthEndpoints.cspublic 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");
}
}
Models/User.csusing 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;
}
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);
Data/AppDbContext.csusing 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);
}
}
Services/UserService.cspublic 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;
}
}
Middleware/ExceptionHandlerMiddleware.cspublic 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" });
}
}
}
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"
}
}
}
Tests/IntegrationTests/UsersApiTests.csusing 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 = "[email protected]", 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("[email protected]");
}
[Fact]
public async Task GetUser_NotFound_Returns404()
{
var response = await _client.GetAsync($"/api/users/{Guid.NewGuid()}");
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
}
appsettings.Development.json with your local PostgreSQL connection string and JWT keydotnet user-secrets init && dotnet user-secrets set "Jwt:Key" "your-256-bit-secret"dotnet restore to install all NuGet packagesdotnet ef migrations add InitialCreate && dotnet ef database updatedotnet watch runhttps://localhost:5001/swagger in a browser# 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
[Authorize] and [AllowAnonymous] attributes on controllers/actions.SaveChangesAsync with ChangeTracker for audit fields.[Required], [StringLength]) on DTOs, or FluentValidation (dotnet add package FluentValidation.AspNetCore) for complex rules.IMemoryCache for in-process, IDistributedCache with Redis (Microsoft.Extensions.Caching.StackExchangeRedis) for distributed.IHostedService or BackgroundService for simple tasks. Hangfire or Quartz.NET for scheduled/persistent jobs.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.mcr.microsoft.com/dotnet/sdk:9.0 for build, mcr.microsoft.com/dotnet/aspnet:9.0 for runtime.builder.Services.AddHealthChecks().AddNpgSql(...) and app.MapHealthChecks("/health") for production readiness probes.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.