Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
[Artikel] Die myCSharp.de Software Architektur
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

[Artikel] Die myCSharp.de Software Architektur

beantworten | zitieren | melden

Stand: Januar 2022

In diesem Artikel wollen wir euch über die Entstehung, die Ziele sowie die aktuellen Umsetzungswege über myCSharp informieren. Die Infrastruktur von myCSharp ist hier beschrieben: [Artikel] Die myCSharp.de Infrastruktur

Disclaimer

Die bei myCSharp angewandte Architektur basiert im Endeffekt auf meiner (Abt) persönlichen Erfahrung und Herangehenweise in dutzenden Projekten aus den letzten Jahren. Wie so oft gibt es immer mehrere Wege nach Rom - das Ziel war es jedoch eine Risiko-arme, bewährte aber auch durchaus Zweck-orientierte Architektur zu haben, was für alle Bereiche von myCSharp gilt. In viele Entscheidungen und Umsetzungen war aber das gesamte Team involiert, auch hier wieder besonders gfoidl.
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Open Source

Vorweg: wir werden myCSharp als Ganzes nicht Open Source zur Verfügung stellen. Ein Open Source Projekt einer Applikation funktioniert nur dann, wenn es dafür auch eine breite Basis gibt - wir jedoch haben bewusst sehr spezifisch entwickelt. Es gibt keine Zielgruppe unserer Applikation, sodass das Aufsuchen und Ausnutzen von Fehlern die einzige Folge wäre.

Da wir aber unser Wissen teilen wollen, werden wir Konzepte und Implementierungen durchaus zeigen bzw. zur Verfügung stellen (bzw. haben das schon getan).
myCSharp auf GitHub
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Motivation

Das Forum wurde Jahrelang auf einer sehr veralteten PHP-Basis betrieben, die auch rechtlichen Anforderungen nicht mehr gerecht wurde und aufgrund des Skillsets quasi nicht mehr weiterentwickelt werden konnte. Vergangene Anpassungen an der Codebase waren jedoch so tief, dass ein Update der Software selbst nicht mehr (ohne weiteres) möglich war - wir waren also in einer Sackgasse.

Wir - bzw. als Betreiber ich - hatten zwei Möglichkeiten:
  • myCSharp wird abgeschalten, weil sowohl für den Betrieb wie auch für die Benutzer nicht mehr sicher
  • myCSharp wird erneuert

In Zahlen
  • Stand heute basiert myCSharp auf ca. 1.5 Millionen Zeilen Code
  • Über 350 dokumentierte Issues/Features (ja, wir halten das sehr sehr sehr einfach)
  • Gesamt über 450 Pull Requests (die teilweise zu groß waren)
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Glossar

Die Dokumentation verwendet .NET-übliche Begrifflichkeiten, möchte sie an dieser Stelle jedoch nochmals beschreiben:
  • Actions: ASP.NET Methoden in einem Controller und im Endeffekt "Request-Endpunkte"
  • Services: Eine Implementierung einer Logik oder Funktionsweise, die eine eine Domäne (zB User, Forum..) betrifft. Services werden i.d.R. als Scope gesehen und in Form eines Repository Patterns umgesetzt. Durch das Scope-Verhalten sind diese i.d.R. nicht Thread-Safe, können als Scoped bzw. Transient in Dependency-Injection Umgebungen registriert werden und leben in einer Web-Anwendung während eines einzelnen Requests.
  • Providers: Eine Implementierung einer spezifischen Funktionalität oder Abhängigkeit, betreffen keine Domäne. Provider werden i.d.R. als Singleton gesehen und in Form eines Strategy Patterns umgesetzt. Durch das Singleton-Verhalten sind diese Thread-Safe, können als Singleton in Dependency-Injection Umgebungen registriert werden und leben daher dauerhaft, auch über mehrere Requests hinweg.
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Die Grundarchitektur

Unsere Grundarchitektur begonn auf den Möglichkeiten von .NET Core 3 sowie ASP.NET Core 3, befindet sich aber - Stand dieses Artikels - auf Basis von .NET 6.
Die Architektur basiert dabei auf folgenden (externen) wichtigsten Komponenten:
Unsere Software Architektur (siehe Bild) ist dabei in drei Grundbestandteile getrennt:
- ASP.NET Core Runtime
- Engine (CQRS)
- Services und Provider

