- .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'
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
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.
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
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" } }
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("
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/
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