System architecture.
I'm Amir and I developed this project myself. On this page I briefly explain how the application is built behind the scenes — from the data model to delivery in the browser. All figures on this page are calculated directly from the code when the application starts, not entered by hand.
Tools
Tech stack.
Everything listed here is actually in use — verified against Program.cs, the .csproj files and the Dockerfile.
Backend
Database
Security
Frontend
Integrations
DevOps
Core
The four layers.
Each layer only knows the layer directly below it.
WebApplication3 Presentation
This is where my controllers, views and view components live. I separated the admin area (Areas/Admin) completely from the public site and secured it with role-based authorisation.
Dependencies: only → Business Logic
public class CvController : Controller
{
private readonly CvManager _cvmanager;
public CvController(CvManager cvmanager)
{
_cvmanager = cvmanager;
}
public IActionResult Index()
{
var value = _cvmanager.ReadAll()
.Where(x => x.Status == true)
.FirstOrDefault();
if (value != null)
{
ViewBag.Name = value.Name;
ViewBag.Path = value.Picture_Path;
}
return View();
}
}
Flow
The journey of a request.
What actually happens when a page of this application is opened in the browser.
Proof, not an example
This is what it looks like in code.
No pseudocode — real, shortened excerpts from this very project, following the same example: GET /Cv/Index.
public class CvController : Controller
{
private readonly CvManager _cvmanager;
public CvController(CvManager cvmanager)
{
_cvmanager = cvmanager;
}
public IActionResult Index()
{
var value = _cvmanager.ReadAll()
.Where(x => x.Status == true)
.FirstOrDefault();
if (value != null)
{
ViewBag.Name = value.Name;
ViewBag.Path = value.Picture_Path;
}
return View();
}
}
public class CvManager : ICvService
{
ICvDAL _DLL;
public CvManager(ICvDAL dLL)
{
_DLL = dLL;
}
public List<Cv> ReadAll() => _DLL.ReadAll();
public Cv ReadById(int t) => _DLL.ReadById(t);
public void Create(Cv t) => _DLL.Create(t);
public void Update(Cv t) => _DLL.Update(t);
public void Delate(Cv t) => _DLL.Delate(t);
}
public class GenericRepository<T> : IGenericDAL<T> where T : class
{
private readonly AddDbContext _context;
public GenericRepository(AddDbContext context)
{
_context = context;
}
public void Create(T t)
{
_context.Add(t);
_context.SaveChanges();
}
public List<T> ReadAll() => _context.Set<T>().ToList();
public T ReadById(int id) => _context.Set<T>().Find(id);
public void Update(T t)
{
_context.Update(t);
_context.SaveChanges();
}
}
public class AddDbContext : IdentityDbContext<User, Roll, int>
{
public AddDbContext(DbContextOptions<AddDbContext> options,
IHttpContextAccessor? httpContextAccessor = null)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Viele-zu-viele zwischen ProjectDetail und Technicalskills
modelBuilder.Entity<ProjectDetail>()
.HasMany(pd => pd.TechStack)
.WithMany(t => t.projectDetails);
}
public DbSet<Project> projects { get; set; }
public DbSet<Cv> cvs { get; set; }
// … 19 weitere DbSets, ein Set pro Entity
}
builder.Services.AddDbContext<AddDbContext>(options =>
{
options.UseSqlite($"Data Source={dbPath}");
});
builder.Services.AddIdentity<User, Roll>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 4;
})
.AddEntityFrameworkStores<AddDbContext>()
.AddDefaultTokenProviders();
// Eine generische Registrierung deckt GenericRepository<T>
// für ALLE Entities gleichzeitig ab:
builder.Services.AddScoped(typeof(IGenericDAL<>), typeof(GenericRepository<>));
builder.Services.AddScoped<ICvDAL, EfCvDAL>();
builder.Services.AddScoped<CvManager>();
Project structure
This is what it looks like in practice.
The solution consists of 4 projects — a classic .NET n-tier layout. Folders can be expanded.
-
- Project.cs, Cv.cs, BlogPost.cs, Certificate.cs, Video.cs …
-
- Abstract/ I*DAL interfaces
- EntityFramework/ Ef*DAL implementations
- Repository/GenericRepository.cs
- Migrations/ 31 EF Core migrations
-
- Abstract/ I*Service interfaces
- Concrete/ *Manager implementations
-
- Controllers/ 18 public controllers
- Views/ + ViewComponents/ 24 reusable components
- Areas/Admin/ role-protected CMS, 56 controllers
- Infrastructure/ Roles.cs, SlugHelper.cs, ArchitectureStatsService.cs …
- Services/ GeminiTextService, CloudflareImageService, GitHubApiClientService
- TagHelpers/ PermissionTagHelper.cs
- dockerfile Multi-stage build (with FFmpeg) for deployment on Render
- Databases/main.db SQLite
Protection
Security.
Only mechanisms that are actually in the code — no wish list.
ASP.NET Identity
Login, registration and password hashing run on ASP.NET Core Identity, not on a home-made solution.
AddIdentity<User, Roll>()Role-based authorisation
Four roles — SuperAdmin, Admin, Blogger, User — control access to every admin action via [Authorize(Roles=…)].
Infrastructure/Roles.csClaim-based UI permissions
A custom TagHelper hides or disables UI elements when the user lacks the matching permission claim.
TagHelpers/PermissionTagHelper.csAnti-forgery tokens
Forms that change data are protected with CSRF tokens.
[ValidateAntiForgeryToken] (7 files)Secure file uploads
Uploads are checked against a whitelist of file extensions and content types, limited to 5 MB, and images are re-encoded on the server.
Areas/Admin/Models/PicSave.csWhat else is inside
Special features.
A few parts of the application that go beyond a plain CRUD CMS.
Admin CMS
56 separate admin controllers — I maintain every piece of content on this site (projects, CV, blog, links, skills …) myself, without touching the database by hand.
Areas/Admin/Controllers/AI text generation (Gemini)
In the blog editor I can have drafts generated via the Gemini API and then revise them by hand.
Services/GeminiTextService.csAI cover images (Cloudflare)
Cover images for blog posts can be generated with Cloudflare Workers AI.
Services/CloudflareImageService.csLive GitHub sync
My real repositories are fetched via the GitHub API and synchronised in the admin area.
Services/GitHubApiClientService.csScroll-driven hero video
On the About me page, a video scrubs in sync with the scroll progress (GSAP ScrollTrigger).
AboutmevideopartialActivity log with auto-cleanup
Admin actions are logged; when the application starts, entries older than 6 months are removed automatically.
Program.cs (startup) + ActivityLogReflection
Decisions & lessons learned.
Honest, not sugar-coated — including the parts I would build differently today.
Decisions
- Why 4 separate projects instead of one? — Entities, DataAccess, BusinessLagiclayer and WebApplication3 are four separate .csproj files, not just folders. This enforces the direction of dependencies technically as well — WebApplication3 cannot accidentally bypass DataAccess and access the database directly.
- Why repository + service instead of EF Core directly in the controller? — GenericRepository<T> covers CRUD for all 26 entities with a single class. The *Manager layer above it leaves room for business logic that goes beyond plain reading/writing (e.g. AddOrUpdateDetailToProject with tech-stack matching).
- Why SQLite instead of SQL Server? — For a single-user portfolio with a manageable amount of data, an embedded file database is enough — no separate database server, easier to deploy and back up (main.db can simply be copied).
- Where the trade-off lies — GenericRepository<T> is deliberately kept simple (Create/ReadAll/ReadById/Update/Delate). More complex queries (e.g. GetProjectWithDetails with several includes) therefore live directly in the respective Ef*DAL classes, not in the generic base class.
What I learned
- Migrations early and often — 13 small, named migrations are easier to follow and, if in doubt, to roll back than a few huge ones.
- Typos in names are expensive — I have since corrected EntityFramwork → EntityFramework (50 affected files) — BusinessLagiclayer still carries its typo. A single misspelled namespace quickly spreads through the whole solution.
- Two permission systems in parallel — role-based ([Authorize(Roles=…)]) for page/action access and claim-based (PermissionTagHelper) for individual UI elements have different jobs — but maintaining both also takes a bit more discipline.
- Public folders really are public — wwwroot is served completely without authentication — any file that ends up there by mistake (e.g. an appsettings.json) is immediately reachable on the web.
Next steps
- Done Security review: files accidentally placed under wwwroot identified and removed
- Planned Upgrade from .NET 6 (out of support) to .NET 8 or a newer LTS
- Planned Unit tests (xUnit) — there is currently no test project in the solution
- Planned CI/CD with GitHub Actions — the workflow folder already exists, but without a workflow file yet
- Done Docker deployment — a multi-stage Dockerfile is in production use (Render)
Questions about the implementation?
I'm happy to explain every decision in detail.