Unsere Solution-Struktur ist dabei:

MyCSharp.Common - Erweiterungsmethoden für .NET Klassen
MyCSharp.BBCode - Unser BBCode Parser
MyCSharp.ContentRenderer - Unsere Content Render Engine, primär für BBCode, Markdown.. zu HTML.
MyCSharp.Portal - Unser Haupt-Logik-Projekt mit allen Features, Engine, Services, Provider, Datenbank...
MyCSharp.Portal.AspNetCore - Erweiterungen für ASP.NET Core
MyCSharp.Portal.Database.Mssql.Build - Unser Datenbank-Buildprojekt (DACPAC Deployment)
MyCSharp.Portal.Features - Unser Feature Management
MyCSharp.Portal.Monitoring - Monitoring, Tracing, Logging
MyCSharp.Portal.Notifications - Notifications Hauptprojekt (E-Mails, Push....)
MyCSharp.Portal.Notifications.EMail - EMail Notifications
MyCSharp.Portal.Notifications.EMail.WebApp - Dev Projekt für unsere Email Razor Templates
MyCSharp.Portal.WebApp - Die tatsächliche Webapplikation

Unsere Datenbank ist dabei wie folgt strukturiert:

MyCSharp.Portal.Database - Hauptnamespace, hier liegen generelle EF Core Features drin
MyCSharp.Portal.Database.Cache - Caching Mechanismen
MyCSharp.Portal.Database.Entities - Unsere Entitäten
MyCSharp.Portal.Database.Entities.Extensions - Erweiterungen, zB für Expressions
MyCSharp.Portal.Database.Mssql - MSSQL-spezifisches, zB DB Context
MyCSharp.Portal.Database.Mssql.Configurations - Entity Mapping Configs für MSSQL
MyCSharp.Portal.Database.Repositories - Repositories und deren Implementierungen
MyCSharp.Portal.Database.Views - Datenbank Views
Hinzu kommen jedoch Modelle aus:

MyCSharp.Portal.Data.Models - Applikationsmodelle, zB. Enums
MyCSharp.Portal.Data.Projections - Applikations Projektionen
MyCSharp.Portal.Data.Projections - Applikations Views (keine DB Views)

Das Namespacing und damit der Grundbaustein der Software Architektur entspricht also 99% den Regeln aus C# bzw. .NET.
Attachments
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Die myCSharp Engine aka Event Architektur

Applikationen basieren in den meisten Fällen auf Domänen, bestehen also aus folgenden Bestandteilen:
  • Entitäten für die Datenbankdarstellung
  • Repositories für den Umgang und das Mapping von Entitäten
  • Business Modelle für die Logik
  • Services für die Logik

Dieser Aufbau ist relativ simpel, aber hat Probleme in Webanwendungen
  • Entitäten müssen nicht ständig neu geladen werden
  • Business Modelle benöten Informationen aus mehreren Entitäten. Man muss also viel DB-Calls absetzen bzw. Ping-Pong-Requests und Roundtrips, um Logik über Business Modelle ausführen zu können.
  • Die Service-Orchestierung ist auch teil der Logik; verlagert sich aber in die UI (bei ASP.NET die Actions). Nur zu lösen mit Service in Service.

Daher basiert unsere Architektur auf Event Sourcing, implementiert mit MediatR. Dies bietet folgende Vorteile:
  • Durch CQRS basiert unsere Anwendung auf "Aktionen", wobei CQRS hier ein Orchestrierungsmittel der Services sind. Die ASP.NET Action kennt daher auch nur die Abhängigkeit zu CQRS (bei uns MediatR), was unsere Webanwendung extrem schlank und einfach hält.
  • CQRS-Bausteine sind Orchestrierungs-Elemente; können wir also sehr flexibel verwenden und darin entweder Services, Provider, Repositories oder auch direkt den DB-Context nutzen, ohne etwas zu brechen oder umsauber gestalten.

