Holger StehleSoftwareentwicklung und -architekturhttps://coders-online.net/Holger StehleSat, 26 Apr 2025 23:57:21 GMTSun, 19 Jan 2025 00:00:00 GMT(De-)Kompression durchführenZwar ist Speicher heutzutage üblicherweise keine Mangelware mehr, dennoch kann sich die Komprimierung großer Datenmengen, beispielsweise zur Übertragung, lohnen. C# bietet dazu einige Möglichkeiten, so unter anderem zur Arbeit mit ZIP-Archiven. Zu diesen möchte ich jedoch in einem separaten Beitrag kommen und stattdessen zeigen, wie sich mit der DeflateStream-Klasse beliebige Daten(-ströme) (de-)komprimieren lassen:

// Daten komprimieren
public static byte[] Compress(byte[] data) {
    using(MemoryStream inStream = new(data)) {
        using(MemoryStream outStream = new()) {
            using(DeflateStream deflate = new(outStream, CompressionMode.Compress)) {
                inStream.CopyTo(deflate);
            }

            return outStream.ToArray();
        }
    }
}

// Daten dekomprimieren
public static byte[] Decompress(byte[] data) {
    using(MemoryStream inStream = new MemoryStream(data)) {
        using(MemoryStream outStream = new()) {
            using(DeflateStream deflate = new(inStream, CompressionMode.Decompress)) {
                deflate.CopyTo(outStream);
            }

            return outStream.ToArray();
        }
    }
}

Um beispielsweise komplette Dateien effizient zu (de-)komprimieren, kann wie folgt vorgegangen werden:

// Datei komprimieren
public static void CompressFile(string inputPath, string outputPath) {
    using(FileStream inStream = File.OpenRead(inputPath)) {
        using(FileStream outStream = File.OpenWrite(outputPath)) {
            using(DeflateStream deflate = new(outStream, CompressionMode.Compress)) {
                inStream.CopyTo(deflate);
            }
        }
    }
}

// Datei dekomprimieren
public static void DecompressFile(string inputPath, string outputPath) {
    using(FileStream inStream = File.OpenRead(inputPath)) {
        using(FileStream outStream = File.OpenWrite(outputPath)) {
            using(DeflateStream deflate = new(inStream, CompressionMode.Decompress)) {
                deflate.CopyTo(outStream);
            }
        }
    }
}
]]>
https://www.coders-online.net/article/DeKompressionDurchfuehren476e6671-d2a5-6d76-3b0c-f163056c458eSat, 20 Jul 2024 18:22:14 GMT
Anwendungssymbol extrahierenDesktop-Anwendungen (*.exe) enthalten häufig ein Programmsymbol welches insbesondere im Windows Explorer und auf der Taskleiste angezeigt wird. Das Ermitteln dieses Anwendungssymbols, beispielsweise für eigene Auflistungen, ist sehr einfach über Icon.ExtractAssociatedIcon() realisierbar:

using(Icon icon = Icon.ExtractAssociatedIcon("devenv.exe")) {
      // Programmsymbol als *.ico-Datei speichern
    using(FileStream fileStream = File.OpenWrite("devenv.ico")) {
        icon.Save(fileStream);
    }

      // Programmsymbol als *.png-Datei speichern
    using(Bitmap bitmap = icon.ToBitmap()) {
        bitmap.Save("devenv.png", ImageFormat.Png);
    }
}

Bitte beachtet wie üblich in solchen Fällen, dass die Verwendung der Grafik rechtlich eingeschränkt sein kann.

]]>
https://www.coders-online.net/article/AnwendungssymbolExtrahierencc001d4e-1a08-f6a1-c39a-7612187641d7Sat, 22 Oct 2022 15:08:26 GMT
Arbeitsplatz sperrenDas Sperren des Arbeitsplatzes lässt sich unter Windows beispielsweise über die Tastenkombination [Win] + [L] erreichen. Aber auch mit C# lässt sich dies sehr einfach per LockWorkStation und DllImport bewerkstelligen:

public static class Workstation {
    [DllImport("user32", EntryPoint = "LockWorkStation")]
    public static extern bool Lock();
}

Die Verwendung gestaltet sich nun äußerst einfach und selbsterklärend:

Workstation.Lock();
]]>
https://www.coders-online.net/article/ArbeitsplatzSperren467debef-bf08-f274-f6eb-addcd9d25c1fSat, 22 Oct 2022 14:23:50 GMT
Byte-Angaben formatierenMit gewisser Regelmäßigkeit kommt man als Softwareentwickler in die Verlegenheit, Dateigrößen statt in der Form 10110 Byte in der "üblichen" Schreibweise mit größeren Einheiten (in diesem Fall 9.87 KiB) darzustellen.

Zwar gäbe es hier wieder einmal die Möglichkeit per DllImport und beispielsweise StrFormatByteSizeW zu arbeiten, jedoch möchten wir in diesem Fall die Implementierung selbst vornehmen, um die Basis (Stichwort: Binärpräfixe) konsistent bestimmen zu können. Ich stelle dazu zwei mögliche Implementierungen vor:

public static class Helper {
    private static readonly IReadOnlyList<string> unitList = [
        "Byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"
    ];


    public static string FormatBytes(double bytes) {
        int index = 0;

        while(bytes >= 1024 && index < Helper.unitList.Count) {
            bytes /= 1024;
            index++;
        }

        return $"{Math.Round(bytes, 2):N2} {Helper.unitList[index]}";
    }
}

Alternativ lässt sich das Problem jedoch auch ohne Schleife - und daher mathematisch etwas eleganter - durch ein ähnliches Vorgehen wie bei der Ermittlung der Anzahl der Ziffern einer Zahl mittels Logarithmen lösen:

public static string FormatBytes(double bytes) {
    int index = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
    bytes /= Math.Pow(1024, index);

    return $"{Math.Round(bytes, 2).ToString("N2")} {Helper.unitList[index]}";
}

Die Verwendung gestaltet sich nun in jedem Fall recht einfach:

string formattedBytes = Helper.FormatBytes(1024);         // 1,00 KiB
formattedBytes = Helper.FormatBytes(1024 * 1024);         // 1,00 MiB
formattedBytes = Helper.FormatBytes(1024 * 1024 * 1024);  // 1,00 GiB

