Dotnet

2025-02-03-aspnet-core-web-apis-restful-services-made-easy

---
title: "Getting Started with ASP.NET Core Web APIs (2025-02-03) [2025-02-03-aspnet-core-web-apis-restful-services-made-easy]"
date: 2025-02-03
category: .NET
tags: ".NET Core, Web API, REST, Getting Started"
author: "Vladimiro Luis"
description: "RESTful APIs are the backbone of modern web applications, enabling seamless communication between clients and servers across platforms. ASP.NET Core..."
featured: false
draft: false

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


![Should output: 8.0.x or higher](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec1-generic.jpg)

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

![Check SQL Server](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec2-database.jpg)


## Check Git
git --version

![Check Git](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec3-generic.jpg)

Step 1: Create the Project

Create Solution and Project Structure

Step 1: Create the Project

## Create solution folder
mkdir TaskManagementAPI
cd TaskManagementAPI

![Create solution folder](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec5-implementation.jpg)


## Create solution file
dotnet new sln -n TaskManagementAPI

![Create solution file](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec6-implementation.jpg)


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

![Create Web API project](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec7-implementation.jpg)


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

![Create class library for domain models](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec8-implementation.jpg)


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

![Create class library for data access](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec9-implementation.jpg)


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

![Create test project](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec10-implementation.jpg)


## 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 projects to solution](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec11-generic.jpg)


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

![Add project references](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec12-generic.jpg)

Final Project Structure:

Architecture Overview: TaskManagementAPI

```bash

## Install Required NuGet Packages

```powershell

![Install Required NuGet Packages](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec13-implementation.jpg)

## Navigate to API project
cd src/TaskManagementAPI.API

![Navigate to API project](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec51-api.jpg)


![Navigate to API project](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec25-api.jpg)


![Navigate to API project](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec14-api.jpg)


## 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 EF Core packages](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec15-implementation.jpg)


## 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 authentication packages](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec16-implementation.jpg)


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

![Install Swagger/OpenAPI](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec17-implementation.jpg)


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

![Install validation packages](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec18-implementation.jpg)


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

![Navigate to Infrastructure project](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec19-generic.jpg)


## Navigate to Core project
cd ../TaskManagementAPI.Core

![Navigate to Core project](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec20-generic.jpg)


## No additional packages needed for core domain

cd ../../

![No additional packages needed for core domain](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec21-pipeline.jpg)

Step 2: Create Domain Models

Task Entity (Core Layer)

Step 2: Create Domain Models

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

Step 3: Implement Data Access Layer

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

Step 4: Create Database with Migrations

// 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 .

![Add initial migration](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec26-database.jpg)


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

![Update database](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec27-database.jpg)


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

![Verify database creation](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec28-implementation.jpg)

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

Step 5: Build the API 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;

![Step 6: Configure Program.cs](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec30-implementation.jpg)


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

Step 7: Test the API

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

![Create a task](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec32-implementation.jpg)


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 all tasks

Get task by ID

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

-Method Get `
-SkipCertificateCheck

Get task by ID

Update task

$updateTaskBody = @{

status = "InProgress"
assignedTo = "john.doe@contoso.com"```
} | ConvertTo-Json

![Update task](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec35-generic.jpg)


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

Filter tasks by status

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

![Delete task](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec37-generic.jpg)


## Step 8: Add Input Validation

### Install FluentValidation

![Step 8: Add Input Validation](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec38-implementation.jpg)


```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;

![Step 9: Add Error Handling Middleware](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec39-implementation.jpg)


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

Step 10: Deploy to Azure

## 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)!"

![Variables](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec41-config.jpg)


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

![Create resource group](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec42-implementation.jpg)


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

Create SQL Server

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

Configure firewall to allow Azure services

Create SQL Database

az sql db create `

--resource-group $resourceGroup `
--server $sqlServer `
--name $sqlDatabase `
--service-objective S0

Create SQL Database

Create App Service Plan

az appservice plan create `

--name $appServicePlan `
--resource-group $resourceGroup `
--sku B1 `
--is-linux

Create App Service Plan

Create Web App

az webapp create `

--resource-group $resourceGroup `
--plan $appServicePlan `
--name $webAppName `
--runtime "DOTNETCORE:8.0"

Create Web App

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)

Get connection string

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"

Set connection string in Web App


## Publish Application

```powershell

![Publish Application](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec50-pipeline.jpg)


## Navigate to API project
cd src/TaskManagementAPI.API

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

![Publish for Linux](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec52-pipeline.jpg)


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

![Create deployment package](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec53-implementation.jpg)


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

Deploy to Azure


## Best Practices

### 1. **API Design**

![Best Practices](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec55-bestpractice.jpg)


- 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

![Key Takeaways](/images/articles/dotnet/2025-02-03-aspnet-core-web-apis-restful-services-made-easy-sec56-takeaway.jpg)


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

AI Assistant
AI Assistant

Article Assistant

Ask me about this article

AI
Hi! I'm here to help you understand this article. Ask me anything about the content, concepts, or implementation details.