Day 3: CRUD Endpoints & The Cache-Aside Pattern
With our Cosmos DB schema and Repository Pattern locked in, today we shift gears into building our RESTful API routing layer. This is where the backend starts to feel like a real product: we expose endpoints for Workspaces and Tasks, and we introduce a caching layer to mimic enterprise-grade performance optimization.
Why Cohesive CRUD Endpoints Matter
A project management platform lives and dies by its ability to orchestrate tasks. These operations form the transactional backbone of our Kanban board. By exposing clean REST endpoints, we make our backend seamlessly consumable by our Angular frontend.
Step 1: Task Architecture & High-Performance Point Reads
We expose our task management routing inside TasksController. Pay close attention to the GetTask route—it takes both the workspaceId and the id.
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using backend.Interfaces;
using backend.Models;
namespace backend.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class TasksController : ControllerBase
{
private readonly ITaskRepository _repository;
private readonly IMemoryCache _cache;
public TasksController(ITaskRepository repository, IMemoryCache cache)
{
_repository = repository;
_cache = cache;
}
[HttpPost]
public async Task<IActionResult> CreateTask([FromBody] TaskItem task)
{
await _repository.AddTaskAsync(task);
// 🔥 CACHE INVALIDATION: Evict the workspace cache so the Kanban board refreshes instantly
_cache.Remove($"tasks_{task.WorkspaceId}");
return CreatedAtAction(nameof(GetTask), new { id = task.Id, workspaceId = task.WorkspaceId }, task);
}
[HttpGet("{workspaceId}/{id}")]
public async Task<IActionResult> GetTask(string workspaceId, string id)
{
// AZ-204 Win: Passing both ID and PartitionKey executes an ultra-fast 1 RU Point Read
var task = await _repository.GetTaskAsync(id, workspaceId);
return task is null ? NotFound() : Ok(task);
}
}
}
Step 2: Implementing the Cache-Aside Pattern
Fetching a workspace's entire Kanban board repeatedly can heavily tax database Request Units (RUs). To save processing costs, we introduce the Cache-Aside Pattern using .NET's native IMemoryCache.
Instead of cluttering our controller with database-vs-cache switching logic, we encapsulate it into a dedicated application service layer:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using backend.Interfaces;
using backend.Models;
namespace backend.Services
{
public class WorkspaceService
{
private readonly ITaskRepository _repository;
private readonly IMemoryCache _cache;
public WorkspaceService(ITaskRepository repository, IMemoryCache cache)
{
_repository = repository;
_cache = cache;
}
public async Task<IEnumerable<TaskItem>> GetWorkspaceTasksWithCacheAsync(string workspaceId)
{
string cacheKey = $"tasks_{workspaceId}";
// Look for data in local memory first
if (!_cache.TryGetValue(cacheKey, out IEnumerable<TaskItem> tasks))
{
// Cache Miss: Fetch from Cosmos DB
tasks = await _repository.GetTasksByWorkspaceAsync(workspaceId);
// Save back to memory with a sliding expiration window
_cache.Set(cacheKey, tasks, TimeSpan.FromMinutes(5));
}
return tasks;
}
}
}
Step 3: The Unified Workspace Controller
Now, our WorkspacesController stays incredibly clean because it consumes our cached service layer directly, keeping execution speeds lightning fast:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using backend.Interfaces;
using backend.Models;
using backend.Services;
namespace backend.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class WorkspacesController : ControllerBase
{
private readonly ITaskRepository _repository;
private readonly WorkspaceService _workspaceService;
public WorkspacesController(ITaskRepository repository, WorkspaceService workspaceService)
{
_repository = repository;
_workspaceService = workspaceService;
}
[HttpPost]
public async Task<IActionResult> CreateWorkspace([FromBody] Workspace workspace)
{
await _repository.AddWorkspaceAsync(workspace);
return CreatedAtAction(nameof(GetWorkspace), new { id = workspace.WorkspaceId }, workspace);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetWorkspace(string id)
{
// Controller reads directly from the caching service orchestration layer
var tasks = await _workspaceService.GetWorkspaceTasksWithCacheAsync(id);
return Ok(new { WorkspaceId = id, Tasks = tasks });
}
}
}
Infrastructure Configuration Note: Don't forget to register memory caching and your new service inside
Program.csviabuilder.Services.AddMemoryCache();andbuilder.Services.AddScoped<WorkspaceService>();!
Architecture Snapshot
graph TD
A[Angular Frontend Client] -->|HTTP Routing| B[.NET API Controllers]
B -->|Mutations / Evictions| E[In-Memory Cache]
B -->|Reads| C[Workspace Cache Service]
C -->|Cache Hit| E
C -->|Cache Miss: Fetch SQL| D[Cosmos DB Emulator]
Day 3 Wrap-Up
By the end of Day 3, we have successfully established:
Complete, semantic RESTful CRUD controller setups for platform control routes.
An active Cache-Aside Pattern strategy mitigating database read overhead.
A proactive Cache Invalidation trigger ensuring data integrity during state mutations.
Coming up next on Day 4: We shift our focus to the client-side UI, constructing our responsive Angular frontend MVP featuring a workspace sidebar navigation layout and an interactive Kanban board interface!