Was folgende Effekte hat:
  • Wir haben keinerlei Business Modelle, sondern übergeben Informationen durch CQRS-Bausteine wie Commands oder Notifications
  • Wir haben als zusätzliche Datendarstellung Views und Projektionen; müsen also nicht immer alle Informationen laden, sondern nur die, die wir brauchen
    Ein Command oder eine Notification sind aus Architektursicht Abstraktionen; das bedeutet, dass eine ASP.NET Action einfach nicht den/die Services bzw. ein/die Repositories aufrufen würde, sondern i.d.R. einen Command, eine Notifications bzw. einen Queries. Es stellt also eine zusätzliche Schicht dar.
  • Durch die Handler-Implementierung von CQRS können wir uns jederzeit an jeder Stelle der CQRS-Pipeline einklinken und sparen uns zB. beim Logging oder bei Validierungen extrem viel Code, was über "leichtere" Pattern nicht möglich wäre.

In einem sehr einfachen Fall - die Ansicht eines Benutzerprofils - sieht es zum Beispiel so aus:


[Route(RouteTemplates.UserWithName)]
[FeatureGate(FeatureFlags.UsersEnabled, FeatureFlags.UsersProfileView)]
public async Task<IActionResult> Profile(int userId, string userNameSlug)
{
    var user = await _eventDispatcher.Get(new GetUserProfileViewProjectionQuery(userId)).ConfigureAwaitFalse();
    if (user is null) return UserProfileNotFoundRedirect(userId);

    UserProfileViewModel vm = new UserProfileViewModel(user).WithContextOf(this);
    return View(ViewsRoot + "UserProfile.cshtml", vm);
}

Wir sehen hier also den Aufruf eines Profils anhand der Id. Die Id wird dazu verwendet einen CQRS-Query zu laden, ohne an dieser Stelle die Implementierung zu kennen.
Der Query ist dabei einfach nur eine Referenz, die angibt, was die Parameter sind und was zurück kommt.


public record GetUserProfileViewProjectionQuery(int UserId) : IQuery<UserProfileView?>;
Durch die Signatur ist bekannt:
  • Es ist ein Query: es gibt daher einen Rückgabetypen, auf dessen Laden gewartet werden soll.
  • Die Rückgabe ist entweder null oder vom Typ UserProfileView (wenn der User nicht existiert, dann kommt null => kein Profil)

Der Handler dieses Queries sieht dabei folgendermaßen aus:


public class GetUserProfileViewProjectionQueryHandler : IQueryHandler<GetUserProfileViewProjectionQuery, UserProfileView?>
{
    private readonly IUserRepository _userRepository;

    public GetUserProfileViewProjectionQueryHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task<UserProfileView?> Handle(GetUserProfileViewProjectionQuery r, CancellationToken ct)
       => _userRepository.GetUserProfile(r.UserId, ct);
}
MediatR schaut in seine Registrierung, welcher QueryHandler zum Query passt und führt diesen aus.
An dieser Stelle passiert nichts anderes als die Anfrage 1:1 an das Repository weiter gereicht wird. Die ProfileView wird also gar nicht im Handler geladen; sondern der Handler orchestriert hier nur das Repository.
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Warum wir das Repository nicht direkt ansprechen, was aus Runtime-sicht effizienter wäre: bei MediatR können sogenannte Behaviors registriert werden, die bei jeder Handler-Ausführung "um den Handler herum" arbeiten können.
Mit solchen Behaviors können wir:
  • Die Laufzeit jedes Handlers messen
  • Berechtigungen prüfen
  • Auf jede Exception eines Handlers reagieren, das wir mit alternativen Pattern in der Form nicht könnten
  • An einer zentralen Stelle mit wenig Aufwand loggen
Ähnlich sieht es bei Commands aus, hier als Beispiel der Command, der ausgeführt wird, wenn jemand sein eigenes Profil aktualisiert:


public class UserProfileUpdateCommandHandler : ICommandHandler<UserProfileUpdateCommand, Unit>
{
    private readonly IMyCSharpDbContext _db;

    public UserProfileUpdateCommandHandler(IMyCSharpDbContext db)
    {
        _db = db;
    }