Abschließend möchte ich auch noch kurz eine beispielhafte Implementierung zur Verwendung im Rahmen einer benutzerdefinierten Formatierung per String.Format skizzieren:

public class FileSizeFormatProvider : ICustomFormatter, IFormatProvider {
    public string Format(string format, object arg, IFormatProvider formatProvider) {
        return Helper.FormatBytes(Convert.ToDouble(arg));
    }

    public object GetFormat(Type formatType) {
        return this;
    }
}

Bitte beachtet jedoch, dass diese Implementierung noch nicht vollständig ist. Weiterführende Informationen zur korrekten Implementierung finden sich im MSDN per ICustomFormatter und IFormatProvider. Verwendung:

formattedBytes = String.Format(new FileSizeFormatProvider(), "{0}", (1024 * 1024));  // 1,00 MiB
]]>
https://www.coders-online.net/article/ByteAngabenFormatierena691e2f2-b6b8-b665-1db1-5b3e2ae48076Sat, 20 Jul 2024 18:24:12 GMT
CSV- zu XML-Dateien konvertierenEin bestehendes Projekt von mir beinhaltet eine erweiterbare Dateiverwaltung. Die Anforderung war es nun, CSV-Dateien zu XML-Dateien zu konvertieren. Die Ausgangsdateien haben folgenden einheitlichen Aufbau:

spalte1,spalte2,spalte3
inhaltA,inhaltB,inhaltC
inhaltD,inhaltE,inhaltF

Zwar ist dieser konkrete Kontext sicherlich etwas speziell, jedoch möchte ich die allgemein verwendbare Kernfunktionalität kurz vorstellen. Ihr könnt euch im Folgenden also auf die Methode ConvertFiles konzentrieren:

public static class CsvToXml {
    public static void ConvertDirectory(string path, SearchOption searchOption) {
        CsvToXml.ConvertFiles(Directory.EnumerateFiles(path, "*.csv", searchOption));
    }

    public static void ConvertFiles(params string[] pathList) {
        foreach(string path in pathList) {
            using(StreamReader fileStream = File.OpenText(path)) {
                string currentLine = fileStream.ReadLine();
                string[] fieldList = currentLine.Split(',');

                XElement root = new("entries");
                XDocument document = new(root);

                while((currentLine = fileStream.ReadLine()) != null) {
                    string[] dataList = currentLine.Split(',');

                    XElement entry = new("entry");
                    root.Add(entry);

                    for(int i = 0; i < dataList.Length; i++) {
                        entry.Add(new XElement(fieldList[i], dataList[i]));
                    }
                }

                document.Save(Path.Combine(
                    Path.GetDirectoryName(path),
                    String.Concat(Path.GetFileNameWithoutExtension(path), ".xml")
                ));
            }
        }
    }
}

Ich habe die beispielhafte Implementierung bereits etwas vereinfacht und wie üblich um Fehlerbehandlungen etc. reduziert, um den Fokus auf die eigentliche Funktionalität zu legen. Die Verwendung gestaltet sich nun sehr einfach:

CsvToXml.ConvertDirectory(@"X:\data", SearchOption.TopDirectoryOnly);

Die zu Beginn beschriebene Beispiel-Datei würde nun zu folgender Datei konvertiert:

<?xml version="1.0" encoding="utf-8"?>
<entries>
  <entry>
    <spalte1>inhaltA</spalte1>
    <spalte2>inhaltB</spalte2>
    <spalte3>inhaltC</spalte3>
  </entry>
  <entry>
    <spalte1>inhaltD</spalte1>
    <spalte2>inhaltE</spalte2>
    <spalte3>inhaltF</spalte3>
  </entry>
</entries>

Natürlich kann diese grundlegende Funktionalität beliebig erweitert werden, beispielsweise für weitere Dateiendungen oder um die Bezeichner der äußeren Elemente (entries und entry) individuell angeben zu können. Je nach gewünschtem Einsatzzweck kann insbesondere auch die SearchOption-Aufzählung interessant sein.

]]>
https://www.coders-online.net/article/CSVZuXMLDateienKonvertieren9d5e10f1-01b8-f2bb-01e1-2b1f44903c62Sat, 20 Jul 2024 18:25:20 GMT
Datei in Papierkorb verschiebenLeider bietet das .NET-Framework mit C# standardmäßig keine in verwaltetem Code implementierte Möglichkeit, Dateien in den Papierkorb zu verschieben statt direkt zu löschen (mit VB.NET interessanterweise jedoch schon).

Es ist möglich, die entsprechende Funktionalität aus VB.NET auch in C# zu verwenden - dazu muss lediglich ein Verweis auf die Assembly Microsoft.VisualBasic ergänzt werden. Anschließend funktioniert folgender Aufruf:

FileSystem.DeleteFile("Datei.ext", UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin)

Alternativ lässt sich eine Erweiterungsmethode für FileInfo schreiben, welche mittels DllImport direkt auf die entsprechende Funktionalität der Windows-API zurückgreift. Dazu implementieren wir folgende Klasse:

public static class RecycleBinExtension {
    public static void SendToRecycleBin(this FileInfo fileInfo) {
        SHFILEOPSTRUCT lpFileOp = new SHFILEOPSTRUCT {
            wFunc  = 0x03,  // FO_DELETE
            pFrom  = fileInfo.FullName + "\0\0",
            fFlags = 0x40   // FOF_ALLOWUNDO
        };

        SHFileOperation(ref lpFileOp);
    }


    [DllImport("shell32")]
    private static extern int SHFileOperation(ref SHFILEOPSTRUCT lpFileOp);

    [StructLayout(LayoutKind.Sequential)]
    private struct SHFILEOPSTRUCT {
        public IntPtr hwnd;
        public uint wFunc;
        public string pFrom;
        public string pTo;
        public ushort fFlags;
        public bool fAnyOperationsAborted;
        public IntPtr hNameMappings;
        public string lpszProgressTitle;
    }
}

Zusätzliche Funktionalität habe ich absichtlich außer Acht gelassen. Die Verwendung gestaltet sich nun sehr intuitiv:

FileInfo fileInfo = new FileInfo("Datei.ext");
fileInfo.SendToRecycleBin();

