Project Management MVP: Day 2
Day 2: Core Data Access, Azure Cosmos DB NoSQL Architecture, & Dependency Injection
Yesterday, we laid down our baseline repository foundation: a clean monorepo housing both Angular and .NET, backed by local cross-platform emulators. Today, we move into the heart of our data tier — designing a highly optimized NoSQL schema in Azure Cosmos DB, mapping out domain models, implementing the Repository Pattern in C#, and wiring up our runtime dependencies.
Along the way, we hit a few classic compilation roadblocks. Here is how we built our data engine and overcame real-world development friction.
Strategy: Why Cosmos DB Over Relational SQL?
For a modern project management platform, choosing a NoSQL architecture over a traditional relational database (like SQL Server) provides specific engineering advantages:
Flexible JSON Modeling: Tasks aren't static. They require dynamic custom tags, variable sub-task arrays, and shifting metadata. NoSQL handles this without requiring destructive schema migrations.
Horizontal Scalability via Partitioning: Cosmos DB forces you to design for scale on day one. By deliberately picking a partition key, we guarantee our data scales horizontally across physical partitions seamlessly.
Cost Efficiency (Free Tier Optimization): Azure gives us an excellent Always Free Tier allowance of 1,000 RU/s (Request Units) of throughput and 25 GB of storage. By using a Shared Throughput Database, we can host multiple containers under a single cost envelope without spending a cent.
Step 1: Real-World Schema & Partitioning Design
To model our entities without breaking NoSQL design principles, we establish two targeted containers inside our shared-throughput database:
Container 1: WorkspacesAndTasks (Partition Key: /workspaceId)
We co-locate both Workspace metadata and individual Tasks inside the same container. For the Workspace document itself, its id doubles as its workspaceId.
Container 2: Users (Partition Key: /id)
Because users belong across multiple distinct workspaces globally, assigning them a workspace partition key would cause unnecessary data duplication. Isolating them into their own container makes lookup clean and direct.
AZ-204 Exam Insight: Notice how every task contains
workspaceId. By executing queries like"SELECT * FROM c WHERE c.type = 'task'"and passing theworkspaceIdas the partition key context, Azure targets a single physical partition. This avoids costly, slow cross-partition scans, making our Kanban board fetches fast and highly cost-efficient.
Step 2: C# Domain Modeling (System.Text.Json Alignment)
We built out strongly typed C# domain models to map directly to these JSON definitions. Because Cosmos DB requires a lowercase string property named "id" for item tracking, we use [JsonPropertyName] attributes to align our clean C# naming styles with NoSQL engine specifications.
The Task Model Example (Models/TaskItem.cs):
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace backend.Models
{
public class TaskItem
{
[JsonPropertyName("id")]
public string Id { get; set; } = Guid.NewGuid().ToString();
[JsonPropertyName("workspaceId")]
public string WorkspaceId { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } = "task";
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("tags")]
public List<string> Tags { get; set; } = new();
[JsonPropertyName("assignedTo")]
public string AssignedTo { get; set; }
[JsonPropertyName("subTasks")]
public List<SubTask> SubTasks { get; set; } = new();
}
}
Step 3: Implementing the Repository Layer & Clean Architecture Reorg
To keep our architecture decoupled, we don't want Cosmos SDK logic bleeding into our application API layer. We created an abstraction interface (backend.Interfaces.ITaskRepository) and isolated the concrete database implementation under an expanded Clean Architecture folder layout: backend/src/Infrastructure/Repositories/TaskRepository.cs.
Here is the decoupled implementation using the official Microsoft.Azure.Cosmos SDK:
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using backend.Interfaces;
using backend.Models;
namespace backend.Infrastructure.Repositories
{
public class TaskRepository : ITaskRepository
{
private readonly Container _container;
public TaskRepository(CosmosClient client, string databaseId, string containerId)
{
_container = client.GetContainer(databaseId, containerId);
}
// 1. Optimized Point Read (Fastest, lowest cost operation in Cosmos DB)
public async Task<TaskItem?> GetTaskAsync(string id, string workspaceId)
{
try
{
ItemResponse<TaskItem> response = await _container.ReadItemAsync<TaskItem>(
id,
new PartitionKey(workspaceId)
);
return response.Resource;
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
}
// 2. In-Partition Query (Fetches the whole Kanban board cleanly)
public async Task<IEnumerable<TaskItem>> GetTasksByWorkspaceAsync(string workspaceId)
{
var sqlQueryText = "SELECT * FROM c WHERE c.type = 'task'";
var queryDefinition = new QueryDefinition(sqlQueryText);
var queryOptions = new QueryRequestOptions { PartitionKey = new PartitionKey(workspaceId) };
using FeedIterator<TaskItem> feedIterator = _container.GetItemQueryIterator<TaskItem>(
queryDefinition,
requestOptions: queryOptions
);
var results = new List<TaskItem>();
while (feedIterator.HasMoreResults)
{
FeedResponse<TaskItem> response = await feedIterator.ReadNextAsync();
results.AddRange(response);
}
return results;
}
public async Task AddTaskAsync(TaskItem task) =>
await _container.CreateItemAsync(task, new PartitionKey(task.WorkspaceId));
public async Task AddWorkspaceAsync(Workspace workspace) =>
await _container.CreateItemAsync(workspace, new PartitionKey(workspace.WorkspaceId));
}
}
Real-World Dev Pitfalls & How We Fixed Them
Moving files around and importing SDK packages often exposes build discrepancies. Here are the two core compilation blockers we solved today:
1. The Transitive Newtonsoft Build Blocker
Upon importing Microsoft.Azure.Cosmos v3 into a modern web API, the compiler threw a major blocker:
error : The Newtonsoft.Json package must be explicitly referenced with version >= 10.0.2...
- The Fix: Even though modern .NET targets
System.Text.Jsonnatively, the underlying engine of the Cosmos v3 SDK still hooks intoNewtonsoftfor internal serialization checks. Explicitly adding the reference directly to the API viadotnet add package Newtonsoft.Jsonbypassed this build target restriction instantly.
2. The Clean Architecture Namespace Desync
When we pushed files into deeper paths (src/Infrastructure/Repositories/), our files fell out of sync, triggering a cascade of CS0234: The type or namespace name 'Models' does not exist in the namespace 'backend' errors.
- The Fix: We methodically trace-aligned our namespace ground truths. By establishing
namespace backend.Modelsat the entity layer, pulling them into our interfaces viausing backend.Models;, and linking our repository withusing backend.Interfaces;, the compiler connected the logical compilation dots perfectly.
Step 4: Bootstrapping & Dependency Injection in modern .NET 9
With a successful build locked in, we integrated our database connection string settings into appsettings.json using the universal local Cosmos DB Emulator master key, then overhauled Program.cs.
We registered the CosmosClient as a Singleton lifecycle (the gold standard for preventing socket exhaustion) and bound ITaskRepository as a Scoped service. We also activated controller routing support (AddControllers() / MapControllers()) to gracefully transition away from basic minimal API boilerplate.
using Microsoft.Azure.Cosmos;
using backend.Interfaces;
using backend.Infrastructure.Repositories;
var builder = WebApplication.CreateBuilder(args);
// Extract configuration limits securely from appsettings
var cosmosSection = builder.Configuration.GetSection("CosmosDb");
string endpointUrl = cosmosSection["EndpointUrl"] ?? throw new InvalidOperationException("Cosmos EndpointUrl missing.");
string primaryKey = cosmosSection["PrimaryKey"] ?? throw new InvalidOperationException("Cosmos PrimaryKey missing.");
string databaseName = cosmosSection["DatabaseName"] ?? throw new InvalidOperationException("Cosmos DatabaseName missing.");
string containerName = cosmosSection["ContainerName"] ?? throw new InvalidOperationException("Cosmos ContainerName missing.");
// Register unified CosmosClient instance as a Singleton
var cosmosClient = new CosmosClient(endpointUrl, primaryKey, new CosmosClientOptions
{
SerializerOptions = new CosmosSerializationOptions { PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase }
});
builder.Services.AddSingleton(cosmosClient);
// Inject Repository Abstractions
builder.Services.AddScoped<ITaskRepository>(sp =>
new TaskRepository(cosmosClient, databaseName, containerName));
builder.Services.AddControllers(); // Enables our upcoming Day 3 routing layer
builder.Services.AddOpenApi(); // Keeps our .NET 9 OpenAPI documentation intact
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.MapControllers(); // Directs incoming traffic smoothly to attribute-routed API endpoints
app.Run();
Architecture Snapshot
Our system layout is evolving predictably. Here is how our data layer separation handles dependencies now:
graph TD
A[Angular Frontend Client] -->|HTTP Calls| B[.NET Core Web API]
B -->|Interface Abstraction| C[Task/Workspace Repositories]
C -->|In-Partition Queries| D[Cosmos DB Emulator: Container Shared Throughput]
Day 2 Wrap-Up
With Day 2 behind us, our core data framework is solidified:
A scalable, dual-container Cosmos DB schema optimized for the Azure Free Tier.
An intelligent partitioning strategy (
/workspaceId) to safeguard downstream query performance.A professional, Clean Architecture C# repository structure that isolates the Cosmos SDK smoothly.
A resilient dependency pipeline verified entirely against our local emulator suite.
Coming up next on Day 3: We will construct our RESTful CRUD controller endpoints in our backend API and layer on the high-performance Cache-Aside Pattern using localized memory boundaries to mimic enterprise performance optimization. Stay tuned!