Home / .NET / Getting Started with ASP.NET Core Web APIs (2025-02-03) [2025-02-03-aspnet-core-web-apis-restful-services-made-easy]
.NET

Getting Started with ASP.NET Core Web APIs (2025-02-03) [2025-02-03-aspnet-core-web-apis-restful-services-made-easy]

RESTful APIs are the backbone of modern web applications, enabling seamless communication between clients and servers across platforms. ASP.NET Core...

What you will learn

Practical execution with concise explanations, real implementation patterns, and production-ready recommendations.

  • .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:

Architecture Overview: TaskManagementAPI

```bash

## Install Required NuGet Packages

```powershell




## 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
{
```text
public int Id { get; set; }

public string Title { get; set; } = string.Empty;

public string? Description { get; set; }

public TaskStatus Status { get; set; } = TaskStatus.;

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
{
```text
 = 0,
InProgress = 1,
Done = 2,
Blocked = 3```
}

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

Project Entity

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

public class Project
{
```text
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(
```text
string Title,
string? Description,
TaskPriority Priority,
DateTime? DueDate,
int? ProjectId,
List<string>? Tags```
);

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

public record TaskResponseDto(
```text
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(
```text
int Id,
string Name```
);

public record TaskFilterDto(
```text
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
{
```text
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>
{
```javascript
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
{
```javascript
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
{
```csharp
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": {
```text
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=TaskManagementDB;Trusted_Connection=True;MultipleActiveResultSets=true"```
  },
  "Logging": {
```text
"LogLevel": {
  "Default": "Information",
  "Microsoft.AspNetCore": "Warning",
  "Microsoft.EntityFrameworkCore": "Information"
}```
  },
  "AllowedHosts": "*",
  "JwtSettings": {
```text
"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"





Expected output:

Build succeeded.
Done. To undo this action, use 'ef migrations remove'

Terminal output for dotnet ef migrations

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
{
```csharp
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.
    };

    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

Step 6: Configure Program.cs

Figure: Configuration and management dashboard with status overview.

// 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 =>
```text
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 = "Vladimir Luis",
        Email = "your.email@contoso.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 =>
{
```javascript
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())
{
```javascript
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())
{
```text
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

Expected output:

info: Now listening on: https://localhost:5001
info: Application started. Press Ctrl+C to shut down.

Terminal output for 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 = @{
```text
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" `
```text
-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@contoso.com"```
} | ConvertTo-Json





Invoke-RestMethod -Uri "https://localhost:7001/api/tasks/1" `
```text
-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

Delete task

Figure: Planner board – task buckets, assignments, and progress charts.

Invoke-RestMethod -Uri "https://localhost:7001/api/tasks/1" `

-Method Delete `
-SkipCertificateCheck





## Step 8: Add Input Validation

### Install FluentValidation





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

namespace TaskManagementAPI.API.Validators;

public class TaskCreateDtoValidator : AbstractValidator<TaskCreateDto>
{
```javascript
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
{
```csharp
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
{
```text
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 `
```text
--name $sqlServer `
--resource-group $resourceGroup `
--location $location `
--admin-user $sqlAdmin `
--admin-password $sqlPassword

Expected output:

{ "name": "rg-myapp-prod", "location": "eastus2", "properties": { "provisioningState": "Succeeded" } }

Terminal output for az group create

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("", $sqlAdmin).Replace("", $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

```powershell





## 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 `
```text
--resource-group $resourceGroup `
--name $webAppName `
--src ./publish.zip

Expected output:

MyApp.Api -> /src/MyApp.Api/bin/Release/net8.0/publish/

Terminal output for dotnet publish

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

## Architecture Decision and Tradeoffs

When designing application development solutions with .NET, consider these key architectural trade-offs:

| Approach | Best For | Tradeoff |
|----------|----------|----------|
| Managed / platform service | Rapid delivery, reduced ops burden | Less customisation, potential vendor lock-in |
| Custom / self-hosted | Full control, advanced tuning | Higher operational overhead and cost |

> **Recommendation:** Start with the managed approach for most workloads and move to custom only when specific requirements demand it.

## Validation and Versioning

- Last validated: April 2026
- Validate examples against your tenant, region, and SKU constraints before production rollout.
- Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.

## Security and Governance Considerations

- Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
- Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
- Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.

## Cost and Performance Notes

- Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
- Baseline performance with synthetic and real-user checks before and after major changes.
- Scale resources with measured thresholds and revisit sizing after usage pattern changes.

## Official Microsoft References

- https://learn.microsoft.com/dotnet/
- https://learn.microsoft.com/aspnet/core/
- https://learn.microsoft.com/azure/developer/dotnet/

## Public Examples from Official Sources

- These examples are sourced from official public Microsoft documentation and sample repositories.
- Documentation examples: https://learn.microsoft.com/dotnet/
- Sample repositories: https://github.com/dotnet/samples
- Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.

## 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

- [ASP.NET Core Documentation](https://docs.microsoft.com/aspnet/core/)
- [EF Core Documentation](https://docs.microsoft.com/ef/core/)
- [REST API Best Practices](https://docs.microsoft.com/azure/architecture/best-practices/api-design)
- [Swagger/OpenAPI Specification](https://swagger.io/specification/)
- [FluentValidation Documentation](https://docs.fluentvalidation.net/)
- [Azure App Service Documentation](https://docs.microsoft.com/azure/app-service/)


## 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!*

Discussion