Im MSDN finden sich weiterführende Informationen zu SHFileOperation und SHFILEOPSTRUCT.

]]>
https://www.coders-online.net/article/DateiInPapierkorbVerschieben608fd2cb-1eb2-09f8-a2c1-e977612f38bcThu, 20 Oct 2022 19:52:06 GMT
DNS-Auflösung durchführenDas Domain Name System ist einer der wichtigsten Dienste in vielen IP-basierten Netzwerken, so insbesondere auch im Internet. Die Hauptaufgabe stellt dabei das Übersetzen von für die menschliche Nutzung praktischen Domains in technisch verarbeitbare IP-Adressen dar. Mit diesem Beitrag möchte ich jedoch gar nicht weiter auf diese Details eingehen, dazu lohnt sich stattdessen beispielsweise ein (empfehlenswerter) Blick in die Wikipedia.

Unser Hauptanliegen soll es nun sein, dieses System per C# zu befragen, um darauf aufbauend in einem separaten Artikel eine konkrete Anwendung umzusetzen. Praktischerweise liefert das .NET-Framework mit System.Net.Dns eine Klasse mit entsprechender Funktionalität - zugegebenermaßen aber nur mit geringem Funktionsumfang, weshalb zur professionellen Arbeit mit den verschiedenen Resource Records das Einbinden externer Programmbibliotheken sinnvoll sein kann. Die Anwendung der Basisfunktionalität ist jedoch denkbar einfach:

IPAddress[] hostAddressList = Dns.GetHostAddresses("coders-online.net");

foreach(IPAddress hostAddress in hostAddressList) {
    /**
     * hostAddress enthält nun jeweils eine IP-Adresse zur Domain;
     * im Falle dieses Beispiels den einzigen Eintrag 217.160.176.125.
    **/
}

Eine praktische Hilfsmethode ist außerdem Dns.GetHostName(), die den Namen des lokalen Rechners liefert. Das MSDN liefert wie üblich weitere Informationen zur Klasse, u. a. mit Hinweisen zu den asynchronen Methoden. Weiterhin interessant im System.Net-Namespace ist natürlich auch die Klasse IPAddress.

]]>
https://www.coders-online.net/article/DNSAufloesungDurchfuehren4f6f0f62-b048-1022-77e5-408e23c1985fSun, 23 Oct 2022 02:32:33 GMT
DNS-Cache leerenUm den DNS-Zwischenspeicher unter Windows zu leeren, gibt es den bekannten Befehl ipconfig /flushdns.

Prinzipiell wäre es zwar möglich, das Programm ipconfig mit dem Argument /flushdns über die Process-Klasse per C# aufzurufen, jedoch ist dies nicht ganz unproblematisch, da die Ausführung längere Zeit dauern kann.

Alternativ hilft uns hier - wieder einmal - die Windows-API in Kombination mit dem DllImport-Attribut:

public static class DnsUtility {
    [DllImport("dnsapi", EntryPoint = "DnsFlushResolverCache")]
    public static extern void FlushCache();
}

  // DNS-Cache leeren
DnsUtility.FlushCache();

Dies ist insbesondere in Verbindung mit der Dns-Klasse nützlich, um aktuelle Daten zu erhalten. Natürlich gibt es einen solchen Zwischenspeicher aber nicht grundlos, d. h. es ist nur äußerst selten ratsam, diesen immer wieder vor einer Namensauflösung zu leeren - Performanzdifferenzen im Rahmen einer Größenordnung sind hier üblich. Im Rahmen des Debuggings oder für besonders kritische Prozesse kann das Vorgehen jedoch durchaus sinnvoll sein.

]]>
https://www.coders-online.net/article/DNSCacheLeeren21350c96-d8c8-7fb6-6dfe-3fe2d7669c7aSat, 22 Oct 2022 16:00:01 GMT
DNS-Sperrlisten abfragenIn diesem Artikel soll eine (sehr einfache) Möglichkeit der Abfrage von DNS-Blacklisten vorgestellt werden, die auch von vielen eMail-Systemen genutzt wird, um potentiellen Spam zu erkennen (und ggf. abzuweisen).

Zu den bekannten Anbietern entsprechender Dienste gehören beispielsweise:

Diese setze ich auch selbst ein und habe jeweils gute Erfahrungen damit gemacht. Bitte informiert euch jedoch vor einem Einsatz beim jeweiligen Anbieter über die genauen Konditionen, der kommerzielle Einsatz ist beispielsweise oftmals kostenpflichtig. Ebenso verhält es sich je nach Anbieter ab einem gewissen Abfragevolumen.

Um nun eine IP-Adresse gegen die entsprechenden Dienste prüfen zu können, müssen wir diese dem oben angegebenen Hostnamen in umgekehrter Reihenfolge voranstellen und eine Namensauflösung durchführen.

Konkret fragen wir also beispielsweise bei einer Prüfung der IP-Adresse 198.51.100.51 auf Spam-Aktivität laut Spamhaus das Ergebnis der Domain 51.100.51.198.zen.spamhaus.org ab.

Per C# könnte man dies nun beispielsweise wie folgt durchführen:

public static bool CheckBlacklisting(IPAddress ipAddress, string blacklist = "zen.spamhaus.org") {
    string ipAddressReversed = String.Join(".", ipAddress.GetAddressBytes().Reverse());
    string hostName = String.Concat(ipAddressReversed, ".", blacklist);

    try {
        foreach(IPAddress hostAddress in Dns.GetHostAddresses(hostName)) {
            /**
             * Achtung: Diese Implementierung ist naiv und sollte nicht direkt
             * übernommen werden. Je nach Anbieter, so beispielsweise auch bei
             * Spamhaus, gilt nicht der gesamte Loopback-Adressbereich als Spam.
             * Daher bitte vorher beim Anbieter informieren und konkretisieren!
            **/
            if(IPAddress.IsLoopback(hostAddress)) {
                return true;
            }
        }

        return false;
    } catch {
        return false;
    }
}

bool checkResult = CheckBlacklisting(IPAddress.Parse("198.51.100.51"));

