Claude-skill-registry aspnet-core-fundamentals
Master ASP.NET Core fundamentals including C#, project structure, routing, middleware, and basic API development. Essential skills for all ASP.NET Core developers.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/aspnet-core-fundamentals" ~/.claude/skills/majiayu000-claude-skill-registry-aspnet-core-fundamentals && rm -rf "$T"
manifest:
skills/data/aspnet-core-fundamentals/SKILL.mdsource content
ASP.NET Core Fundamentals
Skill Overview
Production-grade fundamentals skill for ASP.NET Core 8.0/9.0 development. Implements atomic, single-responsibility design with comprehensive validation, retry logic, and observability.
Core Skills
C# Essentials (C# 12/13)
fundamentals: variables_and_types: - Primitive types (int, string, bool, decimal) - Reference vs value types - Nullable reference types (NRT) - var and target-typed new - Records and record structs control_flow: - if/else, switch expressions - Pattern matching - for, foreach, while loops - LINQ query syntax - Exception handling (try/catch/finally) functions_and_methods: - Method signatures and overloading - Optional and named parameters - ref, out, in parameters - Local functions - Expression-bodied members oop_principles: - Classes and inheritance - Interfaces and abstract classes - Encapsulation (access modifiers) - Polymorphism - Composition over inheritance modern_csharp: - Primary constructors (C# 12) - Collection expressions (C# 12) - Raw string literals - Required members - File-scoped types async_programming: - async/await fundamentals - Task and ValueTask - Cancellation tokens - Async streams (IAsyncEnumerable) - ConfigureAwait considerations
ASP.NET Core Project Setup
project_creation: commands: webapi: dotnet new webapi -n MyApi --use-controllers minimal_api: dotnet new webapi -n MyApi mvc: dotnet new mvc -n MyApp razor: dotnet new razor -n MyApp project_structure: root: - Program.cs (entry point, DI, middleware) - appsettings.json (configuration) - appsettings.Development.json controllers: - Controller classes models: - Entity classes - DTOs services: - Business logic data: - DbContext - Repositories configuration: appsettings_structure: ConnectionStrings: Database connections Logging: Log level configuration AllowedHosts: CORS settings CustomSettings: Application-specific environment_variables: ASPNETCORE_ENVIRONMENT: Development/Staging/Production ASPNETCORE_URLS: Binding URLs configuration_sources: - appsettings.json - appsettings.{Environment}.json - Environment variables - User secrets (development) - Azure Key Vault (production)
Routing & Controllers
attribute_routing: controller_level: "[Route(\"api/[controller]\")]" action_level: "[HttpGet(\"{id}\")]" route_constraints: - "{id:int}" (integer) - "{name:alpha}" (letters only) - "{date:datetime}" (date) - "{id:min(1)}" (minimum value) http_methods: - "[HttpGet]" - Retrieve resource - "[HttpPost]" - Create resource - "[HttpPut]" - Replace resource - "[HttpPatch]" - Partial update - "[HttpDelete]" - Remove resource action_results: success: - Ok(data) - 200 - Created(uri, data) - 201 - NoContent() - 204 - Accepted() - 202 client_errors: - BadRequest(error) - 400 - Unauthorized() - 401 - Forbidden() - 403 - NotFound() - 404 - Conflict() - 409 server_errors: - StatusCode(500) - Internal error model_binding: sources: - "[FromRoute]" - URL path - "[FromQuery]" - Query string - "[FromBody]" - Request body (JSON) - "[FromHeader]" - HTTP headers - "[FromForm]" - Form data - "[FromServices]" - DI container
Middleware Pipeline
middleware_order: 1: Exception handling 2: HTTPS redirection 3: Static files 4: Routing 5: CORS 6: Authentication 7: Authorization 8: Custom middleware 9: Endpoints built_in_middleware: - UseExceptionHandler() - UseHttpsRedirection() - UseStaticFiles() / MapStaticAssets() (.NET 9) - UseRouting() - UseCors() - UseAuthentication() - UseAuthorization() - UseRateLimiter() custom_middleware: inline: app.Use(async (context, next) => { ... }) class_based: app.UseMiddleware<CustomMiddleware>() convention: Must have Invoke/InvokeAsync method
Models & Data Binding
model_classes: entities: purpose: Database representation features: - Navigation properties - Data annotations - Fluent configuration dtos: purpose: API contracts best_practices: - Separate from entities - Use records for immutability - Include only needed fields validation: data_annotations: - "[Required]" - "[StringLength(100)]" - "[Range(1, 100)]" - "[EmailAddress]" - "[RegularExpression(pattern)]" - "[Compare(\"OtherProperty\")]" fluent_validation: purpose: Complex validation rules example: | RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .Must(BeUniqueEmail); model_binding_validation: automatic: ModelState.IsValid problem_details: Automatic 400 response custom_response: Override with filters
Dependency Injection
service_lifetimes: singleton: description: Single instance for application lifetime use_cases: - Configuration - Caching services - Logging caution: Thread-safety required scoped: description: New instance per request use_cases: - DbContext - Request-specific services - Unit of Work transient: description: New instance every time use_cases: - Lightweight stateless services - Factory-created services caution: Memory allocation overhead registration_patterns: interface_based: | services.AddScoped<IProductService, ProductService>(); concrete_type: | services.AddSingleton<MyConfiguration>(); factory: | services.AddScoped<IService>(sp => new MyService(sp.GetRequiredService<IDependency>())); keyed_services: | # .NET 8+ services.AddKeyedSingleton<ICache>("memory", new MemoryCache()); services.AddKeyedSingleton<ICache>("redis", new RedisCache());
Code Examples
Production-Ready Minimal API
var builder = WebApplication.CreateBuilder(args); // Configuration builder.Configuration .AddJsonFile("appsettings.json", optional: false) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) .AddEnvironmentVariables(); // Services builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("Default"))); // Add validation builder.Services.AddValidatorsFromAssemblyContaining<Program>(); // Add problem details builder.Services.AddProblemDetails(); var app = builder.Build(); // Middleware pipeline if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseExceptionHandler(); // Endpoints var products = app.MapGroup("/api/products") .WithTags("Products") .WithOpenApi(); products.MapGet("/", async (IProductService service, CancellationToken ct) => { var result = await service.GetAllAsync(ct); return Results.Ok(result); }) .WithName("GetProducts") .Produces<IEnumerable<ProductDto>>(StatusCodes.Status200OK); products.MapGet("/{id:int}", async (int id, IProductService service, CancellationToken ct) => { var product = await service.GetByIdAsync(id, ct); return product is null ? Results.NotFound() : Results.Ok(product); }) .WithName("GetProduct") .Produces<ProductDto>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); products.MapPost("/", async ( CreateProductRequest request, IValidator<CreateProductRequest> validator, IProductService service, CancellationToken ct) => { var validation = await validator.ValidateAsync(request, ct); if (!validation.IsValid) return Results.ValidationProblem(validation.ToDictionary()); var id = await service.CreateAsync(request, ct); return Results.Created($"/api/products/{id}", new { id }); }) .WithName("CreateProduct") .Produces<object>(StatusCodes.Status201Created) .ProducesValidationProblem(); app.Run();
Controller-Based API
[ApiController] [Route("api/[controller]")] [Produces("application/json")] public class ProductsController : ControllerBase { private readonly IProductService _service; private readonly ILogger<ProductsController> _logger; public ProductsController( IProductService service, ILogger<ProductsController> logger) { _service = service; _logger = logger; } /// <summary> /// Get all products with optional filtering /// </summary> [HttpGet] [ProducesResponseType(typeof(PagedResult<ProductDto>), StatusCodes.Status200OK)] public async Task<ActionResult<PagedResult<ProductDto>>> GetProducts( [FromQuery] ProductQueryParameters query, CancellationToken ct) { var result = await _service.GetProductsAsync(query, ct); Response.Headers.Append("X-Total-Count", result.TotalCount.ToString()); return Ok(result); } /// <summary> /// Get product by ID /// </summary> [HttpGet("{id:int}")] [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task<ActionResult<ProductDto>> GetProduct( int id, CancellationToken ct) { var product = await _service.GetByIdAsync(id, ct); if (product is null) { _logger.LogWarning("Product {ProductId} not found", id); return NotFound(); } return Ok(product); } /// <summary> /// Create a new product /// </summary> [HttpPost] [ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] public async Task<ActionResult<ProductDto>> CreateProduct( [FromBody] CreateProductRequest request, CancellationToken ct) { var product = await _service.CreateAsync(request, ct); return CreatedAtAction( nameof(GetProduct), new { id = product.Id }, product); } /// <summary> /// Update existing product /// </summary> [HttpPut("{id:int}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<IActionResult> UpdateProduct( int id, [FromBody] UpdateProductRequest request, CancellationToken ct) { var success = await _service.UpdateAsync(id, request, ct); if (!success) return NotFound(); return NoContent(); } /// <summary> /// Delete product /// </summary> [HttpDelete("{id:int}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<IActionResult> DeleteProduct(int id, CancellationToken ct) { var success = await _service.DeleteAsync(id, ct); if (!success) return NotFound(); return NoContent(); } }
Custom Middleware
public class RequestLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<RequestLoggingMiddleware> _logger; public RequestLoggingMiddleware( RequestDelegate next, ILogger<RequestLoggingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString(); context.Response.Headers.Append("X-Correlation-ID", correlationId); using var scope = _logger.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId, ["RequestPath"] = context.Request.Path, ["RequestMethod"] = context.Request.Method }); var stopwatch = Stopwatch.StartNew(); try { await _next(context); } finally { stopwatch.Stop(); _logger.LogInformation( "{Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}", context.Request.Method, context.Request.Path, stopwatch.ElapsedMilliseconds, context.Response.StatusCode); } } } // Registration app.UseMiddleware<RequestLoggingMiddleware>();
Configuration with Options Pattern
// appsettings.json { "EmailSettings": { "SmtpServer": "smtp.example.com", "SmtpPort": 587, "SenderEmail": "noreply@example.com", "EnableSsl": true } } // Options class public class EmailSettings { public const string SectionName = "EmailSettings"; public string SmtpServer { get; init; } = string.Empty; public int SmtpPort { get; init; } = 587; public string SenderEmail { get; init; } = string.Empty; public bool EnableSsl { get; init; } = true; } // Registration with validation builder.Services.AddOptions<EmailSettings>() .BindConfiguration(EmailSettings.SectionName) .ValidateDataAnnotations() .ValidateOnStart(); // Usage with IOptions public class EmailService { private readonly EmailSettings _settings; public EmailService(IOptions<EmailSettings> options) { _settings = options.Value; } } // Usage with IOptionsSnapshot (reloads on change) public class EmailService { private readonly IOptionsSnapshot<EmailSettings> _options; public EmailSettings Settings => _options.Value; }
Unit Test Templates
Controller Unit Test
public class ProductsControllerTests { private readonly Mock<IProductService> _serviceMock; private readonly Mock<ILogger<ProductsController>> _loggerMock; private readonly ProductsController _controller; public ProductsControllerTests() { _serviceMock = new Mock<IProductService>(); _loggerMock = new Mock<ILogger<ProductsController>>(); _controller = new ProductsController(_serviceMock.Object, _loggerMock.Object); } [Fact] public async Task GetProduct_WhenExists_ReturnsOk() { // Arrange var productId = 1; var expectedProduct = new ProductDto { Id = productId, Name = "Test" }; _serviceMock .Setup(s => s.GetByIdAsync(productId, It.IsAny<CancellationToken>())) .ReturnsAsync(expectedProduct); // Act var result = await _controller.GetProduct(productId, CancellationToken.None); // Assert var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject; var product = okResult.Value.Should().BeOfType<ProductDto>().Subject; product.Id.Should().Be(productId); } [Fact] public async Task GetProduct_WhenNotFound_ReturnsNotFound() { // Arrange var productId = 999; _serviceMock .Setup(s => s.GetByIdAsync(productId, It.IsAny<CancellationToken>())) .ReturnsAsync((ProductDto?)null); // Act var result = await _controller.GetProduct(productId, CancellationToken.None); // Assert result.Result.Should().BeOfType<NotFoundResult>(); } [Fact] public async Task CreateProduct_WithValidData_ReturnsCreated() { // Arrange var request = new CreateProductRequest { Name = "New Product", Price = 99.99m }; var createdProduct = new ProductDto { Id = 1, Name = request.Name, Price = request.Price }; _serviceMock .Setup(s => s.CreateAsync(request, It.IsAny<CancellationToken>())) .ReturnsAsync(createdProduct); // Act var result = await _controller.CreateProduct(request, CancellationToken.None); // Assert var createdResult = result.Result.Should().BeOfType<CreatedAtActionResult>().Subject; createdResult.ActionName.Should().Be(nameof(ProductsController.GetProduct)); createdResult.RouteValues!["id"].Should().Be(1); } }
Integration Test
public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly WebApplicationFactory<Program> _factory; public ProductsApiTests(WebApplicationFactory<Program> factory) { _factory = factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // Replace database with in-memory services.RemoveAll<DbContextOptions<AppDbContext>>(); services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("TestDb")); }); }); _client = _factory.CreateClient(); } [Fact] public async Task GetProducts_ReturnsSuccessStatusCode() { // Act var response = await _client.GetAsync("/api/products"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); } [Fact] public async Task CreateProduct_WithValidData_ReturnsCreated() { // Arrange var request = new { Name = "Test Product", Price = 99.99 }; var content = new StringContent( JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); // Act var response = await _client.PostAsync("/api/products", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); response.Headers.Location.Should().NotBeNull(); } [Fact] public async Task CreateProduct_WithInvalidData_ReturnsBadRequest() { // Arrange var request = new { Name = "", Price = -1 }; // Invalid var content = new StringContent( JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); // Act var response = await _client.PostAsync("/api/products", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } }
Troubleshooting Guide
Common Issues
| Issue | Symptoms | Resolution |
|---|---|---|
| 404 Not Found | Route not matching | Check route template, HTTP method |
| 415 Unsupported Media Type | Content-Type missing | Add |
| 500 Internal Error | Unhandled exception | Check logs, add exception middleware |
| Model binding fails | Null values | Check property names, [FromBody] attribute |
| DI resolution fails | Service not registered | Add service to DI container |
Debug Checklist
step_1_routing: - Verify controller has [ApiController] attribute - Check route template matches URL - Confirm HTTP method matches action attribute - Validate route constraints step_2_model_binding: - Check JSON property names match - Verify Content-Type header - Inspect ModelState errors - Check for [FromBody], [FromQuery] attributes step_3_di_issues: - Verify service is registered - Check service lifetime compatibility - Look for circular dependencies - Inspect exception details step_4_configuration: - Verify appsettings.json syntax - Check environment name - Confirm configuration binding - Inspect IConfiguration values
Assessment Criteria
- Can create a new ASP.NET Core project
- Understand request/response pipeline
- Write basic REST APIs with proper HTTP methods
- Use dependency injection correctly
- Apply validation to models
- Configure middleware pipeline
- Handle errors with ProblemDetails
- Use async/await correctly
- Write unit and integration tests
- Apply configuration with Options pattern