Laden...
Avatar #avatar-3521.gif
pollito myCSharp.de - Member
Dipl.-Ing. Ingenieurinformatik 78647 Trossingen Dabei seit 26.02.2010 314 Beiträge

Forenbeiträge von pollito Ingesamt 314 Beiträge

17.03.2023 - 16:31 Uhr

Hallo,

ich bin in erster Linie Backend-Entwickler und hatte bis heute kaum Berührungen mit Benutzeroberflächen. Die wenigen Programme mit GUI, die ich gemacht habe, habe ich mit WPF programmiert.

Ich muss mich jetzt in ASP.NET MVC für ein Projekt einarbeiten, welches mich für lange Zeit beanspruchen wird. Dabei sollte ich auch einige Grundlagen von HTML und CSS kennenlernen, damit ich in die Lage versetzt werde, einfache Formulare zu erstellen. Komplexe Gestaltung ist nicht das Ziel, sondern es geht vielmehr darum, Formulare bereitzustellen, mit denen man die Programmfunktionalität manuell testen kann – das höchste aller Gefühle werden Tabellen sein, die viele Datensätze zur Kontrolle und Änderung anzeigen sollen.

Da es hier sicher viele Experten gibt, die sich gut damit auskennen, hoffe ich auf eure Empfehlungen. Könnt ihr mir Literatur, Tutorials oder Videos im Umfeld von ASP.NET MVC empfehlen, die meine "bescheidenen" Ansprüche erfüllen? Wie die Überschrift sagt: "HTML und CSS für Dummies"

Ich danke euch im Voraus!

LG

René

PS: Vielleicht könnt ihr mir auch mit Verwaltung von Fensterkoordinaten bei mehreren Bildschirmen unterschiedlicher Auflösungen helfen 😉

09.03.2023 - 15:07 Uhr

Hallo,

ich brauche zunächst nur einen Hinweis, wie man dieses Thema am sinnvollsten angeht.

Ich habe eine Anwendung, die nicht viel mit .NET anfangen kann – maximal ein .NET-Programm ausführen. Diese Anwendung kann ihre eigenen Koordinaten nur dann richtig ermitteln, wenn Sie auf nur einem oder auf dem Hauptbildschirm ausgeführt wird. Allerdings läuft sie auch in Umgebungen mit mehreren Bildschirmen mit unterschiedlichen Auflösungen. Beispiel: Notebook mit 1920x1080 + 3840×2160 + 1920x1200 (Hauptbildschirm). Wie man hier sieht, eine bunte Mischung.

Nun geht es darum, die genaue Position des Programms zu ermitteln, wobei – wie bereits erwähnt – das Programm maximal in der Lage ist, ein .NET-Programm zu starten. Ich stelle mir vor, dem .NET-Programm wird die PID des rufenden Programms übergeben, um in Return-Value dessen Koordinaten zurück zu liefern. Vielleicht gibt es auch andere Möglichkeiten.

Als in erster Linie Backend-Entwickler habe ich mit dieser Thematik bisher nie was zu tun – daher wende ich mich an euch in der Hoffnung, einen Richtungsanstoß zu bekommen, bevor ich der falschen Fährte nachjage.

Danke un lG

René

04.03.2023 - 14:04 Uhr

PS: Für andere Mail-Pakete (u.a. MailKit) habe ich noch folgende Seite gefunden:
>

Sehr gut, danke nochmals! Was denkst du aber über diesen Thread hier im Forum? SmtpClient MailKit und Office365 Two-Factor-Authentication (2FA)

LG

René

03.03.2023 - 20:08 Uhr

In Zeile 57 (deines Originalcodes)

  
var alternateView = new AlternateView(new MemoryStream(Encoding.UTF8.GetBytes(Mail.Body)), "text/html");  
  

erzeugst du den Stream aus dem original Mail.Body und erst später ersetzt du in diesem die Pfade durch die GUIDs...

Danke, du bist mein Held! Das Wochenende ist gerettet.

Du hast vollkommen recht. Manchmal braucht es tatsächlich einen Blick über die Schulter, insbesondere wenn man sich festgefahren hat und auf der Stelle tritt.

Bevor ich "alternateView" erzeuge, rufe ich die Methode "GetImageFiles()" auf. Diese liefert nicht mehr eine einfache Liste List<string> sondern List<(string imageFileName, string contentId)> – eine Liste aus Tuples. Sie sorgt nun dafür, dass in Mail.Body der Content durch GUIDs entsprechend ersetzt wird und liefert diese GUIDs an zweiter Stelle vom jeweiligen Tuple zurück. Erst dann erzeuge ich mit dem angepasten Mail.Body "alternateView". Durch die Verwendung von Tuples habe ich dann nicht nur den Dateinamen sondern auch die dazugehörige ContentId in Form einer GUID.

Zudem möchte ich noch anmerken, dass die Apostrophe notwendig sind – ohne diese werden die eingebetteten Bilder nicht angezeigt.

Ein kleiner Ablauffehler, der mich viel Zeit gekostet hat. Daher nochmals vielen Dank und lG!

René

Hier der geänderte Testcode, falls es interessiert:

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Web;
using System.Text;

namespace HtmlMail
{
	internal class Program
	{
		static void Main(string[] args)
		{
			HtmlMail HtmlMail = new HtmlMail
			(
				"<H1>Dies ist ein Beispieltext mit eingebetteten Bildern.</H1>" +
				"<p>Erstes Bild: <img src='cid:C:/!/bilder/image1.png'/></p>" +
				"<p>Nun folgt ein weiterer Textabschnitt mit diesem Bild: <img src='cid:C:/!/bilder/image2.png'/></p>" +
				"<p>Und wieder ein bisschen Text in der letzten Zeile</p>"
			);

			HtmlMail.Mail.To.Add("info@example.com");
			HtmlMail.Mail.Subject	= "Testmail in HTML-Format mit mehreren Bildern";
			HtmlMail.Mail.From		= new MailAddress("no-reply@example.com");

			// SMTP-Server konfigurieren
			SmtpClient smtp = new SmtpClient("mail.example.com")
			{
				Port		= 587,
				Credentials	= new System.Net.NetworkCredential("name", "password"),
				EnableSsl	= true
			};

			// E-Mail senden
			smtp.Send(HtmlMail.Mail);

		}
	}

	public class HtmlMail
	{
		public MailMessage Mail { get; set; }

		public HtmlMail(string htmlText)
		{
			Mail = new MailMessage
			{
				IsBodyHtml	= true,
				Body		= htmlText
			};

			Mail.AlternateViews.Add(GetAlternateView());
		}

		private AlternateView GetAlternateView()
		{
			var imageFiles		= GetImageFiles();
			var alternateView	= new AlternateView(new MemoryStream(Encoding.UTF8.GetBytes(Mail.Body)), "text/html");

			foreach (var imageFile in imageFiles)
			{
				// Neues Element erstellen.
				var LinkedImage = new LinkedResource(imageFile.imageFileName, MimeMapping.GetMimeMapping(imageFile.imageFileName))
				{
					ContentId = imageFile.contentId
				};

				alternateView.LinkedResources.Add(LinkedImage);
			}

			return alternateView;
		}

		private List<(string imageFileName, string contentId)> GetImageFiles()
		{
			// Nimmt die ermittelten Namen der Bilddateien auf.
			var ImageFiles = new List<(string, string)>();

			// Dateinamen stehen zwischen "<img src='cid:" und "'/>"
			var matches = new Regex("<img src='cid:(.*?)'/>", RegexOptions.IgnoreCase).Matches(Mail.Body);

			if (matches.Count > 0)
			{
				// Alle Funde untersuchen.
				foreach (Match match in matches)
				{
					// Nur unterstützte Typen aufnehmen.
					if (IsOneSupportedImageFile(match.Groups[1].Value))
					{
						string îmageFileName	= match.Groups[1].Value;
						string contentId		= Guid.NewGuid().ToString().Replace("-", string.Empty).ToString();

						ImageFiles.Add((îmageFileName, contentId));

						// Dateiname durch GUID in Mail.Body ersetzen.
						ReplaceFirstOcurrence(îmageFileName, contentId);
					}
				}
			}

			return ImageFiles;
		}

		private bool IsOneSupportedImageFile(string imageFile)
		{
			// "imageFile" muss einen Wert haben (Dateiname).
			// Der Dateiname muss eine Dateierweiterung haben.
			// Der mit der Datei bzw. Dateierweiterung assoziierte MIME-Type muss mit "image/" beginnen.
			if (
				string.IsNullOrEmpty(imageFile) == true ||
				string.IsNullOrEmpty(Path.GetExtension(imageFile)) == true ||
				MimeMapping.GetMimeMapping(Path.GetExtension(imageFile)).StartsWith("image/", StringComparison.OrdinalIgnoreCase) == false)
			{
				return false;
			}

			return true;
		}

		private void ReplaceFirstOcurrence(string oldSubstring, string newSubstring)
		{
			int index = Mail.Body.IndexOf(oldSubstring);

			if (index != -1)
			{
				Mail.Body = Mail.Body.Substring(0, index) + newSubstring + Mail.Body.Substring(index + oldSubstring.Length);
			}
		}
	}
}
03.03.2023 - 17:03 Uhr

Ich werde eine der E-Mails im Editor öffnen, um nachzuschauen, ob mir was auffällt – am besten mit Thunderbird, um ein lesbares Format zu bekommen.

OK, ich habe in die E-Mail reingeschaut, es fiel mir zunächst aber nichts außergewöhnliches auf. Die Bilddateien sind enthalten und die verweise scheinen richtig zu sein.


----boundary_0_9fe79515-c9e0-4ab1-8834-bee9592b1b89
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: quoted-printable

<H1>Dies ist ein Beispieltext mit eingebetteten Bildern.</H1><p>E=
rstes Bild: <img src=3Dcid:f48419cc365046c7bfcb9ccbf4827699/></p>=
<p>Nun folgt ein weiterer Textabschnitt mit diesem Bild: <img src=
=3Dcid:e369757facb24c14b4b654750a43f617/></p><p>Und wieder ein bi=
sschen Text in der letzten Zeile</p>
----boundary_0_9fe79515-c9e0-4ab1-8834-bee9592b1b89
Content-Type: multipart/related;
 boundary=--boundary_1_f2154f8d-be28-48b1-98b2-e90356cbd7a1; type="text/html"