Das Ergebnis der Namensauflösung, also die erhaltene IP-Adresse, gibt darüber hinaus tlw. Aufschluss über den genaueren Status. Spamhaus liefert dazu beispielsweise in ihren FAQ weiterführende Informationen.

]]>
https://www.coders-online.net/article/DNSSperrlistenAbfragen7ee85cdf-f784-e119-55a3-c5d0c0a372d3Sun, 09 Feb 2025 11:46:05 GMT
eMail versendenGerne möchte ich hier einmal festhalten, wie man eine einfache eMail mit C# versenden kann:

// SMTP-Client zum Mailserver
SmtpClient client = new("mail.domain.tld") {
    // Authentifizierungsdaten setzen
    Credentials = new NetworkCredential() {
        UserName = "Benutzername",
        Password = "Passwort"
    }
};

// Nachricht erstellen
MailMessage message = new() {
    From = new MailAddress("absender@domain.tld", "Absender"),
    Subject = "Test",
    Body = "Text"
};

message.To.Add(new MailAddress("empfänger@domain.tld", "Empfänger"));

// Nachricht versenden
client.Send(message);

Wer keine weiteren Ansprüche wie HTML-Inhalt, Datei-Anhänge etc. hat, kann das eigentliche Senden durchaus auch mit einem Einzeiler ohne das Erzeugen einer MailMessage bewerkstelligen:

// Nachricht versenden
client.Send("absender@domain.tld", "empfänger@domain.tld", "Test", "Text");

Für weitere Informationen zum Thema sollte man sich hauptsächlich den Namensraum System.Net.Mail im MSDN näher ansehen. An dieser Stelle auch noch einmal der Hinweis darauf, Ressourcen wieder freizugeben, worauf ich bei meinen Beispielen für gewöhnlich verzichte, um den Fokus auf das konkrete Beispiel zu legen.

]]>
https://www.coders-online.net/article/eMailVersenden55038f23-150c-cc5b-3b05-1093deb3fc95Sat, 20 Jul 2024 18:26:07 GMT
GET- und POST-Anfragen sendenFür viele APIs - aber auch so manchen Test - ist es hilfreich, GET- und POST-Anfragen per HTTP senden zu können. Selbstverständlich ist das auch per C# sehr einfach möglich, wobei je nach gewünschtem Grad der Kontrollierbarkeit der Anfragen verschiedene Abstrahierungen zur Verfügung gestellt werden:

Die umfangreichsten Möglichkeiten (und geringen Abstraktiongrad) bietet HttpWebRequest:

// GET per HttpWebRequest
HttpWebRequest webRequest = HttpWebRequest.CreateHttp(".../?key1=val1&key2=val2");

using(HttpWebResponse webResponse = (HttpWebResponse) webRequest.GetResponse()) {
    using(StreamReader streamReader = new(webResponse.GetResponseStream())) {
        string response = streamReader.ReadToEnd();
    }
}

// POST per HttpWebRequest
HttpWebRequest webRequest = HttpWebRequest.CreateHttp("...");

byte[] data = Encoding.UTF8.GetBytes("key1=val1&key2=val2");

webRequest.Method = "POST";
webRequest.ContentType = "application/x-www-form-urlencoded";
webRequest.ContentLength = data.Length;

using(Stream stream = webRequest.GetRequestStream()) {
    stream.Write(data, 0, data.Length);
}

using(HttpWebResponse webResponse = (HttpWebResponse) webRequest.GetResponse()) {
    using(StreamReader streamReader = new(webResponse.GetResponseStream())) {
        string response = streamReader.ReadToEnd();
    }
}

Deutlich kompakter lassen sich entsprechende Anfragen per WebClient formulieren:

// GET per WebClient
using(WebClient webClient = new()) {
    string response = webClient.DownloadString(".../?key1=val1&key2=val2");
}

// POST per WebClient
using(WebClient webClient = new()) {
    string response = Encoding.UTF8.GetString(webClient.UploadValues("...", new NameValueCollection() {
        {"key1", "val1"},
        {"key2", "val2"}
    }));
}

Die aktuellste Möglichkeit mit Fokus auf asynchrone Vorgänge stellt HttpClient dar:

// GET per HttpClient
using(HttpClient client = new()) {
    string response = await client.GetStringAsync(".../?key1=val1&key2=val2");
}

// POST per HttpClient
using(HttpClient httpClient = new()) {
    HttpResponseMessage responseMessage = await httpClient.PostAsync("...",
        new FormUrlEncodedContent(new Dictionary<string, string>() {
            {"key1", "val1"},
            {"key2", "val2"}
        })
    );

    string response = await responseMessage.Content.ReadAsStringAsync();
}

Grundsätzlich bestünde auch noch die Möglichkeit, auf TCP-Ebene unter Anwendung des HTTP-Protokolls vorzugehen. Abschließend sei noch erwähnt, dass diese Möglichkeiten nicht für fragliche Aktionen genutzt werden sollte - dazu gehört im Übrigen auch schlicht unnötiger Traffic.

]]>
https://www.coders-online.net/article/GETUndPOSTAnfragenSendenca88e23b-191a-caa9-7b83-c2818ec0fbcaSat, 20 Jul 2024 18:27:36 GMT
Laufwerksschacht öffnen und schließenIn ein paar anderen Artikeln haben wir bereits externe Funktionalität per DllImport verwendet, was wir nun nochmals benötigen werden. Diesmal soll es darum gehen, den Laufwerksschacht zu öffnen und zu schließen.

Wir implementieren dazu folgende (sehr einfache) Klasse:

public static class DiskTray {
    public static void Close() {
        mciSendString("set CDAudio door closed", null, 0, IntPtr.Zero);
    }

    public static void Open() {
        mciSendString("set CDAudio door open", null, 0, IntPtr.Zero);
    }


    [DllImport("winmm")]
    private static extern int mciSendString(
        string lpstrCommand,
        string lpstrReturnString,
        int uReturnLength,
        IntPtr hwndCallback
    );
}

Die Verwendung - beispielsweise im Rahmen einer Fernwartung - gestaltet sich nun äußerst einfach:

DiskTray.Open();   // öffnen
DiskTray.Close();  // schließen

