Getting Started with ASP.NET Core Web APIs: RESTful Services Made Easy

Introduction

RESTful APIs are the backbone of modern web applications, enabling seamless communication between clients and servers across platforms. ASP.NET Core provides a powerful, high-performance framework for building APIs that scale from simple microservices to enterprise-grade applications.

In this comprehensive guide, you'll learn how to build a production-ready RESTful API from scratch, covering authentication, data access, validation, error handling, and deployment. We'll build a complete Task Management API with CRUD operations, demonstrating real-world patterns and best practices used by professional development teams.

What You'll Learn:

  • Setting up an ASP.NET Core Web API project from scratch
  • Implementing RESTful endpoints with proper HTTP verbs and status codes
  • Entity Framework Core for database operations
  • Input validation and error handling
  • JWT authentication and authorization
  • API documentation with Swagger/OpenAPI
  • Testing APIs with real-world scenarios
  • Deployment to Azure App Service

Architecture Overview

┌──────────────────────────────────────────────────────────────┐
│                    Task Management API                        │
├──────────────────────────────────────────────────────────────┤
│                                                               │
│  Client (Web/Mobile)                                         │
│         │                                                     │
│         ▼                                                     │
│  ┌─────────────────┐                                        │
│  │  API Gateway    │  (ASP.NET Core Middleware)            │
│  │  - CORS         │                                        │
│  │  - Auth         │                                        │
│  │  - Logging      │                                        │
│  └────────┬────────┘                                        │
│           │                                                  │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │  Controllers    │                                        │
│  │  - TasksController                                       │
│  │  - AuthController                                        │
│  └────────┬────────┘                                        │
│           │                                                  │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │  Services       │  (Business Logic)                     │
│  │  - ITaskService │                                        │
│  │  - IAuthService │                                        │
│  └────────┬────────┘                                        │
│           │                                                  │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │  Repositories   │  (Data Access)                        │
│  │  - ITaskRepository                                       │
│  └────────┬────────┘                                        │
│           │                                                  │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │  EF Core DbContext                                       │
│  └────────┬────────┘                                        │
│           │                                                  │
│           ▼                                                  │
│     SQL Server Database                                     │
│                                                               │
└──────────────────────────────────────────────────────────────┘

Prerequisites

Required Software

  • .NET 8 SDK: Download from Microsoft
  • Visual Studio 2022 (Community or higher) OR VS Code with C# extension
  • SQL Server 2019+ (LocalDB, Express, or full version)
  • Postman or REST Client for testing
  • Git for version control

Required Knowledge

  • C# programming fundamentals
  • Basic understanding of HTTP protocol and REST principles
  • Familiarity with SQL and relational databases
  • Command-line/terminal basics

Verify Installation

# Check .NET SDK version
dotnet --version
# Should output: 8.0.x or higher

# Check SQL Server
sqlcmd -S localhost -E -Q "SELECT @@VERSION"

# Check Git
git --version

Step 1: Create the Project

Create Solution and Project Structure

# Create solution folder
mkdir TaskManagementAPI
cd TaskManagementAPI

# Create solution file
dotnet new sln -n TaskManagementAPI

# Create Web API project
dotnet new webapi -n TaskManagementAPI.API -o src/TaskManagementAPI.API

# Create class library for domain models
dotnet new classlib -n TaskManagementAPI.Core -o src/TaskManagementAPI.Core

# Create class library for data access
dotnet new classlib -n TaskManagementAPI.Infrastructure -o src/TaskManagementAPI.Infrastructure

# Create test project
dotnet new xunit -n TaskManagementAPI.Tests -o tests/TaskManagementAPI.Tests

# Add projects to solution
dotnet sln add src/TaskManagementAPI.API/TaskManagementAPI.API.csproj
dotnet sln add src/TaskManagementAPI.Core/TaskManagementAPI.Core.csproj
dotnet sln add src/TaskManagementAPI.Infrastructure/TaskManagementAPI.Infrastructure.csproj
dotnet sln add tests/TaskManagementAPI.Tests/TaskManagementAPI.Tests.csproj

