Laden...

Forenbeiträge von mleadb Ingesamt 10 Beiträge

14.02.2022 - 14:10 Uhr

Sehr nice. Herzlichen Dank 👍 👍

Was natürlich auffählt sind die vielen Models (Projections/Views) die Ihr in jeder Schicht spezifisch definiert.

Ich gehen davon aus, dass Ihr für den Controller auch ein eigenes "SubmitModel" definiert und dann auf das CQRS "CommandModel" mappt?
Greift die Validierung (FluentValidation) auf das SubmitModel oder validiert Ihr in einer tieferen Schicht oder gar "doppelt"?

10.02.2022 - 15:01 Uhr

Danke für die lehrreichen Posts!

Finde eure Solution-Struktur eine sehr gute und vorallem praxisorientierte Vorlage.
Wenn Ihr die Ordner/Namespace-Struktur hier ebenfalls veröffentlichen würdet, wäre das echt klasse.

Habe jahrelang das nopCommerce Project als Vorlage gebraucht, jedoch gefällt mir eure Struktur besser und MSDN conform.
Das Naming ist ja leider immer wieder schwierig (besonders wenn man perfektionist ist 😉. z.B. findet man unter "Infrastructure" unterschiedlich Definitionen (z.B. hier wird der Begriff für "Startup Stuff" verwendet. Oder eben für externe Dienste/DB).

Mehr Details was hinter den folgenden Projektem steckt wäre sehr interessant:

  • MyCSharp.Portal - Unser Haupt-Logik-Projekt mit allen Features, Engine, Services, Provider, Datenbank...
  • MyCSharp.Portal.Features - Unser Feature Management
  • MyCSharp.Portal.WebApp - Die tatsächliche Webapplikation

Ich gehe davon aus, dass die effektiven Mvc spezifische Features (Controllers/ViewModels/Views) im "MyCSharp.Portal.WebApp/Features/FeatureA" etc. liegen?
Wo liegen welche "Features"?

23.01.2022 - 16:35 Uhr

Erstmals Danke für deine umfassende Antwort Abt.

Das Problem lag tatsächlich in meiner AsyncDuplicateReaderWriterLock Implementierung.
(Den Code habe ich von Stephen Cleary übernommen und auf den VisualStudio.AsyncReaderWriterLock abgeändert. Siehe hier)


using Microsoft.VisualStudio.Threading;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public sealed class AsyncDuplicateReaderWriterLock
{
    private sealed class RefCounted<T>
    {
        public RefCounted(T value)
        {
            RefCount = 1;
            Value = value;
        }

        public int RefCount { get; set; }
        public T Value { get; }
    }

    private static readonly Dictionary<object, RefCounted<AsyncReaderWriterLock>> _asyncReaderWriterLocks = new();

    private AsyncReaderWriterLock GetOrCreate(object key)
    {
        RefCounted<AsyncReaderWriterLock>? item;
        lock (_asyncReaderWriterLocks)
        {
            if (_asyncReaderWriterLocks.TryGetValue(key, out item))
            {
                ++item.RefCount;
            }
            else
            {
                item = new RefCounted<AsyncReaderWriterLock>(new AsyncReaderWriterLock(new JoinableTaskContext()));
                _asyncReaderWriterLocks[key] = item;
            }
        }
        return item.Value;
    }

    public async Task<IDisposable> ReadLockAsync(object key)
    {
        await GetOrCreate(key).ReadLockAsync();
        return new Releaser { Key = key };
    }

    public async Task<IDisposable> UpgradeableReadLockAsync(object key)
    {
        await GetOrCreate(key).UpgradeableReadLockAsync();
        return new Releaser { Key = key };
    }

    public async Task<Releaser> WriteLockAsync(object key)
    {
        await GetOrCreate(key).WriteLockAsync();
        return new Releaser { Key = key };
    }

    public sealed class Releaser : IDisposable
    {
        public object Key { get; set; } = null!;

        public void Dispose()
        {
            RefCounted<AsyncReaderWriterLock> item;
            lock (_asyncReaderWriterLocks)
            {
                item = _asyncReaderWriterLocks[Key];
                --item.RefCount;
                if (item.RefCount == 0)
                    _asyncReaderWriterLocks.Remove(Key);
            }
            item.Value.Dispose();
        }
    }
}

Anwendung:


    using (await _lock.WriteLockAsync(key))
    {
        await WriteAllBytesAsync(filePath, content);
    }

Dieser Code führt jedoch zu einem Deadlock.

Habe dann den Code so angepasst bis es funktioniert.


using Microsoft.VisualStudio.Threading;
using System.Collections.Generic;
using System.Linq;

public sealed class AsyncDuplicateReaderWriterLock
{
    private sealed class RefCounted<T>
    {
        public RefCounted(T value)
        {
            RefCount = 1;
            Value = value;
        }

        public int RefCount { get; set; }
        public T Value { get; }
    }

    private static readonly object _lock = new();
    private static Dictionary<object, RefCounted<AsyncReaderWriterLock>> _asyncReaderWriterLocks = new();

    public AsyncReaderWriterLock Create(object key)
    {
        RefCounted<AsyncReaderWriterLock>? item;
        lock (_lock)
        {
            if (_asyncReaderWriterLocks.TryGetValue(key, out item))
            {
                ++item.RefCount;
            }
            else
            {
                item = new(new AsyncReaderWriterLock(new JoinableTaskContext()));
                _asyncReaderWriterLocks[key] = item;
            }

            // Avoid memory leak:
            _asyncReaderWriterLocks = _asyncReaderWriterLocks
                .Where(x => x.Value.RefCount > 0)
                .ToDictionary(x => x.Key, x => x.Value);
        }
        return item.Value;
    }
}

Anwendung:


    using (await _lock.Create(key).WriteLockAsync())))
    {
        await WriteAllBytesAsync(filePath, content);
    }