Für interessierte Leser finden sich im MSDN weiterführende Informationen zu mciSendString.

]]>
https://www.coders-online.net/article/LaufwerksschachtOeffnenUndSchliessen4fea7192-5a0c-a36d-104f-fbc75a8ad63bSat, 22 Oct 2022 14:34:22 GMT
Papierkorb leerenUm den Papierkorb zu leeren gibt es leider keine in verwaltetem Code zur Verfügung stehende Funktionalität des .NET-Frameworks für C#. Es ist also - wieder einmal - an der Zeit für DllImport.

Wir werden dazu eine Klasse RecycleBin mit integrierten RecycleFlags erstellen:

public static class RecycleBin {
    public static void Clear(RecycleFlags recycleFlags = RecycleFlags.None) {
        SHEmptyRecycleBin(IntPtr.Zero, null, recycleFlags);
    }


    [DllImport("shell32")]
    private static extern uint SHEmptyRecycleBin(
        IntPtr hwnd, string pszRootPath, RecycleFlags dwFlags
    );

    public enum RecycleFlags : uint {
        None           = 0x0,  // Standardverhalten
        NoConfirmation = 0x1,  // keine Rückfrage zur Bestätigung
        NoProgressUI   = 0x2,  // keine Fortschrittsanzeige
        NoSound        = 0x4   // kein Abschlussgeräusch
    }
}

Die Verwendung gestaltet sich nun wieder gewohnt einfach:

  // Papierkorb leeren (Standard)
RecycleBin.Clear();

  // Papierkorb ohne Rückfrage leeren
RecycleBin.Clear(RecycleBin.RecycleFlags.NoConfirmation);

Im MSDN gibt es wie üblich weitere Details zu SHEmptyRecycleBin.

]]>
https://www.coders-online.net/article/PapierkorbLeerena63dadc6-c62f-9547-824f-b50a112727a1Thu, 20 Oct 2022 19:52:06 GMT
Ressourcen freigebenDank verwaltetem Code und automatischer Speicherbereinigung muss man sich bei C# zumeist nicht explizit darum kümmern, angeforderte Ressourcen wieder freizugeben. An manchen Stellen kann dies jedoch notwendig sein: Dies ist beispielsweise der Fall, wenn nicht verwaltete oder externe Ressourcen (z. B. Dateien, Datenbanken) involviert sind.

Zur Freigabe von Ressourcen sollte die entsprechende Dispose()-Methode aufgerufen werden, welche Objekte zur Verfügung stellen, welche IDisposable implementieren. Dies kann beispielsweise wie folgt passieren:

Class obj = new Class();

try {
    /**
     * Anwendungs-Code
    **/
} finally {
    if(obj != null) {
        obj.Dispose();
    }
}

Es gibt jedoch noch eine elegantere Variante, und zwar mit Hilfe der using-Anweisung:

using(Class obj = new Class()) {
    /**
     * Anwendungs-Code
    **/
}

Durch dieses Vorgehen werden die Ressourcen automatisch nach Verlassen des Gültigkeitsbereichs freigegeben; dazu wird im Hintergrund (in etwa) der oben genannte "manuelle" Code generiert. Zu beachten gilt, dass der using-Block - wie bereits durch die umschließenden geschweiften Klammern zu sehen - einen eigenen Gültigkeitsbereich darstellt, was demnach bei Variablendeklaration und -verwendung berücksichtigt werden muss.

]]>
https://www.coders-online.net/article/RessourcenFreigebenc0c484f3-0529-2b85-458d-bfdb5ea6542bSun, 23 Oct 2022 02:45:42 GMT
Syslog-Nachrichten versendenSyslog beschreibt einen Protokollierungsstandard. Dieser Beitrag soll die beispielhafte Implementierung eines C#-Clients aufzeigen. Dazu werden UDP-basierte Nachrichten im (älteren) BSD-Format nach RFC 3164 erzeugt.

Sehr viele NAS können übrigens als Empfänger entsprechender Nachrichten dienen; im Folgenden die entsprechende Klasse:

public class Syslog : IDisposable {
    private const int Facility = 1;  // user-level messages
    private const int FacilityFactor = 8;

    private readonly UdpClient udpClient;


    public Syslog(string hostname = "localhost", int port = 514) {
        this.udpClient = new UdpClient(hostname, port);
    }

    public void Send(Severity severity, string message, [CallerMemberName] string caller = null) {
        byte[] data = Encoding.UTF8.GetBytes(String.Format("<{0}>{1} {2} {3}",
            (Syslog.Facility * Syslog.FacilityFactor) + (int) severity,
            DateTime.Now.ToString("MMM dd HH:mm:ss"),
            Dns.GetHostName(),
            ((!String.IsNullOrWhiteSpace(caller)) ? (caller + " ") : String.Empty) + message
        ));

        this.udpClient.Send(data, data.Length);
    }

    public void Dispose() => this.udpClient?.Dispose();


    public enum Severity {
        Emergency,     // [0] system is unusable
        Alert,         // [1] action must be taken immediately
        Critical,      // [2] critical conditions
        Error,         // [3] error conditions
        Warning,       // [4] warning conditions
        Notice,        // [5] normal but significant condition
        Informational, // [6] informational messages
        Debug,         // [7] debug-level messages
    }  // https://tools.ietf.org/html/rfc3164
}

Es handelt sich hierbei - wie üblich - um eine minimale Implementierung ohne Fehlerbehandlung, welche außerdem keine Konfiguration des Facility-Wertes ermöglicht. Die Verwendung gestaltet sich nun jedoch sehr einfach:

using(Syslog syslog = new Syslog("syslog.example.net")) {
    syslog.Send(Severity.Notice, "Dies ist eine weniger interessante Meldung.");
    syslog.Send(Severity.Warning, "Dies ist eine sehr interessante Meldung.", "MyApp");
}
]]>
https://www.coders-online.net/article/SyslogNachrichtenVersendenb657ab32-a3cc-a8f6-3ddb-531d02b41161Sat, 20 Jul 2024 18:28:35 GMT
Temporäre Datei erstellen und implizit löschenHin und wieder kann es - beispielsweise bei der Arbeit mit Datenströmen - sinnvoll bzw. notwendig sein, mit Dateien zu arbeiten, die nur vorübergehend benötigt werden. Das .NET-Framework unterstützt dies mit der Methode Path.GetTempFileName(), ohne jedoch die Datei implizit wieder zu entfernen.