# Add project references
dotnet add src/TaskManagementAPI.API reference src/TaskManagementAPI.Core
dotnet add src/TaskManagementAPI.API reference src/TaskManagementAPI.Infrastructure
dotnet add src/TaskManagementAPI.Infrastructure reference src/TaskManagementAPI.Core
dotnet add tests/TaskManagementAPI.Tests reference src/TaskManagementAPI.API

Final Project Structure:

TaskManagementAPI/
├── TaskManagementAPI.sln
├── src/
│   ├── TaskManagementAPI.API/          (Web API, Controllers, Middleware)
│   ├── TaskManagementAPI.Core/         (Domain Models, Interfaces, DTOs)
│   └── TaskManagementAPI.Infrastructure/  (EF Core, Repositories)
└── tests/
    └── TaskManagementAPI.Tests/        (Unit & Integration Tests)

Install Required NuGet Packages

# Navigate to API project
cd src/TaskManagementAPI.API

# Install EF Core packages
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.0
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.0.0

# Install authentication packages
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.0
dotnet add package System.IdentityModel.Tokens.Jwt --version 7.0.0

# Install Swagger/OpenAPI
dotnet add package Swashbuckle.AspNetCore --version 6.5.0

# Install validation packages
dotnet add package FluentValidation.AspNetCore --version 11.3.0

# Navigate to Infrastructure project
cd ../TaskManagementAPI.Infrastructure
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.0

# Navigate to Core project
cd ../TaskManagementAPI.Core
# No additional packages needed for core domain

cd ../../

Step 2: Create Domain Models

Task Entity (Core Layer)

// src/TaskManagementAPI.Core/Entities/TaskItem.cs
namespace TaskManagementAPI.Core.Entities;

public class TaskItem
{
    public int Id { get; set; }
    
    public string Title { get; set; } = string.Empty;
    
    public string? Description { get; set; }
    
    public TaskStatus Status { get; set; } = TaskStatus.Todo;
    
    public TaskPriority Priority { get; set; } = TaskPriority.Medium;
    
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    
    public DateTime? DueDate { get; set; }
    
    public DateTime? CompletedAt { get; set; }
    
    public string CreatedBy { get; set; } = string.Empty;
    
    public string? AssignedTo { get; set; }
    
    public List<string> Tags { get; set; } = new();
    
    // Navigation properties
    public int? ProjectId { get; set; }
    public Project? Project { get; set; }
}

public enum TaskStatus
{
    Todo = 0,
    InProgress = 1,
    Done = 2,
    Blocked = 3
}

public enum TaskPriority
{
    Low = 0,
    Medium = 1,
    High = 2,
    Critical = 3
}

Project Entity

// src/TaskManagementAPI.Core/Entities/Project.cs
namespace TaskManagementAPI.Core.Entities;

public class Project
{
    public int Id { get; set; }
    
    public string Name { get; set; } = string.Empty;
    
    public string? Description { get; set; }
    
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    
    public string OwnerId { get; set; } = string.Empty;
    
    public bool IsActive { get; set; } = true;
    
    // Navigation properties
    public ICollection<TaskItem> Tasks { get; set; } = new List<TaskItem>();
}

DTOs (Data Transfer Objects)

// src/TaskManagementAPI.Core/DTOs/TaskDtos.cs
namespace TaskManagementAPI.Core.DTOs;

public record TaskCreateDto(
    string Title,
    string? Description,
    TaskPriority Priority,
    DateTime? DueDate,
    int? ProjectId,
    List<string>? Tags
);

public record TaskUpdateDto(
    string? Title,
    string? Description,
    TaskStatus? Status,
    TaskPriority? Priority,
    DateTime? DueDate,
    string? AssignedTo,
    List<string>? Tags
);

public record TaskResponseDto(
    int Id,
    string Title,
    string? Description,
    TaskStatus Status,
    TaskPriority Priority,
    DateTime CreatedAt,
    DateTime? DueDate,
    DateTime? CompletedAt,
    string CreatedBy,
    string? AssignedTo,
    List<string> Tags,
    ProjectSummaryDto? Project
);