    public async Task<Unit> Handle(UserProfileUpdateCommand r, CancellationToken ct)
    {
        // load
        var profile = await _db.UserProfiles.GetByIdAsync(r.UserId, ct).ConfigureAwaitFalse();
        if (profile is null) throw new UserNotFoundException(r.UserId);

        // update
        profile.DateOfBirth = r.DateOfBirth;
        profile.JobTitle = r.JobTitle;
        profile.Company = r.Company;
        profile.City = r.City;
        profile.Interests = r.Interests;
        profile.HomepageUrl = r.HomepageUrl;
        profile.BlogUrl = r.BlogUrl;
        profile.LinkedInUrl = r.LinkedInUrl;
        profile.XingUrl = r.XingUrl;
        profile.FacebookUrl = r.FacebookUrl;
        profile.TwitterHandle = r.TwitterHandle;
        profile.Description = ContentCleanupProvider.Cleanup(r.Description);

        profile.LastUpdatedOn = DateTimeOffset.UtcNow;

        // save
        await _db.SaveChangesAsync(ct, updateChangeTrackerStates: false)
            .ThrowIfNoResults().ConfigureAwaitFalse();

        return Unit.Value;
    }
}
An dieser Stelle sieht man die direkte Implementierung, die erst eine DB-Entität lädt, diese aktualisiert und wieder schreibt.
Welche Alternativen es gäbe?
  • Profile Update im Service und Repository implementieren
  • Nur ein Update an die Datenbank schicken, ohne vorher zu laden und die Werte zu prüfen
Warum wir das nicht getan haben?
  • Da wir dieses Update nur an dieser Stelle benötigen, gibt es kein Vorteil das durch zwei weitere Instanzen, zuerst den UserService und dann das UserRepository, zu schleusen. Anders wäre, wenn das von mehreren Stellen aus verwendet werden würde.
  • Zugegeben, das ist nun tieferes Wissen und auch Optimierung: EF Core kann anhand eines Hash-Vergleichs erkennen, welche Werte sich wirklich geändert haben und schickt nur die zu ändernden Eigenschaften an die Datenbank. Daher kann ich auch Werte setzen ohne zu prüfen, ob der neue Wert dem alten entsprcht. Verwendet man den Update-Mechanismus in EF Core, dann werden immer alle Werte übertragen und in der Datenbank gesetzt, was zur Folge hat, dass die Datenbank an dieser Stelle auch den Index neu aufbauen wird. Das kostet - in den meisten Fällen - mehr Gesamtperformance als einfach die Entität zu aktualisieren, sofern es sich um eine einfache Entität handelt wie in diesem Beispiel.

Um Zurück auf die Aktionen zu kommen: eine Aktion kann eine Kette von Commands, Notifications und Queries auslösen, aus unterschiedlichsten Zwecken und in unterschiedlichen Reihenfolgen.
Als Beispiel dazu die ASP.NET Action, wenn man ein Forenthema aufruft:


[HttpGet]
[Route(RouteTemplates.ForumThreadWithTitle)]
[FeatureGate(FeatureFlags.ForumEnabled)]
public async Task<IActionResult> View(PortalWebUserId? webUserId, int threadId, string threadTitle, int? postId = null, int? page = null)
{
    // Aufbau und Filtern der Paging Parameter des Requests
    PagingOptions pageOptions = new(page ?? 1);

    // Laden der Thread View aus der Datenbank
    ForumThreadView? view = await _eventDispatcher
        .Get(new GetForumThreadContentViewQuery(threadId, webUserId, pageOptions.Skip, pageOptions.Take))
        .ConfigureAwaitFalse();

    if (view is null)
    {
        return RedirectWithError(_router.ToForum(), $"Thread {threadId} konnte nicht gefunden werden.");
    }

    // Benutzerauthorisierung
    //   wir haben uns hier für die ASP.NET Authorization entschieden, die in der Action geprüft werden muss.
    _forumAuthorization.ThrowIfUserBoardAccessDenied(view.AllowedBoardRoles);

    // Aufbau des Pagings
    Paginator paging = new(view.TotalPostCount, pageOptions, pageRoute => _router.ToForumThreadView(view.ThreadId, view.ThreadTopic, pageRoute));
    if (!paging.PageIsValid(pageOptions.Page))
    {
        return Redirect(_router.ToForumThreadView(view.ThreadId, view.ThreadTopic, paging.FirstPage));
    }

    // Zählen der Views
    {
        IClientInformation? clientInfo = _userTypeDetectionService.GetClientInformation();
        if (clientInfo?.IsBrowser == true)
        {
            // Hier wird eine CQRS Notification ausgelöst
            //   Uns interessiert nicht, wann diese verarbeitet wurde oder ob diese erfolgreich war
            await _eventDispatcher
                .PublishFireAndForget(new ForumThreadViewHitNotification(view.ThreadId, DateTimeOffset.UtcNow, webUserId))
                .ConfigureAwaitFalse();
        }
    }

    // View
    ForumThreadViewViewModel vm = new ForumThreadViewViewModel(view, paging).WithContextOf(this);
}
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Alle CQRS Bestandteile dieses Requests:
  • Es wird in der ASP.NET Core Request Piepine über ein Command geprüft, ob der User angemeldet ist. Diese Prüfung ist eine Stored Procedure Datenbank abfrage, da zeitgleich zur Prüfung auch das Akitivätsdatum des Profils aktualisiert wird. Dies erspart DB-Roundtrip Calls.
  • Es wird über ein Query geprüft, ob das Forum Feature "Forum" aktiv ist.
  • Es wird über ein Query die Forenansicht geladen.
  • Es wird über ein Query die Authorisierung geprüft.
  • Es wird über eine Notification der Themenbesuch hoch gezählt.

Fazit von unser Event Engine:
  • Wir haben eine sehr übersichtliche Architektur, wie die Applikation funktioniert.
  • Wir können die Aktionen jederseit mit simpelsten Code erweitern.
  • Unsere Logik hat keine primäre Abhängigkeit untereinander, sondern es ist alles weitgehend isoliert und Änderungen haben keine versteckten Folgen.
  • Wir haben durch die MediatR Implementierung mit einfachsten Code-Mitteln mit Möglichkeit alle unsere Applikationimplementierungen (operation architecture) zu überwachen, zu messen und auf Fehler zu reagieren.
  • Wir müssen uns nicht verkünsteln, sondern können viele Dinge auch über Keep-it-simple umsetzen, was uns schneller und einfacher macht.
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Datenbank Architektur

Die gesamte Datenbank-Logik basiert auf Entity Framework Core sowie MSSQL. Dabei sei gesagt, dass wir bewusst MSSQL als harte Abhängigkeit gewählt haben, da wir keine Motivation oder Notwendigkeit einer Multi-Datenbank sehen.
Davon unabhängig ist, dass wir jedoch für verschiedene Daten durchaus verschiedene Datenbank-Engines verwenden könnten - aber eben nicht mehrere Datenbanken für die gleiche Entität.

Diese Vorgehensweise ermöglicht, dass wir spezifischer und damit effizienter sowie performanter programmieren können bzw. dies auch die Abfragen sind.
Wie bereits angeschnitten basiert unsere Architektur nicht nur auf Entitäten, sondern auch auf Datensparsamkeit, sodass wir nur die Daten laden müssen, die wir auch laden wollen.
Dabei haben wir also zwei zusätzliche Elemente:

Wollen wir also zB. von Forenthemen nur wissen, welche Id und welchen Titel dieses trägt, dann haben wir folgende Möglichkeiten:
  • Man lädt die gesamte Entität
  • Man lädt manuell nur gewisse Spalten
  • Man lädt die über Projektionen typisiert nur das, was man will
Wir haben uns bei den aller meisten Queries für den 3. Punkt entschieden, weil wir damit typisiert, schlank und Entwicklungssucher Daten laden können.
Für Projektionen verwenden wir AutoMapper Projections, das uns 99% der Arbeit abnimmt. Eine Projektion hat dabei zwei Bestandteile:
  • Die Projektion
  • Das Mapping der Projektion
Für das Beispiel sieht das so aus:


public class ForumThreadTopicProjection
{
    public int ThreadId { get; set; }
    public string ThreadTopic { get; set; } = null!;
}

