Wann kommen dabei die eingangs genannten Requests (via IIS) ins Spiel?
Es ist eine Web-Anwendung, der Nutzer tut irgendwas und als Folge davon werden Benachrichtigungen (nicht zwingend an den selben Benutzer) versendet.
Hat jeder Benutzer hier seine eigenen Daten od. sind die Daten für viele / alle Benutzer die gleichen?
Sowohl als auch - vermutlich 😄 Tatsächlich weiß ich das nicht sicher, das wird erst noch erarbeitet und hängt nicht nur von uns ab.
Es sollte schon auch vom Worst-Case ausgegangen werden. Oder zumindest von einer 90% Häufigkeit
Daher die 8000 gleichzeitiger Nutzer, das wäre der geschätzte Worst-Case.
Allerdings kostet das bisher schon (zu) viel Zeit, es steht also auch die Frage im Raum, ob es nicht besser wäre, diesen Worst-Case einfach zu akzeptieren, solange es in den meisten Fällen vernünftig läuft und die Anwendung sich nicht komplett aufhängt.
Was heißt "sofort" (siehe vorhin)?
Wohin wird die Antwort geschickt?
Ggf. haben wir hier ein Kommunikationsproblem?
Mit "sofort" meine ich aus Code-Sicht "sofort", also blöd gesagt: In der nächsten Code-Zeile brauche ich das Ergebnis.
Entweder ich warte im Code synchron oder asynchron (mit await), hauptsache ich kann direkt in der nächsten Zeile im selben Request mit der selben Transaktion darauf reagieren. Ich kann die API-Calls aber nicht im Hintergrund ausführen lassen und nach Beendigung den Benutzer informieren, auch wenn es sich für ihn ggf. wie "sofort" anfühlt.
Die Antwort (z.B. eine Id) wird dann in die DB gespeichert, um irgendwann später (Hintergrund-Job) den Status abfragen und darauf reagieren zu können.
Wenn beim Versenden etwas schief ging, dann muss darauf reagiert werden, indem die komplette Transaktion zurückgerollt wird und dem Benutzer ein Fehler angezeigt wird.
Das Status-Abfragen ist dagegen leichter, weil es (vermutlich) nur im Hintergrund-Thread läuft, da können wir dann hoffentlich asynchron arbeiten.
Welche "Arbeit" und wie werden die (bzw. sollen) die Fehlerinfos angeboten?
Was auch immer der Request tun sollte, das kann alles mögliche sein, effektiv alles, was die Anwendung so tut.
Und angeboten werden die Fehlerinfos nur generisch, weil das dann sehr wahrscheinlich ein Programmierfehler oder schlechte Absprache mit Dritten war, also nichts, wofür der Anwender etwas kann bzw. was er beheben kann. Aber das steht auch noch nicht 100% fest, wir bekommen dazu erst noch Infos.
Warum bringst du eine DB ins Spiel?
Das war eine Überlegung, wie wir das Problem umgehen können.
Wenn wir die Nachrichten nicht versenden, sondern erst speichern und dann in einem Hintergrund-Thread asynchron versenden, gäbe es das Problem nicht mehr.
Das funktioniert aber nicht, da der Request, der die Nachricht versendet, nach dem Versenden selber mit dem Ergebnis weiter arbeiten können muss.
Wie ist hier der Zusammenhang mit den Requests?
Es wird wohl kein async Task max. 2 Monate brauchen
"Prozess" ist hier aus Anwendersicht gemeint.
Also wenn Du online z.B. irgendetwas beantrags, dann füllst Du Formulare aus, bestätigst Kram, wirst informiert, etc.
Das alles ist eine Web-Anwendung, das alles passiert also über Web-Requests.
Und der Zeitraum, in dem Du so einen Antrag einreichen darfst, ist fix vorgegeben und dein Zeitfenster sind 2 Monate.
Mit "sync over async" und max. 8000 Requests wird es so od. so Probleme geben, v.a. in Hinsicht "sofort"
Das ist mir/uns bewusst. Im Worst-Case ist's langsam, daran können wir im Moment nichts ändern, hauptsache die Anwendung läuft weiter und "fängt" sich irgendwann wieder, wenn weniger Leute online sind.
Das ist auch ein generelles Thema, da habe ich gar keinen Einfluss drauf.
Was passiert denn nun wenn ein Request zum IIS kommt?
Wird dann das externe API aufgerufen?
Es passiert alles mögliche. Im Request stehen Daten, es werden Daten in der DB aktualisiert, der Code stellt fest, dass Person X eine Nachricht braucht und ruft die API auf. Es kann aber auch ein Hintergrund-Thread sein, der nachts los rennt und Anträge (um beim Beispiel zu bleiben) aktualisiert und wenn irgendetwas aktualisiert wurde, werden Personen informiert, das läuft wieder über die API.
Wenn nun für eine Hack-Lösung ein weiterer Hack eingebaut wird, so wird es wohl auch nicht besser werden.
Natürlich wird das nicht besser, das ist klar.
Aber das Projekt soll eh neu entwickelt werden - dann natürlich alles asynchron und mit Erfahrungen aus den Fehlern
Deshalb hält sich auch der Wille in Grenzen, solche grundlegenden Probleme anzugehen. Wäre es wenig Aufwand, ok, aber das wäre sehr viel Aufwand, also lieber dafür sorgen, dass es keinen Crash und keinen Deadlock gibt und der Rest ist dann halt so.
Und auch die vielen möglichen Fallstricke, wenn sich das Verhalten vom ThreadPool oder dem IIS ändert:
Ja, das ist ein Problem, aber keines, was wir sinnvoll lösen können.
Ich kann das mit in die Überlegungen einbeziehen, ob es besser ist, die Krücke zu bauen, oder ob ich lieber den ganzen API-Client einmal für HttpClient und einmal für WebClient anbiete. Aber da das Projekt eh nicht mehr super lange leben wird, tendiere ich eher zur Krücke, als jetzt noch mehr Zeit da rein zu stecken.
Mit den spärlichen Infos ist es mir schier unmöglich konstruktiv beizutragen. Wie vorhin schon gefragt, beschreib doch die wichtigen Punkte und nicht warum was nicht geändert werden kann
Im Grunde hast Du alles, was ich weiß
Ich glaube viel eher, wir hängen an einem Kommunikationsproblem fest?
Ich kenne das Projekt leider auch nicht ^^
Ich sitze quasi zwischen den Tischen.
Aber so weit ich das verstehe:
Das ganze Thema soll Benachrichtigungen versenden, das können entweder E-Mails sein, oder andere Wege.
Mir geht's dabei um so einen anderen Weg, das läuft das dann über einen Dienst, auf den ich nur bedingt (kaum) Einfluss habe.
Dieser externe Dienst nimmt (über eine Web-API) Daten entgegen, validiert ziemlich viel und teilt mir mit, ob alles ok ist, oder nicht.
Ich bin derjenige, der den intern genutzten C#-Client dafür entwickelt.
Deshalb kann ich auch nicht sicher sagen, wie der Ablauf konkret sein wird: Es ist effektiv alles, überall, wo Benachrichtigungen raus gehen.
Und das ist auch der Grund, warum wir keine grundlegenden Strukturen (sync->async oder SignalR) umbauen können, wir müssten effektiv das ganze Projekt umbauen.
Und deshalb weiß ich auch keine Größenordnungen. Die 8000 sind eine Schätzung, die man mir mitgeteilt hat - in der Realität wahrscheinlich sehr viel weniger, die 8000 wären dann also eher ein absoluter Ausnahmefall und solange das irgendwie trotzdem verarbeitet werden kann, auch wenn es langsam ist, bin ich prinzipiell fein damit.
Die komplexen Validierungen sind auch der Grund, warum ich immer sofort eine Antwort brauche. Die Daten, die da validiert werden, kommen nicht (nur) von uns, wir können sie also nicht einfach korrigieren, wenn was schief ist und einen zweiten Versuch starten. Wir müssen die Arbeit also abbrechen und Fehlerinformationen anbieten können, wenn es ein Problem gibt. Würden wir die Nachrichten erst in die DB schreiben und irgendwann später versenden, hätten wir diese Möglichkeit nicht mehr, oder wir müssten sämtliche Prozesse grundlegend umstrukturieren.
Oder, wenn es um Status-Updates geht, dann brauchen wir natürlich den Status der Benachrichtigungen, um dann darauf reagieren zu können.
Das Zeitfenster von zwei Monaten kommt daher, dass es Prozesse gibt, an die sich die Anwender halten müssen. Ab Datum X wird ein Prozess freigegeben und für zwei Monate können die Anwender dann an diesem Prozess arbeiten (z.B. einen Antrag stellen), danach wird's dann wieder zu gemacht. Es kann also sein, dass sie sofort ab Datum X los laufen, oder erst 6 Wochen später, oder ein Tag vor Deadline.
Die Kommunikation findet über das Netzwerk statt und das ist - nach meinem Wissen - bisher nur mit dem veralteten WebClient möglich und auch da war's ja eher eine Hack, als eine vernünftige Lösung. Die gleiche Code-Basis soll aber auch in anderen modernen Projekten genutzt werden, ich möchte also möglichst vermeiden, mir eine veraltete Technologie ans Bein zu binden, für ein Projekt, was vermutlich eh in ein paar Jahren grundlegend modernisiert wird.
Ich habe also die Wahl:
Mir fällt es schwer, abzuschätzen, wie groß das Risiko bei Option 2 ist, das versuche ich hier herauszufinden.
Deine Klarstellungen zum ThreadPool hat aber eines erreicht: Meine Annahme zur 2. "Deadlock-Art" war falsch, das Risiko fällt also weg.
Dass die generelle Performance bei vielen gleichzeitigen Requests schlecht sein könnte, das ist verkraftbar. Das kommt vermutlich sowieso nicht so oft vor. Die Anwendung hat generell eine schlechte Performance, es gibt allso Mechanismen, die das behandeln ^^
Entscheidend ist nur, dass die Anwendung weiter läuft, auf gar keinen Fall darf sich alles aufhängen.
Mein Ziel mit dieser Frage ist also teils eine brauchbare Lösung für sync->async zu finden (auch für mich für den Lerneffekt) und um einzuschätzen, welche der beiden Optionen besser ist: WebClient vs. async->sync-Krücke.
Aktuell - maßgeblich durch deine Klarstellung zum ThreadPool - tendiere ich dazu, mit Task.Factory.StartNew einen neuen Task auf dem ThreadPool zu erstellen und synchron darauf zu warten. Damit habe ich das Deadlock-Risiko durch ASP.NET umgangen und bzgl. die Befürchtung, dass es ein zweites Deadlock-Risiko gibt, hast Du aus der Welt geschafft.
Also so:
private TResult execute<TResult>(Func<Task<TResult>> executeAsync, CancellationToken cancellationToken)
{
var task = Task.Factory.StartNew(
function: static x => ((Func<Task<TResult>>)x)(),
state: executeAsync,
cancellationToken,
TaskCreationOptions.None, // Wäre hier eine Option sinnvoll? HideScheduler?
TaskScheduler.Default
).Unwrap();
return task.GetAwaiter().GetResult();
}
ganz hab ich das eigentliche Problem nicht verstanden. Kannst du das etwas allgemeiner beschreiben?
Es geht um eine mit .NET 4.6.2 entwickelte ASP.NET-Anwendung, die intern von vorne bis hinten synchron arbeitet.
Und es geht um ein Framework, das asynchron arbeitet, was aus der alten Anwendung heraus genutzt werden muss.
Die betreffenden Codesytellen auf asynchron umstellen wäre viel zu aufwändig, da das ungefähr die komplette Anwendung betreffen würde.
Es bleibt also die Herausforderung, aus synchronen Code heraus synchron auf asynchronen Code zu warten - was natürlich ein Problem ist.
Ich sehe dabei zwei Deadlock-"Arten":
Bisher hebe ich zwei Möglichkeiten auf dem Schirm:
Beides würde dazu führen, dass ich synchron auf die Beendigung des asynchronen Codes warten kann, aber beides birgt auch ein gewisses Deadlock-Risiko, das ich nicht einschätzen kann - hoffentlich Du? 😄
Lässt sich das größenordnungsmäßig angeben?
Schwer, da es - wie ich eben erfahren habe - doch nicht nur eine Funktion ist, sondern so gut wie alle Funktionen.
Die Schätzung ist aber MAXIMAL 8000 (eher sehr viel weniger) gleichzeitiger Nutzer, was an sich nicht viel ist, aber es reicht ja schon, wenn nur wenige Nutzer ungünstig gleichzeitig etwas tun wollen.
Warten die alle auf das gleiche bestimmte Datum od. jeder für sein eigenes od. gibt es Gruppen von Datums auf die gewartet wird?
Meine ursorüngliche Aussage, dass sie auf ein Datum warten, war falsch.
Ich können es tatsächlich nicht konkret sagen, wir haben leider auch kein brauchbares Logging, was etwas darüber verraten könnte.
Aber in der Regel sieht es so aus, dass ab einem Datum ein Prozess aus Sicht des Nutzers frei gegeben wird und dann können die Nutzer los legen. Die Nutzer haben dann aber kein Limit (z.B. max. X Tickets verfügbar) und sie haben ein Zeitfenster von 2 Monaten, in dem sie arbeiten können.
Es kann also sein, dass viele Nutzer direkt zu Beginn los rennen, oder ganz zum schluss nochmal in letzter Sekunde. Wahrscheinlicher ist aber, dass es sich über die 2 Monate verteilt.
Und es gibt natürlich auch die wage DDoS-Gefahr.
Ein schlecht skallierender Webserver ist da natürlich sowieso ein Problem, aber ich muss ja nicht noch ein potentielles Deadlock-Risiko drauf legen ^^
Müssen die Requests warten od. kann z.B. via SignalR das nur 1x laufen und die Benachrichtigungen, etc. werden dann zu den Clients gesendet?
Du meinst, die Kommunikation z.B. in die DB legen und dann in einem Hintergrund-Thread abarbeiten?
Das funktioniert leider auch nicht, weil die Requests die Antwort oder ggf. Fehler-Informationen selber sofort brauchen.
Aufsplitten, dass das Frontend erst später informiert wird, kommt leider auch nicht in Frage.
IIRC verwendet der IIS pro Request einen (Windows-) Thread (ob dieser vom Windows ThreadPool stammt od. nicht weiß ich nicht), ...
Das - ob der Thread vom ThreadPool stammt - wäre ggf. ein entscheidendes Detail?
Eine Annahme von mir für ein potentielles Deadlock-Risiko war ja, dass alle ThreadPool-Threads beschäftigt sind, indem sie Tasks auf den ThreadPool ausführen wollen.
Wenn die Requests aber gar nicht auf dem ThreadPool ausgeführt werden, dann würde der problematische Code gar nicht auf dem ThreadPool laufen, also fällt dieses Risiko weg.
..., somit ist das Beschränken auf letztlich einen Thread so od. so eher fraglich ohne massive Skalierungsproblem zu haben und gleichzeitig die Gefahr von latenten Deadlocks groß.
Meinst Du damit die Tatsache, dass die Requests alle synchron arbeiten und damit über die ganze Laufzeit diesen einen Thread blockieren, obwohl eigentlich 99% der Zeit nur auf IO gewartet werden muss?
Das ist mir bewusst, ändern kann ich das aber leider nicht.
Oder meinst Du, dass ich den CustomTaskScheduler mit nur einem Thread implementiert habe?
Das basierte auf der Annahme, dass es im Großen und Ganzen sowieso nie mehr als ein Task gleichzeitig (pro Request) sein wird, also würde ein Thread + eine Queue dafür ausreichen. Es könnte zwar sein, dass irgendwo mehrere Tasks parallel gestartet werden, aber solange mit await und nicht mit Wait() darauf gewartet wird, ist das ja kein Problem. Der Code läuft dann zwar langsamer, aber das ist vermutlich nicht weiter relevant.
Blazor gefällt mit nicht so, da müsste ich meinen C# Teil ja wieder als Webassembly ausliefern.
Wie kommst Du darauf?
Du musst kein WebAssembly ausliefern, nur wenn Du es auch nutzen willst.
Wie MAUI oder Electron das machen, weiß ich nicht, aber auf der Photino-Website steht dazu:
Photino.Blazor is not Server-Side Blazor, nor is it WASM Client-Side Blazor. When the .NET (console) application starts, it uses the full .NET 6 framework. The framework can be pre-installed on the machine or it can come packaged with the application in the case of a “single file application.” Execution of all .NET code in Blazor is redirected to the same framework as the console app.
Es fehlt sich aber wie Server-Side an, man hat z.B. nicht diesen furchtbaren JavaScript-Debugger.
Das größte ist wohl im Moment wohl https://github.com/dlemstra/Magick.NET was ich nur nutze um WMF/EMF in SVGs zu wandeln. Vlt. finde ich da ja noch was kleineres...
Dabei hilft dir ggf. das Trimming, das kann ja alles das raus kürzen, was Du nicht nutzt.
Ob das funktioniert hängt aber auch stark vom Framework ab, nicht jedes Framework unterstützt das.
Zu Avalonia kann ich deine Frage nicht beantworten, aber wenn's darum geht, schlank und Multiplattform zu sein, würde ich auf Blazor setzen mit Photino.NET.
Photino.NET ist ein ziemlich leichtgewichtiges Konkurrenz-Framework zu Electron, allerdings (vermutlich?) lange nicht so ausgereift, wie Electron.
Für einfache Projekte ohne spezielle Anforderungen sollte das aber völlig ausreichen, behaupte ich.
Allerdings kann Photino.NET nur Desktop-Anwendungen, leider kein Mobile.
Wenn Du Blazor mit Photino.NET oder Electron nutzt, dann hast Du auch den Vorteil, dass Du den Großteil des Codes auch für eine klassische Website oder mit MAUI Hybrid (MAUI mit Blazor) nutzen und damit wirklich alle Plattformen abdecken.
Avalonia bietet das alles aus einer Hand, das macht es natürlich einfacher. Was dir lieber ist, musst Du wissen 😃
Mit Blazor kannst Du aber die ganze riesige Bandbreite an Web-Technologien nutzen - was sowohl Vorteil, als auch Nachteil sein kann ^^
Davon eine eigene Instanz zu erzeugen ist ein potentielles Sicherheitsrisiko.
Das verstehe ich nicht? Angenommen, der ThreadPool würde nicht statisch arbeiten, sondern alle Threads je Instanz verwalten, wo ist dann das Sicherheitsrisiko? Die ThreadPool-Klasse hätte dann eine Current-Property und man kann seine eigene Instanz mit eigenen Threads erzeugen, das würde mein Problem auf einen Schlag aus der Welt schaffen.
Oder willst Du darauf hinaus, dass der ThreadPool von Windows verwaltet wird (es gibt ja eine "UseWindowsThreadPool"-MSBuild-Property) und damit zwei vollständig getrennte ThreadPool-Instanzen gar nicht möglich sind?
Deine Implementierung SingleThreadTaskScheduler ist also quasi ein Custom Thread Pool. Ich sehe
Das sieht aus, als würde da etwas fehlen? 😃
Sind die Aufgaben in den Tasks, auf die Du wartest, identisch?
Cachen ist leider nicht möglich. Es wird kein Ergebnis errechnet, sondern eine externe Komponente gesteuert und überwacht.
In Deinem Fall is eventuell doch eigenes Management statt Pool der bessere Weg.
Zu der Schlussfolgerung bin ich auch gekommen.
Wenn das Ausführen der Tasks auf dem ThreadPool unvermeidbar ein Deadlock-Risiko birgt, ist die einzig logische Konsequenz: Kein ThreadPool ^^
Wobei die Tasks selber ziemlich schnell laufen sollten, der "äußere" Request-Thread ist das, was langsam ist, daran kann ich aber im Moment nichts ändern.
Mein TaskScheduler oben scheint ja zu laufen, aber ist der sicher? Siehst Du da Lücken?
Oder produziere ich mir damit ein weiteres Deadlock-Risiko, weil es nur ein Thread ist?
So wie ich es verstehe, ist ja jeder Task-Step beenden, bevor der nächste Step an den TaskScheduler übergeben wird, also sollte das kein Problem sein.
Die Alternative wäre, mehrere Threads zu verwalten, also im Prinzip ein eigener ThreadPool - schade, dass der .NET-ThreadPool komplett statisch arbeitet und ich mir keine eigene Instanz bauen kann 😕
Ein Task.Wait() aus meinem Worker-Thread führt natürlich zu einem Deadlock, aber das ist klar und kann ich ggf. mit einem Check verhindern, ob die Queue beim Enqueue leer ist.
Hm - nach dem, wie ich die Doku dazu verstehe, wäre UnsafeQueueUserWorkItem das, was ich nutzen müsste, da hier kein ExecutionContext weitergegeben wird - soweit so gut, damit fällt zumindest schon einmal der SynchronizationContext weg, das muss ich dann nicht mehr manuell tun.
Aber habe ich damit nicht immer noch das Problem, dass ein Deadlock auftreten kann, wenn alle ThreadPool-Threads die Ausführung je eines Tasks mit UnsafeQueueUserWorkItem in die ThreadPool-Queue legen, die aber nie ausgeführt werden, weil alle ThreadPool-Threads darauf warten?
Das wäre ein Extrembeispiel, aber dennoch möglich, oder übersehe ich etwas?
Ich hab deinen Vorschlag Mal ausprobiert:
public void Cancel(BereitstellungType type, Guid id, CancellationToken cancellationToken = default)
=> execute((type, id, cancellationToken), static (api, x) => api.CancelAsync(x.type, x.id, x.cancellationToken), cancellationToken);
private void execute<TState>(TState state, Func<IMyAsyncApi, TState, Task> executeAsync, CancellationToken cancellationToken)
=> executeCore(state, executeAsync, cancellationToken);
private TResult execute<TState, TResult>(TState state, Func<IMyAsyncApi, TState, Task<TResult>> executeAsync, CancellationToken cancellationToken)
=> executeCore(state, executeAsync, cancellationToken).Result;
private TTask executeCore<TTask, TState>(TState state, Func<IMyAsyncApi, TState, TTask> executeAsync, CancellationToken cancellationToken)
where TTask : Task
{
var closure = new Closure<TTask, TState>()
{
Api = _asyncApi,
State = state,
ExecuteAsync = executeAsync,
};
var enqueued = ThreadPool.UnsafeQueueUserWorkItem(
callBack: static state => ((Closure<TTask, TState>)state).Execute(),
state: closure);
if (!enqueued)
throw new InvalidOperationException("Could not enqueue work item.");
closure.Wait(cancellationToken);
closure.ExecutedTask.GetAwaiter().GetResult();
return closure.ExecutedTask;
}
private sealed class Closure<TTask, TState> : ManualResetEventSlim
where TTask : Task
{
private TTask? _executedTask;
public required IMyAsyncApiApi { get; init; }
public required TState State { get; init; }
public required Func<IMyAsyncApi, TState, TTask> ExecuteAsync { get; init; }
public TTask ExecutedTask
{
get => _executedTask ?? throw new InvalidOperationException("Task has not been executed yet.");
}
public void Execute()
{
_executedTask = ExecuteAsync(Api, State);
_executedTask.GetAwaiter().GetResult();
Set();
}
}
Die Tests laufen durch, das ist schon Mal gut.
Aber ist es auch Deadlock-Sicher? Das ist ja das, was mir so Kopfschmerzen bereitet, ans Laufen bekommen ist leicht, aber sicher gegen Deadlocks machen, das ist unangenehm 😕 Und wenn ich etwas übersehe, dann brennt irgendwann in vielleicht 5 Jahren die Luft und niemand weiß, wo oben und unten ist.
Ich hab jetzt eine erste Version des TaskSchedulers.
Die UnitTests (naja, eher Integration / Ende-zu-Ende-Tests), die ich damit auf synchron umgebaut habe, laufen durch.
Aber ich werde das Gefühl nicht los, dass das ganze eine sehr wackelige Angelegenheit ist.
Und Deadlocks testen kann ich immer noch nicht, ich kann nur versuchen, einige Tests für den TaskScheduler zu schreiben.
internal sealed class SingleThreadTaskScheduler : TaskScheduler, IDisposable
{
private readonly ManualResetEventSlim _workerThreadPausedEvent = new(false);
private readonly CancellationTokenSource _disposedCancelTokenSource = new();
private readonly ConcurrentQueue<Task> _tasksQueue = new();
private readonly Thread _workerThread;
public override int MaximumConcurrencyLevel => 1;
public SingleThreadTaskScheduler(string workerThreadName)
{
_workerThread = new Thread(processTasks)
{
IsBackground = true,
Name = workerThreadName,
};
}
protected override IEnumerable<Task> GetScheduledTasks() => _tasksQueue.ToArray();
protected override void QueueTask(Task task)
{
if (_disposedCancelTokenSource.IsCancellationRequested)
throw new ObjectDisposedException(GetType().Name);
if (_workerThread.ThreadState.HasFlag(ThreadState.Unstarted))
_workerThread.Start();
_tasksQueue.Enqueue(task);
_workerThreadPausedEvent.Set();
}
protected override bool TryDequeue(Task task) => false;
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
if (Thread.CurrentThread.ManagedThreadId == _workerThread.ManagedThreadId)
return TryExecuteTask(task);
return false;
}
private void processTasks()
{
while (!_disposedCancelTokenSource.IsCancellationRequested)
{
try
{
if (_tasksQueue.TryDequeue(out var task))
{
TryExecuteTask(task);
}
else
{
_workerThreadPausedEvent.Wait(_disposedCancelTokenSource.Token);
_workerThreadPausedEvent.Reset();
}
}
catch (Exception)
{
}
}
}
public void Dispose()
{
if (_disposedCancelTokenSource.IsCancellationRequested)
return;
_disposedCancelTokenSource.Cancel();
while (_tasksQueue.TryDequeue(out _))
;
_workerThreadPausedEvent.Set();
if (!_workerThread.ThreadState.HasFlag(ThreadState.Unstarted))
{
_workerThread.Join(1000);
_workerThread.Abort();
}
_disposedCancelTokenSource.Dispose();
_workerThreadPausedEvent.Dispose();
}
}
Der wird wie folgt genutzt:
public MyResult DoWork(MyArgs args, CancellationToken cancellationToken = default)
=> execute(() => _asyncVersion.DoWorkAsync(args, cancellationToken), cancellationToken);
private void execute(Func<Task> executeAsync, CancellationToken cancellationToken)
=> execute(executeAsync, static t => t.Unwrap(), cancellationToken);
private TResult execute<TResult>(Func<Task<TResult>> executeAsync, CancellationToken cancellationToken)
=> execute(executeAsync, static t => t.Unwrap(), cancellationToken).Result;
private TTask execute<TTask>(Func<TTask> executeAsync, Func<Task<TTask>, TTask> unwrap, CancellationToken cancellationToken)
where TTask : Task
{
var allowedThreadId = Interlocked.CompareExchange(ref _allowedThreadId, Environment.CurrentManagedThreadId, 0);
if (allowedThreadId != 0 && allowedThreadId != Environment.CurrentManagedThreadId)
throw new InvalidOperationException("Only one thread is allowed to use this API.");
var wrapperTask = Task.Factory.StartNew(executeAsync, cancellationToken, TaskCreationOptions.None, _taskScheduler);
var task = unwrap(wrapperTask);
task.GetAwaiter().GetResult();
return task;
}
Der Autor der Antwort auf Stackoverflow schreibt doch selber:
However, all three methods cause the potential for deadlock and thread pool starvation issues.
Das löst mein Problem nicht, das ist es doch gerade, was ich verhindern will.
Hallo zusammen,
ich habe das vor, was man eigentlich nicht tun sollte, ich weiß 😃
Ich habe ein ziemlich altes ASP.NET-Projekt (nicht ASP.NET Core) mit .NET 4.6.2, das von vorne bis hinten synchron arbeitet.
Das zu ändern kommt auch nicht in Frage, aber ich muss asynchronen Code darin aufrufen.
Das ganze läuft auf einem IIS.
Der betreffende Code kann sehr oft gleichzeitig laufen, also z.B. sehr viele Nutzer, die ein bestimmtes Datum abwarten und dann alle auf einmal los rennen, Requests produzieren und ThreadPool-Threads füllen. Die Requests laufen auch recht lange, es kann also durchaus sein, dass sehr viele ThreadPool-Threads (oder ggf. sogar alle) gleichzeitig belegt sind.
Die generelle Performance ist natürlich nicht egal, aber zweitrangig, hauptsache es läuft und es gibt keinen Deadlock.
Wenn ich nun auf asynchronen Code mit .Wait()
warte, dann sorgt das bei ASP.NET ja für einen Deadlock. Das kann ich wie folgt umgehen:
var syncContext = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(null);
myAsyncMethod().Wait();
}
finally
{
SynchronizationContext.SetSynchronizationContext(syncContext);
}
Aber bei dieser Arbeitsweise sehe ich das Risiko, dass es trotzdem zu einem (quasi-) Deadlock kommen kann, wenn zu viele ThreadPool-Threads belegt sind, die alle darauf warten, dass Platz auf dem ThreadPool frei wird, damit der Task laufen kann.
Ich habe gelesen, dass folgendes ausreichen soll:
Task.Run(() => myAsyncMethod()).Wait();
Mir ist aber nicht klar, wie das ausreichen soll? Der neue Task geht doch auch über den Default oder Current TaskScheduler und damit auf den ThreadPool.
Stattdessen würde ich folgendes nutzen:
Task.Factory.StartNew(() => myAsyncMethod(), TaskCreationOptions.LongRunning);
So wie ich das verstehe, bekommt der Task dann VIELLEICHT einen komplett neuen Thread und muss nicht auf den ThreadPool warten.
Aber reicht das aus? Und das würde ja auch bedeuten, dass sehr viele neue Threads erstellt werden müssen.
Meine Überlegung war daher, einen eigenen TaskScheduler zu schreiben, dessen Instanz für die Dauer eines Request intern einen Thread in einer Endlosschleife nutzt, um die Tasks abzuarbeiten. Auf den Task warte ich dann mit RunSynchronously(TaskScheduler)
. Da die Requests synchron arbeiten, würde hier ein Thread ausreichen und ich habe effektiv alles, was einen Deadlock verursachen kann, aus dem Spiel genommen.
Würde das ausreichen, um ein Deadlock zuverlässig zu verhindern?
Und kann ich das irgendwie automatisiert testen?
Ich habe versucht einen UnitTest zu schreiben, der alle ThreadPool-Threads füllt und so diesen Zustand künstlich herstellt.
Der Test funktioniert auch, aber nur manchmal, vermutlich weil im Hintergrund immer sporadisch wieder Threads frei werden und meinen Test kaputt machen.
Meine andere Idee wäre, ein kleines Programm zu schreiben, was das isoliert in einem eigenen Prozess testet, ohne dass irgendetwas anderes Threads braucht. Dieses Programm kann ich dann aus dem "UnitTest" heraus aus starten und das Ergebnis abfangen.
Wirklich richtig klingt das auch nicht, aber ich würde gerne sicherstellen, dass es keinen Deadlock geben kann.
Habt Ihr dazu Tipps, oder habe ich einen Verständnisfehler?
Du kannst Console.ReadKey(true) nutzen, um Tasten entgegenzunehmen, ohne dass sie angezeigt werden.
Die Taste kannst Du dann prüfen und - sofern gültig - das Zeichen selber in die Konsole schreiben.
Vieles musst Du dann aber selber machen, z.B. sind Tausender-Trennzeichen ein eigentlich gültiger Teil einer Zahl, würdest Du mit einer Prüfung auf nur Ziffern aber verbieten. Gleiches auch bei Nachkommastellen oder anderen Darstellungen von Zahlen.
Ob es das wert ist, musst Du dir überlegen, aber wenn die Eingabeoptionen sehr beschränkt sind (z.B. Ziffern eines Menüs) spricht da mMn. nichts gegen.
"Microsoft.Extensions.DependencyInjection.Abstractions" ist ein NuGet-Package, das von "Microsoft.Extensions.DependencyInjection" genutzt wird und eigentlich automatisch mit installiert werden sollte.
Das Problem ist, dass er das nicht findet, der Grund dafür ist aber nicht in dem von dir gezeigten Code, sondern irgendwo anders.
Hast Du ungewöhnliche Projekt-Einstellungen (csproj-Datei)? Dann wären diese Einstellungen wichtig.
Oder benutzt Du eine andere Software (z.B. Unity), sodass in Verbindung damit etwas schief gegangen ist? Dann müsstest Du dich damit auseinandersetzen, ggf. behandeln die Abhängigkeiten anders.
Oder tritt der Fehler nicht beim Debuggen auf, sondern erst, wenn Du das Programm irgendwo anders hin kopierst und von dort aus startest? Dann hast Du zu wenig kopiert, generell sollte man das Programm nicht einfach nur kopieren, sondern die Publish-Funktion nutzen, das kannst Du dann gezielt einstellen und bekommst alles ausgegeben, was notwendig ist - je nach Einstellungen.
Ich habe viel Geld in die Hand genommen um eine Weiterbildung aus eigener Tasche zu bezahlen
Ich bin mir nicht sicher, ob das Geld so gut angelegt ist, ich hab noch von keiner guten Weiterbildung gehört, nach der man dann auch vernünftig produktiv arbeiten konnte.
Ich habe einmal so eine Weiterbildung zu Angular gemacht und mein Eindruck war: Sehr oberflächlich und der Lehrer hatte über den beschränken Umfang des Unterrichts nur wenig Ahnung von der Thematik. Die besten Leute findest Du in der Regel in der freien Wirtschaft bei "normalen" Projekten, aber ich bezweifle, dass viele davon tatsächlich aktiv unterrichten. Und wenn sie tatsächlich unterrichten, dann vermutlich eher intern, die meisten Firmen haben Azubis oder Junior-Entwickler, die ebenso Hilfe brauchen.
Das alles kann aber auch nur meine "Bubble" sein, kann ich nicht beurteilen.
Ein Azubi von uns hat aber letztens ein Buch gefunden, das er bisher ganz gut findet:
https://dl.ebooksworld.ir/books/Pro.CSharp.10.with.NET.6.Andrew.Troelsen.Phil.Japikse.Apress.9781484278680.EBooksWorld.ir.pdf
(Kann man auch kaufen, wenn dir ein Buch aus Papier lieber ist)
Ist Englisch und sicher kein einfaches Englisch, aber es geht einmal quer durch alles, was man so an Grundlagen braucht (nicht Basics, für mich sind die Grundlagen umfangreicher) und es geht (dem Inhaltsverzeichnis nach zu urteilen) ziemlich ins Detail, was ggf. sehr beim Verständnis helfen kann - je nach Lerntyp.
Oder das:
https://www.amazon.de/Visual-Studio-2019-Objektorientierung-Programmiertechniken/dp/3836264587
(Das gibt's nicht als kostenlose Online-Variante)
Ist Deutsch, aber etwas älter und geht weniger tief ins Detail, sollte aber kein Problem sein, als Anfang reicht's.
Ich hab damals die Version von 2010 gelesen und fand's eigentlich gut verständlich.
Aber klar, ein Buch ist kein Lehrer, aber der Großteil der Arbeit (egal ob privat oder beruflich) besteht darin, neues zu lernen und teils sehr umfangreiche Quellen zu wälzen, es lohnt sich also, früh damit anzufangen.
Ich kann einige Dinge hier nicht einfach im Forum erfragen da Sie Teil meiner Einsendeaufgabe sind und ich diese
mit Sicherheit nicht öffentlich posten darf.
Frag doch einfach nach, ob Du das darfst?
Ansonsten versuche, das Problem in einem neuen Projekt nachzustellen, dann hast Du keine Informationen, die nicht veröffentlicht werden dürfen und allein das neu durchdenken für dieses neue Projekt kann extrem beim Verständnis helfen. Ggf. klärt sich das Problem sogar von selber, nur weil Du es im kleinen Umfang zu reproduzieren versuchst? Das kommt durchaus oft vor.
Ggf. schläfst Du auch mal eine Nacht drüber und fängst am nächsten Tag von vorne an.
Es ist normal, dass Du am Anfang bei manchen Problemen verzweifelst, hat unser Azubi auch gehabt, obwohl er mehrere gute Entwickler hat, die ihm Dinge erklären. Der Trick ist, seine Gedanken zu ordnen (Z.B. ein Spaziergang oder Google mal Rubberduck-Debugging, dabei hilft auch ein Forum, ohne je wirklich eine Frage zu stellen) und dann das Problem von anderer anderen Seite anzugehen, die Du bisher vielleicht nicht betrachtet hast.
Wenn Du ein Anfänger bist, ist das reine Lernen der Grundlagen nur ein Teil der Arbeit, wirklich entscheidend ist eben dieser Prozess der Problemlösung, dabei sammelst Du die wertvollsten Erfahrungen und lernst am meisten.
Ich merk(t)e das auch immer wieder bei mir selber, wenn ich jemanden direkt zum Fragen habe, neige ich dazu, "zu schnell" zu fragen, aber wenn ich erst einmal an dem Punkt war, nicht weiterzukommen, dann aber doch die Lösung gefunden habe, habe ich immer viel mehr mitgenommen - vom Erfolgserlebnis mal ganz zu schweigen.
Natürlich ist ein Lehrer, der gut erklären kann, viel wert, aber die Thematik ist sehr umfangreich und solche Erklärungen können sehr zeitraubend sein. Ich habe z.B. unserem Azubi letztens mehrere Stunden ein einziges Thema erklärt und Beispiele erörtert. Er ist nicht dumm, eher im Gegenteil, aber es dauert eben, bis das Verständnis wirklich sitzt. Und diese Zeit fehlt den meisten.
hab noch nie gelesen das Nachhilfe kostenlos ist
Vielleicht ist das nur meine Wahrnehmung oder ein Generationen-Ding, aber ich habe manchmal dein Eindruck, dass viele "erwarten", dass man ihnen "einfach so" hilft. Sieht man auch hier immer mal wieder, manche Leute fordern, dass man ihnen hilft, sind aber nicht bereit, den Ratschlägen (z.B. lies diesen und jeden verlinkten Artikel) zu folgen. Ich will damit nicht sagen, dass das auf dich zutrifft, aber ich könnte mir vorstellen, dass dieser Eindruck nicht nur bei mir hängengeblieben ist.
Ganz generell, um auf deine ursprüngliche Frage zurückzukommen:
Ich sehe bei mir das größte Problem, dass ich zwar gerne erkläre, aber lieber komplexe Zusammenhänge, als Grundlagen und ich möchte niemandem Hilfe zusagen, wenn ich dann am Ende nicht nur kaum Zeit, sondern nicht einmal die Motivation dazu habe. Und ich denke, dass es vielen anderen hier ähnlich geht.
So ein Forum ist auch viel Arbeit, aber hier hilft man vielen Leuten auch in Zukunft und wenn einer keine Zeit hat, sind noch andere da, die helfen können, das macht es in einem Forum deutlich einfacher.
Niemand hat sich bei dir gemeldet?
Wundert mich ehrlich gesagt nicht, sorry 😃
Du musst bedenken:
Die meisten von uns sind berufstätig.
Die meisten haben Familie, Freunde, Hobbies, etc.
Und bestimmt einige haben auch noch private Projekte, an denen sie arbeiten wollen.
Und dann gibt es auch noch andere Leute, die hier um Hilfe bitten.
Softwareentwicklung ist kompliziert, C# ist vergleichsweise einfach zu lernen, aber immer noch kompliziert.
Jemandem da wirklich zu helfen, was auch ausführlichere Erklärungen erfordert, ist sehr zeitaufwändig.
Und das alles auch noch komplett kostenlos.
Und Du fragst dich, wieso sich niemand meldet? 😃
Wenn Du Verständnisprobleme hast, darfst Du jederzeit hier eine Frage stellen und man wird dir helfen, aber erwarte nicht, dass sich einer mit dir hin setzt und die 1000en Probleme durch gehst, die Du haben wirst.
Probleme sind normal, das gehört dazu und es ist Teil des Lernprozesses, diese Probleme zu lösen - am besten alleine, denn dabei lernst Du am meisten 😉
Und wenn Du wirklich keine Lösung findest, dann hast Du immer noch dieses Forum und kannst um Hilfe bitten.
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles/ColorPaletteStyles.xaml"/>
<!-- ... -->
<ResourceDictionary>
<DataTemplate DataType="{x:Type viewModels:DefaultViewViewModel}">
<views:DefaultView/>
</DataTemplate>
<!-- ... -->
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Bzw. Du solltest die DataTemplates ggf. auch eigene ResourceDictionary-Dateien auslagern, um es übersichtlicher zu gestalten.
ich habe bereits versucht die DataTemplates in ein REsourceDictionary auszulagern - leider funktioniert auch das scheinbar nicht.
Den Satz hab ich leider erst übersehen.
Was heißt denn "funktioniert nicht"?
Denn genau so sollte es eigentlich gehen.
Ich habe sowas mal mit dem WiX Toolset gemacht. Kann viel, ist aber nervig zu bedienen.
Für Freunde würde ich das Projekt einfach publishen und den Ordner weiter geben, kein Installer.
Und deine Probleme hängen von deinem Code ab, kann gut sein, dass dein Programm einfach nicht damit umgehen kann, wenn es von woanders aus gestartet wird.
Also musst Du selber suchen. Erfahrung ja, aber deinen Code kennst nur Du.
Zum Testen kannst Du eine Log-Datei hardcoded auf deinem Desktop ablegen lassen und Infos rein schreiben.
Oder Du testest, ob Du auch einen Installer mit einer Debug-Version bauen kannst, dann schreib irgendwo "Debugger.Launch()" und wenn dein Programm dort ankommt, versucht er einen Debugger zu starten - bietet dir dann Visual Studio zum Start an.
Im Installationsordner ist die angelegte SQLite-Datenbank und die Log-Datei nicht aufzufinden
Da sollte auch keine Datenbank liegen - zumindest wenn Du die gängigen Installationsordner unter C: meinst.
Das Programm liegt normalerweise unter ProgramFiles (und Co.), Benutzerdaten kommen dann ins Benutzerprofil, z.B. unter AppData. Oder Du legst eine Config-Datei (Microsoft.Extensions.Configuration), dann kann man selber konfigurieren, wo das Zeug liegt, sofern Bedarf besteht.
Guck dir Task.Delay mal an, das nutzt Du anstelle dem Sleep().
Und ggf. kannst Du auch das Polling selber asynchron aufbauen, die meisten Microsoft-APIs, bei denen es Sinn ergibt, unterstützen auch asynchrone Methoden.
Visual Studio hat manchmal so einen Schluckauf.
Das passiert bei generiertem Code (hinter WPF wird Code generiert, auch die InitializeComponent()-Methode), Visual Studio bekommt dann nicht mit, dass sich da etwas geändert hat.
Die genauen Gründe kenne ich nicht, es ist vermutlich einfach nur ein Bug in Visual Studio, also nichts worauf wir Einfluss haben.
Wenn die API nichts dafür hat, musst Du eben selber Zeile für Zeile prüfen und zählen.
Kurzer Blick in die Doku zeigt:
workSheet.Cells[1, "A"] = "ID Number";
workSheet.Cells[1, "B"] = "Current Balance";
Ich rate mal, dass man über den Indexer auch den Wert abrufen kann und wenn da etwas drin steht, ist die Zelle beschrieben und Du kannst deinen Index +1 zählen.
Aber ich verstehe immer noch nicht, warum Du nicht einfach beim Befüllen mit zählst.
Ist doch egal, woher die Daten kommen, wenn Du sie in die Excel-Datei schreibst, zählst Du einen Counter mit hoch und hast deine letzte beschriebene Zeile.
Abgesehen davon:
Wenn Du nicht zwingend etwas brauchst, was nur mit Interop geht, würde ich dir von Interop abraten. Interop ist furchtbar, außerdem gibt es einige Einschränkungen, wie z.B. es muss Office in der passenden Architektur installiert sein und mehrere Dateien gleichzeitig bearbeiten geht meine ich auch nicht.
Lieber mit Hilfe von DocumentFormat.OpenXml, oder - mein Favorit - Du nutzt das Framework ClosedXml, was das durchaus komplexe OpenXml-ObjektModel deutlich vereinfacht. In beiden Fällen erstellst Du die xlsx-Datei, speicherst sie ab und kannst sie dem Nutzer anbieten.
Du weißt, wo deine Tabelle beginnt (vermutlich die 1. Zeile) und Du weißt, wie viele Zeilen Du eingefügt hast - wo ist das Problem?
Anzahl Zeilen + 1 (für Header) und Du hast die letzte beschriebene Zeile, schreib ein "H" davor und Du hast die Zell-Referenz.
Wie wärs mit einem simplen Default Output?
Der Output ist ja immer der gleiche ^^
Der Plan war ja, dass man dann im Code sämtliche Informationen hat, wie man sich so vorstellen kann, damit man anschließend selber die UI aufbauen und entsprechend filtern kann. Ansonsten habe ich die Problematik, dass ich Filter-Optionen konfigurierbar machen muss und das wird eklig ^^
Unterschiede gibt es, wenn man zusätzliche Inhalte (ReadMe, Icons, etc.) laden will, darauf fallen dann auch ein Großteil der Einstellungen an.
Der Rest sind dann die Lizenzprüfung und sowas wie Klassenname, public vs. internal, etc.
Wenn ich das Laden von zusätzlichen Inhalten und die Lizenzprüfung herausziehen kann (Doku und/oder technisch), dann wird die Doku schon deutlich einfacher.
Das Problem mit der Dependency auf ImageSharp kann ich aber nur mit einem Plugin-System lösen.
Abgesehen davon wäre ggf. auch ein JSON-Output ein Gedanke wert, spricht ja eigentlich nichts gegen.
Wäre praktisch, wenn das in der Console direkt (zB via Spectre) verfügbar is, sodass das Simple eben sofort da ist und eigene Sachen für das Komplexe da ist.
Hm - damit könnte man das CLI-Tool direkt mit einer UI ausstatten, kann es aber weiterhin automatisiert laufen lassen - gefällt mir.
Das würde ich dann aber weiter aufziehen, da das Tool ja eigentlich nicht für die einmalige Anwendung gedacht ist, sondern dass es permanent bei jedem Build läuft und man das ganze Thema auch für die Zukunft vom Tisch hat.
Ich würde es dann so aufbohren, dass man (auch) eine JSON-Config aufbauen und über die UI bearbeiten/speichern kann. Bei erneuter Ausführung würde es dann die JSON-Config lesen.
Ja. Kam heute zu mir weil heute die Deadline ist 😄
Also wieder sehr frühzeitig, ich fühle mit dir 😄
Hi,
danke für das Feedback.
Das Setup des Tools ist zu aufwändig, die Readme zu lang
Das ist ggf. dem geschuldet, dass es viel tut 😄
Mir ist gerade auch nicht ganz klar, wie ich damit umgehen sollte.
Ggf. reicht es aus, wenn ich mehrere Wiki-Seiten erstelle, die die spezialisierteren Fälle dann weiter beleuchten und in der ReadMe steht nur das wichtigste.
Das Sample auf GitHub funktioniert nicht
Korrigiere ich, sobald ich dazu komme, danke.
Du verwendest mit ImageSharp eine Dependency, wie bei kommerzieller Verwendung eine Lizenz erfordert; nicht sofort ersichtlich (könnte man vielleicht raus machen bzw. als Tool-Plugin machen?)
Das wird verwendet, wenn man Package-Bilder hinzufügen lässt.
Diese Bilder können teilweise ziemlich groß sein, ImageSharp rechnet die klein.
Ein Plugin wäre eine gute Option, aber dazu müsste ich mir erst Mal Gedanken machen, wie ich das mit der Projekt-Struktur behandle, da bin ich nicht ganz zufrieden mit und plugin-fähig ist sie auch nicht.
Ggf. könnte ich auf diese Weise auch das Problem mit den (zu) vielen Funktionen angehen, indem ich das ganze Projekt in einzelne Plugins aufsplitte und dann separat beschreibe.
Aber das muss ich mir erst einmal in Ruhe durch den Kopf gehen lassen.
Es wäre toll, wenn dieses Tool stand-alone alle Anhängigkeiten scannen und sowohl in einer Baum- wie auch in einer Tabellen-Ansicht alle Abhängigkeiten zeigen würde.
Das ist ja eigentlich das Ziel 😃 Nur dass man die Ansicht alleine bauen muss/kann, mein Tool macht nur die Rohdaten im Code verfügbar.
Stand-alone ist natürlich eine valide Anforderung, aber leider nicht mal eben so gemacht, ohne das Feature mit den Bildern ganz raus zu streichen, oder in Kauf zu nehmen, dass viel zu große Bilder embedded werden.
Gibt es beim Kunden eine Deadline?
Du kennst das ja ggf. auch, 15 Projekte auf der Pipeline, aber arbeiten kann man nur an einem Projekt gleichzeitig 😄
Wie hängt der Bug und diese Option denn mit den Extension Types zusammen?
Bedenke, dass das Tool nicht 100% verlässlich ist.
Bei einem kleinen Test mit einem frisch erstellten WinForms-Projekt sind mir mehrere Punkt aufgefallen, unter Anderem:
Ich will damit sagen:
Kontrolliere alles peinlich genau, damit Du später keine unangenehmen Überraschungen hast.
Die offizielle:
Bei C# hast Du normalerweise gar keine erkennbare Reihenfolge, weil Du nur Methoden hast.
Also ja: Ist egal, Du kannst die Methoden herum sortieren, wie Du lustig bist - idealerweise so, wie es am übersichtlichsten ist.
Eine scheinbare Ausnahme ist die Program.cs, wenn die keine Klassenstruktur (top-level statements) hat, dann generiert der Compiler die Klassenstruktur drum herum. Dort definierte Methoden werden dann als Local functions übersetzt, die in der Main-Methode liegen.
Local functions sind die zweite Ausnahme, allerdings ist bei nicht statischen Local functions die Reihenfolge des Aufrufs doch relevant, da von in der Local function auf die lokalen Variablen der Methode zugegriffen werden kann, in der sie definiert wird, entsprechend kann die Methoden auch nur auf die Variablen zugreifen, die vor dem Aufruf definiert wurden und bekommt auch nur diese Werte. Das ist der Grund, warum ich Local functions möglichst am Ende definiere, oder einfach direkt statisch, oder ich nutze sie einfach gar nicht.
Mir wurde damals zu Ausbildungszeiten aber der Stil beigebracht, dass die Methoden in der Reihenfolge definiert werden, in der sie aufgerufen werden.
Dadurch ergibt sich dann zwangsläufig das, was Du bemerkt hast: Die Methoden werden nach der Nutzung definiert.
Ich persönlich folge dem aber nicht mehr, ich sortiere und gruppiere Methoden lieber nach Aufgabenbereich und Sichtbarkeit.
Darüber hinaus sortiere ich alle Inhalte einer Klasse nach Art des Inhalts:
var a1 = new int[] { 1, 5, 7, 10 };
var a2 = a1[1..];
var b1 = new int[] { 2, 4, 6, 8 };
var b2 = b1[..2];
Console.WriteLine(string.Join(" ", a1));
Console.WriteLine(string.Join(" ", a2));
Console.WriteLine();
Console.WriteLine(string.Join(" ", b1));
Console.WriteLine(string.Join(" ", b2));
// 1 5 7 10
// 5 7 10
//
// 2 4 6 8
// 2 4
Was Du da geschrieben hast, ist ein binäres Oder.
Das Contains sucht also nicht 1 oder 5, sondern nur 5.
Richtig ist:
bool containsNumber = wuerfel.Contains(1) || wuerfel.Contains(5);
Contains ist generisch, das kann mit jedem Typ umgehen, den Du da hast.
Wenn es nur string kann, hast Du also irgendetwas anderes falsch gemacht.
Zum Lernen solltest Du es selber schreiben, ein Dictionary hilft dabei.
Damit kannst Du dann die Zahl als Key und deinen Zähler als Wert behandeln.
Oder in kurz, ohne viel Lerneffekt:
foreach (var group in wuerfel.GroupBy(x => x))
Console.WriteLine($"{group.Key} => {group.Count()}");
Zumindest wenn ich deine Frage richtig verstanden habe
Ich habe nie mit Unity gearbeitet, aber nach dem, was ich gesehen/gehört habe, könnten Unity und gängige UI-Frameworks kaum weiter voneinander entfernt sein.
Wenn dein Ziel also nur ist, mit Unity helfen zu können, warum lernst Du dann nicht Unity?
Aber zum Lernen der Sprach-Grundlagen ist WPF ganz gut geeignet, es ist nicht zu komplex, zwingt aber zur Verwendung von fortgeschrittenen Dingen.
Anfangen solltest aber auch Du mit der Konsole, die Frage, wo eine Klassenvariable hin muss, darfst Du bei WPF nicht stellen müssen, das muss alles fest sitzen.
Man guckt zig Youtube Videos und liest hier und da und jeder macht es anders.
Ich halte nichts von YouTube Videos ^^
Ich hab noch keine gute Video-Serie gesehen, die meisten sind entweder schlecht oder bleiben oberflächlich.
Aber es gibt ohne Ende andere Quellen im Internet.
Z.B. Microsoft bietet selber eine Tutorial-Reihe an.
Oder Du liest ein gutes Buch, ein kostenloses Buch wäre z.B. "Visual C# 2012", besser wäre aber der kostenpflichtige Nachfolger von 2019, weil sich dazwischen einiges geändert hat.
Es gibt auch hier einen Artikel zu dem Thema:
https://mycsharp.de/forum/threads/78856/faq-wie-finde-ich-den-einstieg-in-csharp
Windows Forms nutzt man nicht mehr und ist veraltet.
Man nutzt jetzt WPF.
Damit bist Du leider auch nicht mehr aktuell 😉
Ja, WinForms ist alt, sehr alt und wird nicht mehr weiterentwickelt.
Das gleiche gilt allerdings auch für WPF.
WPF verfolgt allerdings einen deutlich moderneren Ansatz, der damals Maßstäbe gesetzt hat, die man auch heute noch wiedererkennen kann.
Wenn Du nur eine einfache kleine Windows-Desktop-Anwendung entwickeln willst, ist WPF keine schlechte Wahl. Es ist alt, aber immer noch ausreichend und war damals gut durchdacht. Der Quasi-Nachfolger ist MAUI (basiert eigentlich auf Xamarin), allerdings hat das noch einige Macken, weshalb ich das ganz besonders einem Anfänger nicht empfehlen kann. Alternativ gibt's noch Community-Projekte wie Avalonia, das noch aktiv gepflegt wird, auf mich aber nicht so gut durchdacht wirkt.
Die Zukunft geht aber eher in Richtung Blazor und Web im allgemeinen.
MAUI kann ein Blazor-Frontend darstellen, so hast Du die Möglichkeit, den riesigen Umfang an Frameworks aus dem Web in einer Desktop-App zu nutzen. Mit Blazor funktioniert MAUI dann auch ausreichend gut. Alternativ kenne ich noch Electron und Photino.NET, Letzteres wirkt noch halb fertig, funktioniert aber schon jetzt sehr gut und ist sehr leichtgewichtig
Das alles macht Blazor sehr interessant, da man mit dem Web eine Frontend-Basis hat, die jeder mehr oder weniger versteht und die überall funktioniert, man muss also nicht das ganze Frontend doppelt und dreifach bauen, nur weil man mehrere Ziel-Geräte unterstützen will.
Ob das für dich ein relevantes Argument ist, musst Du dir aber selbst überlegen.
WPF hat damals Maßstäbe gesetzt, aber einen wirklich würdigen Nachfolger gibt es leider noch nicht.
Also ist WPF nicht pauschal falsch und zum Lernen auch deutlich einfacher, als die Alternativen.
Aber wenn Du "man nutzt jetzt WPF" schreibst, ist das leider falsch, "man" (also generell) nutzt es heute nicht mehr, außer man hat gute Gründe.
Die Zukunft liegt eher bei Web-Dechnologien und bei .NET heißt das aktuell Blazor.
Ob Du nun mit WPF lernen, oder nochmal umsatteln solltest, kann ich dir nicht sagen.
Blazor arbeitet mit ASP.NET Core und die Einstiegshürde ist vermutlich um einiges größer (dafür gibt's aber sehr gute Artikel von Microsoft), allerdings ist dieser Technologie-Stack dafür auch um einiges sicherer und der geradlinigere Ansatz für die Kommunikation von der UI mit dem "Code Behind" ist mit Blazor leichter zu verstehen, als MVVM mit WPF.
WPF kann man prinzipiell genauso nutzen, wie Windows Forms, ist dann halt umständlicher und man legt sich selber Steine in den Weg. An deinem Code sollte das konzeptionell nur wenig ändern.
Wenn man es richtig machen will, dann nutzt man aber MVVM, darauf wurde WPF ausgelegt. [Artikel] MVVM und DataBinding
Und in dem verlinkten Artikel findest Du Beispiele.
Die Klassenvariable landet dann einfach in der Klasse, wo Du sie nutzen willst.
Aber wenn Du es in deinem Code sehen willst:
public partial class MainWindow : Window
{
private int _zahl;
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
_zahl++;
}
private void wuerfel1_img_MouseDown(object sender, MouseButtonEventArgs e)
{
MessageBox.Show(_zahl.ToString());
}
}
Aber wie gesagt: Mit WPF arbeitet man eigentlich anders.
Sorry, aber wenn Du nicht weißt, wo/wie man Klassenvariablen nutzt, dann sind sowohl WinForms, als auch WPF zu hoch für dich.
Du solltest mit den Grundlagen anfangen und erst wenn die sitzen, machst Du mit komplexen Frameworks wie WinForms oder WPF weiter.
Klassenvariablen:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/fields
Wenn Du dir einen Zustand merken willst, leg in der Klasse eine Variable an.
Die kannst Du dann in einer Methode setzen und in der anderen den Wert abrufen.
Und gegen Spagetthi Code:
wuerfel1_img.Source = new BitmapImage(new Uri($@"C:\Users\Marco\source\repos\Spielereien\Farkle\Ressources\img\wuerfelbild {wuerfel[0]}.png"));
Oder Du baust ein Mapping:
var myMapping = new Dictionary<int, Uri>()
{
[1] = new Uri(@"C:\Users\Marco\source\repos\Spielereien\Farkle\Ressources\img\wuerfelbild 1.png"),
[2] = new Uri(@"C:\Users\Marco\source\repos\Spielereien\Farkle\Ressources\img\wuerfelbild 2.png"),
// ...
}
wuerfel1_img.Source = myMapping[wuerfel[0]];
Die for-Schleife sieht by the way aus, als sollte sie da nicht doppelt stehen.
Und Dateipfade so anzugeben ist eine blöde Idee, wie soll das denn auf einem anderen PC funktionieren?
WinForms kann Ressourcen verwalten, die Du dann direkt abrufen kannst, ohne dass Du irgendwo Dateien herum liegen hast.
Oder Du stellst in den Eigenschaften der Datei ein, dass sie ins Ziel-Verzeichnis kopiert wird, dann landet sie neben der exe und Du kannst sie mit einem relativen Pfad suchen.
Auf diese Weise sparst Du dir den hardcoded Pfad und dein Programm funktioniert auch auf einem anderen PC:
Es gibt die Einsteiger-Doku zu ASP.NET Core von Microsoft
Und für Postman gibt's auch entsprechende Anleitungen.
SoapUI habe ich nie genutzt.
Ich hab das Gefühl, Du wirfst hier einiges durcheinander. SoapUI, Postman, VS2019 - die Liste passt nicht?
Du solltest dich lieber erst einmal mit den Grundlagen befassen, sonst passiert genau das, was Du schon bemerkt hast: Du wirst erschlagen.
SoapUI, PostMan oder direkt mit VS2019
SoapUI und Postman sind zwei Testing-Tools für Web-APIs.
Beide können REST, aber SoapUI ist auf SOAP ausgelegt.
SOAP und REST sind zwei verschiedene Ansätze für eine Web-API, Du willst REST, nicht SOAP.
Ich persönlich nutze Postman am liebsten, ich musste es aber auch noch nie für SOAP nutzen.
Und VS2019 ist eine Entwicklungsumgebung, also ein völlig anderes Thema. Außerdem ist VS2022 aktuell.
ich muss ein REST Interface erstellen und mich zu einem Server verbinden.
Mit einer REST-API kommuniziert man mit einem HttpClient. Das macht man aber nicht so wie in deinem Snippet, sondern mit der HttpClientFactory.
Es gibt allerdings auch das Framework "Refit", das arbeitet intern genauso, nimmt dir aber einiges an Arbeit ab.
Ich an deiner Stelle würde aber erst einmal ohne Refit experimentieren - natürlich nur experimentieren, für den Lerneffekt.
Dabei bekomme ich keinen Server und muss diesen selbst erstellen.
Eine REST-API erstellst Du mit ASP.NET Core.
Wichtig: ASP.NET Core, es gibt noch einen Vorgänger, den meine ich nicht.
Für ASP.NET Core gibt's eine gute Einsteiger-Doku von Microsoft.
Und zum XML Serialisieren bietet .NET auch entsprechende Klassen.
Oder Du setzt auf JSON, das kommt mit weniger Overhead aus und Du musst in ASP.NET Core nichts weiter konfigurieren, da es von Haus aus mit JSON arbeitet.
Bau eine ASP.NET Core Web-API, die kannst Du dann (selber) hosten und von der App aus darauf zugreifen.
Die Web-API ist auch der übliche Weg für sowas, sowohl privat, als auch professionell.
Die solltest Du auch bauen, wenn Du den Server selber hostest, da Du über die API weit mehr Kontrolle hast.
Einen eigenen Server mit einer kleinen DB drauf möchte ich nicht betreiben, da mir das Risiko durch Angriffe zu hoch ist.
Ich hab bei mir einen RaspberryPi laufen und greife von unterwegs per VPN von der FritzBox (gibt's noch andere? Alternativ ein eigener VPN-Server) darauf zu.
Einrichtung ist nicht ganz einfach, aber es läuft und ich bin in öffentlichen Netzwerken sicher und kann auch auf sowas wie einen eigenen PasswortManager oder mein PiHole zugreifen.
Risiko für Angriffe sehe ich nicht, da die App ja nur für euch beide ist, da müsste es schon jemand auf euch beide direkt abgesehen haben.
Außerdem ist die Kommunikation ja durch das VPN abgesichert.
Naja, ich will dir ja nur ungern den Wind aus den Segeln nehmen, aber das gibt es schon 😃
Visual Studio bringt die resx-Dateien mit, Visual Studio generiert auf der Basis dann Code.
Den Code kannst Du entweder direkt nutzen, oder Du nutzt eine Abstraktion, z.B. Microsoft.Extensions.Localization.
Und zusätzlich gibt's noch die Visual Studio Extension ResXManager, das müsste auch die restlichen deiner Ideen unterstützen.
Ebenso würde ich gerne wissen, hat jemand von euch schon Erfahrungen gesammelt in der Entwicklung von VS Erweiterungen?
Schon ja - wenn Du konkrete Fragen hast, frag ^^
Felder (C#-Programmierhandbuch)
Eigenschaften (C#-Programmierhandbuch)
Ich finde, eine kurze Google-Recherche kann man erwarten.
Ok, es hat sich als bescheuerter Fehler herausgestellt ...
Es hat einfach nur folgendes gefehlt:
permissions:
contents: write
Danach läuft die Authentifizierung automatisch ...
Die InProcess-Implementierung ist trivial zu coden.
Nur wenn es ein Unary-Call ist.
Bei Streaming-Calls unterscheidet sich der Client-Aufruf mit der Server-Implementierung sehr.
// Server
public override async Task DuplexStreamingCall(IAsyncStreamReader<Request> requestStream, IServerStreamWriter<Response> responseStream, ServerCallContext context)
{
while (await requestStream.MoveNext())
{
_logger.LogInformation("DuplexStreamingCall: {RequestData}", requestStream.Current.RequestData);
var response = new Response
{
ResponseData = $"Duplex response to {requestStream.Current.RequestData}"
};
await responseStream.WriteAsync(response);
await Task.Delay(100);
}
}
// Client
public static async Task DuplexStreamingCall(TestService.TestServiceClient client)
{
using (var call = client.DuplexStreamingCall())
{
var responseTask = Task.Run(async () =>
{
await foreach (var response in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Duplex Streaming Response: {response.ResponseData}");
}
});
for (int i = 0; i < 5; i++)
{
await call.RequestStream.WriteAsync(new Request { RequestData = $"Message {i}" });
await Task.Delay(100);
}
await call.RequestStream.CompleteAsync();
await responseTask;
}
}
Insofern wäre die mMn. einzige wirklich praktikable Option um sowas zu erreichen, das Mediator-Pattern. Die Services nutzen dann den Mediator und geben die Daten durch und wenn die Arbeit im selben Prozess stattfinden soll, wird der Mediator (hinter einer Abstraktion natürlich) aus dem Client heraus genutzt und kann sich dabei auch um den DI-Scope kümmern.
DI-Scope muss verwaltet werden
Damit meine ich, dass bei einer Client-Server-Architektur jeder Request/Call auf dem Server in einem DI-Scope resultiert.
Wenn ich nun auf eine reine Client-Architektur umstellen will und dafür eine einfache Implementierung schaffe, fällt das weg, das muss ich dann auch erst umsetzen. Mache ich das nicht, dann habe ich unterschiedliches Verhalten zwischen den zwei Varianten mit potentiell weitreichenden Auswirkungen.
Nach meiner Erfahrung wird eben eine proto eher selten geändert
Mit proto habe ich keine Erfahrungen, aber wenn ich meine Erfahrungen mit REST darauf übertrage, sind meine Erfahrungen sehr anders.
Tatsächlich muss bei meiner derzeitigen Arbeit eigentlich ständig irgendwas an der API geändert werden.
Ggf. arbeiten wir in anderen Gebieten?
Wenn die API für die Verwendung nach außen für externe Systeme gedacht ist, kann ich mir sehr gut vorstellen, dass sie sich nur selten ändert.
Wenn die API aber für die Kommunikation zwischen zwei zusammen (im selben Team) entwickelten Projekten (Client-Server) gedacht ist, kann das wieder anders aussehen.
Bei uns dient die REST-API hauptsächlich zur Kommunikation zwischen dem Web-Frontend und dem ASP.NET-Backend sowie zu anderen intern entwickelten System und bei der derzeitigen Auftragslage ist da ziemlich viel Bewegung.
Tatsächlich arbeiten wir aktuell mit einem Source-Generator, der Client-Code auf Basis der Controller-Definitionen generiert, allerdings ist der ziemlich komplex und bringt auch ein paar Nachteile mit sich.
Daher würd eich diese einfach händisch coden -- aus Kosten/Nutzen-Sicht.
Ja das stimmt natürlich, aus Sicht einer Firma wäre das für ein Projekt definitiv besser. Für viele Projekte könnte das aber wieder anders aussehen.
Und ich schreibe hier gar nicht für meine Firma, das ganze ist meine private Idee und würde dann auch auf NuGet landen, sodass man in den Projekten gar keinen Aufwand mehr hat - im Idealfall natürlich.
Natürlich, man kann es auch "zu Fuß" machen, muss das dann aber auch immer bei jeder Änderung tun.
Und dadurch gäbe es auch eine ganze Menge Overhead:
Und ich kann für die InProcess-Implementierung nicht einfach nur das Interface im Server-Service implementieren, da die Methoden-Signaturen völlig anders sind.
Man könnte natürlich eine weitere Abstraktion z.B. mit dem Mediator-Pattern dazwischen schieben, da würde das etwas einfacher werden, aber es gibt immer noch mehr Aufwand, der für jede Änderung anfällt.
Mit meiner Idee wäre das alles nicht nötig, man hat genau den Aufwand, den man bei gRPC sowieso immer hat und auch braucht, aber darüber hinaus nicht mehr.
Oder übersehe ich etwas?
'n Abend,
ich versuche mich gerade an den GitHub Actions (meine Erfahrung hält sich also in Grenzen ) und möchte für ein VSIX Projekt einen Workflow erstellen, der automatisch baut und einen Release inkl. Tag erstellt.
Da es ein altes VSIX Projekt ist, muss das alles auf Windows 2019 laufen und alle bereits vorhandenen Actions, die ich gefunden habe, liefen nur unter Linux, die einzige Option, die ich sehe, ist also die git cli.
Das ist mein Test-Workflow:
name: Test
on:
workflow_dispatch:
jobs:
test:
runs-on: windows-2019
steps:
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
- name: Create version tag
run: |
$url = git config --get remote.origin.url
$url = $url -replace "https://", "https://${{ secrets.GITHUB_TOKEN }}@"
git remote set-url origin $url
git config user.name "GitHub Action"
git config user.email "<>"
git tag run${{ github.run_number }}
git push origin run${{ github.run_number }}
Mein Problem ist, dass ihm die Berechtigungen fehlen:
remote: Write access to repository not granted.
fatal: unable to access 'https://meine/schöne/url: The requested URL returned error: 403
Das oben ist einer meiner Versuche.
Ansonsten habe ich auch versucht:
Beides habe ich online gefunden und einfach nur Just4Fun ausprobiert
Hat jemand eine Idee, wie ich das ans Laufen bekomme?
Besten Dank für die Hilfe 😃
Z.B. können die vom gRPC-Tooling erstellten Services auch ohne gRPC aufgerufen werden.
Genau das meine ich
Ich versuche es mal mit etwas Code zu verdeutlichen:
// Netzwerk - so wie man gRPC normalerweise benutzt
using var netChannel = GrpcChannel.ForAddress("https://localhost:1234");
await TestAsync(new TestService.TestServiceClient(netChannel));
// InProcess - meine Idee
await using var services = new ServiceCollection()
.AddInProcessGrpcChannel(b => b
.WithService(InProcessServiceInfo.Create<TestServiceImpl>())
.UseScopedServiceProvider())
.BuildServiceProvider();
var inpChannel = services.GetRequiredService<InProcessGrpcChannel>();
await TestAsync(new TestService.TestServiceClient(inpChannel));
// TestAsync()
static async Task TestAsync(TestService.TestServiceClient client)
{
// Der Code hier weiß nicht, ob die Kommunikation über das Netzwerk oder im selben Prozess stattfindet.
}
Der entscheidende Faktor ist die TestAsync-Methode, die für beides identisch ist.
Mein InProcess-Channel (bzw. der Invoker) sucht in seiner Liste von bekannten Services nach dem richtigen Service und der Methode und ruft beides auf. Es findet keine Netzwerk-Kommunikation statt, keine Serialisierung, kein Protokoll, es wird nur die Methode direkt aufgerufen.
Das, was ich von gRPC nutze, ist den vom gRPC-Tooling erstellte Code als Abstraktion, um die Services/Methoden zu suchen, zu instanziieren (ggf. in einem Scope) und aufzurufen.
Ich glaube, Du hast mich falsch verstanden.
Ich nutze gRPC im Wesentlichen nur als Kommunikations-Layer, es wird nichts serialisiert und es gibt kaum zusätzlichen Overhead.
Die aus den proto-Dateien generierten Typen bieten mir aber alles, was ich brauche, um die Services und Methoden zu finden und genau das ist es, was ich nutze.
Effektiv ist mein Idee (bzw. mein erster Entwurf) also nur ein Quasi-Ersatz für die gRPC-Kommunikation hinter der generierten Abstraktion. Das Protokoll dahinter gibt es also gar nicht mehr, es gibt nur noch die Abstraktion der offiziellen .NET-Implementierung.
Aber klar, wenn man nur im selben Prozess kommunizieren will, ist das ganze überflüssig und unnötig. Aber es geht ja gerade um die Projekte, wo man beides haben möchte, abhängig von der Konfiguration.
Hallo zusammen,
ich habe mich mit gRPC beschäftigt und dabei über eine Idee nachgedacht:
Ist ein nahtloser Wechsel zwischen Client-Server und Monolith ohne großem zusätzlichen Aufwand möglich?
Die Idee ist, eine Channel-Implementierung zu entwickeln, die in der Lage ist, Client-Aufrufe im selben Prozess zu verarbeiten, möglicherweise mit einem DI-Scope. Je nach Anwendungs-Konfiguration wird dann der originale Channel oder der InProcess-Channel verwendet.
Die Anwendung wäre weiterhin strikt in zwei Bereiche unterteilt: Client und Server. Der Client kommuniziert mit dem für gRPC generierten Client und je nach Konfiguration läuft die Kommunikation entweder über das Netzwerk, oder im selben Prozess.
Diese Idee richtet sich natürlich an eine sehr spezifische Zielgruppe, da sie nur für Anwendungen sinnvoll ist, die diese Art des Wechsels benötigen. Denkbar wäre z.B. eine Anwendung, die entweder als eigenständige Desktop-Anwendung oder im Team mit einem zentralen Server genutzt werden soll. Bei einer Desktop-Anwendung möchte man natürlich keinen separaten Server starten müssen.
Für Tests könnte es auch nützlich sein, ich halte es aber für besser, die Business-Logik vernünftig zu abstrahieren und dann direkt ohne gRPC zu testen. Außerdem kann ein InProcess-Channel niemals das Verhalten einer echten Netzwerk-Kommunikation vollständig abbilden.
Warum ich nun dieses Thema auf mache:
Was denkt ihr über diese Idee?
Welche Funktionen haltet ihr für wichtig?
Welche Risiken sollten explizit getestet werden?
Besten Dank 😃
Zitat von Peter Bucher
Wenn man eine private Nachricht schreiben möchte - im Darkmode! - ist die Schrift weiss auf weissem Hintergrund, man sieht das geschriebene also leider nicht.
Schau dir mal die Browser-Extension "Dark Reader" an - hat mir Abt mal empfohlen.
Ist natürlich nicht so gut, wie das Original-DarkTheme, aber es funktioniert ganz gut.
Entweder CSV mit leeren Zellen, oder Du erstellst eine xlsx-Datei, z.B. damit:
https://github.com/ClosedXML/ClosedXML oder https://github.com/ClosedXML/ClosedXML.Report
CSV ist einfacher, XLSX erlaubt natürlich sehr viel mehr Anpassungen.