aspnet-core-fundamentals

pluginagentmarketplace's avatarfrom pluginagentmarketplace

Master ASP.NET Core fundamentals including C#, project structure, routing, middleware, and basic API development. Essential skills for all ASP.NET Core developers.

⭐0stars🔀0forks📁View on GitHub🕐Updated Nov 19, 2025

When & Why to Use This Skill

This Claude skill provides a comprehensive foundation for building production-ready ASP.NET Core applications using the latest .NET 8/9 standards. It covers essential C# 12/13 features, modern project structuring, and robust API development patterns. By integrating best practices for Dependency Injection, middleware pipelines, and automated testing, it enables developers to accelerate the creation of scalable, maintainable, and high-performance backend services.

Use Cases

  • Scaffolding production-grade ASP.NET Core Web APIs using both Minimal API and Controller-based architectures.
  • Implementing complex middleware pipelines for centralized logging, global exception handling, and security headers.
  • Designing robust data models with advanced validation using Data Annotations and FluentValidation.
  • Setting up sophisticated Dependency Injection configurations including keyed services and the Options pattern for configuration management.
  • Generating comprehensive test suites utilizing xUnit, Moq, and FluentAssertions for both unit and integration testing.
  • Troubleshooting common ASP.NET Core issues related to routing, model binding, and service registration.
nameaspnet-core-fundamentals
version"2.0.0"
descriptionMaster ASP.NET Core fundamentals including C#, project structure, routing, middleware, and basic API development. Essential skills for all ASP.NET Core developers.
sasmp_version"1.3.0"
bonded_agentaspnet-core-backend
bond_typePRIMARY_BOND
skill_typeatomic
responsibilitysingle # Single-responsibility design
typestring
requiredfalse
default"sqlserver"
pattern"^[6-9]\\.0$"
allowed_values["sqlserver", "postgresql", "mysql", "sqlite", "inmemory"]
enabledtrue
max_attempts3
backoff_typeexponential
initial_delay_ms1000
max_delay_ms10000
jittertrue
levelInformation
include_parameterstrue
sensitive_fields["connectionString", "password", "apiKey"]
namespaceskill.aspnetcore.fundamentals
span_nameaspnetcore-fundamentals
frameworkxunit
mockingmoq
assertionsfluent_assertions
coverage_target80

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 Content-Type: application/json
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

References