Ehrlichgesagt ist mir nicht klar, warum die vorherige Implementierung nicht funktionieren soll?!
.Dispose() wird ja eigentlich ausgeführt und somit sollte der Lock ja eigentlich wieder "Released" werden. Mmm...

Nun ja AsyncReaderWriterLock.WriteLockAsync() gibt mir ein Awaitable zurück. Dieses dann "Awaiter GetAwaiter()" und dieser dann "Releaser GetResult()".
Vermutlich müsste ich an diesen Releaser rankommen und diesen dann "Disposen"... (nicht getestet).
Ich dachte eigentlich, dass ein "asyncReaderWriterLockInstance.Dispose" genügen würde...

22.01.2022 - 21:02 Uhr

Mich würde interessieren wie Ihr den Read/Write eurer Dateien (Bilder) in einer ASP.NET Core Anwendung Thread-Safe macht.
Da in ASP.NET Core nun "alles" async und somit sehr performant abläuft benutze ich natürlich die async Varianten von der File-Klasse.
Vor async konnte ich das einfach mit dem ReaderWriterLockSlim absichern.

Leider scheint der AsyncReaderWriterLock vom "Microsoft.VisualStudio.Threading" Package zu einem Deadlock zu führen!? (Ist ja auch nicht für den allgemeinen Gebrauch gedacht)
Der SemaphoreSlim versteht leider kein "EnterReadLock" / "EnterWriteLock" / "EnterUpgradeableReadLock", somit keine alternative.

Möglich wäre natürlich auch das ganze ohne "Locking" laufen zu lassen und bei der Write IOException einfach mehrfach zu "retry"en.
Jedoch scheint mir das bei vielen Reads (FileStreamResult) sehr gewagt zu sein.

Leider finde ich zu diesem Thema sehr wenig Infos 😲

11.02.2021 - 18:29 Uhr

Weil Webseiten mit statischem Content extrem abhängig vom SEO sind und Suchmaschinen SEO (besser) indizieren, wenn dies server-seitig passiert.

Das meinte ich eben mit dem PreRendering. (Bei Blazor-Wasm ist das mit Dependency-Injection sehr gut gelöst)
Damit hätte man das SEO Problem eigentlich behoben.

Tue mich da noch etwas schwer von den normalen MVC-Views wegzukommen (ebenfalls wegen SEO).

11.02.2021 - 14:04 Uhr

Aber dazu kommt ein extra Artikel.

👍

Es sieht ja so aus als würdet Ihr keine Client-Side Framework wie Angular/React/Blazor-Wasm verwenden.
Was waren die Gründe dagegen?

Finde Blazor-Wasm schon sehr interessant. Das Prerendering ist schon sehr nice gelöst.
Leider die Download-Grösse (Trimmed ~2-3MB)... naja.
Bin gespannt wies um die Zukunft von Blazor (Wasm) steht.