Um diesen kleinen Makel zu beheben, bietet sich eine kompakte Implementierung wie die folgende Lösung an, welche auf using basiert:

public class TemporaryFile : IDisposable {
    public string Name {
        get;
        private set;
    }

    public TemporaryFile() {
        this.Name = Path.GetRandomFileName();
    }

    public void Dispose() => File.Delete(this.Name);
}

Die Verwendung gestaltet sich nun sehr komfortabel, wobei die Datei innerhalb der entsprechenden Anweisung existiert und wie üblich verwendet werden kann; anschließend wird diese automatisch wieder gelöscht:

/* Datei existiert noch nicht */

using(TemporaryFile temporaryFile = new TemporaryFile()) {
    /* Datei existiert ab sofort */

    File.WriteAllBytes(temporaryFile.Name, new byte[] {
        0x01, 0x03, 0x03, 0x07
    });

    /* Datei existiert weiterhin */
}

/* Datei existiert nicht mehr */
]]>
https://www.coders-online.net/article/TemporaereDateiErstellenUndImplizitLoeschenaa354468-d856-dec7-0b53-38ac1c41ddddSat, 20 Jul 2024 18:29:24 GMT
Traceroute durchführenDieser Beitrag soll ein einfaches Tracerouting per C# nachbilden. Dazu implementieren wir im Folgenden eine einfache (und wie üblich auf das Wesentliche reduzierte) Klasse, welche die Funktionalität durch sukzessive Erhöhung des TTL-Wertes für schlichte Ping-Pakete umsetzt:

public class Traceroute : IDisposable {
    private static readonly byte[] buffer = [
        0x01, 0x03, 0x03, 0x07
    ];  // Daten zur Übertragung

    private readonly Ping ping = new();

    public IEnumerable<PingReply> Send(string hostNameOrAddress, int timeout = 3000) {
        // Paket mit TTL=1 initialisieren
        PingOptions pingOptions = new(1, false);

        PingReply pingReply = null;

        do {
            pingReply = this.ping.Send(hostNameOrAddress, timeout, Traceroute.buffer, pingOptions);

            // TTL für nächsten Schritt inkrementieren
            pingOptions.Ttl++;

            yield return pingReply;
        } while(pingReply.Status != IPStatus.Success);
    }

    public void Dispose() => this.ping.Dispose();
}

Die Verwendung gestaltet sich nun sehr einfach:

using(Traceroute traceroute = new Traceroute()) {
    foreach(PingReply pingReply in traceroute.Send("coders-online.net")) {
        Console.WriteLine(pingReply.Address);
    }
}

Um die entsprechenden Hostnamenzu den jeweiligen IP-Adressen zu ermitteln, kann beispielsweise die Methode Dns.GetHostEntry(...) dienen:

using(Traceroute traceroute = new Traceroute()) {
    foreach(string hostNameOrAddress in traceroute.Send("coders-online.net").Select(pR => {
        try {
            return Dns.GetHostEntry(pR.Address).HostName;
        } catch {
            return pR.Address.ToString();
        }
    })) {
        Console.WriteLine(hostNameOrAddress);
    }
}
]]>
https://www.coders-online.net/article/TracerouteDurchfuehren02ee5962-f036-c640-2253-d5c3f6be392cSat, 20 Jul 2024 18:30:48 GMT
Wake on LAN durchführenWake on LAN ist ein Standard zum Starten eines deaktivierten Rechners über eine kompatible Netzwerkkarte. Dazu wird lediglich ein bestimmtes Paket im Netzwerk gesendet; mit C# lässt sich das beispielsweise so lösen:

public static class WakeOnLan {
    public static void SendMagicPacket(byte[] macAddress) {
        using(UdpClient udpClient = new()) {
            udpClient.Connect(IPAddress.Broadcast, 9);

            byte[] magicPacket = new byte[(6 + (16 * 6))];

            for(int i = 0; i < 6; i++) {
                magicPacket[i] = 0xFF;
            }

            for(int i = 1; i <= 16; i++) {
                for(int j = 0; j < 6; j++) {
                    magicPacket[(i * 6) + j] = macAddress[j];
                }
            }

            udpClient.Send(magicPacket, magicPacket.Length);
        }
    }

    public static void SendMagicPacket(string macAddress) {
        SendMagicPacket(macAddress
            .Split(':', '-')
            .Select(m => Byte.Parse(m, NumberStyles.AllowHexSpecifier))
            .ToArray()
        );
    }
}

Das Paket enthält sechs mal den Wert FF16, gefolgt von der 16-maligen Wiederholung der MAC-Adresse und wird per Broadcast im Netzwerk versendet. Durch die Methodenüberladung wird die Übergabe der MAC-Adresse sowohl per Zeichenkette als auch per Byte-Feld ermöglicht:

WakeOnLan.SendMagicPacket("11:22:33:44:55:66");

  /* - oder - */
  
WakeOnLan.SendMagicPacket(new byte[] {
    0x11, 0x22, 0x33, 0x44, 0x55, 0x66
});
]]>
https://www.coders-online.net/article/WakeOnLANDurchfuehren4e90ebce-f1fa-0299-15e5-e41032bada0cSat, 20 Jul 2024 18:31:36 GMT
Windows Defender ansprechenWindows stellt unter der Bezeichnung Antimalware Scan Interface native Funktionalität zur Interaktion mit Windows Defender bereit. Wir werden für C# also - wieder einmal - auf DllImport setzen:

public static class AmsiWrapper {
    public static Result Scan(string content, string name = null) {
        AmsiInitialize(nameof(AmsiWrapper), out IntPtr context);
        AmsiScanString(context, content, name, IntPtr.Zero, out Result result);
        AmsiUninitialize(context);

        return result;
    }

    public enum Result {
        Clean = 0,
        NotDetected = 1,
        Detected = 32768
    }


    [DllImport("Amsi")]
    private static extern int AmsiInitialize(string appName, out IntPtr amsiContext);

    [DllImport("Amsi")]
    private static extern int AmsiScanString(
        IntPtr amsiContext,
        string @string,
        string contentName,
        IntPtr session,
        out Result result
    );