----boundary_1_f2154f8d-be28-48b1-98b2-e90356cbd7a1
Content-Type: text/html
Content-Transfer-Encoding: base64

PEgxPkRpZXMgaXN0IGVpbiBCZWlzcGllbHRleHQgbWl0IGVpbmdlYmV0dGV0ZW4gQmls
ZGVybi48L0gxPjxwPkVyc3RlcyBCaWxkOiA8aW1nIHNyYz0nY2lkOkM6LyEvYmlsZGVy
L2ltYWdlMS5wbmcnLz48L3A+PHA+TnVuIGZvbGd0IGVpbiB3ZWl0ZXJlciBUZXh0YWJz
Y2huaXR0IG1pdCBkaWVzZW0gQmlsZDogPGltZyBzcmM9J2NpZDpDOi8hL2JpbGRlci9p
bWFnZTIucG5nJy8+PC9wPjxwPlVuZCB3aWVkZXIgZWluIGJpc3NjaGVuIFRleHQgaW4g
ZGVyIGxldHp0ZW4gWmVpbGU8L3A+

Nun habe ich den base64-Teil lesbar gemacht und siehe da, was ich erhalte:


<H1>Dies ist ein Beispieltext mit eingebetteten Bildern.</H1><p>Erstes Bild: <img src='cid:C:/!/bilder/image1.png'/></p><p>Nun folgt ein weiterer Textabschnitt mit diesem Bild: <img src='cid:C:/!/bilder/image2.png'/></p><p>Und wieder ein bisschen Text in der letzten Zeile</p>

Das kommt mir bekannt vor...

Wenn der Mail-Client auf den base64-Teil zugreift, um anhand der Verweise (ContentId) auf den Content zuzugreifen, geht das in die Hose. Aber warum ist es so?

03.03.2023 - 16:37 Uhr

Da es ja mit einem Dateinamen funktioniert, was passiert, wenn du <GUID>.png als ContentId verwendest?

Gerade eben getestet: Es ändert sich nichts.

Ich werde eine der E-Mails im Editor öffnen, um nachzuschauen, ob mir was auffällt – am besten mit Thunderbird, um ein lesbares Format zu bekommen.

03.03.2023 - 16:07 Uhr

Müssten die Bilder im Idealfall nicht über einen Http(s) Url oder als Base64 embedded werden?

Weiß ich nicht. Wenn ich die Ersetzungen im HTML-String nicht mache und als LinkedResource.ContentId den Dateinamen verwende, funktionert es. Das unabhängig von den Apostrophen im HTML-String.

Ebenfalls solltest du nicht mehr auf System.Net.Mail setzen.
Nimm dafür MailKit, Umstellung geht bei sauberen Code innerhalb von wenigen Minuten und dort kannst du dann deine Html Mail einfacher umsetzen.

Siehe dazu den Hinweis:

>

T-Virus

OK, danke. Wie bereits in meiner vorigen Antwort geschrieben, werde ich das tun.

LG

René

03.03.2023 - 16:02 Uhr

Danke! Ich konnte wegen Kundenbesuchs nicht früher antworten.

Wie sieht denn der ersetzte HTML-Body (für <img .../>) aus?

Vor deinem Tipp:

<H1>Dies ist ein Beispieltext mit eingebetteten Bildern.</H1><p>Erstes Bild: <img src='cid:aa13115f642549e1a9010e8df926fd52'/></p><p>Nun folgt ein weiterer Textabschnitt mit diesem Bild: <img src='cid:d248fbbab6e7441a9fdf849bc1ad3546'/></p><p>Und wieder ein bisschen Text in der letzten Zeile</p>

Nach deinem Tipp (ohne Apostrophe):

<H1>Dies ist ein Beispieltext mit eingebetteten Bildern.</H1><p>Erstes Bild: <img src=cid:aa13115f642549e1a9010e8df926fd52/></p><p>Nun folgt ein weiterer Textabschnitt mit diesem Bild: <img src=cid:d248fbbab6e7441a9fdf849bc1ad3546/></p><p>Und wieder ein bisschen Text in der letzten Zeile</p>

Es ändert sich aber nichts: Die Bilder werden nicht angezeigt.

Dort dürfen dann keine einfachen Anführungsstriche mehr drin sein, sondern nur

  
<img src=cid:ID/>  
  

