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
References