Blazor-Server ist mir leider wieder zu sehr "WebForms". Da geschehen zuviele Dinge im Hintergrund... (z.B auch die dauerhafte SignalR-Connection).

11.02.2021 - 11:51 Uhr

Würde mit Amazon SES 3€ kosten, siehe Amazon SES Preiskalkulator

Das wäre in der Tat sehr günstig. Muss ich mal genauer anschauen. Meine Devise derzeit "never toch a running system" 😉

Ich bin zwar ein großer Freund von Microservices und mache seit vielen Jahren auch kaum was anderes; die Anforderungen haben wir jedoch im Falle des Forums nicht, sodass das Portal prinzipiell wie klassische monolithische Webapplikation aufgebaut ist.

Würde mich genauer interessieren wie Ihre euer Projekt strukturiert habt.

Ich selber fahre mit der "Features Folder"-Struktur sehr gut. (Ist ja eigentlich auch ne Art Microservices)
Finde das Standart-Template ("Tech Folders") (Controllers/Models/Views,etc.) einfach schrecklich.
Irgendwann kommst du an einen Punkt, wo die Übersichtlichkeit einfach stark leidet.

Alle Aktionen innerhalb des Portals wurden mit Hilfe des CQRS-Patterns über MediatR umgesetzt. Dies wird näher im Artikel der Software Architektur erklärt, der aktuell noch in der Mache ist.

Ein Artikel dazu fände ich spannend.
Nutze zwar nicht das MediatR. Jedoch ist es IMO schon Pflicht, die Submit-/ViewModels ModelBuilding und CRUD-Operationen in separate Klassen auszulagern.

10.02.2021 - 20:17 Uhr

Mails verschicken kann man entweder via SMTP über nen eigenes Postfach (so machen wir es) oder auch extrem günstig über Amazon Simple Mail Service.

Wo habt Ihr das Postfach den "gehostet"?
Bei vielen Mails (> 200 Mails/Tag) scheint mir das keine Lösung zu sein.

Mögen die meisten Spam-Filter überhaupt nicht (und ist bei den meisten VM Anbietern auch deaktiviert).

Das mit dem Spam-Filter ist tatsächlich immer so eine Sache 🤔
Direkt verschicke ich die Mails natürlich auch nicht. Das wäre der sichere tot für meine Website 😉
Ein zuverlässiger Mailversand ist ein absolutes Muss.

SMTP relay service - MailChannels - 80$ für 40'000 Mails/Monat
Funktioniert tadellos. Kann den Service auch gleich für mehrere Domains nutzen.

10.02.2021 - 15:06 Uhr

Und die Performance unter Linux ist in den meisten Fällen viel besser als unter Windows

Interessant. Wusste ich nicht.

Wie handelt Ihr das SSL/TLS Zertifikat?

Mit "LettuceEncrypt" soll das ja automatisch gehen. Aber eben nur wenn keine WebServer vorgeschaltet ist. Mit dem IIS voran geht das ja leider nicht.

Bei Deinem Hetzner IaaS Angebot fehlen Dir ja dutzende von Features, was Dir Managed Hosting von Azure (odder AWS / GCP) hier bietet.

Würde mich interessieren, welche Features lohnenswert sind.

Bei den meisten meiner Web-Apps brauche ich in der Regel nur den SQL-Server Express und ein Mail-Server (MailEnabled) drauf.
Remote-Desktop Zugriff sehr praktisch. Kann mir gar nicht vorstellen wie das ohne gehen soll 😉.
Nehme an, bei Azure connecte ich direkt mit meinem lokalen SQL Server Management Studio zur DB?

10.02.2021 - 13:25 Uhr

Sehr interessante Beiträge Abt!

Spannend finde ich, dass Ihr die ASP.NET Core App auf einem Linux-Server hostet.

Wo seht Ihr die Nachteile eines Linux-Servers gegenüber einem Windows-Server im Bezug auf eine ASP.NET Core Anwendung?

  • Der IIS ist ja schon sehr ausgereift. Wie sicher schätzt Ihr den Kestrel-Server ein?
  • Wie handelt Ihr das SSL/TLS Zertifikat?
  • Gibt es Features die Ihr vermisst?
  • Performance: IIS in-process, etc.

Cloud-Hosting scheint mir immer noch sehr teuer zu sein.
Bei Hetzner krieg ich einen "Dedicated Root Server" ab ca. 70 Euro mit Top Hardware (inkl. Windows Server 2019 Standard Edition).