(s. z.B.
>
- mußt in der Ansicht bei //Creating AlternateView for HTML Mail ein bißchen scrollen)

Wie beschrieben: Das habe ich gemacht, denn bei mir standen noch die Apostrophe, leider aber erfolglos.

Aber du solltest wirklich auf MailKit umstellen.

Danke, ich kannte das nicht. Ich schaue es mir an.

Nun habe ich festgestellt, dass die Test-E-Mail eine Größe aufweist, die über der Summe beider Bilddateien liegt. Das erwarte ich auch, wenn ich Bilddateien einbette. Nun habe ich die E-Mail an ein IMAP-Postfach geschickt (die anderen Test waren mit einem Exchange-Postfach) und die Bilder werden ebenso wenig angezeigt. Allerdings erscheinen diese als Anlage (siehe Dateianhang). Also die Bilddaten sind in der E-Mail, sie werden aber nicht dargestellt.

LG

René

03.03.2023 - 02:24 Uhr

Hallo,

ich möchte eine HTML-Mail mit eingebetteten Bildern versenden. Dazu habe ich den weiter unten angefügten Testcode erstellt. Leider werden aber die Bilder nicht mitgesendet – an deren Stelle sieht man am Mailclient (hier Outlook) die bekannten kleinen viereckigen Platzhalter.

Ich habe Stunden gedebuggt, kann trotzdem den Fehler nicht finden.

Das Testprogramm soll in einem HTML-Formatierten Text alle Bilddateien durch GUIDs ohne Bindestriche ersetzen:

"<H1>Dies ist ein Beispieltext mit eingebetteten Bildern.</H1><p>Erstes Bild: <img src='cid:C:/!/bilder/image1.png'/></p><p>Nun folgt ein weiterer Textabschnitt mit diesem Bild: <img src='cid:C:/!/bilder/image2.png'/></p><p>Und wieder ein bisschen Text in der letzten Zeile</p>"

In meinem Beispiel

C:/!/bilder/image1.png

und

C:/!/bilder/image2.png

Der Grund dafür ist der, dass die CIDs eindeutig sein müssen, was nicht garantiert werden kann, wenn mehrmals dieselbe Bilddatei in den HTML-Text eingefügt wird. Nach dieser Mimik sieht der HTML-String wie folgt aus:

"<H1>Dies ist ein Beispieltext mit eingebetteten Bildern.</H1><p>Erstes Bild: <img src='cid:a3b0dc1d67c14cf2a55719061d3142d2'/></p><p>Nun folgt ein weiterer Textabschnitt mit diesem Bild: <img src='cid:3970d814d85e49129ec5529ec3d6a4b1'/></p><p>Und wieder ein bisschen Text in der letzten Zeile</p>"

Wenn ich mir im Debugger die "AlternateView.LinkedRessources" anschaue, sieht m. E. alles OK aus:

[0]: ContentId = "a3b0dc1d67c14cf2a55719061d3142d2", Name = "C:\\!\\bilder\\image1.png"

und

[1]: ContentId = "3970d814d85e49129ec5529ec3d6a4b1", Name = "C:\\!\\bilder\\image2.png"

Die Pfade und Dateinamen stimmen so wie die MIME-Typen usw. auch. Das verlinkte Bild hier, zeigt die zwei LinkedRessources im Debugger an.

Wie eingangs beschrieben: Ich stehe auf dem Schlauch und weiß nicht, wo der Fehler liegt. Ich bin für jede Hilfe dankbar. Wenn jemand die Lust verspürt, den Code zu testen, so braucht er zwei Bilddateien, Pfade und Namen anpassen und die richtigen Zugangstaten zum SMTP-Server.

Das Testprogramm habe ich mit dem Framework 4.8 (Erweiterung eines alten Programmes) gemacht, sollte aber unter .NET Core auch laufen.

Vielen Dank für die Geduld und liebe Grüße

René

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Web;
using System.Text;

namespace HtmlMail
{
	internal class Program
	{
		static void Main(string[] args)
		{
			HtmlMail HtmlMail = new HtmlMail
			(
				"<H1>Dies ist ein Beispieltext mit eingebetteten Bildern.</H1>" +
				"<p>Erstes Bild: <img src='cid:C:/!/bilder/image1.png'/></p>" +
				"<p>Nun folgt ein weiterer Textabschnitt mit diesem Bild: <img src='cid:C:/!/bilder/image2.png'/></p>" +
				"<p>Und wieder ein bisschen Text in der letzten Zeile</p>"
			);

			HtmlMail.Mail.To.Add("info@example.com");
			HtmlMail.Mail.Subject	= "Testmail in HTML-Format mit mehreren Bildern";
			HtmlMail.Mail.From		= new MailAddress("no-reply@example.com");

			// SMTP-Server konfigurieren
			SmtpClient smtp = new SmtpClient("example.com")
			{
				Port		= 587,
				Credentials	= new System.Net.NetworkCredential("name", "password"),
				EnableSsl	= true
			};

			// E-Mail senden
			smtp.Send(HtmlMail.Mail);
		}
	}

	public class HtmlMail
	{
		public MailMessage Mail { get; set; }

		public HtmlMail(string htmlText)
		{
			Mail = new MailMessage
			{
				IsBodyHtml	= true,
				Body		= htmlText
			};

			Mail.AlternateViews.Add(GetAlternateView());
		}

		private AlternateView GetAlternateView()
		{
			var alternateView = new AlternateView(new MemoryStream(Encoding.UTF8.GetBytes(Mail.Body)), "text/html");

			foreach (string imageFile in GetImageFiles())
			{
				// Neues Element erstellen.
				var LinkedImage = new LinkedResource(imageFile, MimeMapping.GetMimeMapping(Path.GetExtension(imageFile)))
				{
					// ContentId erzeugen. Wir nutzen hier eine GUID ohne Bindestriche.
					ContentId = Guid.NewGuid().ToString().Replace("-", string.Empty)
				};

				// Dateiname durch GUID in Mail.Body ersetzen.
				ReplaceFirstOcurrence(imageFile, LinkedImage.ContentId);

				alternateView.LinkedResources.Add(LinkedImage);
			}

			return alternateView;
		}

		private List<string> GetImageFiles()
		{
			// Nimmt die ermittelten Namen der Bilddateien auf.
			var ImageFiles = new List<string>();

			// Dateinamen stehen zwischen "<img src='cid:" und "'/>"
			var matches = new Regex("<img src='cid:(.*?)'/>", RegexOptions.IgnoreCase).Matches(Mail.Body);

			if (matches.Count > 0)
			{
				// Alle Funde untersuchen.
				foreach (Match match in matches)
				{
					// Nur unterstützte Typen aufnehmen.
					if (IsOneSupportedImageFile(match.Groups[1].Value))
					{
						ImageFiles.Add(match.Groups[1].Value);
					}
				}
			}

			return ImageFiles;
		}

		private bool IsOneSupportedImageFile(string imageFile)
		{
			// "imageFile" muss einen Wert haben (Dateiname).
			// Der Dateiname muss eine Dateierweiterung haben.
			// Der mit der Datei bzw. Dateierweiterung assoziierte MIME-Type muss mit "image/" beginnen.
			if (
				string.IsNullOrEmpty(imageFile) == true ||
				string.IsNullOrEmpty(Path.GetExtension(imageFile)) == true ||
				MimeMapping.GetMimeMapping(Path.GetExtension(imageFile)).StartsWith("image/", StringComparison.OrdinalIgnoreCase) == false)
			{
				return false;
			}

			return true;
		}

		private void ReplaceFirstOcurrence(string oldSubstring, string newSubstring)
		{
			int index = Mail.Body.IndexOf(oldSubstring);

			if (index != -1)
			{
				Mail.Body = Mail.Body.Substring(0, index) + newSubstring + Mail.Body.Substring(index + oldSubstring.Length);
			}
		}
	}
}
22.12.2022 - 15:21 Uhr

Die Vermutung, dass Du All Inkl nutzt lässt mich aber dazu schließen, dass AWS und Azure Dir zu teuer sein werden (weil Du deren Mehrleistung (und Mehr-Sicherheit?) wohl nicht benötigst).

Bisher haben wir uns über Azure und AWS keinen Kopf gemacht. Für die hier angesprochene Anwendung wäre das auch Overkill, denn es handelt sich um eine einfache Datenbankanwendung, welche Daten nur temporär verwaltet. Diese sind weder ein Betriebsgeheimnis, noch werden persönliche und/oder wichtige Daten verwaltet. Dennoch ein sehr interessantes Thema – danke fürs Ansprechen.

auch wenn ein Zertifikat (der private Schlüssel) gestohlen wird, kann man ohne Passphrase nichts damit anfangen.
Weil das ist und bleibt immer der Angriffsvektor.

Richtig, aber die All-inkl-VPN-Lösung ist, so wie sie ausgeliefert wird, noch anfälliger als die von uns verwendete SSH-Lösung: Konfigurationsdatei und Zertifikat klauen und ich bin drin.

Nochmals Danke und frohe Weihnachten!

René

22.12.2022 - 12:22 Uhr

Zunächst sorry, dass ich etwas abschweife, aber ich denke, das kann für den einen oder anderen interessant sein.

Ich habe weiter geforscht und bin zur Erkenntnis gekommen, dass mein Provider all-inkl den Zugriff auf Datenbanken über einen SSH-Tunnel unterbindet. Zwar ist SSH möglich, nicht jedoch einen SSH-Tunnel einzurichten. Daher habe ich den Support kontaktiert, der – wie immer – sehr schnell antwortete:

vielen Dank für Ihre Anfrage.

Der Zugriff auf die Datenbanken über einen SSH-Tunnel ist bei uns nicht möglich. Über eine VPN-Verbindung ist dennoch ein verschlüsselter Transfer möglich. Auf unserer Website finden Sie folgenden Link zur Einrichtung:


>

Allerdings kommt bei uns VPN nicht in Frage, denn die Software soll leicht zu deployen sein, ohne einen OpenVPN-Client installieren zu müssen, der auch programmgesteuert überwacht und ferngesteuert werden müsste.

Daraufhin habe ich gefragt, warum kein SSH-Tunnel möglich ist und ob dies zumindest in Managed-Servern (auch das haben wir bei denen) gehen würde. Schließlich haben andere Provider dieses Feature im Portfolio – sie werben sogar damit und stellen Anleitungen zur Verfügung. Dazu gehören IONOS, Mittwald, Hosteurope und weitere. All-inkl meint dazu:

Der SSH Tunnel ist bei uns vor Jahren aus Sicherheitsgründen abgeschafft worden.


>

Auch auf einem Managed-Server können wir dies leider nicht freischalten.

Das hat mich nachdenklich gemacht. Wir verwenden nämlich SSH ausschließlich mit Zertifikaten, welche zusätzlich durch eine sehr starke Passphrase gesichert sind. D. h. auch wenn ein Zertifikat (der private Schlüssel) gestohlen wird, kann man ohne Passphrase nichts damit anfangen. Dagegen ist bei deren VPN-Lösung das Zertifikat nicht gesichert, so dass es beim Entwenden sofort missbraucht werden kann. Für mich ein sehr großes Fragezeichen.

Nun habe ich all-inkl gefragt, ob sie irgendwann neben PHP, Phyton und Perl, auch .NET anbieten würden. Dann könnte ich meine Anwendung auf ASP.NET umschreiben. Die Antwort war kurz und prägnant:

Die Implementierung von .NET ist derzeit bei uns nicht geplant.

Wir sind seit sehr vielen Jahren Kunde bei all-inkl und schätzen deren schnellen Support und Robustheit deren Produkte. Allerdings scheint es mir an dieser Stelle ein wenig zu viel des Guten zu sein. Schließlich dreht sich die Welt weiter und man muss einigermaßen mit der Zeit gehen.

Kennt jemand von euch Provider/Tarife auf Linux-Basis (kein Root-Server), bei denen SSH-Tunneling möglich ist und auch .NET unterstützt wird?

Ansonsten werden wir auf einen vorhandenen Root-Server ausweichen, den wir bei einem anderen Provider mieten, um uns schnell aus der Affaire zu ziehen.

Vielen Dank für eure Hilfe!

René

21.12.2022 - 21:58 Uhr

UPDATE++++

Ich habe das gleiche Testprogramm aber mit anderen Verbindungs- und Zugangsdaten ausgeführt. Im Wesentlichen handelt es sich um andere Internet-Provider. Hier stelle ich fest, dass die anderen Provider keine Probleme machen und alles läuft, wie es sein sollte. Dabei ist mir etwas aufgefallen: Den Fehler macht nicht die Methode ForwardedPortLocal, wie ich fälschlicherweise vermutete. Wenn ich nach der Portweiterleitung anhalte, sehe ich in allen Fällen folgendes:


TCP    127.0.0.1:3306         0.0.0.0:0                ABHÖREN         42608

Also das ist korrekt und ich hatte mich geirrt.

Nun habe ich bei den fehlerfreien Programmläufen direkt nach MySqlConnection.Open angehalten und siehe da, folgendes Bild bot sich mir an:


Proto    Lokale Adresse         Remoteadresse          Status           PID

  TCP    127.0.0.1:3306         0.0.0.0:0              ABHÖREN         51156
 [sshtest2.exe]
  TCP    127.0.0.1:3306         127.0.0.1:62024        HERGESTELLT     51156
 [sshtest2.exe]
  TCP    127.0.0.1:62024        127.0.0.1:3306         HERGESTELLT     51156
 [sshtest2.exe]

Hier sieht man, dass zwei Verbindungen hergestellt sind und dass der Port 62024 als "Zwischenport" verwendet wird.

Nun glaube ich langsam, dass der Provider (all-inkl) in die Suppe spuckt und das Aushandeln des "Zwischenports" unterbindet.

Ich werde daher ein Ticket bei denen eröffnen, um sicher zu sein, dass die es unterbinden.

21.12.2022 - 20:37 Uhr

Doch, so funktioniert SSH.NET. Nachdem man eine SSH-Verbindung hergestellt hat, kann man die Portweiterleitung einrichten. Die Methode


new ForwardedPortLocal("127.0.0.1", 3306, "127.0.0.1", 3306);

leistet das:


-	portfwd	{Renci.SshNet.ForwardedPortLocal}	Renci.SshNet.ForwardedPortLocal
		BoundHost		"127.0.0.1"		string
		BoundPort		3306			uint
		Host			"127.0.0.1t"	string
		IsStarted		true			bool
		Port			3306			uint

ForwardedPortLocal.BoundHost und ForwardedPortLocal.BoundPort stellen die Quelle dar während ForwardedPortLocal.Host und ForwardedPortLocal.Port das Ziel – es ist durchaus möglich und in den meisten Fällen sogar die Regel, dass Quelle und Ziel (visuell) gleich sind.

21.12.2022 - 18:29 Uhr

Danke! Ich hatte in meiner unendlichen Verzweiflung vergessen, zu erzählen, dass ich dies schon alles durchgetestet habe.

Mittlerweile habe ich auch die Verbindungszeichenkette so angepasst, dass ich für meine Tests "nur" an der Methode ForwardedPortLocal drehen muss.


			var portfwd = new ForwardedPortLocal("127.0.0.1", 3306, "127.0.0.1", 3306);
			sshclient.AddForwardedPort(portfwd);
			portfwd.Start();
			using MySqlConnection MySqlCon = new($"server={portfwd.BoundHost}; port={portfwd.BoundPort}; user id=...");

Ich habe mich auch zu Tode gegoogelt, wobei ich nicht alleine zu sein scheine. Allerdings keiner der Tipps konnte weiterhelfen. Daher habe ich das direkt auf GitHub unter Local port forwarding "ForwardedPortLocal" does not work · Issue #1066 · sshnet/SSH.NET beschrieben. Bisher aber kein Feedback, leider.

Das Ausführen auf anderen Rechnern bzw. sogar mit Admin-Rechten brachte nichts. Nun gehen mir die Ideen aus.

21.12.2022 - 17:49 Uhr

Danke! Ich hatte in meiner unendlichen Verzweiflung vergessen, zu erzählen, dass ich dies schon alles durchgetestet habe.

21.12.2022 - 15:16 Uhr

Hallo,

ich habe mehrere Anwendungen, die SSH.NET (SSH.NET) nutzen. Hierbei handelt es sich bisher immer um Datenbankzugriffe auf MySQL- bzw. MariaDB-Datenbanken. Nun muss ich eine weitere Anwendung erstellen, die ebenfalls auf eine MariaDB-Datenbank zugreift – das treibt mich aber zum Wahnsinn.

Die SSH-Verbindung wird dabei erfolgreich hergestellt und ich kann beliebige Shell-Befehle absetzen. So weit, so gut. Allerdings möchte ich auf die Datenbank zugreifen, so dass ich eine lokale Portweiterleitung einrichte – diese scheint jedoch nicht zu wirken, obwohl im Debugger alles richtig erscheint.

Nun ein wenig Code, bevor ich weiter erkläre:


			ConnectionInfo ConnNfo = new ConnectionInfo("domäne.tld", 22, "ssh-user",
				new AuthenticationMethod[]
				{
					new PrivateKeyAuthenticationMethod("ssh-user", new PrivateKeyFile[]
						{
							new PrivateKeyFile(@"C:\Daten\Visual Studio\Projects\Z-Tests\sshtest2\.ssh\private.key", "PassPhrase")
						}
					),
				}
			);

			using var sshclient = new SshClient(ConnNfo);

			sshclient.Connect();

			var portfwd = new ForwardedPortLocal(IPAddress.Loopback.ToString(), 3306, "localhost", 3306);
			sshclient.AddForwardedPort(portfwd);
			portfwd.Start();

			using MySqlConnection MySqlCon = new ("server=127.0.0.1; user id=benutzer; password=kennwort; database=datenbank; Allow Zero Datetime=True; persistsecurityinfo=False");

			/*
			 * Unbehandelte Ausnahme
			 * 
			 * MySql.Data.MySqlClient.MySqlException: "Reading from the stream has failed."
			 * 
			 * Innere Ausnahme
			 * EndOfStreamException: Attempted to read past the end of the stream.
			 * 
			 */
			MySqlCon.Open();			// <--- Ausnahme

			MySqlCon.Close();
			sshclient.Disconnect();

Im Debugger sehe ich, dass die lokale Portweiterleitung richtig eingerichtet und gestartet ist:


-	portfwd	{Renci.SshNet.ForwardedPortLocal}	Renci.SshNet.ForwardedPortLocal
		BoundHost		"127.0.0.1"		string
		BoundPort		3306			uint
		Host			"localhost"		string
		IsStarted		true			bool
		Port			3306			uint


Auch der SSH-Client ist richtig eingerichtet und ich kann u. a. Shell-Befehle wir z. B.


Console.WriteLine(sshclient.CreateCommand("ls -lah ~/.ssh").Execute());

erfolgreich ausführen.

Nun halte ich das Programm vor MySqlCon.Open() an und schaue mir die im System benutzten Ports mit


netstat -bano -p TCP

an. Dabei stelle ich folgendes fest (Ausschnitt):




Aktive Verbindungen

  Proto  Lokale Adresse         Remoteadresse          Status           PID

  TCP    10.0.70.5:59619        192.168.70.204:49172   WARTEND         0
  TCP    10.0.70.5:59620        192.168.70.204:49172   WARTEND         0
  TCP    10.0.70.5:59773        192.168.70.204:135     HERGESTELLT     5732
 [spoolsv.exe]
  TCP    10.0.70.5:59774        192.168.70.204:135     HERGESTELLT     5732
 [spoolsv.exe]
  TCP    10.0.70.5:59775        192.168.70.204:135     HERGESTELLT     5732
 [spoolsv.exe]
  TCP    10.0.70.5:59776        192.168.70.204:49172   SYN_GESENDET    5732
 [spoolsv.exe]
  TCP    10.0.70.5:59777        192.168.70.204:49172   HERGESTELLT     5732
 [spoolsv.exe]
  TCP    127.0.0.1:3306         0.0.0.0:0              ABHÖREN         42608
 [sshtest2.exe]
  TCP    127.0.0.1:8884         0.0.0.0:0              ABHÖREN         4
 Es konnten keine Besitzerinformationen abgerufen werden.
  TCP    127.0.0.1:9395         127.0.0.1:56791        HERGESTELLT     6704
 

Bei Zeile


TCP    127.0.0.1:3306         0.0.0.0:0                ABHÖREN         42608

sehe ich den Port und die PID = 42608, die mein Testprogramm sshtest2.exe bekommen hat.

Wie ist das zu deuten? Ich hätte folgendes erwartet:


TCP    127.0.0.1:3306         127.0.0.1:3306           ABHÖREN         42608

Ich verstehe das einfach nicht, denn andere Programme, die genauso arbeiten, funktionieren tadellos.

Ein Parameter unterscheidet sich: Es handelt sich um einen anderen Internet-Provider (all-inkl, Webspace-Tarif ALL-INKL Business, Webhosting Tarif ALL-INKL BUSINESS - ALL-INKL.COM). Allerdings kann ich mir beim besten Willen nicht vorstellen, dass dies etwas damit zu tun hat, da es sich um eine lokale Portweiterleitung handelt.

Kennt sich jemand von euch damit aus und kann mir weiterhelfen?

Schonmal lieben Dank und viele Grüße

René

15.12.2022 - 10:02 Uhr

Und im grunde liegt es auch auf der Hand bei einer stark typisierten Sprache wie C# wird ein Enum Wert auch als ein Enum Wert betrachtet.
Aber warum nimmst du nicht direkt die Enum Werte im dictionary als Key?
Dann kann kannst du das (Un-)Boxing aussparen.

Danke, habe das bereits geändert. Ich habe mit Scheuklappen gelesen, was ich lesen wollte und ob der späten Stunde nicht weiter nachgedacht.

Nochmals vielen Dank!

15.12.2022 - 09:59 Uhr

Ach so, der Wert entspricht zwar einem int, der Datentyp ist jedoch ein anderer (hier RV). Einleuchtend – da habe ich die MS-Hilfe zu wörtlich genommen bzw. missverstanden.

Vielen Dank für die rasante Hilfe!

Einen schönen Tag und bis zur nächsten Frage.

René

15.12.2022 - 09:48 Uhr

Hallo an alle,

ich habe wieder einmal ein Verständnisproblem – diesmal bei der Verwendung von enum. Wie Microsoft unter "Enumerationstypen: C#-Referenz" beschreibt, sind die einzelnen Werte, sofern man nichts anders angibt, standradmäßig int – in diesem Fall genau das, was ich brauche.

Nun füge ich nachstehend ein kurzes Testprogramm bei, welches meine Frage veranschaulicht.

  • Ich definiere ein enum RV mit vordefinierten int-Werten.
  • Im nachfolgendem Dictionary<int, string> RetVal nutze ich die enum-Werte als Key.

Frage:
Warum muss ich beim Konstruieren des Dictionary auf int casten, wenn die Werte per Definition sowieso von diesem Datentyp sind?


namespace enum_dic
{
	internal class Program
	{
		static void Main(string[] args)
		{
			foreach (var rv in Constants.RetVal)
			{
				Console.WriteLine($"{rv.Key,4}\t{rv.Value}");
			}

			Console.ReadLine();
		}
	}


	internal static class Constants
	{
		internal enum RV
		{
			Success						=    0,
			WrongParameterNumber		=   -1,
			OrderNotFound				=   -2,
			CannotWriteLogFile			=   -3,
			ErrorInCommandLine			=  -50,
			DataViewNotCreated			=  -60,
			NotDataFound				=  -61,
			MoreThanOneArticle			=  -70,
			ArticleNotFoundInPartList	=  -72,
			ErrorWhileQueryingDatabase2	=  -96,
			ErrorWhileQueryingDatabase	=  -97,
			WorkerMayBeStartedOnlyOnce	=  -98,
			WorkerCouldNotBeSetup		=  -99,
			UntreatedException			= -100
		}

		internal static readonly Dictionary<int, string> RetVal = new()
		{
			{ (int) RV.Success,                       "Erfolgreiche Ausführung" },
			{ (int) RV.WrongParameterNumber,          "Falsche Parameterzahl" },
			{ (int) RV.OrderNotFound,                 "Auftrag nicht auffindbar" },
			{ (int) RV.CannotWriteLogFile,            "Zugriff auf Logdatei nicht möglich" },
			{ (int) RV.ErrorInCommandLine,            "Fehler beim Prüfen der Kommandozeilenparameter" },
			{ (int) RV.DataViewNotCreated,            "Die DataView konnte nicht erstellt werden." },
			{ (int) RV.NotDataFound,                  "Daten konnten nicht gefunden werden." },
			{ (int) RV.MoreThanOneArticle,            "Mehr als ein E11- bzw. E33-Artikel in der Stückliste gefunden." },
			{ (int) RV.ArticleNotFoundInPartList,     "Der gesuchte Artikel wurde in der Stückliste nicht gefunden." },
			{ (int) RV.ErrorWhileQueryingDatabase2,   "Fehler beim Abfragen der Datenbank. Spalten bzw. Zeilen dürfen nicht Null sein." },
			{ (int) RV.ErrorWhileQueryingDatabase,    "Fehler beim Abfragen der Datenbank" },
			{ (int) RV.WorkerMayBeStartedOnlyOnce,    "Der Worker darf nur einmal gestartet werden." },
			{ (int) RV.WorkerCouldNotBeSetup,         "Der Worker konnte nicht eingerichtet werden." },
			{ (int) RV.UntreatedException,            "Unbehandelte Ausnahme" }
		};
	}
}

Danke und liebe Grüße

René

12.12.2022 - 08:54 Uhr

Danke, dann bin ich in guter Gesellschaft und nicht alleine.

Eine erfolgreiche Arbeitswoche!

LG

René

11.12.2022 - 21:39 Uhr

Hier bietet sich ggf. die Verwendung des
>
an,
wenn sichergestellt ist das der Wert nicht null ist.

Danke! Wie sagte eine berühmte Fernsehdame? "Hier werden Sie geholfen!"

Du kannst lachen, aber ich kannte diesen Postfixoperator nicht. Mit dem kann ich an der besagten Stelle in der Tat, die Compilerwarnung abschalten:


ds.Tables["Artikel"]!.PrimaryKey = new DataColumn[1] { ArtikelColumns["NUMMER"]! }

Ob das aber sicher und übersichtlicher als die zwei hässlichen #pragma-Zeilen ist, bin ich mir noch nicht ganz im Klaren – wahrscheinlich aber ja, denn in beiden Fällen würde das Programm genau an dieser Stelle eine Ausnahme werfen, wenn meine Behauptung der vollkommenen Korrektheit nicht zutreffend wäre.

@Abt
Wenn ich Microsoft https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving?WT.mc_id=DT-MVP-5001507 richtig verstehe, kann ich durchaus diesen Postfixoperator für meinen Zweck nutzen:

You can also use the null-forgiving operator when you definitely know that an expression can't be null but the compiler doesn't manage to recognize that. In the following example, if the IsValid method returns true, its argument isn't null and you can safely dereference it:

Sehr interessant, denn ich habe durch eure Hilfe was neues gelernt.

Vielen Dank und einen erfolgreichen Start in die neue Woche!

LG

René

11.12.2022 - 21:16 Uhr

Hallo Th69,

in jeder erdenklichen Kombination werden beide Warnungen ausgegeben. Wie gesagt, an dieser Stelle nicht schlimm, denn ich weiß, dass alles OK ist. Daher habe ich nur für die betroffene Zeile diese Compilerwarnungen deaktiviert:


#pragma warning disable CS8601, CS8602     // Mögliche Nullverweiszuweisung bzw. Dereferenzierung eines möglichen Nullverweises
			ds.Tables["Artikel"].PrimaryKey	= new DataColumn[1] { ArtikelColumns["NUMMER"] };
#pragma warning restore CS8602, CS8601

Nicht schlimm, mich hatte nur interessiert, warum der Compiler an dieser Stelle so beharrlich ist.

Danke und Grüße

René

10.12.2022 - 11:52 Uhr

Hallo nochmals,

ich stelle fest, dass nach jedem Laden eines Projektes die Häkchen in der Spalte "Erstellen" (Erstellen | Batch erstellen" bzw. Alt+E+C einfach wieder weg sind. Das war aber mal anders und ich weiß nicht, ob man das dauerhaft beeinflussen kann und wenn, wo?

Kann das jemand bestätigen oder ist es ein lokales, nur bei mir erscheinendes Problem?

Hier die Infos über meine Installation:


Microsoft Visual Studio Professional 2022
Version 17.4.2
VisualStudio.17.Release/17.4.2+33122.133
Microsoft .NET Framework
Version 4.8.09032
Installierte Version: Professional

Weitere Infos in der Dateianlage.

Schönen Dank und ein verschneites Wochenende!

LG

René

10.12.2022 - 11:39 Uhr

jetzt ist die Abfrage ja genau falsch herum, es müsste doch so aussehen:

  
if (ds != null && ds.Tables != null && ds.Tables["Artikel"] != null && ArtikelColumns["NUMMER"] != null)  
  

Um Gottes Willen! Ich freue mich auf Weihnachten – ein paar Feiertage werden mir gut tun.

Dennoch ändert sich an den zwei Warnungen nichts. Es ist auch nicht ganz wichtig, denn ich weiß schon (vorher geprüft), dass alles korrekt ist. Daher:


#pragma warning disable CS8601, CS8602     // Mögliche Nullverweiszuweisung bzw. Dereferenzierung eines möglichen Nullverweises                                                                                          
			ds.Tables["Artikel"].PrimaryKey	= new DataColumn[1] { ArtikelColumns["NUMMER"] };
#pragma warning restore CS8602, CS8601

Dennoch würde ich gerne wissen, warum VS das als Warnung einstuft.

Aber müsste zumindestens für die erste Warnung nicht einfach der ?.-Operator reichen?

  
ds?.Tables["Artikel"]?.PrimaryKey = ...  
  

Geht nicht: "Die linke Seite einer Zuweisung muss eine Variable, eine Eigenschaft oder ein Indexer sein"

Bei der zweiten kommt es darauf an, wie ArtikelColumns definiert und zugewiesen ist.
Ich denke, gemeint ist, daß diese Variable selbst null sein kann, daher entweder auch auf ArtikelColumns != null prüfen oder

  
new DataColumn[1] { ArtikelColumns?["NUMMER"]};  
  

Dann kann jedoch danach null als Wert in dem DataColumn-Array stehen.

Auch schon getestet, aber es ändert sich weiterhin nichts.

Danke und lG

René

10.12.2022 - 11:02 Uhr

Deine Prüfung macht auch wenig Sinn, wenn ds != null geht er schon rein. ds.Tables["Artikel"] kann ja immer noch fehlen.

Ja, hast du Recht, sorry. Ich habe nun die Abfrage über die Zwischenablage kopiert, um keinen doofen Fehler mehr zu machen:


if (ds == null || ds.Tables == null || ds.Tables["Artikel"] == null || ArtikelColumns["NUMMER"] == null)
{
	ds.Tables["Artikel"].PrimaryKey = new DataColumn[1] { ArtikelColumns["NUMMER"] };
}

Es ändert sich jedoch nichts (siehe Bild in der Dateianlage).

LG

René

09.12.2022 - 15:28 Uhr

Hallo,

ich habe folgende Frage. Visual Studio 2022 meckert in der unten stehenden Zeile gleich zwei Male:


ds.Tables["Artikel"].PrimaryKey = new DataColumn[1] { ArtikelColumns["NUMMER"] };

Bei "ds.Tables["Artikel"].PrimaryKey" meldet VS: Mögliche Nullverweiszuweisung

und bei "ArtikelColumns["NUMMER"]": Dereferenzierung eines möglichen Nullverweises

Die grün unterwellten Stellen habe ich hier unterstrichen.

Ich weiß aber, dass dies nicht vorkommen kann, daher schalte ich diese Warnungen für die Zeile ab:


#pragma warning disable CS8601, CS8602    // Mögliche Nullverweiszuweisung bzw. Dereferenzierung eines möglichen Nullverweises
ds.Tables["Artikel"].PrimaryKey = new DataColumn[1] { ArtikelColumns["NUMMER"] };
#pragma warning restore CS8602, CS8601

Nicht schön, aber zunächst egal.

Nun es ist so, dass an vielen Stellen VS Warnungen unterbindet, wenn man davor eine Prüfung macht. Daher habe ich testhalber folgenden (hässlichen) Code getestet:


if (ds != null && ds.Tables != null || ds?.Tables?["Artikel"] != null && ArtikelColumns["NUMMER"] != null)
{
	ds.Tables["Artikel"].PrimaryKey = new DataColumn[1] { ArtikelColumns["NUMMER"] };
}

Es hat sich aber nichts geändert.

Versagt hier IntelliSense oder mache ich grundsätzlich was falsch und ich merke es nur nicht?

Liebe Grüße

René
09.09.2022 - 13:34 Uhr

OK, Problem gelöst. Ich habe mich von der Fehlermeldung in die Irre treiben lassen und an der falschen Stelle nach der Fehlerursache gesucht. Nun bin ich Schritt für Schritt systematisch vorgegangen und siehe da, ganz am Anfang schmiert der Dienst ab, ohne etwas zu machen. Der Fehler lag am "displayname" des Dienstes – dieser darf maximal 80 Zeichen lang sein, während ich 110 Zeichen verwendete. Das wusste ich nicht und die Fehlermeldung sagte auch was ganz anderes.

Lieben Dank für eure Hilfe! Das Wochenende ist gerettet!

LG

René

09.09.2022 - 12:30 Uhr

In meiner Verzweiflung habe ich den Wert in der Registry "stark" erhöht:

Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
ServicesPipeTimeout (REG_DWORD): 0x180000

Danke nochmals. Ich werde jetzt systematisch schrittweise vorgehen.

09.09.2022 - 11:56 Uhr

Die beiden Einträge bekomme ich, wenn ich den Dienst starten will:


Protokollname: System
Quelle:        Service Control Manager
Datum:         09.09.2022 10:35:40
Ereignis-ID:   7000
Aufgabenkategorie:Keine
Ebene:         Fehler
Schlüsselwörter:Klassisch
Benutzer:      Nicht zutreffend
Computer:      RKK-XPS.isential.local
Beschreibung:
Der Dienst "WooComMesserschmidt" wurde aufgrund folgenden Fehlers nicht gestartet: 
Der Dienst antwortete nicht rechtzeitig auf die Start- oder Steuerungsanforderung.


Protokollname: System
Quelle:        Service Control Manager
Datum:         09.09.2022 10:35:40
Ereignis-ID:   7009
Aufgabenkategorie:Keine
Ebene:         Fehler
Schlüsselwörter:Klassisch
Benutzer:      Nicht zutreffend
Computer:      RKK-XPS.isential.local
Beschreibung:
Das Zeitlimit (1572864 ms) wurde beim Verbindungsversuch mit dem Dienst WooComMesserschmidt erreicht.

Danke und lG

René

08.09.2022 - 22:16 Uhr

Danke. Was mich aber dabei sehr wundert, ist die Tatsache, dass der Fehler ziemlich schnell Auftritt – keine 30, sondern maximal zwei Sekunden.

08.09.2022 - 20:37 Uhr

Hallo,

heute melde ich mich wieder einmal mit einem Problem, das ich trotz Suche nicht gelöst bekomme.

Ich habe einen Windows-Dienst für .Net 6 programmiert, der von BackgroundService erbt:


namespace WooComMesserschmidt
{
	internal class Worker : BackgroundService
	{
		private	readonly HttpClient						Client			= new()
		private	string									BaseAddress		= string.Empty;
		private	readonly IConfiguration					Configuration;
		private	string									ConfigFile		= string.Empty;
		private readonly Dictionary<string, dynamic?>	_ConfigPar;
		internal static	 Dictionary<string, string>		ConfigPar		= new();
		private readonly ILogger<Worker>				_logger;

		public struct LogInfo
		{
			public ILogger<Worker>? Logger;
			public string?			LogFile;

			public LogInfo(ILogger<Worker>? logger, string logfile)
			{
				Logger	= logger;
				LogFile	= logfile;
			}
		}

		public static LogInfo logInfo;

		public Worker(ILogger<Worker> logger, IConfiguration configuration, Dictionary<string, dynamic?> configpar)
		{
			Configuration	= configuration;
			_ConfigPar      = configpar;
			_logger         = logger;

			Init();
		}
...

Nun finden in der Methode "Init()" relativ umfangreiche Aufgaben statt. Wenn diese durch sind, geht es hier weiter:


		protected override async Task ExecuteAsync(CancellationToken stoppingToken)
		{
			// Abfrageintervall in Millisekunden
			if (int.TryParse(ConfigPar[cpQueryInterval], out int QueryInterval) == false)
			{
				QueryInterval = QueryIntervalStd;
				Log.LogInformation(logInfo, $"Abfrageintervall konnte nicht ermittelt werden. Es wird daher der Standardwert von {QueryInterval} Millisekunden verwendet. Prüfen Sie die Angaben in der Parameterdatei.");
			}

			Log.LogInformation(logInfo, $"{Process.GetCurrentProcess().ProcessName} gestartet.");
			Log.LogInformation(logInfo, $"Worker arbeitet mit Abfrageintervall von {QueryInterval} Millisekunden.");

			while (stoppingToken.IsCancellationRequested == false)
			{
				// Neue Bestellungen verarbeiten.
				await ProcessNewOrders();

				// Artikel aktualisieren – zurzeit nur den Preis aktualisieren.
				await UpdateProducts();

				// DHL-Dateien verarbeiten.
				Dhl_Polling();

				// Nun legen wir uns aufs Ohr für ein Weilchen.
				await Task.Delay(QueryInterval, stoppingToken);
			}
		}
...

Der Dienst soll alle paar Minuten Bestellungen aus einem E-Shop holen, Artikel aktualisieren und DHL-Sendungen verarbeiten.

Nun, wenn ich das Programm in der Kommandozeile manuell starte (also nicht als Dienst), funktioniert alles, wie erwartet. Nun habe ich das Programm als Dienst registriert und bekomme jedes Mal beim Versuch, den Dienst zu starten, folgenden Fehler:

Fehlermeldung:
Fehler 1053: Der Dienst antwortete nicht rechtzeitig auf die Start- oder Steuerungsanforderung

Gestartet wir alles in Main:


		private static async Task<int> Main(string[] args)
		{
			try
			{
				IHost host = Host.CreateDefaultBuilder(args)
					.UseWindowsService(options =>
					{
						options.ServiceName = ServiceName;
					})
					.ConfigureServices(services =>
					{
						services.AddSingleton<Dictionary<string, dynamic?>> (_ConfigPar);
						
						services.AddHostedService<Worker>();
					})
					.Build();

				await host.RunAsync();

				Log.LogInformation((LogInfo)_ConfigPar[cpLogInfo], $"{Process.GetCurrentProcess().ProcessName} beendet.");

				if (Debugger.IsAttached == true)
				{
					Console.ReadLine();
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine($"{ex}{Environment.NewLine}{Environment.NewLine}");
...

Wie gehe ich vor, um diesen Fehler zu vermeiden?

Vielen Dank und lG

René

21.08.2022 - 08:54 Uhr

Natürlich nicht. Das sind von der PayPal-Sandbox zur Verfügung gestellten Daten

19.08.2022 - 18:36 Uhr

Lieben Dank, das hat mir weitergeholfen.

Im ersten Fall reicht es ein simples ToString() vollkommen aus:


var result = meta.Value.ToString();

Heute ist wahrlich nicht mein Tag.

Im Zweiten Fall ist die Sache auch einfach – aus dem Objekt wird ein JSON-String (meta.Value.ToString()) gemacht und dieser wird dann mit JsonSerializer.Deserialize<Zahlungsanweisung>(meta.Value.ToString()) in die dafür vorgesehenen Klassen deserialisiert:


Zahlungsanweisung? za = JsonSerializer.Deserialize<Zahlungsanweisung>(meta.Value.ToString());

Dann erhalte ich das gewünschte Ergebnis:


Zahlungsanweisung
    Transaktionsnummer    "5GB5061965299422G"             string
    Zahlungsart           "PAY_UPON_INVOICE"              string
    Zahlungsinformation
        BIC                   "DEUTDEDBPAL"               string
        Bank                  "Deutsche Bank"             string
        IBAN                  "DE63120700888000584157"    string
        Kontoinhaber          "PayPal Europe"             string

Danke nochmals und ein schönes Wochenende.

LG

René

19.08.2022 - 15:28 Uhr

Hallo,

ich möchte mit .NET 6 aus einem E-Shop die Zahlungsanweisung über dessen RESR-API ermitteln (dafür nutze ich "System.Text.Json" und "System.Text.Json.Serialization)". Es funktioniert auch alles, aber an einer Stelle drehe ich mich im Kreis. Hier ein kleiner Ausschnitt aus den JSON-Daten (E-Shop-Bestellung):


   "meta_data": [
        {
            "id": 31477,
            "key": "_shipping_address_type",
            "value": ""
        },
        {
            "id": 31479,
            "key": "_additional_costs_include_tax",
            "value": "yes"
        },
        {
            "id": 31480,
            "key": "_shipping_dhl_address_type",
            "value": "regular"
        },
        {
            "id": 31481,
            "key": "_gzd_version",
            "value": "3.9.8"
        },
        {
            "id": 31482,
            "key": "_new_order_email_sent",
            "value": "true"
        },
        {
            "id": 31484,
            "key": "is_vat_exempt",
            "value": "no"
        },
        {
            "id": 31487,
            "key": "reference_number",
            "value": "5GB5061965299422G"
        },
        {
            "id": 31488,
            "key": "instruction_type",
            "value": "PAY_UPON_INVOICE"
        },
        {
            "id": 31489,
            "key": "payment_due_date",
            "value": "2022-09-17"
        },
        {
            "id": 31490,
            "key": "bank_name",
            "value": "Deutsche Bank"
        },
        {
            "id": 31491,
            "key": "account_holder_name",
            "value": "PayPal Europe"
        },
        {
            "id": 31492,
            "key": "international_bank_account_number",
            "value": "DE63120700888000584157"
        },
        {
            "id": 31493,
            "key": "bank_identifier_code",
            "value": "DEUTDEDBPAL"
        },
        {
            "id": 31494,
            "key": "_payment_instruction_result",
            "value": {
                "reference_number": "5GB5061965299422G",
                "instruction_type": "PAY_UPON_INVOICE",
                "recipient_banking_instruction": {
                    "bank_name": "Deutsche Bank",
                    "account_holder_name": "PayPal Europe",
                    "international_bank_account_number": "DE63120700888000584157",
                    "bank_identifier_code": "DEUTDEDBPAL"
                }
            }
        },
usw.

In der Klasse "Orders", die die Bestellungen aufnimmt, habe ich unter anderen folgendes:


internal class Orders
{
	...
	/// <summary>
	/// Metadaten wie u. v. a. Zahlungsinformation
	/// </summary>
	[JsonPropertyName("meta_data")]
	public List<MetaData> Metadaten { get; set; } = new();
	...
}

Und die Klasse "MetaData":


public class MetaData
{
	/// <summary>
	/// ID
	/// </summary>
	[JsonPropertyName("id")]
	public int Id { get; set; }

	/// <summary>
	/// Key
	/// </summary>
	[JsonPropertyName("key")]
	public string Key get; set; } = string.Empty;

	/// <summary>
	/// Value
	/// </summary>
	[JsonPropertyName("value")]
	public object Value { get; set; }
}

Nun ist es so, dass das Objekt "Value" mal einen String und mal eine weitere JSON-Struktur darstellt. Für den zweiten Fall (siehe nachstehenden code)


        {
            "id": 31494,
            "key": "_payment_instruction_result",
            "value": {
                "reference_number": "5GB5061965299422G",
                "instruction_type": "PAY_UPON_INVOICE",
                "recipient_banking_instruction": {
                    "bank_name": "Deutsche Bank",
                    "account_holder_name": "PayPal Europe",
                    "international_bank_account_number": "DE63120700888000584157",
                    "bank_identifier_code": "DEUTDEDBPAL"
                }
            }
        },

habe ich mir folgendes für "Value" überlegt:


public class Zahlungsanweisung
{
	/// <summary>
	/// Transaktionsnummer
	/// </summary>
	[JsonPropertyName("reference_number")]
	public string Transaktionsnummer { get; set; } = string.Empty;

	/// <summary>
	/// Zahlungsart
	/// </summary>
	[JsonPropertyName("instruction_type")]
	public string Zahlungsart { get; set; } = string.Empty;

	/// <summary>
	/// Zahlungsinformation
	/// </summary>
	[JsonPropertyName("recipient_banking_instruction")]
	public Zahlungsinformation Zahlungsinformation { get; set; } = new();
}

Und für die Zahlungsinformation:


public class Zahlungsinformation
{
	[JsonPropertyName("bank_name")]
	public string Bank { get; set; } = string.Empty;

	[JsonPropertyName("account_holder_name")]
	public string Kontoinhaber { get; set; } = string.Empty;

	[JsonPropertyName("international_bank_account_number")]
	public string IBAN { get; set; } = string.Empty;

	[JsonPropertyName("bank_identifier_code")]
	public string BIC { get; set; } = string.Empty;
}

Hier habe ich gleich zwei Fragen:

  1. Frage:

Wenn ich mir im Debugger einen der Metadaten-Elemente anschaue, der einem String entspricht, sieht es so aus:


Id		31479							int
Key		"_additional_costs_include_tax"	string
Value	ValueKind = String : "yes"		object {System.Text.Json.JsonElement}

Wie konvertiere ich diesen Wert (hier "yes") in einen gewöhnlichen String?

  1. Frage

Im Fall dass es sich nicht um einen String handelt – wie im vorigen Punkt –, bekommen ich die Zahlungsanweisung. Diese sieht im Debugger so aus:


Id		31494							int
Key		"_payment_instruction_result"	string
Value	ValueKind = Object : "{"reference_number":"5GB5061965299422G","instruction_type":"PAY_UPON_INVOICE","recipient_banking_instruction":{"bank_name":"Deutsche Bank","account_holder_name":"PayPal Europe","international_bank_account_number":"DE63120700888000584157","bank_identifier_code":"DEUTDEDBPAL"}}"	object {System.Text.Json.JsonElement}

Wie kann ich a) diese Infos in "Zahlungsanweisung" und "Zahlungsinformation" packen und b) wie kann ich das mit "MetaData.Value" verknüpfen?

Irgendwie drehe ich mich im Kreis und habe eine Denkblockade.

Vielen Dank und liebe Grüße

René

10.08.2022 - 08:59 Uhr

Danke euch beiden!

Der Ansatz mit AntMe finde ich sehr interessant. Ich denke, wenn das gut gemacht ist, dass es für Kinder besser als ein dicker Schinken ist. Wenn der Junge nach einer gewissen Zeit weiterhin motiviert bleibt, kann er zu einem Buch wie z. B. Schrödinger greifen. Auch diesen Ansatz finde ich interessant, allerdings ist das Buch mittlerweile relativ alt – vielleicht kommt aber eine neue Auflage.

Und sollte mein Junge dabei bleiben, hat er mich immer noch, wenn er Fragen hat...

Nochmals vielen Dank und liebe Grüße

René

09.08.2022 - 11:48 Uhr

Jetzt habe ich eine vielleicht merkwürdige Frage. Mein ältestes Enkelkind ist 9 Jahre alt – es wird erst zu Weihnachten 10. Es scheint so zu sein, dass es ziemlich technikbegabt ist. Sehr gute Noten in Mathe, Deutsch, Sport usw. Dagegen Musik, Kunst o. ä. nur soviel, wie unbedingt nötig – das ist nicht seins.

Er hat mich vor einigen Wochen überrascht, indem er mir eigene (einfache) Spieleerweiterungen von Roblox gezeigt hat. Diese programmiert er in der Skriptsprache "Lua", von der ich bis dato nie gehört hatte. Er hat sich sogar ein Programmierbuch bestellt (ein dicker Schinken), um tiefer einzusteigen.

Nun erzählte er mir gestern, dass er lieber eine "richtige" Programmiersprache erlernen wollte. Er hätte schon Visual Studio heruntergeladen und installiert und würde gerne C# lernen. Da war ich wieder baff! Er ist 9 und Kinder in diesem Alter sollten was anderes an der frischen Luft machen. Dann zeigte er mir einige Bücher, die er sich ausgeguckt hatte und fragte mich, welches ich empfehlen würde. Keine Ahnung!

Gibt es überhaupt etwas für Kinder/Jugendliche in diesem Bereich (Bücher, Bibliotheken, Spiele), was ihnen die C#-Sprache etwas sanfter näherbringt? Wenn er einen wirklich dicken C#-Schinken in die Hände nimmt, brechen ihm die Handgelenke.

Ich bin damit etwas überfordert und ich weiß auch nicht, ob dies für einen Neujährigen OK ist. Das kommt allerdings alleine von ihm aus und wie es scheint, hat er Spaß daran.

Lieben Dank und viele Grüße

René

06.08.2022 - 13:52 Uhr

Manchmal sucht man am Suchbegriff vorbei... Vielen Dank!

Ich kenne mich mit PDF nahezu Null aus. Muss die gewünschte Ziel-Schriftart in das Dokument eingebettet sein, um eine vorhandene durch die Ziel-Schriftart zu ersetzen, oder reicht es, wenn diese Schrift in Windows vorhanden ist?

Nochmals vielen Dank für deine Tipps und ein schönes Wochenende.

LG

René

05.08.2022 - 12:01 Uhr

Hallo,

ich habe wieder einmal eine Frage – diesmal bezüglich PDF.

Ich bekomme fertige PDF-Dateien. Diese sind relativ einfach gestrickt, denn sie stammen aus einem Programm, welches PCL-Dateien erstellt, die nur Text und einige Formatierungen (Fett, Kursiv, Unterstrichen usw.) enthalten. Die PCL-Dateien werden mit Hilfe eines anderen Programms (VeryPDF PCL Converter, ältere Version 1.5) in PDF umgewandelt.

Die PDF-Dateien bleiben durchsuchbar, also der Text wird nicht in Grafik umgewandelt.

Jetzt habe ich das Problem, dass ich erst hier ansetzen kann: Ich bekomme eine PDF-Datei und sollte einigen Texten eine andere Schriftart verpassen, die in das Dokument nicht eingebettet ist (sonst bräuchte man das nicht zu machen). Spezifisch geht es darum, zunächst den Text zu erkennen und danach diesem eine Barcodeschriftart zu verpassen.

Nun meine Frage: Gibt es bereits Werkzeuge dafür? Wie geht man damit am besten um?

Ich möchte keine fertige Lösung sondern Tipps, wie man dieses Problem sinnvoll angehen kann.

Danke und LG

René

23.06.2022 - 16:20 Uhr

Wenn man in der Textdatei die Anwendung mit Parametern zum Start angibt, kann man das wunderbar als Backdoor verwenden, um Schadcode zu laden.
Herrlich 🙂

Sicher, wenn man keine Prüfungen vornimmt.

23.06.2022 - 14:57 Uhr

Danke für die Gedankenanstöße. Da die Anwendung auch eine Netzwerkfreigabe benötigt, würde sich diese anbieten, um darüber zu kommunizieren. Auf den entsprechenden Clients dann eine einfache Applikation mit einem File-Watcher. Aber auch die anderen Ansätze sind gut und ich werde mir das übers Wochenende anschauen.

Nochmals Dankeschön an alle und schönes Schwitzen!

LG

René

23.06.2022 - 10:53 Uhr

Es wäre kein Problem, ein Service oder ein automatisch im Benutzerkontext startendes Programm auf der Arbeitsstation zu installieren, welches dann diese Aufgabe übernimmt. Welche Kommunikationswege sind aber in diesem Fall am geeignetsten?

23.06.2022 - 10:01 Uhr

Hallo,

ein Programm startet weitere lokale Programme wie u. a. Office-Programme (Excel, Outlook, Word usw.). Bisher funktioniert alles ohne Probleme.

Seit kurzem wird dieses Programm auch als Remote-Desktop-Anwendung auf einem Remote-Desktop-Server (früher Terminal-Server) ausgeführt – auch das funktioniert gut. Allerdings fehlen auf dem Server einige der vom Programm gestartete Programm wie z. B. die genannten Office-Programme. Es ist nicht erwünscht, die Office-Programme auf dem Remote-Desktop-Server zu installieren.

Nun "darf" ich überlegen, ob es eine Möglichkeit gibt, dass die Anwendung, die auf dem Remote-Desktop-Server ausgeführt wird, Programme auf dem Client startet. Name, Parameter usw. der zu startenden Programme können in der Anwendung hinterlegt werden.

Mir geht es nicht darum, wie ich etwas machen kann, sondern um das "Was".

Welche Technologien sind für die gestellte Aufgabe am besten geeignet?

Danke im Voraus und libee Grüße

René

14.05.2022 - 13:36 Uhr

Warum die Methoden umständlich per Reflection in ein Dictionary packen gerade wenn die statisch sind und nicht bei der Definition.

Ganz einfach: Es werden einmalig beim Programmstart die Methoden der passenden Klasse zur Nachverarbeitung der Spaltenwerte der Quelle ermittelt und diese in ein temporäres Dictionary gepackt. Gleichzeitig werden alle Spalten inkl. deren Spaltennamen in der Quelle ermittelt. Nun findet ein Abgleich statt: Gibt es für die gerade untersuchte Spalte in der Quelle eine passende Methode (Nachverarbeitungsmethode)? – Die Kriterien wurden bereits eingangs dargelegt (static, public, keine Parameter, Rückgabe bool und Name wie die Spalte aber Präfix dazu).

Wenn eine Nachverarbeitungsmethode gefunden wurde, wird diese mit der sonstigen Feldinformation gespeichert.

Ich hatte auch erwähnt, dass der Pflegeaufwand auf ein Minimum reduziert werden soll: Kommen weitere Spalten in der Quelle dazu bzw. müssen bestehende Spalten anders behandelt werden, so soll es reichen, die jeweilige Klasse für die Quelle um die passenden Methoden (Signatur und Name müssen passen) zu erweitern. Das restliche Programm soll unangetastet bleiben.

13.05.2022 - 22:27 Uhr

Im Übrigen werden die Methoden durch ihr Präfix im Editor bei der Codevervollständigung alle schön gebündelt angezeigt. Auch ein Vorteil.
Sofern du die Methoden manuell aufrufst dann stimmt das. Ich bin davon ausgegangen, dass du die Methoden nur via Reflection aufrufst.

Eigentlich werden sie nicht manuell aufgerufen – da hast du Recht. Dennoch mag ich die Namensgebung, welche eine direkte Zuordnung zwischen einer Spalte und deren Verarbeitung herstellt. Das mag auch historische Gründe haben: Unser Hauptprodukt (eine große, 30 Jahre alte Datenbankanwendung) fußt darauf und wir sind immer gut damit zu recht gekommen. Die Macht der Gewöhnung...

Für diesen einen Fall nutzen uns die Attributen leider nichts: [...]
Warum die Attribute dabei nichts nutzen sollen, hast du aber nicht beschrieben.

Bei dieser Anwendung sehe ich keinen Mehrwert darin, wenn wir weiterhin auf unserer alten Zuordnung Feld/Spalte --> Methode beharren.

Nach deiner ausführlicheren Beschreibung, würde ich es jetzt so umsetzen:

  
[File("Parts_22")]  
public class FilePostProcessor_Parts_22 : BaseProcessor  
{  
    [PostProcess("PartNumber")]  
    public static bool TransformPartNumberIntoXyz()   
    {  
        // do something  
    }  
  
    [PostProcess("Art2NoH")]  
    public static bool TransformArt2NoHIntoZyx()   
    {  
        // do something  
    }  
}  
  

BaseProcess ist deine Basisklasse mit der gemeinsamen Funktionalität.
Ein weiterer Vorteil wenn du die Attribute verwendest:

  • Du kannst einen Roslyn Analyzer bauen, der bei vorhandenem PostProcess Attribute die Methodensignatur erzwingt.

Ja, richtig, aber durch unsere Vorgehensweise wird eine Methode nie aufgerufen, wenn Sie nicht die richtige Signatur und Namen hat.

  • Du kannst zur Runtime überprüfen ob alle Methoden die das Attribute haben, auch die richtige Signatur haben.
    In deinem Fall kann es schnell passieren, dass eine Signatur falsch ist und das fällt dann fast nicht auf. (Außer dass die Nachverarbeitung des Feldes nicht stattfindet)

Auch richtig, aber auch das ist in dieser Anwendung sekundär: Nachverarbeitung haben einen definierten Zweck und werden ohne ausführliche Tests nicht implementiert. Der erste Test ist der Aufruf über Reflection – erst wenn dieser stattfindet, wird die Logik implementiert.

Ich hab das jetzt nur der Vollständigkeit halber geschrieben. Vielleicht hat mal jemand ein ähnliches Problem und sucht etwas Inspiration.

Auf jeden Fall bedanke ich mich ausdrücklich bei dir, denn du hast mir einige Denkanstöße gegeben, die ich anderorts gut gebrauchen kann.

Schönes Wochenende dir und allen anderen!

13.05.2022 - 19:59 Uhr

Für diesen einen Fall nutzen uns die Attributen leider nichts: Z. B. liefert die Quelle Daten über die Datei "Parts_22", welche u. a. über 102 Spalten verfügt – 58 davon brauchen eine sogenannte Nachverarbeitung, so dass es dafür 58 Methoden gibt.

In Anlehnung an die Quelle bekommt die Klasse ebenfalls den Namen "Parts_22" – alle solche Klassen werden von einer anderen Klasse abgeleitet, die gemeinsame Funktionalität bietet.

Wenn nun für eine Spalte eine Nachverarbeitungsmethode notwendig ist, so heißt diese Methode wie die nachzuverarbeitende Spalte mit dem Präfix "nv". Wird nun beispielweise die Quelle um eine Spalte erweitert (z. B. Art2NoH), so wird diese zwar an das Zielsystem übertragen, mangels Nachverarbeitungsmethode aber nicht weiter beachtet. Ist das aber erwünscht, so reicht es, die Klasse Parts_22 um die Methode nvArt2NoH mit der passenden Signatur zu erweitern und schon wird diese ausgeführt.

Im Übrigen werden die Methoden durch ihr Präfix im Editor bei der Codevervollständigung alle schön gebündelt angezeigt. Auch ein Vorteil.

Das ist der Hintergrund.

Und deutsch und englisch: Manchmal verwende ich auch spanisch... 🙂

Nochmals lieben Dank für die vielen Vorschläge und hasta la próxima!

13.05.2022 - 19:02 Uhr

Danke euch Dreien! Beides funktioniert, wobei ich mich für die Einschränkung entschieden habe, da sie etwas durchsichtiger ist.

Ich verstehe, dass das Festzurren am Namen für ernste Mienen sorgt. Für meine Anwendung ist das aber so gewollt: Es sollen Klassen erstellt werden, die u. a. Werte aus anderen Quellen verarbeiten und an ein anderes System überstellen – also eine typische Schnittstelle zwischen zwei Systemen.

Jede Schnittstelle ist eine Klasse, die sog. Nachverarbeitungsmethoden bietet bzw. bieten kann. Diese Nachverarbeitungsmethoden werden für jedes Feld/Spalte automatisch aufgerufen, sofern sie in der benutzten Klasse vorhanden sind und unter anderem deren Methodenname den Feldname mit Präfix "nv" entsprechen. Beispiel: PartNumber ==> **nv**PartNumber.

Da es um sehr viele Schnittstellen mit vielen Feldern/Spalten geht, soll der Programmier- und Verwaltungsaufwand auf ein Minimum reduziert werden, daher diese Mimik: Es reicht, eine Klasse um eine passende Nachverarbeitungsmethode zu ergänzen, damit diese automatisch aufgerufen wird.

13.05.2022 - 17:41 Uhr

Hallo,

ich brauche wieder einmal euer Expertenwissen. Ich habe unten stehenden Code einer generischen Methode. Diese untersucht die über <T> übermittelte Klasse, wobei sie die Methoden der Klasse ermittelt, die alle nachstehend aufgeführten Kriterien erfüllen:

  • public
  • static
  • Name beginnt mit "nv"
  • keine Parameter
  • Rückgabewert bool

Das funktioniert auch. Nun aber zu meinem Problem:

Wie kann ich die über T übergebende Klasse instanziieren? So was wie "var Test = new typeof(T)()" oder ähnlich. Ich stehe auf dem Schlauch.

namespace myTest
{
	class Program
	{
		static void Main(string[] args)
		{
			myMethod<KlasseA>()
			myMethod<KlasseB>()
			myMethod<KlasseC>()
		}

		public void myMethod<T>()
		{
			// Nimmt alle zur <T> passenden Methoden auf.
			var MethodMap = new Dictionary<string, Func<bool>>();

			// Uns interessieren nur öffentliche, statische Methoden.
			MethodInfo[] myMethods = typeof(T).GetMethods(BindingFlags.Public | BindingFlags.Static);

			// Darüber hinaus möchten wir nur die Methoden, die mit "nv"" beginnen, parameterlos sind und bool zurückliefern.
			foreach (MethodInfo myMethod in myMethods.Where(m => m.Name.StartsWith(c.PostProcessingPrefix) == true && m.GetParameters().Length == 0 && m.ReturnType == typeof(bool)))
			{
				MethodInfo? Method = typeof(T).GetMethod(myMethod.Name, Type.EmptyTypes);

				if (Method != null)
				{
					MethodMap.Add(myMethod.Name, (Func<bool>)Delegate.CreateDelegate(typeof(Func<bool>), Method));
				}
			}

			// Wie kann ich an dieser Stelle die über T übergebende Klasse instanziieren?
		}
	}

	public class KlasseA
	{
		...
	}

	public class KlasseB
	{
		...
	}

	public class KlasseC
	{
		...
	}
}
21.04.2022 - 22:56 Uhr

Warum nicht einfach so?

  
return (Action<int>)Delegate.CreateDelegate(typeof(Action<int>), myMethodInfo);  
  

Frei nach dem Motto, warum einfach, wenn es kompliziert geht? Nochmals herzlichen Dank, es funktioniert super.

Ich habe den Code oben entsprechend angepasst.

21.04.2022 - 22:48 Uhr

Du hast mich kurz vor dem Ins-Bett-gehen nochmals angetriggert. Ich habe das Beispiel etwas modifiziert, wodurch es m. E. klarer wird:


using System.Linq.Expressions;
using System.Reflection;

Dictionary<string, Action<int>> MethodenMap	= new Dictionary<string, Action<int>>();

for (int i = 1; i <= 3; i++)
{
	MethodenMap.Add($"Func{i}", CreateAction($"Func{i}"));
}

for (int i = 1; i <= 3; i++)
{
	MethodenMap[$"Func{i}"](i);
}

Console.WriteLine("Enter, um zu beenden...");
Console.ReadLine();

static Action<int> CreateAction(string FuncName)
{
	MethodInfo? Methode = typeof(test).GetMethod(FuncName, new[] { typeof(int) });

	if (Methode == null)					throw new ArgumentNullException	("Methode");
	if (Methode.IsStatic == false)			throw new ArgumentException		("Die übergebende Methode muss statisch sein.", "Methode");
	if (Methode.IsGenericMethod == true)	throw new ArgumentException		("Die übergebende Methode darf nicht generisch sein.", "Methode");

	// Danke an Palladin007 – es sieht schon übersichtlicher aus! ;-)
	return (Action<int>)Delegate.CreateDelegate(typeof(Action<int>), Methode);

	//return (Action<int>)Methode.CreateDelegate(Expression.GetDelegateType
	//	(
	//		Methode.GetParameters()
	//			.Select(p => p.ParameterType)
	//			.Concat(new[] { Methode.ReturnType })
	//			.ToArray())
	//	);
}

public static class test
{
	public static void Func1(int a)
	{
		Console.WriteLine($"Func{a}");
	}
	public static void Func2(int a)
	{
		Console.WriteLine($"Func{a}");
	}
	public static void Func3(int a)
	{
		Console.WriteLine($"Func{a}");
	}
}

Vielen Dank und gute Nacht!

21.04.2022 - 21:56 Uhr

Nachtrag
Ich habe das ganze , wie von Palladin007 empfohlen, auf Action umgestellt:


Dictionary<string, Action<int>> MethodenMap	= new Dictionary<string, Action<int>>();

for (int i = 1; i <= 3; i++)
{
	MethodenMap.Add($"Func{i}", GenerateDelegate(typeof(test).GetMethod($"Func{i}", new[] { typeof(int) })) as Action<int>);
}

for (int i = 1; i <= 3; i++)
{
	MethodenMap[$"Func{i}"](i);
}

Der Aufruf MethodenMap[$"Func{i}"](i); ist dadurch in der Tat eleganter, aber da habe ich gleich eine weitere Frage:

Ist die Umwandlung des Delegate in eine Action mit dem as-Operator (hier as Action<int>) der richtige Weg oder gibt es andere (bessere) Möglichkeiten?