All topics
Backend · Learning hub

ASP.NET Core notes for developers

Master ASP.NET Core with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Backend notes
ASP.NET Core

Middleware, Routing & Configuration

ASP.NET Core: Middleware, Routing & Configuration ASP.NET Core is a cross-platform, high-performance framework for building web APIs, web apps, and microservice

ASP.NET Core: Middleware, Routing & Configuration

ASP.NET Core is a cross-platform, high-performance framework for building web APIs, web apps, and microservices. The request pipeline is built from middleware components chained together.

Program.cs — App Bootstrap

var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddScoped<IArticleService, ArticleService>();
builder.Services.AddSingleton<ICacheService, RedisCacheService>();

var app = builder.Build();

// Middleware pipeline (order matters)
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseAuthentication();   // must be before UseAuthorization
app.UseAuthorization();

app.MapControllers();
app.Run();

Middleware

// Custom middleware (class-based)
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 watch = Stopwatch.StartNew();
        _logger.LogInformation("Request {Method} {Path}", context.Request.Method, context.Request.Path);

        await _next(context);  // call next middleware

        watch.Stop();
        _logger.LogInformation("Response {StatusCode} in {Elapsed}ms",
            context.Response.StatusCode, watch.ElapsedMilliseconds);
    }
}

// Extension method for clean registration
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app)
        => app.UseMiddleware<RequestLoggingMiddleware>();
}

// Use in Program.cs
app.UseRequestLogging();

// Inline middleware (simple cases)
app.Use(async (context, next) =>
{
    context.Response.Headers["X-Content-Type-Options"] = "nosniff";
    await next(context);
});

// Terminal middleware (no next call)
app.Run(async context =>
{
    await context.Response.WriteAsync("Fallback response");
});

Configuration

// appsettings.json
// {
//   "ConnectionStrings": { "Default": "Server=...;Database=..." },
//   "JwtSettings": { "Secret": "...", "ExpiryMinutes": 60 },
//   "FeatureFlags": { "NewDashboard": true }
// }

// Bind to strongly-typed class
public class JwtSettings
{
    public string Secret { get; set; } = string.Empty;
    public int ExpiryMinutes { get; set; } = 60;
}

// In Program.cs
builder.Services.Configure<JwtSettings>(
    builder.Configuration.GetSection("JwtSettings"));

// In a service
public class TokenService
{
    private readonly JwtSettings _settings;

    public TokenService(IOptions<JwtSettings> options)
    {
        _settings = options.Value;
    }
}

// Read directly
var secret = builder.Configuration["JwtSettings:Secret"];
var connStr = builder.Configuration.GetConnectionString("Default");

// Environment overrides: appsettings.Development.json > appsettings.json
// Env vars override JSON: JWTSET__SECRET=value (double underscore = colon)
// Secrets manager (dev): dotnet user-secrets set "JwtSettings:Secret" "value"

Routing

// Attribute routing (controllers)
[ApiController]
[Route("api/[controller]")]    // → api/articles
public class ArticlesController : ControllerBase
{
    [HttpGet]                  // GET api/articles
    [HttpGet("{id:int}")]      // GET api/articles/42 (with constraint)
    [HttpGet("search")]        // GET api/articles/search
    [HttpGet("{id}/comments")] // GET api/articles/42/comments
    [HttpPost]                 // POST api/articles
    [HttpPut("{id}")]          // PUT api/articles/42
    [HttpDelete("{id}")]       // DELETE api/articles/42
}

// Route constraints
// {id:int}       — integer
// {id:guid}      — GUID
// {name:alpha}   — letters only
// {age:int:min(1):max(120)}
// {slug:regex(^[a-z0-9-]+$)}
ASP.NET Core

Web API: Controllers, Minimal APIs & Validation

ASP.NET Core: Web API Controllers & Minimal APIs ApiController Pattern [ApiController] [Route("api/[controller]")] [Authorize] // require auth for all endpoints

ASP.NET Core: Web API Controllers & Minimal APIs

ApiController Pattern

[ApiController]
[Route("api/[controller]")]
[Authorize]   // require auth for all endpoints
public class ArticlesController : ControllerBase
{
    private readonly IArticleService _service;

    public ArticlesController(IArticleService service)
    {
        _service = service;
    }

