2
0

Initial exercise setup.

Additions

* Instructions for manually starting the database.
* Each phase now has a success criteria with how to verify.
* Troubleshooting section for working with the Docker DB container.
* All phases of the exercise are described in README.md (with as little advanced hints as possible).
* DFD to illustrate phase 1
* Debugging starts the database container and applies schema migrations (no setup required).
* Launch settings for VS Code, VS 2022 and Rider (IntelliJ IDEA).
* Swagger UI is available at <http://localhost:8080/swagger/index.html> for testing.
This commit is contained in:
2025-06-26 11:32:01 +02:00
parent 5a6009b76d
commit cf91281a51
21 changed files with 504 additions and 0 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
Dockerfile
docker-compose.yaml
[b|B]in
[O|o]bj

77
.gitignore vendored
View File

@@ -12,3 +12,80 @@
# Built Visual Studio Code Extensions
*.vsix
**/[b|B]in
**/[O|o]bj
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
*.env
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
[Aa][Rr][Mm]64[Ee][Cc]/
bld/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Build results on 'Bin' directories
**/[Bb]in/*
# Uncomment if you have tasks that rely on *.refresh files to move binaries
# (https://github.com/github/gitignore/pull/3736)
#!**/[Bb]in/*.refresh
# Visual Studio 2015/2017 cache/options directory
.vs/
# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries

13
.idea/.idea.service/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/contentModel.xml
/projectSettingsUpdater.xml
/.idea.service.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
.idea/.idea.service/.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
service

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

6
.idea/.idea.service/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

8
Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
# Normally, we would use a multi-stage build, but for simplicity, we are using a single stage here.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore \
&& dotnet publish -c release -o /app
WORKDIR /app
ENTRYPOINT ["dotnet", "service.dll"]

136
README.md
View File

@@ -1,2 +1,138 @@
# dotnet-interview-exercise
This exercise is designed to test your skills in C# and .NET.
## Technologies used in this exercise
* .NET 8.0 / C#
* Minimal ASP.NET Core Web API
* Entity Framework Core
* PostgreSQL Database (Docker)
* HttpClient for downstream REST API calls
* ???
## Prerequisites
* Latest [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
* Visual Studio Code
* Docker Desktop and docker compose plugin
* **Disable Copilot completions** (that would make it too easy...)
* Be ready for sharing your screen in Zoom
* Launch settings have been configured to run and debug the web application
## Exercise
### Phase 1
Note: For all API and schemas, see <https://jsonplaceholder.typicode.com/>.
* Implement the missing code in JsonPlaceholderClient
* Setup of the client.
* Method for fetching one post from the external API at `https://jsonplaceholder.typicode.com/posts/{id}`.
* In our endpoint handler, fetch the post with the ID provided.
* Store the post received in the database using the `Post` entity, but leave out `UpdatedAt` for now.
* Use <http://localhost:8080/swagger/index.html> for testing.
```mermaid
sequenceDiagram
title Overview
actor B as Swagger UI
participant S as Service
participant E as External Service
participant D as Database
B->>S: GetPostById
S<<->>E: GetPostById
S->>D: Store Post
S->>B: Return Post
```
**Success Criteria: Verify that the post has been saved with the database with:**
```bash
$ docker compose exec --env PGPASSWORD=password db psql -h localhost -U user --db mydatabase -c "select * from posts;"
id | user_id | title | body | updated_at
----+---------+----------------------------------------------------------------------------+-----------------------------------------------------+------------------------
1 | 1 | sunt aut facere repellat provident occaecati excepturi optio reprehenderit | quia et suscipit +| 1970-01-01 00:00:00+00
| | | suscipit recusandae consequuntur expedita et cum +|
| | | reprehenderit molestiae ut ut quas totam +|
| | | nostrum rerum est autem sunt rem eveniet architecto |
(1 row)
```
### Phase 2
* Check whether we already have the post in the database.
* If we do, return the post from the database, but without the internal `updated_at` timestamp.
* If we don't, fetch the post from the external API and store it in the database, and return it.
* **Success Criteria: Check the service logs to see if the post was read from the database, or whether the client made a call to the downstream service.**
### Phase 3
* Set the `UpdatedAt` property of the `Post` entity to the current date and time when storing it in the database.
* If the post in the database has been updated less than 1 minute ago, return the one from the database.
* If the post in the database is older than 1 minute (extra short value for testing), fetch the post from the external API again and update it in the database before returning.
* **Success Criteria: Check the service logs to see if the post was read from the database, or whether the client made a call to the downstream service.**
### Phase 4
* Think about how we could improve resiliency in downstream API calls.
* For setup, pull in the changes from the `phase4` branch.
* It adds chaos-injecting code in `Program.cs` to simulate random failures in downstream API calls.
* Execute a couple of requests with increasing ids until an error happens.
* Implement resiliency measures to handle these kinds of failures.
* **Success Criteria: Check the service logs to see the resiliency measures in action.**
### Phase 5
* Now increase the chaos outcomes rate of 503 to 95% to simulate a partial outage. What is the outcome on our endpoint?
* Improve the error response returned by handling the downstream error accordingly.
* **Success Criteria: Service should return the appropriate error response that warrants retrying the call from the client side.**
### Bonus Phase 6 (discussion only)
* The current DB schema solution has big drawbacks. What are they?
* How would you implement schema migrations that avoid these issues?
* What do you need to consider in the setup for proper schema migrations?
### Bonus Phase 7 (discussion only)
* Imagine the following deployment scenario:
* The service is deployed to and running in the Elastic Container Service (ECS) in Amazon AWS
* The downstream service requires authentication
* How would you store and retrieve the required credentials?
* What options do you see to also require authentication on our little service?
## Troubleshooting
### Start database manually
Open a terminal at the repo root and execute
```bash
$ docker compose up -d db
[+] Running 3/3
✔ Network dotnet-interview-exercise_default Created 0.0s
✔ Volume "dotnet-interview-exercise_db_data" Created 0.0s
✔ Container dotnet-interview-exercise-db-1 Started 0.1s
```
### Cleanup database
In case something went wrong, wipe and rebuild the database
```bash
$ docker compose down -v
[+] Running 3/3
✔ Container dotnet-interview-exercise-db-1 Removed. 0.2s
✔ Network dotnet-interview-exercise_default Removed. 0.2s
✔ Volume dotnet-interview-exercise_db_data Removed. 0.0s
$ docker compose up -d db
[+] Running 3/3
✔ Network dotnet-interview-exercise_default Created 0.0s
✔ Volume "dotnet-interview-exercise_db_data" Created 0.0s
✔ Container dotnet-interview-exercise-db-1 Started 0.1s
```

View File

@@ -0,0 +1,7 @@
-- Schema generated from this sample: https://jsonplaceholder.typicode.com/posts/1
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL
);

View File

@@ -0,0 +1,3 @@
-- This SQL script adds a new column 'updated_at' to the 'posts' table.
ALTER TABLE posts
ADD COLUMN updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP;

36
docker-compose.yaml Normal file
View File

@@ -0,0 +1,36 @@
services:
service:
build:
context: .
dockerfile: Dockerfile
depends_on:
- db
networks:
- default
ports:
- "8080:8080"
environment:
ASPNETCORE_ENVIRONMENT: Docker
db:
image: postgres:alpine
restart: unless-stopped
networks:
- default
ports:
- "5432:5432" # Also for local development
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydatabase
volumes:
- db_data:/var/lib/postgresql/data
- ./dbschemas:/docker-entrypoint-initdb.d
networks:
default:
driver: bridge
volumes:
db_data:
driver: local

38
service.sln Normal file
View File

@@ -0,0 +1,38 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "service", "service\service.csproj", "{8572A3DF-7A4C-4552-8B30-22D794E703A6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
ProjectSection(SolutionItems) = preProject
docker-compose.yaml = docker-compose.yaml
Dockerfile = Dockerfile
README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dbschemas", "dbschemas", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
dbschemas\01-create-posts-table.sql = dbschemas\01-create-posts-table.sql
dbschemas\02-posts-add-updated.sql = dbschemas\02-posts-add-updated.sql
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8572A3DF-7A4C-4552-8B30-22D794E703A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8572A3DF-7A4C-4552-8B30-22D794E703A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8572A3DF-7A4C-4552-8B30-22D794E703A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8572A3DF-7A4C-4552-8B30-22D794E703A6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DD652F00-A6DC-4579-AF4C-12EA8B5C7ECC}
EndGlobalSection
EndGlobal

10
service/AppDbContext.cs Normal file
View File

@@ -0,0 +1,10 @@
using Microsoft.EntityFrameworkCore;
namespace service;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Post> Posts { get; set; }
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace service;
public class JsonPlaceholderClient
{
private readonly HttpClient _client;
public JsonPlaceholderClient(HttpClient client)
{
_client = client;
// TODO: Configure the client as needed.
}
public async Task<PostModel?> GetPostByIdAsync(int id, CancellationToken ct = default)
{
// TODO: Implement the logic to call the external service and retrieve the post by ID.
throw new NotImplementedException("This method will be implemented in Phase 1.");
}
}

27
service/Post.cs Normal file
View File

@@ -0,0 +1,27 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace service;
[Table("posts")]
public class Post
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("user_id")]
public int UserId { get; set; }
[Column("title")]
[Required]
public string Title { get; set; }
[Column("body")]
[Required]
public string Body { get; set; }
[Column("updated_at")]
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.Parse("1970-01-01T00:00:00Z"); // TODO: Remove the default value in Phase 3.
}

12
service/PostModel.cs Normal file
View File

@@ -0,0 +1,12 @@
using System;
namespace service;
[Serializable]
public class PostModel
{
public int Id { get; set; }
public int UserId { get; set; }
public string Title { get; set; }
public string Body { get; set; }
}

46
service/Program.cs Normal file
View File

@@ -0,0 +1,46 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Polly.Simmy;
using service;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var services = builder.Services;
services.AddRequestTimeouts();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddDbContext<AppDbContext>(optionsBuilder =>
{
// Configure the database connection string.
var connectionString = builder.Configuration.GetValue<string>("PostgresConnection");
Console.WriteLine($"Connecting to PostgreSQL database with connection string: {connectionString}");
optionsBuilder.UseNpgsql(connectionString);
});
var httpClientBuilder = services.AddHttpClient<JsonPlaceholderClient>();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(); // Swagger UI is available at http://localhost:8080/swagger/index.html
app.UseRequestTimeouts();
app.MapGet("/posts/{id}", async (AppDbContext dbContext, JsonPlaceholderClient client, int id) =>
{
// TODO: (Phase 1) Implement the logic to retrieve a post by ID and store it in the database.
// Consider some minimal error handling in case the post is not found (e.g. Id > 100).
return Results.Ok();
})
.WithRequestTimeout(TimeSpan.FromSeconds(29))
.WithName("GetPostById")
.WithOpenApi();
app.Run();

View File

@@ -0,0 +1,14 @@
{
"profiles": {
"service": {
"commandName": "Project",
"launchBrowser": true,
"useSSL": false,
"launchUrl": "http://localhost:8080/swagger/index.html",
"applicationUrl": "http://localhost:8080/swagger/index.html",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
}
}
}

View File

@@ -0,0 +1,3 @@
{
"PostgresConnection": "Server=db;Database=mydatabase;User Id=user;Password=password;"
}

11
service/appsettings.json Normal file
View File

@@ -0,0 +1,11 @@
{
"Urls": "http://0.0.0.0:8080",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"PostgresConnection": "Server=localhost;Database=mydatabase;User Id=user;Password=password;"
}

20
service/service.csproj Normal file
View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.6.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="docker compose up -d db" />
</Target>
</Project>