System-Architektur.
Ich bin Amir und ich habe dieses Projekt selbst entwickelt. Auf dieser Seite erkläre ich kurz, wie die Anwendung im Hintergrund aufgebaut ist — vom Datenmodell bis zur Auslieferung im Browser. Alle Zahlen auf dieser Seite werden beim Start der Anwendung direkt aus dem Code berechnet, nicht von Hand eingetragen.
Werkzeuge
Tech-Stack.
Alles, was hier steht, ist tatsächlich in Verwendung — geprüft anhand von Program.cs, den .csproj-Dateien und dem Dockerfile.
Backend
Datenbank
Sicherheit
Frontend
Integrationen
DevOps
Kernstück
Die vier Schichten.
Jede Schicht kennt nur die Schicht direkt darunter.
WebApplication3 Presentation
Hier sitzen meine Controller, Views und ViewComponents. Den Admin-Bereich (Areas/Admin) habe ich vollständig von der öffentlichen Seite getrennt und mit rollenbasierter Autorisierung abgesichert.
Abhängigkeiten: nur → 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();
}
}
Ablauf
Der Weg einer Anfrage.
Was tatsächlich passiert, wenn im Browser eine Seite dieser Anwendung aufgerufen wird.
Beweis, kein Beispiel
So sieht das im Code aus.
Kein Pseudocode — echte, gekürzte Ausschnitte aus genau diesem Projekt, entlang desselben Beispiels: 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>();
Projektstruktur
So sieht das konkret aus.
Die Solution besteht aus 4 Projekten — ein klassisches .NET-N-Schichten-Layout. Ordner lassen sich aufklappen.
-
- Project.cs, Cv.cs, BlogPost.cs, Certificate.cs, Video.cs …
-
- Abstract/ I*DAL-Interfaces
- EntityFramework/ Ef*DAL-Implementierungen
- Repository/GenericRepository.cs
- Migrations/ 31 EF-Core-Migrationen
-
- Abstract/ I*Service-Interfaces
- Concrete/ *Manager-Implementierungen
-
- Controllers/ 18 öffentliche Controller
- Views/ + ViewComponents/ 24 wiederverwendbare Komponenten
- Areas/Admin/ rollen-geschütztes CMS, 56 Controller
- Infrastructure/ Roles.cs, SlugHelper.cs, ArchitectureStatsService.cs …
- Services/ GeminiTextService, CloudflareImageService, GitHubApiClientService
- TagHelpers/ PermissionTagHelper.cs
- dockerfile Multi-Stage-Build (mit FFmpeg) für das Deployment auf Render
- Databases/main.db SQLite
Absicherung
Sicherheit.
Nur Mechanismen, die tatsächlich im Code stehen — keine Wunschliste.
ASP.NET Identity
Login, Registrierung und Passwort-Hashing laufen über ASP.NET Core Identity, nicht über eine selbstgebaute Lösung.
AddIdentity<User, Roll>()Rollenbasierte Autorisierung
Vier Rollen — SuperAdmin, Admin, Blogger, User — steuern über [Authorize(Roles=…)] den Zugriff auf jede Admin-Aktion.
Infrastructure/Roles.csClaim-basierte UI-Berechtigungen
Ein eigener TagHelper blendet UI-Elemente aus oder deaktiviert sie, wenn der Nutzer die passende Permission-Claim nicht hat.
TagHelpers/PermissionTagHelper.csAnti-Forgery-Tokens
Formulare, die Daten verändern, sind mit CSRF-Tokens abgesichert.
[ValidateAntiForgeryToken] (7 Dateien)Sichere Datei-Uploads
Uploads werden gegen eine Whitelist aus Dateiendung und Content-Type geprüft, auf 5 MB begrenzt und Bilder serverseitig neu kodiert.
Areas/Admin/Models/PicSave.csWas zusätzlich drinsteckt
Besondere Features.
Ein paar Teile der Anwendung, die über ein reines CRUD-CMS hinausgehen.
Admin-CMS
56 eigenständige Admin-Controller — ich pflege jeden Inhalt dieser Seite (Projekte, CV, Blog, Links, Skills …) selbst, ohne Datenbank-Zugriff von Hand.
Areas/Admin/Controllers/KI-Textgenerierung (Gemini)
Im Blog-Editor kann ich Entwürfe per Gemini-API generieren lassen und danach von Hand überarbeiten.
Services/GeminiTextService.csKI-Titelbilder (Cloudflare)
Titelbilder für Blog-Posts lassen sich über Cloudflare Workers AI generieren.
Services/CloudflareImageService.csLive-GitHub-Sync
Meine echten Repositories werden über die GitHub-API abgerufen und im Admin-Bereich synchronisiert.
Services/GitHubApiClientService.csScroll-gesteuertes Hero-Video
Auf der Über-mich-Seite scrubbt ein Video synchron zum Scroll-Fortschritt (GSAP ScrollTrigger).
AboutmevideopartialActivity-Log mit Auto-Cleanup
Admin-Aktionen werden protokolliert; beim Start der Anwendung werden Einträge, die älter als 6 Monate sind, automatisch entfernt.
Program.cs (Startup) + ActivityLogReflexion
Entscheidungen & Lessons Learned.
Ehrlich, nicht geschönt — inklusive der Stellen, die ich heute anders bauen würde.
Entscheidungen
- Warum 4 eigenständige Projekte statt eines? — Entities, DataAccess, BusinessLagiclayer und WebApplication3 sind vier separate .csproj-Dateien, nicht nur Ordner. Das erzwingt die Abhängigkeitsrichtung auch technisch — WebApplication3 kann nicht versehentlich an DataAccess vorbei direkt auf die Datenbank zugreifen.
- Warum Repository + Service statt nur EF Core direkt im Controller? — GenericRepository<T> deckt CRUD für alle 26 Entities mit einer einzigen Klasse ab. Die *Manager-Schicht darüber hält Platz für Geschäftslogik, die über reines Lesen/Schreiben hinausgeht (z.B. AddOrUpdateDetailToProject mit Tech-Stack-Abgleich).
- Warum SQLite statt SQL Server? — Für ein Einzelnutzer-Portfolio mit überschaubarem Datenvolumen reicht eine eingebettete Datei-Datenbank — kein separater DB-Server nötig, einfacher zu deployen und zu sichern (main.db lässt sich einfach kopieren).
- Wo der Trade-off liegt — GenericRepository<T> ist bewusst simpel gehalten (Create/ReadAll/ReadById/Update/Delate). Komplexere Abfragen (z.B. GetProjectWithDetails mit mehreren Includes) liegen deshalb direkt in den jeweiligen Ef*DAL-Klassen, nicht in der generischen Basisklasse.
Was ich gelernt habe
- Migrationen früh und oft — 13 kleine, benannte Migrationen sind einfacher nachzuvollziehen und im Zweifel zurückzurollen als wenige riesige.
- Tippfehler in Namen sind teuer — EntityFramwork → EntityFramework habe ich inzwischen korrigiert (50 betroffene Dateien) — BusinessLagiclayer trägt seinen Tippfehler noch. Ein einzelner falsch geschriebener Namespace zieht sich schnell durch die ganze Solution.
- Zwei Berechtigungssysteme parallel — Rollenbasiert ([Authorize(Roles=…)]) für Seiten-/Aktionszugriff und claim-basiert (PermissionTagHelper) für einzelne UI-Elemente haben unterschiedliche Aufgaben — beide zu pflegen kostet aber auch etwas mehr Disziplin.
- Öffentliche Ordner sind wirklich öffentlich — wwwroot wird komplett unauthentifiziert ausgeliefert — jede Datei, die dort versehentlich landet (z.B. eine appsettings.json), ist sofort im Netz erreichbar.
Nächste Schritte
- Erledigt Sicherheits-Review: versehentlich unter wwwroot abgelegte Dateien identifiziert und entfernt
- Geplant Upgrade von .NET 6 (Support ausgelaufen) auf .NET 8 oder neuer LTS
- Geplant Unit-Tests (xUnit) — aktuell gibt es kein Testprojekt in der Solution
- Geplant CI/CD mit GitHub Actions — der Workflow-Ordner existiert bereits, aber noch ohne Workflow-Datei
- Erledigt Docker-Deployment — Multi-Stage-Dockerfile ist produktiv im Einsatz (Render)
Fragen zur Umsetzung?
Ich erkläre Ihnen gerne jede Entscheidung im Detail.