public record ProjectSummaryDto(
    int Id,
    string Name
);

public record TaskFilterDto(
    TaskStatus? Status,
    TaskPriority? Priority,
    int? ProjectId,
    string? AssignedTo,
    DateTime? DueDateFrom,
    DateTime? DueDateTo,
    int PageNumber = 1,
    int PageSize = 20
);

Repository Interfaces

// src/TaskManagementAPI.Core/Interfaces/ITaskRepository.cs
namespace TaskManagementAPI.Core.Interfaces;

public interface ITaskRepository
{
    Task<TaskItem?> GetByIdAsync(int id);
    Task<PagedResult<TaskItem>> GetAllAsync(TaskFilterDto filter);
    Task<TaskItem> CreateAsync(TaskItem task);
    Task<TaskItem> UpdateAsync(TaskItem task);
    Task<bool> DeleteAsync(int id);
    Task<bool> ExistsAsync(int id);
    Task<List<TaskItem>> GetTasksByProjectIdAsync(int projectId);
    Task<List<TaskItem>> GetTasksByUserAsync(string userId);
}

public class PagedResult<T>
{
    public List<T> Items { get; set; } = new();
    public int TotalCount { get; set; }
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
    public bool HasPreviousPage => PageNumber > 1;
    public bool HasNextPage => PageNumber < TotalPages;
}

Step 3: Implement Data Access Layer

Database Context

// src/TaskManagementAPI.Infrastructure/Data/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using TaskManagementAPI.Core.Entities;

namespace TaskManagementAPI.Infrastructure.Data;

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

    public DbSet<TaskItem> Tasks => Set<TaskItem>();
    public DbSet<Project> Projects => Set<Project>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Configure TaskItem
        modelBuilder.Entity<TaskItem>(entity =>
        {
            entity.ToTable("Tasks");
            
            entity.HasKey(e => e.Id);
            
            entity.Property(e => e.Title)
                .IsRequired()
                .HasMaxLength(200);
            
            entity.Property(e => e.Description)
                .HasMaxLength(2000);
            
            entity.Property(e => e.CreatedBy)
                .IsRequired()
                .HasMaxLength(100);
            
            entity.Property(e => e.AssignedTo)
                .HasMaxLength(100);
            
            entity.Property(e => e.Status)
                .HasConversion<string>()
                .HasMaxLength(20);
            
            entity.Property(e => e.Priority)
                .HasConversion<string>()
                .HasMaxLength(20);
            
            // Configure Tags as JSON column (EF Core 8 feature)
            entity.Property(e => e.Tags)
                .HasColumnType("nvarchar(max)")
                .HasConversion(
                    v => string.Join(',', v),
                    v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
                );
            
            // Indexes for performance
            entity.HasIndex(e => e.Status);
            entity.HasIndex(e => e.CreatedBy);
            entity.HasIndex(e => e.AssignedTo);
            entity.HasIndex(e => e.DueDate);
            
            // Relationship with Project
            entity.HasOne(e => e.Project)
                .WithMany(p => p.Tasks)
                .HasForeignKey(e => e.ProjectId)
                .OnDelete(DeleteBehavior.SetNull);
        });

        // Configure Project
        modelBuilder.Entity<Project>(entity =>
        {
            entity.ToTable("Projects");
            
            entity.HasKey(e => e.Id);
            
            entity.Property(e => e.Name)
                .IsRequired()
                .HasMaxLength(200);
            
            entity.Property(e => e.Description)
                .HasMaxLength(1000);
            
            entity.Property(e => e.OwnerId)
                .IsRequired()
                .HasMaxLength(100);
            
            entity.HasIndex(e => e.OwnerId);
        });

        // Seed data
        modelBuilder.Entity<Project>().HasData(
            new Project
            {
                Id = 1,
                Name = "Default Project",
                Description = "Default project for tasks",
                OwnerId = "system",
                CreatedAt = DateTime.UtcNow
            }
        );
    }
}

Repository Implementation

// src/TaskManagementAPI.Infrastructure/Repositories/TaskRepository.cs
using Microsoft.EntityFrameworkCore;
using TaskManagementAPI.Core.Entities;
using TaskManagementAPI.Core.Interfaces;
using TaskManagementAPI.Core.DTOs;
using TaskManagementAPI.Infrastructure.Data;

namespace TaskManagementAPI.Infrastructure.Repositories;

public class TaskRepository : ITaskRepository
{
    private readonly ApplicationDbContext _context;

    public TaskRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<TaskItem?> GetByIdAsync(int id)
    {
        return await _context.Tasks
            .Include(t => t.Project)
            .FirstOrDefaultAsync(t => t.Id == id);
    }

    public async Task<PagedResult<TaskItem>> GetAllAsync(TaskFilterDto filter)
    {
        var query = _context.Tasks
            .Include(t => t.Project)
            .AsQueryable();

        // Apply filters
        if (filter.Status.HasValue)
            query = query.Where(t => t.Status == filter.Status.Value);

        if (filter.Priority.HasValue)
            query = query.Where(t => t.Priority == filter.Priority.Value);

        if (filter.ProjectId.HasValue)
            query = query.Where(t => t.ProjectId == filter.ProjectId.Value);

        if (!string.IsNullOrEmpty(filter.AssignedTo))
            query = query.Where(t => t.AssignedTo == filter.AssignedTo);

        if (filter.DueDateFrom.HasValue)
            query = query.Where(t => t.DueDate >= filter.DueDateFrom.Value);

        if (filter.DueDateTo.HasValue)
            query = query.Where(t => t.DueDate <= filter.DueDateTo.Value);

        // Get total count before pagination
        var totalCount = await query.CountAsync();

        // Apply pagination
        var items = await query
            .OrderByDescending(t => t.CreatedAt)
            .Skip((filter.PageNumber - 1) * filter.PageSize)
            .Take(filter.PageSize)
            .ToListAsync();

        return new PagedResult<TaskItem>
        {
            Items = items,
            TotalCount = totalCount,
            PageNumber = filter.PageNumber,
            PageSize = filter.PageSize
        };
    }

    public async Task<TaskItem> CreateAsync(TaskItem task)
    {
        _context.Tasks.Add(task);
        await _context.SaveChangesAsync();
        return task;
    }

    public async Task<TaskItem> UpdateAsync(TaskItem task)
    {
        _context.Entry(task).State = EntityState.Modified;
        await _context.SaveChangesAsync();
        return task;
    }

    public async Task<bool> DeleteAsync(int id)
    {
        var task = await _context.Tasks.FindAsync(id);
        if (task == null)
            return false;

        _context.Tasks.Remove(task);
        await _context.SaveChangesAsync();
        return true;
    }

    public async Task<bool> ExistsAsync(int id)
    {
        return await _context.Tasks.AnyAsync(t => t.Id == id);
    }

    public async Task<List<TaskItem>> GetTasksByProjectIdAsync(int projectId)
    {
        return await _context.Tasks
            .Where(t => t.ProjectId == projectId)
            .Include(t => t.Project)
            .ToListAsync();
    }

    public async Task<List<TaskItem>> GetTasksByUserAsync(string userId)
    {
        return await _context.Tasks
            .Where(t => t.CreatedBy == userId || t.AssignedTo == userId)
            .Include(t => t.Project)
            .OrderByDescending(t => t.CreatedAt)
            .ToListAsync();
    }
}

Step 4: Create Database with Migrations

Update Connection String

// src/TaskManagementAPI.API/appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=TaskManagementDB;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  },
  "AllowedHosts": "*",
  "JwtSettings": {
    "Secret": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
    "Issuer": "TaskManagementAPI",
    "Audience": "TaskManagementAPIClients",
    "ExpirationInMinutes": 60
  }
}

Run Migrations

# Navigate to API project
cd src/TaskManagementAPI.API

# Add initial migration
dotnet ef migrations add InitialCreate --project ../TaskManagementAPI.Infrastructure --startup-project .

# Update database
dotnet ef database update --project ../TaskManagementAPI.Infrastructure --startup-project .

# Verify database creation
sqlcmd -S (localdb)\mssqllocaldb -d TaskManagementDB -Q "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"

Step 5: Build the API Controller

Tasks Controller

// src/TaskManagementAPI.API/Controllers/TasksController.cs
using Microsoft.AspNetCore.Mvc;
using TaskManagementAPI.Core.DTOs;
using TaskManagementAPI.Core.Entities;
using TaskManagementAPI.Core.Interfaces;

namespace TaskManagementAPI.API.Controllers;

[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class TasksController : ControllerBase
{
    private readonly ITaskRepository _taskRepository;
    private readonly ILogger<TasksController> _logger;

    public TasksController(ITaskRepository taskRepository, ILogger<TasksController> logger)
    {
        _taskRepository = taskRepository;
        _logger = logger;
    }

    /// <summary>
    /// Get all tasks with optional filtering and pagination
    /// </summary>
    [HttpGet]
    [ProducesResponseType(typeof(PagedResult<TaskResponseDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<PagedResult<TaskResponseDto>>> GetTasks([FromQuery] TaskFilterDto filter)
    {
        var pagedResult = await _taskRepository.GetAllAsync(filter);
        
        var response = new PagedResult<TaskResponseDto>
        {
            Items = pagedResult.Items.Select(MapToResponseDto).ToList(),
            TotalCount = pagedResult.TotalCount,
            PageNumber = pagedResult.PageNumber,
            PageSize = pagedResult.PageSize
        };

        return Ok(response);
    }

    /// <summary>
    /// Get a specific task by ID
    /// </summary>
    [HttpGet("{id}")]
    [ProducesResponseType(typeof(TaskResponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<TaskResponseDto>> GetTask(int id)
    {
        var task = await _taskRepository.GetByIdAsync(id);
        
        if (task == null)
        {
            _logger.LogWarning("Task with ID {TaskId} not found", id);
            return NotFound(new { message = $"Task with ID {id} not found" });
        }

        return Ok(MapToResponseDto(task));
    }

    /// <summary>
    /// Create a new task
    /// </summary>
    [HttpPost]
    [ProducesResponseType(typeof(TaskResponseDto), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<TaskResponseDto>> CreateTask([FromBody] TaskCreateDto taskDto)
    {
        var task = new TaskItem
        {
            Title = taskDto.Title,
            Description = taskDto.Description,
            Priority = taskDto.Priority,
            DueDate = taskDto.DueDate,
            ProjectId = taskDto.ProjectId,
            Tags = taskDto.Tags ?? new List<string>(),
            CreatedBy = User.Identity?.Name ?? "anonymous", // Will be replaced with JWT claims
            Status = TaskStatus.Todo
        };

        var createdTask = await _taskRepository.CreateAsync(task);
        
        _logger.LogInformation("Created task with ID {TaskId}", createdTask.Id);

        return CreatedAtAction(
            nameof(GetTask),
            new { id = createdTask.Id },
            MapToResponseDto(createdTask)
        );
    }

    /// <summary>
    /// Update an existing task
    /// </summary>
    [HttpPut("{id}")]
    [ProducesResponseType(typeof(TaskResponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<TaskResponseDto>> UpdateTask(int id, [FromBody] TaskUpdateDto taskDto)
    {
        var existingTask = await _taskRepository.GetByIdAsync(id);
        
        if (existingTask == null)
        {
            return NotFound(new { message = $"Task with ID {id} not found" });
        }

        // Update only provided fields
        if (taskDto.Title != null)
            existingTask.Title = taskDto.Title;
        
        if (taskDto.Description != null)
            existingTask.Description = taskDto.Description;
        
        if (taskDto.Status.HasValue)
        {
            existingTask.Status = taskDto.Status.Value;
            if (taskDto.Status.Value == TaskStatus.Done && !existingTask.CompletedAt.HasValue)
            {
                existingTask.CompletedAt = DateTime.UtcNow;
            }
        }
        
        if (taskDto.Priority.HasValue)
            existingTask.Priority = taskDto.Priority.Value;
        
        if (taskDto.DueDate.HasValue)
            existingTask.DueDate = taskDto.DueDate;
        
        if (taskDto.AssignedTo != null)
            existingTask.AssignedTo = taskDto.AssignedTo;
        
        if (taskDto.Tags != null)
            existingTask.Tags = taskDto.Tags;

        var updatedTask = await _taskRepository.UpdateAsync(existingTask);
        
        _logger.LogInformation("Updated task with ID {TaskId}", id);

        return Ok(MapToResponseDto(updatedTask));
    }

    /// <summary>
    /// Delete a task
    /// </summary>
    [HttpDelete("{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> DeleteTask(int id)
    {
        var deleted = await _taskRepository.DeleteAsync(id);
        
        if (!deleted)
        {
            return NotFound(new { message = $"Task with ID {id} not found" });
        }

        _logger.LogInformation("Deleted task with ID {TaskId}", id);

        return NoContent();
    }

    /// <summary>
    /// Get tasks assigned to current user
    /// </summary>
    [HttpGet("my-tasks")]
    [ProducesResponseType(typeof(List<TaskResponseDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<List<TaskResponseDto>>> GetMyTasks()
    {
        var userId = User.Identity?.Name ?? "anonymous";
        var tasks = await _taskRepository.GetTasksByUserAsync(userId);
        
        return Ok(tasks.Select(MapToResponseDto).ToList());
    }

    private static TaskResponseDto MapToResponseDto(TaskItem task)
    {
        return new TaskResponseDto(
            task.Id,
            task.Title,
            task.Description,
            task.Status,
            task.Priority,
            task.CreatedAt,
            task.DueDate,
            task.CompletedAt,
            task.CreatedBy,
            task.AssignedTo,
            task.Tags,
            task.Project != null ? new ProjectSummaryDto(task.Project.Id, task.Project.Name) : null
        );
    }
}

Step 6: Configure Program.cs

// src/TaskManagementAPI.API/Program.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using TaskManagementAPI.Core.Interfaces;
using TaskManagementAPI.Infrastructure.Data;
using TaskManagementAPI.Infrastructure.Repositories;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();

// Configure database
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Register repositories
builder.Services.AddScoped<ITaskRepository, TaskRepository>();

// Configure Swagger/OpenAPI
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "Task Management API",
        Version = "v1",
        Description = "A comprehensive RESTful API for task management",
        Contact = new OpenApiContact
        {
            Name = "Your Name",
            Email = "your.email@example.com"
        }
    });

    // Add XML comments support
    var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
});

// Configure CORS
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

// Add logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "Task Management API v1");
        c.RoutePrefix = string.Empty; // Serve Swagger UI at root
    });
}

app.UseHttpsRedirection();

app.UseCors("AllowAll");

app.UseAuthorization();

app.MapControllers();

// Seed database on startup
using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    dbContext.Database.Migrate();
}

app.Run();

Step 7: Test the API

Run the Application

cd src/TaskManagementAPI.API
dotnet run

The API will start on https://localhost:7001 and Swagger UI will be available at the root URL.

Test Endpoints with PowerShell

# Create a task
$createTaskBody = @{
    title = "Implement user authentication"
    description = "Add JWT authentication to the API"
    priority = "High"
    dueDate = "2025-03-15T00:00:00Z"
    projectId = 1
    tags = @("authentication", "security")
} | ConvertTo-Json

Invoke-RestMethod -Uri "https://localhost:7001/api/tasks" `
    -Method Post `
    -Body $createTaskBody `
    -ContentType "application/json" `
    -SkipCertificateCheck

# Get all tasks
Invoke-RestMethod -Uri "https://localhost:7001/api/tasks" `
    -Method Get `
    -SkipCertificateCheck

# Get task by ID
Invoke-RestMethod -Uri "https://localhost:7001/api/tasks/1" `
    -Method Get `
    -SkipCertificateCheck

# Update task
$updateTaskBody = @{
    status = "InProgress"
    assignedTo = "john.doe@example.com"
} | ConvertTo-Json

Invoke-RestMethod -Uri "https://localhost:7001/api/tasks/1" `
    -Method Put `
    -Body $updateTaskBody `
    -ContentType "application/json" `
    -SkipCertificateCheck

# Filter tasks by status
Invoke-RestMethod -Uri "https://localhost:7001/api/tasks?status=InProgress" `
    -Method Get `
    -SkipCertificateCheck

# Delete task
Invoke-RestMethod -Uri "https://localhost:7001/api/tasks/1" `
    -Method Delete `
    -SkipCertificateCheck

Step 8: Add Input Validation

Install FluentValidation

// src/TaskManagementAPI.API/Validators/TaskCreateDtoValidator.cs
using FluentValidation;
using TaskManagementAPI.Core.DTOs;

namespace TaskManagementAPI.API.Validators;

public class TaskCreateDtoValidator : AbstractValidator<TaskCreateDto>
{
    public TaskCreateDtoValidator()
    {
        RuleFor(x => x.Title)
            .NotEmpty().WithMessage("Title is required")
            .MaximumLength(200).WithMessage("Title cannot exceed 200 characters");

        RuleFor(x => x.Description)
            .MaximumLength(2000).WithMessage("Description cannot exceed 2000 characters")
            .When(x => !string.IsNullOrEmpty(x.Description));

        RuleFor(x => x.DueDate)
            .GreaterThan(DateTime.UtcNow).WithMessage("Due date must be in the future")
            .When(x => x.DueDate.HasValue);

        RuleFor(x => x.Tags)
            .Must(tags => tags == null || tags.Count <= 10)
            .WithMessage("Cannot have more than 10 tags")
            .Must(tags => tags == null || tags.All(t => t.Length <= 50))
            .WithMessage("Tag length cannot exceed 50 characters");
    }
}

Register Validators in Program.cs

// Add to Program.cs
using FluentValidation;
using FluentValidation.AspNetCore;

builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<TaskCreateDtoValidator>();

Step 9: Add Error Handling Middleware

// src/TaskManagementAPI.API/Middleware/ErrorHandlingMiddleware.cs
using System.Net;
using System.Text.Json;

namespace TaskManagementAPI.API.Middleware;

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorHandlingMiddleware> _logger;

    public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred");
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var code = HttpStatusCode.InternalServerError;
        var result = string.Empty;

        switch (exception)
        {
            case ArgumentException:
                code = HttpStatusCode.BadRequest;
                break;
            case KeyNotFoundException:
                code = HttpStatusCode.NotFound;
                break;
            case UnauthorizedAccessException:
                code = HttpStatusCode.Unauthorized;
                break;
        }

        result = JsonSerializer.Serialize(new
        {
            error = exception.Message,
            statusCode = (int)code,
            timestamp = DateTime.UtcNow
        });

        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;

        return context.Response.WriteAsync(result);
    }
}

// Add extension method
public static class ErrorHandlingMiddlewareExtensions
{
    public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ErrorHandlingMiddleware>();
    }
}

Add to Program.cs:

app.UseErrorHandling(); // Add before UseAuthorization()

Step 10: Deploy to Azure

Create Azure Resources

# Variables
$resourceGroup = "rg-taskapi"
$location = "eastus"
$appServicePlan = "asp-taskapi"
$webAppName = "taskapi-$(Get-Random -Minimum 1000 -Maximum 9999)"
$sqlServer = "sql-taskapi-$(Get-Random -Minimum 1000 -Maximum 9999)"
$sqlDatabase = "TaskManagementDB"
$sqlAdmin = "sqladmin"
$sqlPassword = "P@ssw0rd$(Get-Random -Minimum 1000 -Maximum 9999)!"

# Create resource group
az group create --name $resourceGroup --location $location

# Create SQL Server
az sql server create `
    --name $sqlServer `
    --resource-group $resourceGroup `
    --location $location `
    --admin-user $sqlAdmin `
    --admin-password $sqlPassword

# Configure firewall to allow Azure services
az sql server firewall-rule create `
    --resource-group $resourceGroup `
    --server $sqlServer `
    --name AllowAzureServices `
    --start-ip-address 0.0.0.0 `
    --end-ip-address 0.0.0.0

# Create SQL Database
az sql db create `
    --resource-group $resourceGroup `
    --server $sqlServer `
    --name $sqlDatabase `
    --service-objective S0

# Create App Service Plan
az appservice plan create `
    --name $appServicePlan `
    --resource-group $resourceGroup `
    --sku B1 `
    --is-linux

# Create Web App
az webapp create `
    --resource-group $resourceGroup `
    --plan $appServicePlan `
    --name $webAppName `
    --runtime "DOTNETCORE:8.0"

# Get connection string
$connectionString = az sql db show-connection-string `
    --client ado.net `
    --server $sqlServer `
    --name $sqlDatabase | ConvertFrom-Json

$connectionString = $connectionString.Replace("<username>", $sqlAdmin).Replace("<password>", $sqlPassword)

# Set connection string in Web App
az webapp config connection-string set `
    --resource-group $resourceGroup `
    --name $webAppName `
    --settings DefaultConnection=$connectionString `
    --connection-string-type SQLAzure

Write-Host "Web App URL: https://$webAppName.azurewebsites.net"

Publish Application

# Navigate to API project
cd src/TaskManagementAPI.API

# Publish for Linux
dotnet publish -c Release -o ./publish

# Create deployment package
Compress-Archive -Path ./publish/* -DestinationPath ./publish.zip -Force

# Deploy to Azure
az webapp deployment source config-zip `
    --resource-group $resourceGroup `
    --name $webAppName `
    --src ./publish.zip

Write-Host "Deployment complete! API available at: https://$webAppName.azurewebsites.net"

Best Practices

1. API Design

  • Use plural nouns for resource names (/tasks, not /task)
  • Use HTTP verbs correctly (GET, POST, PUT, DELETE)
  • Return appropriate status codes (200, 201, 400, 404, 500)
  • Version your API (/api/v1/tasks)
  • Include pagination for list endpoints

2. Security

  • Always validate input data
  • Use HTTPS in production
  • Implement authentication and authorization (JWT, OAuth2)
  • Sanitize error messages (don't expose stack traces)
  • Use parameterized queries to prevent SQL injection
  • Implement rate limiting

3. Performance

  • Use async/await for all I/O operations
  • Implement caching where appropriate
  • Add database indexes on frequently queried columns
  • Use projection (Select) to return only needed fields
  • Implement pagination for large datasets

4. Error Handling

  • Use global exception handling middleware
  • Return consistent error response format
  • Log all errors with correlation IDs
  • Provide meaningful error messages to clients

5. Documentation

  • Generate OpenAPI/Swagger documentation
  • Add XML comments to controllers and models
  • Include example requests/responses
  • Document authentication requirements

6. Testing

  • Write unit tests for business logic
  • Implement integration tests for API endpoints
  • Use in-memory database for testing
  • Test error scenarios and edge cases

Key Takeaways

  1. ASP.NET Core provides a powerful, performant framework for building RESTful APIs
  2. Entity Framework Core simplifies data access with LINQ and automatic migrations
  3. Repository pattern separates data access logic from business logic
  4. DTOs decouple API models from database entities
  5. Swagger/OpenAPI provides automatic API documentation and testing interface
  6. Dependency injection enables testable, maintainable code
  7. Middleware centralizes cross-cutting concerns like error handling and logging

Additional Resources

Next Steps

Enhance your API:

  • Add JWT authentication and user management
  • Implement versioning (API v1, v2)
  • Add response compression
  • Implement caching with Redis
  • Add real-time updates with SignalR
  • Create a frontend with Blazor or React
  • Implement background jobs with Hangfire
  • Add Application Insights for monitoring

Ready to build production-ready APIs? Start with this foundation and extend it with authentication, caching, and advanced features. Share your API projects and experiences!