[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public class ForumThreadTopicProjectionProfile : AutoMapper.Profile
{
    public ForumThreadTopicProjectionProfile()
    {
        CreateProjection<ForumThreadEntity, ForumThreadTopicProjection>()
            .ForMember(p => p.ThreadId, opt => opt.MapFrom(e => e.Id))
            .ForMember(p => p.ThreadTopic, opt => opt.MapFrom(e => e.Topic));
    }
}
Egal wo wir diese Projektionsklasse in unserem Datenbank-Code verwenden, AutoMapper erzeugt durch das Mapping dazu automatisiert die Expression und die EF Core Expression Engine dazu automatisiert den SQL Code.
Dies funktioniert nicht nur für eine einzelne Entität, sondern für alle Relationen, die in den Entitäten gepflegt sind. Bei der Profilansicht werden zB. viele Informationen über mehrere Tabellen hinweg abgefragt:


[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public class UserForumHeadProjectionProfile : AutoMapper.Profile
{
    public UserForumHeadProjectionProfile()
    {
        CreateProjection<UserAccountEntity, UserForumHeadProjection>()
            .ForMember(p => p.UserId, opt => opt.MapFrom(e => e.Id)) // "UserAccount" Tabelle
            .ForMember(p => p.UserName, opt => opt.MapFrom(e => e.UserName)) // "UserAccount" Tabelle
            .ForMember(p => p.RegisteredOn, opt => opt.MapFrom(e => e.Profile.RegisteredOn)) // "UserProfile" Tabelle
            .ForMember(p => p.UserAvatar, opt => opt.MapFrom(e => e.Avatar)) // "UserAvatar" Tabelle
            .ForMember(p => p.UserForumStats, opt => opt.MapFrom(e => e)); // Eine indizierte View in MSSQL
    }
}
Zusätzlich zu den Projektionen, die also Darstellungen von Runtime SELECTS sind, haben wir Views. Views sind dabei nichts anderes als MSSQL Views, die sowohl dynamisch sind wie aber auch indizierte Views bzw. Rückgaben von Stored Procudures sein können. Des weiteren sind Views von uns Projektionen, die wir manuell erzeugen müssen, zB. weil das Aufgrund der Query-Art oder des SQL Codes nicht über eine AutoMapper Projektion umsetzbar ist (zB. Multi-Selects, also mehrere Abfrageergebnisse pro DB-Call).

Diese sind sehr simpel, und bestehen einfach nur aus einer Klasse mit Eigenschaften.


// EF Core View
public record UsersSearchView(int Count, IList<UserSearchUserView> Users) : IEFCoreView

// MSSQL View
public class ForumThreadPostCountStatsDbView : IDbView
{
    public int ForumThreadId { get; set; }
    public long PostsCount { get; set; }
}
Da diese Daten-Klassen nicht alleine stehen, haben auch wir Repositories bzw. Abfrage-Methoden.
In den Entity-Repositories haben wir grundlegend folgenden schematischen Aufbau:


// Query Referenz auf alle Benutzer
public IQueryable<UserAccountEntity> QueryUsers(DbTrackingOptions to)
    => DbContext.UserAccounts.With(to);
// Query Referenz auf einen Benutzer
public IQueryable<UserAccountEntity> QueryUser(int userId, DbTrackingOptions to)
    => QueryUsers(to).Where(UserAccountQuery.HasId(userId));
Diese beiden Referenz-Basen sind dann Ausgangspunkte für spezifische Queries, wie zB:


// Für weitere Query-Referenzen im Repository:
public Task<bool> ExistsId(int userId, CancellationToken ct) => QueryUser(userId, DbTrackingOptions.Disabled).AnyAsync(ct);
// Für Projektionen in Services:
public async Task<PortalUser?> GetById(int id, CancellationToken ct)
   => await _userRepository.QueryUser(id, DbTrackingOptions.Disabled).ProjectTo<PortalUser>(_mapper.ConfigurationProvider).SingleOrDefaultAsync<PortalUser?>(ct);
Um Queries nicht ständig in Delegates schreiben zu müssen, haben wir auch diese zentralisiert, um Code zu sparen, Tippfehler zu vermeiden, Komplexität aus den Queries zu nehmen. Die gezeigte Clause UserAccountQuery.HasId(userId) sieht zum Beispiel so aus:


public static Expression<Func<UserAccountEntity, bool>> HasId(int id) => x => x.Id == id;
Auch dies funktioniert mit allen Query-Arten von EF Core, solange sie Teil des Relationsmodells der Entitäten sind.

Wir haben also eine sehr hohe Einfachheit und eine sehr hohe Wiederverwendbarkeit in unsere DB Schicht.
Dadurch ist diese auch wirklich sehr schlank, sehr effizient und sehr flexibel. EF Core ist großartig, wenn man es so verwendet!
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Das myCSharp Identity Management

Wir vertrauen der Implementierung von ASP.NET Core Identity, für die Authentifizierung und Authorisierung der Benutzer. Dazu muss man jedoch sagen, dass wir die Standard Implementierungen (zB UserManager) nicht verwenden, sondern so gut wie alle Implementierungen des Identity Managers selbst durchgeführt haben. Wir haben nur die Sicherheitsrelevanten Bausteine wie zB. das Hashing von Passwörtern, Zwei-Faktor-Authentifizierung, Cookie Handling.. dem Standard belassen.

Unsere eigenen Identity Manager verwenden daher nicht mehr Entity Framework direkt, sondern unsere Engine.
So ist gewährleistet, dass jegliche Interaktion "mit dem Herz" nur über unsere Engine erfolgt. ASP.NET Core Identity ist also nur für den Web-Stack verantwortlich.

Das myCSharp Notification Management

Wir verwenden die Notifications der Engine, um jegliche Arten von Notifications zu nutzen; primär also E-Mails. Wir haben jedoch bereits teilweise WebSockets implementiert, aber noch nicht scharf geschalten.
Wir rendern derzeit alle E-Mails über eine Implementierung von ASP.NET Razor Templates; also so, wie auch ASP.NET Core später Razor zu HTML-Content umformt.

Dies ist extrem simpel umzusetzen, kostet jedoch durchaus etwas CPU/RAM und entsprechende Allocations.
Wir planen hier bereits einen Ersatz (Liquid Templates via Fluid), der deutlich effizienter ist, um E-Mails zu generieren.

Verschickt werden unsere E-Mails mit Hilfe des MailKits und einem SMTP-Gateway.
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.500

Themenstarter:

beantworten | zitieren | melden

Optimierungen
  • Wir haben eine eigene Fassung des Microsoft.FeatureManagement-Providers geschrieben, da die aktuelle Implementierung ein paar Defizite hat (diese wurden auch gemeldet, Reaktion war eher "meh": Fixed race conditions in FeatureManagerSnapshot. Mittlerweile ist diese Verbesserung in das Feature Management eingeflossen: Support concurrency in IFeatureManagerSnapshot
  • Wir haben einen eigenen User Agent Parser geschrieben:
  • Die meiste Leistung des Forums frisst das Content Rendering, also zB. das Rendern von BBCode, Smilies, Markdown... zu HTML; hierfür haben wir eine eigene, extrem für unsere Zwecke optimierte Render Pipeline geschrieben.

Probleme

Wir hatten während der Entwicklung durchaus Stolpersteine, dazu:
  • Es gab mehrfach Regressions in EF Core, was uns immer wieder nach hinten geworfen hatte; vor allem in Sachen Expressions
  • Encodings haben uns immer wieder Fallen gestellt

Technische Schulden
  • Es gab in der alten PHP-Forenwelt durchaus "interessante" Implementierungen und Limits, die uns heute noch Probleme machen und ohne größeren (manuellen) Aufwand nicht zu lösen sind, zB. dass Posts teilweise über 8000 Zeichen groß sind und wir deshalb alle 700.000 Posts nicht indizieren können, was das Forum in sich langsamer macht - vor allem die Suche.

Fazit

Wir sind sehr zufrieden - aber auch wir haben natürlich noch viel Möglichkeiten und viel vor :-)
private Nachricht | Beiträge des Benutzers
Alf Ator
myCSharp.de - Member



Dabei seit:
Beiträge: 629

beantworten | zitieren | melden

Zitat von Abt

Fazit

Wir sind sehr zufrieden - ... :-)

Ich auch, top!
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von Alf Ator am .
private Nachricht | Beiträge des Benutzers