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
- ASP.NET Core provides a powerful, performant framework for building RESTful APIs
- Entity Framework Core simplifies data access with LINQ and automatic migrations
- Repository pattern separates data access logic from business logic
- DTOs decouple API models from database entities
- Swagger/OpenAPI provides automatic API documentation and testing interface
- Dependency injection enables testable, maintainable code
- Middleware centralizes cross-cutting concerns like error handling and logging
Additional Resources
- ASP.NET Core Documentation
- EF Core Documentation
- REST API Best Practices
- Swagger/OpenAPI Specification
- FluentValidation Documentation
- Azure App Service Documentation
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!