    [HttpGet]
    [AllowAnonymous]
    [ProducesResponseType<IEnumerable<ArticleDto>>(StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll([FromQuery] ArticleQueryParams query)
    {
        var articles = await _service.GetAllAsync(query);
        return Ok(articles);
    }

    [HttpGet("{id:int}")]
    [ProducesResponseType<ArticleDto>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(int id)
    {
        var article = await _service.GetByIdAsync(id);
        return article is null ? NotFound() : Ok(article);
    }

    [HttpPost]
    [ProducesResponseType<ArticleDto>(StatusCodes.Status201Created)]
    [ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create([FromBody] CreateArticleRequest request)
    {
        // [ApiController] auto-validates and returns 400 on failure
        var article = await _service.CreateAsync(request, User);
        return CreatedAtAction(nameof(GetById), new { id = article.Id }, article);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, [FromBody] UpdateArticleRequest request)
    {
        var success = await _service.UpdateAsync(id, request, User);
        return success ? NoContent() : NotFound();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        await _service.DeleteAsync(id);
        return NoContent();
    }
}

Validation

// Data annotations
public class CreateArticleRequest
{
    [Required]
    [StringLength(200, MinimumLength = 5)]
    public string Title { get; set; } = string.Empty;

    [Required]
    [MinLength(50)]
    public string Content { get; set; } = string.Empty;

    [Url]
    public string? ImageUrl { get; set; }

    [Range(1, 10)]
    public int Priority { get; set; }
}

// FluentValidation (recommended for complex rules)
// dotnet add package FluentValidation.AspNetCore
public class CreateArticleValidator : AbstractValidator<CreateArticleRequest>
{
    public CreateArticleValidator()
    {
        RuleFor(x => x.Title)
            .NotEmpty()
            .Length(5, 200)
            .Must(title => !title.Contains("spam"))
                .WithMessage("Title cannot contain 'spam'");

        RuleFor(x => x.Content)
            .NotEmpty()
            .MinimumLength(50);

        RuleFor(x => x.ImageUrl)
            .Must(url => Uri.TryCreate(url, UriKind.Absolute, out _))
                .When(x => x.ImageUrl != null)
                .WithMessage("Invalid URL");
    }
}

// Register in Program.cs
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<CreateArticleValidator>();

Minimal APIs (.NET 6+)

// Program.cs — no controllers needed
var app = builder.Build();

var articles = app.MapGroup("/api/articles").RequireAuthorization();

articles.MapGet("/", async (IArticleService svc, [AsParameters] ArticleQueryParams query)
    => await svc.GetAllAsync(query));

articles.MapGet("/{id:int}", async (int id, IArticleService svc) =>
{
    var article = await svc.GetByIdAsync(id);
    return article is null ? Results.NotFound() : Results.Ok(article);
});

articles.MapPost("/", async (CreateArticleRequest req, IArticleService svc, ClaimsPrincipal user) =>
{
    var article = await svc.CreateAsync(req, user);
    return Results.CreatedAtRoute("GetArticle", new { id = article.Id }, article);
}).WithName("GetArticle");

articles.MapPut("/{id}", async (int id, UpdateArticleRequest req, IArticleService svc) =>
    await svc.UpdateAsync(id, req) ? Results.NoContent() : Results.NotFound());

articles.MapDelete("/{id}", async (int id, IArticleService svc) =>
{
    await svc.DeleteAsync(id);
    return Results.NoContent();
});

// Typed Results (compile-time checked return types)
articles.MapGet("/{id}", async Task<Results<Ok<ArticleDto>, NotFound>> (int id, IArticleService svc) =>
{
    var article = await svc.GetByIdAsync(id);
    return article is null ? TypedResults.NotFound() : TypedResults.Ok(article);
});
ASP.NET Core

Authentication, EF Core & Background Services

ASP.NET Core: Authentication, EF Core & Background Services JWT Authentication // dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer // Program.cs

ASP.NET Core: Authentication, EF Core & Background Services

JWT Authentication

// dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

// Program.cs
builder.Services.AddAuthentication(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:Secret"]!))
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
    options.AddPolicy("PremiumUser", policy =>
        policy.RequireClaim("subscription", "premium", "enterprise"));
});

// Token generation
public string GenerateToken(User user)
{
    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
        new Claim(ClaimTypes.Email, user.Email),
        new Claim(ClaimTypes.Role, user.Role),
    };

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!));
    var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: _config["Jwt:Issuer"],
        audience: _config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(60),
        signingCredentials: credentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

Entity Framework Core

// DbContext
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Article> Articles => Set<Article>();
    public DbSet<User> Users => Set<User>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<Article>(entity =>
        {
            entity.HasKey(a => a.Id);
            entity.Property(a => a.Title).HasMaxLength(200).IsRequired();
            entity.HasOne(a => a.Author)
                  .WithMany(u => u.Articles)
                  .HasForeignKey(a => a.AuthorId)
                  .OnDelete(DeleteBehavior.Cascade);
            entity.HasIndex(a => a.Slug).IsUnique();
        });
    }
}

// Queries
var articles = await _db.Articles
    .Where(a => a.Status == "published")
    .Include(a => a.Author)
    .OrderByDescending(a => a.CreatedAt)
    .Skip(page * pageSize).Take(pageSize)
    .AsNoTracking()           // faster for read-only queries
    .ToListAsync();

// Create/Update/Delete
var article = new Article { Title = "New Article", AuthorId = userId };
_db.Articles.Add(article);
await _db.SaveChangesAsync();

// Migrations
// dotnet ef migrations add InitialCreate
// dotnet ef database update

Background Services

// IHostedService / BackgroundService
public class DataSyncService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<DataSyncService> _logger;

    public DataSyncService(IServiceScopeFactory scopeFactory, ILogger<DataSyncService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // Use scope to resolve scoped services (DbContext etc.)
                using var scope = _scopeFactory.CreateScope();
                var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                await SyncData(db, stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Sync failed");
            }

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

// Register
builder.Services.AddHostedService<DataSyncService>();

Key Libraries & Patterns

  • MediatR: CQRS pattern — Commands/Queries/Notifications decouple controllers from business logic.

  • Carter: lightweight Minimal API module system — organize endpoints into ICarterModule classes.

  • Serilog: structured logging with sinks (file, Seq, Elastic, Application Insights).

  • Polly: resilience — retry, circuit breaker, timeout, bulkhead for HttpClient calls.

  • Health checks: app.MapHealthChecks("/health") with database/redis/external dependency checks.

  • Response caching: [ResponseCache] attribute or IOutputCacheStore for API response caching.

  • Rate limiting: AddRateLimiter() with fixed/sliding/token bucket/concurrency limiters (built-in .NET 7+).

Keep your ASP.NET Core knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever