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
72%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./dotnet/aspnetcore-project-starter/SKILL.mdScaffold 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 testsProgram.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 = "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);
}
}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.181fcbc
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.