    [DllImport("Amsi")]
    private static extern void AmsiUninitialize(IntPtr amsiContext);
}

Die Verwendung der empfohlenen Funktion AmsiResultIsMalware ist kurioserweise leider nicht möglich, da diese schlicht nicht in Amsi.dll gefunden werden kann. Es gilt also die Hinweise von AMSI_RESULT zu beachten.

Die Überprüfung gestaltet sich nun jedoch grundsätzlich äußerst einfach durch Übergabe des fraglichen Inhalts:

if(AmsiWrapper.Scan("sehrGefährlicherInhalt") == AmsiWrapper.Result.Detected) {
    /* Inhalt als Bedrohung erkannt */
} else {
    /* Inhalt nicht als Bedrohung erkannt */
}

Zum Test der Erkennung potentiell bedrohlicher Inhalte kann beispielsweise die EICAR-Testdatei dienen.

Vor einem tatsächlichen Einsatz sollten mindestens noch Überprüfungen der Rückgabewerte (→ HRESULT) mit entsprechender (Fehler-)Behandlung ergänzt werden.

]]>
https://www.coders-online.net/article/WindowsDefenderAnsprechen8e6a426f-8b64-70a4-cf09-f51ac10ae7ceFri, 21 Oct 2022 20:08:44 GMT
WPF-Fenster ohne Titelleiste verschiebenEin WPF-Fenster, dessen Eigenschaft WindowStyle der Wert None zugewiesen ist, besitzt keine standardmäßige Titelleiste mehr - was ja letztlich auch Sinn der Sache ist.

Eine häufig gewünschte und auch meist sehr sinnvolle Funktionalität ist jedoch, dass der Benutzer das Fenster dennoch verschieben können soll. Dazu ist einfach eine Ereignisbehandlung für MouseLeftButtonDown auf einem Steuerelement des Fensters (oder im Zweifelsfall gar auf dem Fenster selbst) und rufen die DragMove-Methode des Fensters auf:

private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
    this.DragMove();
}

Schon lässt sich das Fenster wie gewohnt bei gedrückter linker Maustaste verschieben.

]]>
https://www.coders-online.net/article/WPFFensterOhneTitelleisteVerschieben9e877c29-7490-8200-011a-49f8d8c0ce17Sun, 23 Oct 2022 02:50:26 GMT
Zeitserver abfragenIm Folgenden möchte ich drei Möglichkeiten vorstellen, wie man mit C# über ein Netzwerk eine Zeitangabe abrufen kann. Es gilt zu beachten, dass es sich um sehr reduzierte Beispiele handelt und beispielsweise keine umfassende Fehlerbehandlung berücksichtigt wird. Wir verwenden standardmäßig jeweils einen Server des NIST.

Beim Time Protocol handelt es sich um ein sehr einfaches (und etwas betagtes) Protokoll. Der Dienst steht für gewöhnlich unter Port 37 zur Verfügung. Details zum Protokoll können in RFC 868 nachgelesen werden:

public static DateTime GetTime(string host = "time.nist.gov", int port = 37) {
    using(TcpClient client = new(host, port)) {
        using(NetworkStream networkStream = client.GetStream()) {
            // Antwort lesen
            byte[] timeBuffer = new byte[4];
            networkStream.Read(timeBuffer, 0, timeBuffer.Length);

            // Byte-Reihenfolge ggf. umkehren
            if(BitConverter.IsLittleEndian) {
                Array.Reverse(timeBuffer);
            }

            return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc)
                .AddSeconds(BitConverter.ToUInt32(timeBuffer, 0))
                .ToLocalTime();
        }
    }
}

Beim Daytime Protocol verhält es sich ähnlich, ist jedoch textbasiert. Der Dienst steht für gewöhnlich unter Port 13 zur Verfügung. Details zum Rückgabeformat können in RFC 867 und der Dienstbeschreibung des NIST nachgelesen werden - wir werden uns auf den minimalen Part (Datum und Uhrzeit) beschränken.

public static DateTime GetDaytime(string host = "time.nist.gov", int port = 13) {
    using(TcpClient client = new(host, port)) {
        using(StreamReader streamReader = new(client.GetStream())) {
            // Antwort lesen und auftrennen
            string[] response = streamReader.ReadToEnd().Split(' ');

            // Datum und Uhrzeit ermitteln
            int[] date = Array.ConvertAll(response[1].Split('-'), Int32.Parse);
            int[] time = Array.ConvertAll(response[2].Split(':'), Int32.Parse);

            return new DateTime(
                CultureInfo.CurrentCulture.Calendar.ToFourDigitYear(date[0]),
                date[1], date[2], time[0], time[1], time[2],
                DateTimeKind.Utc
            ).ToLocalTime();
        }
    }
}

Beim Network Time Protocol handelt es sich um einen weit verbreiteten Standard zur Zeitsynchronisierung in Computernetzwerken über Port 123. Details zum Protokoll können in RFC 5905 nachgelesen werden - wir werden uns wieder auf einen minimalen Teil beschränken. Als Server bietet sich hier natürlich auch die PTB an.

public static DateTime GetNetworkTime(string host = "time.nist.gov", int port = 123) {
    using(UdpClient client = new()) {
        client.Connect(host, port);

        // Anfrage senden
        const byte header = (
              (0 << 6)  // LI   = 0 (keine Warnung; zur Verdeutlichung ausformuliert)
            | (4 << 3)  // VN   = 4 (Version)
            | (3 << 0)  // Mode = 3 (Client)
        );  // 00_100_011

        byte[] request = new byte[48];
        request[0] = header;

        client.Send(request, request.Length);

        // Antwort lesen
        IPEndPoint endPoint = new(IPAddress.Any, 0);
        byte[] response = client.Receive(ref endPoint);

        // Daten ab Offset aus ggf. umgekehrter Byte-Reihenfolge ermitteln
        const int offset = 40;
        const int size = sizeof(uint);

        if(BitConverter.IsLittleEndian) {
            Array.Reverse(response, offset, size);
            Array.Reverse(response, offset + size, size);
        }

        ulong integerPart = BitConverter.ToUInt32(response, offset);
        ulong fractionalPart = BitConverter.ToUInt32(response, offset + size);

        return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc)
            .AddMilliseconds((integerPart * 1000) + (fractionalPart * 1000 / UInt32.MaxValue))
            .ToLocalTime();
    }
}

Insbesondere bei diesem letzten Beispiel möchte ich noch einmal explizit darauf hinweisen, dass es sich um eine sehr reduzierte Implementierung handelt. Die Rückgabe erfolgt bei allen Beispielen jeweils bereits in lokaler Zeit.

]]>
https://www.coders-online.net/article/ZeitserverAbfragen60ea2389-d04e-bd0d-ed42-8dce4eea02c5Sat, 20 Jul 2024 18:32:58 GMT
Zertifikat einer Adresse ermittelnManchmal möchte man gerne Zertifikate von Webseiten automatisiert abrufen. Im Rahmen eines Monitorings bietet es sich beispielsweise an, die Gültigkeitsdauer zu überwachen. Dazu eine beispielhafte Implementierung:

public static X509Certificate2 GetCertificate(string hostName, int port = 443) {
    using(TcpClient client = new(hostName, port)) {
        using(SslStream stream = new(client.GetStream())) {
            stream.AuthenticateAsClient(hostName);

            return new X509Certificate2(stream.RemoteCertificate);
        }
    }
}

Die Implementierung ist dabei - wie üblich - auf das Wesentliche beschränkt; es findet keine umfassende Problembehandlung statt. Sofern die Methode in größerem Kontext zum Einsatz kommen soll, empfiehlt sich auf jeden Fall ein Blick auf die beteiligten Klassen TcpClient, SslStream und X509Certificate2 im MSDN.

Nun stehen uns diverse Informationen zur Verfügung; einige Beispiele zur Ausgabe auf der Konsole:

X509Certificate2 certificate = GetCertificate("coders-online.net");

Console.WriteLine(certificate.Subject);    // "CN=coders-online.net"

Console.WriteLine(certificate.NotBefore);  // 30.04.2017 08:01:00
Console.WriteLine(certificate.NotAfter);   // 29.07.2017 08:01:00

Im Besonderen sei abschließend noch auf RemoteCertificateValidationCallback hingewiesen, mit welchem sich u. a. das Verhalten bei Zertifizierungsfehlern beeinflussen lässt.

]]>
https://www.coders-online.net/article/ZertifikatEinerAdresseErmitteln056dbc3f-6810-ff13-fb37-cda3f847d8dcSat, 20 Jul 2024 18:33:43 GMT
ZIP-Archive (ent-)packenZIP-Archive sind praktisch, um Dateien und Verzeichnisstrukturen komprimiert und zusammenhängend speichern und austauschen zu können und werden außerdem auf praktisch jeder Plattform unterstützt. Daher möchte ich mit diesem Beitrag zeigen, wie sich mit C# entsprechende Archive automatisch erstellen lassen.

Das .NET-Framework liefert im Namensraum System.IO.Compression grundlegende Funktionalität zur Arbeit mit entsprechenden Archiven, auf die wir in diesem Beitrag setzen werden.

Zuerst möchte ich ein paar Möglichkeiten aufzeigen, um ZIP-Archive zu erstellen:

// Archiv aus bestehendem Verzeichnis erstellen
ZipFile.CreateFromDirectory("data", "data.zip");

// Archiv aus bestehenden Dateien erstellen
using(FileStream fileStream = File.OpenWrite("data.zip")) {
    using(ZipArchive zipArchive = new(fileStream, ZipArchiveMode.Create)) {
        zipArchive.CreateEntryFromFile("data/a.txt", "a.txt");
        zipArchive.CreateEntryFromFile("data/b.txt", "b.txt");
    }
}

// Archiv manuell zusammenstellen
using(FileStream fileStream = File.OpenWrite("data.zip")) {
    using(ZipArchive zipArchive = new(fileStream, ZipArchiveMode.Create)) {
        ZipArchiveEntry zipArchiveEntry = zipArchive.CreateEntry("README");

        using(StreamWriter streamWriter = new(zipArchiveEntry.Open())) {
            streamWriter.Write("Hallo Welt!");
        }
    }
}

Abschließend möchten wir auf bestehende Archive zugreifen:

// Archiv vollständig entpacken
ZipFile.ExtractToDirectory("data.zip", "data");

// Archiv einlesen und verarbeiten
using(FileStream fileStream = File.OpenRead("data.zip")) {
    using(ZipArchive zipArchive = new(fileStream, ZipArchiveMode.Read)) {
        foreach(ZipArchiveEntry zipArchiveEntry in zipArchive.Entries) {
              // "README" separat berücksichtigen
            if(zipArchiveEntry.Name == "README") {
                using(StreamReader streamReader = new(zipArchiveEntry.Open())) {
                      // Inhalt einlesen und auf Konsole ausgeben
                    Console.WriteLine(streamReader.ReadToEnd());
                }
            } else {
                  // Eintrag als Datei speichern
                zipArchiveEntry.ExtractToFile(zipArchiveEntry.Name);
            }
        }
    }
}

Durch die Verwendung von Datenströmen besteht grundsätzlich die Möglichkeit, nahezu beliebige Quellen (und Ziele) für Archiv-Einträge heranzuziehen.

]]>
https://www.coders-online.net/article/ZIPArchiveEntPacken5d0b2770-cb57-e59c-2f1d-5f7d8c1d7304Sat, 20 Jul 2024 18:34:34 GMT
Zwischenablage verwendenDa sich früher oder später wohl jeder Entwickler damit konfrontiert sieht, mit der Zwischenablage interagieren zu müssen, möchte ich hier prägnant zeigen, wie sich Text in der Zwischenablage speichern und wieder auslesen lässt:

// Text in Zwischenablage speichern
Clipboard.SetText("Test-Text");

// Text aus Zwischenablage auslesen
string content = Clipboard.GetText();

Die weiteren Methoden (z. B. für Grafiken) sind grundsätzlich genauso einfach zu verwenden und weitestgehend selbsterklärend. Im MSDN finden sich wie üblich weitere Hinweise und Beispiele.

]]>
https://www.coders-online.net/article/ZwischenablageVerwenden7d540ae4-9b27-9f2a-ae3c-41db61e8bd16Sat, 22 Oct 2022 14:27:14 GMT