Posts mit dem Label C# werden angezeigt. Alle Posts anzeigen
Posts mit dem Label C# werden angezeigt. Alle Posts anzeigen

Freitag, 31. Juli 2009

Anwendung nicht im Alt + Tab - Fenster anzeigen

Manches mal hat man die Anforderung, dass eine Applikation nicht in dem Fenster angezeigt werden soll, das aktiv wird, wenn man die Tastenkombination Alt + Tab verwendet, um zwischen den laufenden Applikationen umzuschalten.

Eine kurze, prägnante und gute Anleitung dazu ist mir unter http://www.csharp411.com/hide-form-from-alttab/ über den Weg gelaufen. Das soll hier einfach mal für den Fall vermerkt werden, dass ich mal in die Verlegenheit kommen sollte. Zudem hilft es ja vielleicht auch dem ein oder anderen weiter.

Dienstag, 7. Juli 2009

Microsoft Message Queue MSMQ

Vor einigen Wochen habe ich einen Crashkurs innerhalb weniger Stunden in Sachen MSMQ durchlaufen müssen, an dessen Ende eine Biztalk Lösung stand. Da ich auf einige Hindernisse gestoßen war, fasse ich mal meine Erfahrungen zusammen.

Kurz gesagt, MQ dient der asynchronen Kommunikation zweier Applikationen. Und das auch über Rechnergrenzen, oder gar Domänengrenzen hinweg. Dazu legt nach dem FIFO Prinzip eine Anwendung auf ihrem System Nachrichten ab, und eine andere lauscht auf die Adresse und greift die Nachricht ab. MSMQ kann sich dabei verschiedener Kommunikations Protokolle bedienen. (engl. Einführung in MSMQ)

MSMQ unterstützt gegenwärtig drei Queue Typen:
- Public Queues (benötigt Active Directory, Adressen werden dort direkt veröffentlich und sind von allen in der selben AD erreichbar, bzw. über Domängrenzen über Replikation)
- Private Queues (benötigt kein AD, die Adresse muss der empfangenden Anwendung daher aber auch bekannt sein, selbst mit AD!)
- Transaktions Queues (Sicherheit verbunden mit hohen Overhead Kosten)

Zum Entwickeln empfehlen sich Hilfstools, denn das größte Problem meines Erachtens (eigentlich das Einzige das ich hatte) ist die Kommunikationsinfrastruktur zu konfigurieren, solange man nicht eine sehr simple Netzwerkstrutur vor sich hat.

Zu einem gibt es dafür von MS das MQBench Paket und dann noch von Cogin ein freies MSMQ First Aid Kit.

Im bin Verzeichnis von MQBench gibt es 2 Tools: MSMQRecv bzw. MSMQSend, mit denen man über die Kommandozeile das Empfangen und Senden testen kann. Zum Vorgehen empfiehlt es sich zuerst einmal lokal zu testen, ob die MQ korrekt eingerichtet wurde. Funktioniert das, kann man zum eigentlichen Empfangsrechner über gehen der die Messages später abgreifen soll. Diese Tools haben aber einen Nachteil, sie unterstützen nur den Typ Public Queue, da sie nur das Pathnamen Format beherrschen. Aber der Pathname MUSS im AD nachgeschlagen werden, sobald es über Rechnergrenzen hinweg geht, d.h. Lokal funktionieren Pathnamen auch wunderbar mit Privat Queues!

Beispiel lokaler(!) Zugriff auf private MQ:
msmqrecv .\private$\MeineTestNachrichten

Remote Zugriff:
msmqrecv <rechnername>\private$\MeineTestNachrichten
-> Error: 0xc00e0003 (= Queue Not Found)
(geht nicht, Path wurde nicht in AD publiziert, da privat!)


Anmerkung: ein Versuch das ganze mit einem Formatname anstatt Pathname aufzurufen führt zu einem 0xc00e0014 (= Illegal Queue Pathname), die Tools können nur Pathname!

Das selbe gilt leider auch für das FirstAidKit!

Wie aber Eingangs erwähnt, unterstützt MSMQ unterschiedliche Kommunikationprotokolle, wie z.B. TCP. Dadurch hat man also die Möglichkeit ohne AD Lookup direkt auf die MQ eines anderen Rechners zuzugreifen. Um aber den Protokoll Zugriff beschreiben zu können, benötigt man das Direct Format Name Format. Da ich kein freies Tool kenne das einem das abnimmt, hab ich mir kurzerhand was in .NET zusammengecoded.

(Eine Anmerkung zum MSMQ Biztalk Adapter, dieser unterstützt das "Direct Format Name" Format ebenfalls von Haus aus.)

Unter System.Messaging gibt es im .NET Framework das richtige Werkzeug dazu:

...
MessageQueue mq = null;
bool cr = false;
string strFormatnameaddress = @"FormatName:DIRECT=TCP:192.168.1.114\test";
 
try
{
   msg.Priority = MessagePriority.Normal;
   mq = new MessageQueue(strFormatnameaddress);
 
   cr = mq.CanRead;
}
catch (System.Messaging.MessageQueueException ex)
{
   Console.WriteLine("MSMQ Error: " + ex.ToString());
}
catch (Exception ex)
{
   Console.WriteLine("Error: " + ex.ToString());
}
finally
{
   mq.Close();
}
 
return cr;
...

Das CanRead Flag gibt uns auf jeden Fall Informationen dazu, ob wir nun überhaupt auf die MQ zugreifen können, oder nicht. Jedenfalls ist die Programmierung sehr simpel, das gilt auch für das empfangen, bzw. senden von Messages:
...
// empfang aller Messages in der MQ
Message[] msgs = mq.GetAllMessages(); 
 
...
 
// senden einer Message in die MQ
Message msg = new Message();
msg.Label = "Test Message";
msg.Body = "Das ist nur ein Test";
mq.Send(msg);
...

Das eigentliche Problem fängt nun da an, wenn man keinen Zugriff auf die Message Queue bekommt. Das läßt sich in der Regel auf Security Einstellungen zurückführen. Was kein großes Problem ist, solange man sich am gleichen AD befindet und man über den Pathnamen zugreifen will (Es reicht in der Regel dem User des lesenden Rechners einfach die Zugriffsrechte auf die MQ zu gewähren, notfalls den Anonymus Access erlauben). Befindet man sich in unterschiedlichen AD (aber noch unter dem gleichen Forrest) benötigt man eigentlich nur noch zusätzlich eine funktionierende AD Replikation für den Pathnamen Zugriff. Ansonsten sollte in der Regel der Direct Format Name Zugriff funktionieren.

Was aber wenn sich die Rechner in unterschiedlichen Forrests befinden? Die unterschiedlichen Forrests müßen sich diese Vertrauen, und das bedeutet entweder Zertifikate, oder die "Umsonst"-Variante: ein Loch in die "Mauer schlagen". Dazu benötigt es nicht viel mehr als eines zusätzlichen Eintrages in die Registry des MQ Servers:
http://msdn.microsoft.com/en-us/library/cc236138(PROT.10).aspx
1.) HKLM\Software\Microsoft\MSMQ\Parameters\security\NewRemoteReadServerAllowNoneSecurityClient dword = 1
2.) Zugriffsrechte für Anonymus

Das gibt dem Admin sicherlich richtige Magenschmerzen ;)


Dazu hat John Breakwell von Microsoft, der einem auch bei einem Microsoft Call Support leistet, ein ausführliches Blog verfasst. Folgende Einträge beschäftigen sich mit MSMQ und Rechnern in unterschiedlichen Forrests: http://blogs.msdn.com/johnbreakwell/archive/2008/02/14/how-do-i-send-msmq-messages-between-domains.aspx http://blogs.msdn.com/johnbreakwell/archive/2008/06/27/cross-forest-msmq-you-need-to-be-trusting.aspx http://blogs.msdn.com/johnbreakwell/archive/2008/10/13/authenticating-msmq-messages-between-forests.aspx

Freitag, 5. Dezember 2008

throw ex vs throw

Es ist eigentlich ein alter Hut, der Unterschied zwischen throw ex und throw sollte jedem bekannt sein. Throw ex schneidet den alten Stack Trace ab und verpackt sozusagen den Fehler neu, Throw hingegen nicht.

Aber dennoch halten sich irgendwie hartnäckig gegenteilige Meinungen, vermutlich aus den Gegenbenheiten aus Java resultierend, aber ich habe auch schon einige ellenlange Threads gelesen in denen das vollkommen Falsche dargelegt wurde, bis hin zu Projektleitern die von einem Verlangen ein ganzes Projekt umzuprogrammieren, weil sie der Meinung wären es wäre genau anders herum. 

Den Beweis zu führen ist recht simpel, entweder man schaut sich den MSIL Code an und erkennt das throw zu einem rethrow compiliert, während throw ex, ein throw ex bleibt. Oder aber man testet es einfach mit einem Programm folgender Art selber aus, um auch den letzten Kritiker hoffentlich verstummen zu lassen...

   1:  class ThrowVsThrowEx
   2:  {
   3:     static void Main(string[] args)
   4:     {
   5:        try
   6:        {
   7:           ReThrow();
   8:        }
   9:        catch(Exception ex)
  10:        {
  11:           Console.WriteLine("ReThrow sent: " + ex.ToString());
  12:        }
  13:   
  14:        Console.Write(System.Environment.NewLine);
  15:   
  16:        try
  17:        {
  18:           ThrowNew();
  19:        }
  20:        catch (Exception ex)
  21:        {
  22:           Console.WriteLine("ThrowNew sent: " + ex.ToString());
  23:        }
  24:   
  25:        Console.ReadLine();
  26:     }
  27:   
  28:     static void ReThrow()
  29:     {
  30:        try
  31:        {
  32:           DontCallMe();
  33:        }
  34:        catch
  35:        {
  36:           throw;
  37:        }
  38:     }
  39:   
  40:     static void ThrowNew()
  41:     {
  42:        try
  43:        {
  44:           DontCallMe();
  45:        }
  46:        catch(Exception ex)
  47:        {
  48:           throw ex;
  49:        }
  50:     }
  51:   
  52:     static void DontCallMe()
  53:     {
  54:        throw new ApplicationException("I said don't call me!!");
  55:     }
  56:  }


Ergebnis von throw:

ReThrow sent: System.ApplicationException: I said don't call me!! at DemoExceptions.ThrowVsThrowEx.DontCallMe() in C:\Projects\DemoExceptions\D emoExceptions\ThrowVsThrowEx.cs:line 54 at DemoExceptions.ThrowVsThrowEx.ReThrow() in C:\Projects\DemoExceptions\Demo Exceptions\ThrowVsThrowEx.cs:line 36 at DemoExceptions.ThrowVsThrowEx.Main(String[] args) in C:\Projects\DemoExcep tions\DemoExceptions\ThrowVsThrowEx.cs:line 7


und das Ergebnis von throw ex:

ThrowNew sent: System.ApplicationException: I said don't call me!! at DemoExceptions.ThrowVsThrowEx.ThrowNew() in C:\Projects\DemoExceptions\Dem oExceptions\ThrowVsThrowEx.cs:line 48 at DemoExceptions.ThrowVsThrowEx.Main(String[] args) in C:\Projects\DemoExcep tions\DemoExceptions\ThrowVsThrowEx.cs:line 18


Wunderbar zu erkennen wie throw ex den Ursprung abgeschnitten hat und stattdessen bei sich "selbst" anfängt... keine Worte :)

Mittwoch, 29. Oktober 2008

Hexadezimaldarstellung eines Strings nach Int32 umrechnen - bbo-Style

Wie tropensturm Hex-Strings nach Int32 konvertiert, haben wir heute schon gelernt. Natürlich hat er gleich wieder Forderungen gestellt, also kommt hier meine Version.

Zunächst muss ich anmerken, dass ich wesentlich pragmatischer an die Sache herangegangen bin. Ich habe Dinge wie Encoding und Little- / Big-Endian vernachlässigt, denn ich brauchte schnell eine funktionierende Lösung für einen Test.

Folgender Code ist bei mir dabei heraus gekommen:

   1:  public static int ConvertHexStringToInt(string hexString)         
   2:  {
   3:      string hex = "0x";
   4:   
   5:      for (int i = 0; i < hexString.Length; i++)
   6:      {
   7:          hex += ((int)hexString[i]).ToString("X");
   8:      }
   9:   
  10:      return Convert.ToInt32(hex, 16);
  11:  }

Im Prinzip ist das, was ich hier tue, eigentlich recht einfach.
Zunächst lege ich mir eine String-Variable an, die die hexadezimale Darstellung des übergebenen Strings aufnehmen soll. In der for-Schleife sorge ich entsprechend dafür, dass dies auch so ist. In Zeile 7 wandle ich zunächst jeden Buchstaben des Übergabe-Strings in seine ASCII-Int32-Repräsentation, die anschließend nach Hexadezimal konvertiert und dem finalen String angehängt wird.
Ist die Schleife für alle Zeichen des Strings durchlaufen worden, haben wir einen String im Format 0xA9AE.
Diesen String konvertiere ich dann in Zeile 10 noch nach Int32, wobei ich eine Basis von 16 (also hexadezimal) angebe und gebe das Ergebnis zurück.

So weit, so einfach...
... leider aber auch so "beschränkt".
Durch das Konvertieren jedes Buchstabens in seine Int32-Repräsentation in Zeile 7 wird diese Lösung auf Zeichen beschränkt, die im ASCII-Zeichensatz vorhanden sind. Werden Zeichen verwendet, die in diesem nicht auftauchen, führt das zu falschen Ergebnissen.

FAZIT:
Für meine Zwecke ist diese Implementierung absolut ausreichend, da ich mir sicher sein kann, dass mein Übergabe-String nur Zeichen enthält, die auch im ASCII-Zeichensatz auftauchen. Für einen produktiven Einsatz ist aber auf jeden Fall tropensturms Lösung vorzuziehen, da diese eben auch mit nicht ASCII-konformen Zeichen umgehen kann.

bbo-Style: 0
tropensturm-Style: 1

Hexadezimaldarstellung eines Strings nach Int32 umrechnen

BBo hat mir eine Aufgabenstellung hingeworfen und wir haben diese dann unterschiedlich gelöst. Es ging darum die Hexadezimaldarstellung eines 2 Byte langen String nach Int32 zu konvertieren. Das war etwas schwieriger als wir dachten. Hier nun meine Lösungsvariante, vielleicht postet BBo ja noch seine ;)

Das Erste was mir durch den Kopf ging ist, dass dies etwas gefährlich ist in Bezug auf das Stringencoding. Denn je nach dem wie der String decodiert wurde, hat man ja je nach Umgebung eine andere Hexadezimalwertdarstellung nach dem Encodieren. Darum habe ich einfach zur Absicherung das Encoding mit eingebaut in meine Lösung. Theoretisch kann man in einer homogenen Welt darauf verzichten, doch meine praktischen Erfahrungen in den letzten Monaten haben mir oft genug die Finger verkokelt. Man kann ja selber mal damit herumspielen und mit unterschiedlichen Encodings die unterschiedlichen Ergebnisse vergleichen...

   1:  // test
   2:  int erg = TwoByteStringHexValueToInt32("Aÿ", System.Text.Encoding.GetEncoding(1252));
   3:  ...
   4:   
   5:  public static int TwoByteStringHexValueToInt32(string input, Encoding encoding)
   6:  {
   7:     if (input.Length != 2)
   8:        throw new ArgumentException("TwoByteStringHexValueToInt32 - parameter input is allowed to be 2 byte long only!");
   9:              
  10:     if (encoding == null)
  11:        encoding = System.Text.Encoding.Default;
  12:   
  13:     int result = 0;
  14:     byte[] ba = encoding.GetBytes(input); // split to byte array of 2
  15:   
  16:     if (System.BitConverter.IsLittleEndian) // when little endian we need to do a workaround
  17:     {
  18:        result = (Convert.ToInt32(ba[0]) << 8) + Convert.ToInt32(ba[1]);
  19:     }
  20:     else
  21:     {
  22:        result = (int)System.BitConverter.ToInt16(ba, 0);
  23:     }
  24:   
  25:     return result;
  26:  }

In Zeile 14 geht es richtig los, da zerleg ich den Input string in ein 2-Byte Array in Abhängigkeit (!) vom Encoding des Strings. 1252 ist das herkömmliche Encoding unter Windows für lateinische Schriftsysteme, u.a. Deutsch. Danach steht also in unserem Byte Array die Hexdarstellung unseres Strings.

Danach mache ich einen kleinen Kunstgriff in Zeile 16, x86 Syteme sind LittleEndian kodiert, Motorola z.B. normalerweise BigEndian. Hier eine kleine Info wenn man damit nichts anfangen kann. Im Klartext, normalerweise rechnet man den Wert in unserem Bytearray wie in Zeile 22 gezeigt um. Mit dem Bitkonverter nach 16Bit Integer, werden 2 Bytes aus dem Bytearray ab der Position 0 gelesen und in Integer umgerechnet (Int32 bräuchte 4 Bytes, darum dieser Umweg).

Und GENAU hier schlägt die Sache mit dem Little/Big Endian System zu! Denn wir haben zuvor oben unseren String eigentlich verkehrt herum abgelegt für unser x86 System!! Denn das rechnet von Hinten nach Vorne Bytedarstellungen nach Integer um!

Daher das Byteshifting in Zeile 18, falls ein Little Endian System vorliegt. Ich greife mir das erste Byte konvertiere es nach Int32 und verschiebe es mal 8 Bits nach "links" (= 1 Byte, wir haben ja 2 Bytes und schieben das Erste sozusagen nach "vorne"), um dann einfach das Ergebnis aus der zweiten Byte Konvertierung dazu addieren.

Nachtrag: Der Rückweg

   1:  public static byte[] Int16ToBigEndianByteArray(short input)
   2:  {
   3:     int higher = input >> 8;     // shift 1 byte -> get higher byte
   4:     int lower  = input & 0x00ff; // mask -> get lower byte
   5:   
   6:     byte[] ba = new byte[2];
   7:     ba[0] = Convert.ToByte(higher);
   8:     ba[1] = Convert.ToByte(lower);
   9:   
  10:     return ba;
  11:  }
  12:   
  13:   
  14:  ...
  15:  // test
  16:  byte[] ba = Int16ToBigEndianByteArray((short)31910);
  17:  string res = System.Text.Encoding.GetEncoding(1252).GetString(ba);

Donnerstag, 16. Oktober 2008

Embedded Ressources und ein bisschen mehr

Dieser Blog entsteht aus einer Handvoll Diskussionen mit BBo über ein paar Tage hinweg. Irgendwie hab ich alle Punkte in ein Beispiel reingestopft bekommen :)

Die Ursprungsfrage drehte sich darum, wie greift man auf eine eingebettete Ressource zur Laufzeit in meiner Assembly zu. Später kamen noch so Sinnesfragen hinzu wozu einen statischen Konstruktor, oder wie stell ich einfach fest ob die Konfiguration von log4net geklappt hat oder nicht,usw.

Zur "embedded resource", als erstes hab ich eine XML angelegt, die einzig die Configurations Daten für Log4Net enthält. Ganz wichtig: unter Properties ist unter Build Action embedded resource auszuwählen, sonst finden wir das nachher nicht:

Der Zugriff selber dann ist in Zeile 29 vom Code zu sehen. Beim auslesen der Ressource erhält man einen ganz normalen Stream, der kann dann wie gewohnt verarbeitet werden, log4net erlaubt uns eine Konfiguration direkt mit dem Stream. In Zeile 24 ist übrigens die Abfrage, ob Log4Net konfiguriert wurde, zu sehen.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using System.Xml;
   5:  using System.IO;
   6:   
   7:  using Threading = System.Threading;
   8:  using Reflection = System.Reflection;
   9:  using L4N = log4net;
  10:   
  11:  namespace StaticLogWrapper
  12:  {
  13:      [Serializable]
  14:      public class Log
  15:      {
  16:          private static readonly L4N.ILog log = L4N.LogManager.GetLogger(typeof(Log));
  17:   
  18:          static Log()
  19:          {
  20:              // try configure the standard way
  21:              L4N.Config.XmlConfigurator.Configure();
  22:   
  23:              // is log4net configured? 
  24:              if (L4N.LogManager.GetRepository().Configured)
  25:                  return; // we are finished
  26:   
  27:              // when not set log4net config from internal setup
  28:              string name = "StaticLogWrapper.Log4NetConfig.xml";
  29:              Stream objStream = Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(name);
  30:              L4N.Config.XmlConfigurator.Configure(objStream);
  31:              objStream.Close();
  32:          }
  33:          
  34:          ...

Donnerstag, 9. Oktober 2008

Strings verschlüsseln mit C# - Part 2

Vor einiger Zeit hat chico beschrieben, wie man einen String mit C# verschlüsseln kann. Leider hatte der Beitrag den kleinen Schönheitsfehler, dass die Methode zur Verschlüsselung ein Byte-Array zurückgeliefert hat. Für manche Situationen mag das durchaus ausreichend sein, doch wenn man den verschlüsselten String beispielsweise in einer Datenbank ablegen möchte, wird man mit dem Byte-Array vermutlich nicht weit kommen. Daher möchte ich heute, aufbauend auf dem ersten Artikel zur Thematik, erläutern, wie man statt dem Byte-Array einen String zurückliefern kann.

Auch hier müssen zuerst wieder ein Schlüssel sowie ein Initialisierungsvektor angelegt werden.

   1:  private readonly byte[] key = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 
   2:      13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 };
   3:  private readonly byte[] iv = new byte[] { 65, 110, 68, 26, 69, 178, 200, 219 };

Die Methode zur Verschlüselung sieht sich ebenfalls noch ziemlich ähnlich. Es wurde lediglich eine Zeile geändert, eine neue kam hinzu.

   1:  /// <summary>
   2:  /// Verschlüsselt einen Eingabestring.
   3:  /// </summary>
   4:  /// <param name="input">Der zu verschlüsselnde String.</param>
   5:  /// <returns>Byte-Array mit dem verschlüsselten String.</returns>
   6:  public string StringVerschluesseln(string input)
   7:  {
   8:      try
   9:      {
  10:          // MemoryStream Objekt erzeugen
  11:          MemoryStream memoryStream = new MemoryStream();
  12:   
  13:          // CryptoStream Objekt erzeugen und den Initialisierungs-Vektor
  14:          // sowie den Schlüssel übergeben.
  15:          CryptoStream cryptoStream = new CryptoStream(
  16:          memoryStream, 
  17:          new TripleDESCryptoServiceProvider().CreateEncryptor(this.key, this.iv),
  18:          CryptoStreamMode.Write);
  19:   
  20:          // Eingabestring in ein Byte-Array konvertieren
  21:          byte[] toEncrypt = new ASCIIEncoding().GetBytes(input);
  22:   
  23:          // Byte-Array in den Stream schreiben und flushen.
  24:          cryptoStream.Write(toEncrypt, 0, toEncrypt.Length);
  25:          cryptoStream.FlushFinalBlock();
  26:   
  27:          // Ein Byte-Array aus dem Memory-Stream auslesen
  28:          byte[] encrypted = memoryStream.ToArray();
  29:   
  30:          // Stream schließen.
  31:          cryptoStream.Close();
  32:          memoryStream.Close();
  33:   
  34:          // Konvertierung in Base64-String
  35:          string encryptedString = Convert.ToBase64String(encrypted);
  36:   
  37:          // Rückgabewert.
  38:          return encryptedString;
  39:      }
  40:      catch (CryptographicException e)
  41:      {
  42:          Console.WriteLine(string.Format(
  43:              CultureInfo.CurrentCulture,
  44:              "Fehler beim Verschlüsseln: {0}", 
  45:              e.Message));
  46:   
  47:          return null;
  48:      }
  49:  }

Die Änderung finden wir in Zeile 21, statt ASCIIEncoding wird jetzt Encoding.UTF8 verwendet. Hinzugekommen ist Zeile 35. Hier wird das ermittelte Byte-Array in einen Base64String umgewandelt, der dann auch zurückgegeben wird.
Genau wie in der Verschlüsselungs-Methode ändert sich auch in ihrem Pendant wenig.

   1:  /// <summary>
   2:  /// Entschlüsselt einen String aus einem Byte-Array.
   3:  /// </summary>
   4:  /// <param name="data">Das verscghlüsselte Byte-Array.</param>
   5:  /// <returns>Entschlüsselter String.</returns>
   6:  public string StringEntschluesseln(string data)
   7:  {
   8:      try
   9:      {
  10:          // Verschluesselten Base64String in Byte-Array konvertieren
  11:          byte[] encrypted = Convert.FromBase64String(data);
  12:   
  13:          // Ein MemoryStream Objekt erzeugen und das Byte-Array
  14:          // mit den verschlüsselten Daten zuweisen.
  15:          MemoryStream memoryStream = new MemoryStream(encrypted);
  16:   
  17:          // Ein CryptoStream Objekt erzeugen und den MemoryStream hinzufügen.
  18:          // Den Schlüssel und Initialisierungsvektor zum entschlüsseln verwenden.
  19:          CryptoStream cryptoStream = new CryptoStream(
  20:          memoryStream,
  21:          new TripleDESCryptoServiceProvider().CreateDecryptor(this.key, this.iv), 
  22:          CryptoStreamMode.Read);
  23:   
  24:          // Buffer erstellen um die entschlüsselten Daten zuzuweisen.
  25:          byte[] fromEncrypt = new byte[data.Length];
  26:   
  27:          // Read the decrypted data out of the crypto stream
  28:          // and place it into the temporary buffer.
  29:          // Die entschlüsselten Daten aus dem CryptoStream lesen
  30:          // und im temporären Puffer ablegen.
  31:          cryptoStream.Read(fromEncrypt, 0, fromEncrypt.Length);
  32:   
  33:          // Den Puffer in einen String konvertieren und zurückgeben.
  34:          return Encoding.UTF8.GetString(fromEncrypt).Replace("\0", string.Empty);
  35:      }
  36:      catch (CryptographicException e)
  37:      {
  38:          Console.WriteLine(String.Format(
  39:              CultureInfo.CurrentCulture,
  40:              "Fehler beim Entschlüsseln: {0}",
  41:              e.Message));
  42:   
  43:          return null;
  44:      }
  45:  }

Die erste Änderung findet sich - natürlich - direkt in der Signatur der Methode (Zeile 6). Entgegengenommen wird jetzt natürlich kein Byte-Array mehr, sondern ein String. Entsprechend haben wir in Zeile 11 dann die Umwandlung des Base64Strings in ein Byte-Array, das dann weiter verarbeitet wird.  Schlussendlich haben wir dann in Zeile 34 die finale Konvertierung in den Rückgabe-String. Auch hier wird statt ASCIIEncoding wieder UTF8 verwendet, schließlich ist das ja jetzt die Basis. Eine Besonerheit ist hierbei, dass am fertigen String einige "\0"s hängen. Diese müssen noch entfernt werden, was hier durch den Aufruf von Replace geschieht.

Der jetzt zurückgegebene String entspricht wieder genau dem, der ursprünglich verschlüsselt wurde. Alles andere wäre natürlich auch inakzeptabel.

Bleiben jetzt nur die Fragen, warum der Umstieg von ASCIIEncoding auf UTF8 sein musste und warum das Byte-Array nicht direkt in einen normalen String, sondern in einen Base64String konvertiert wird. Beides hängt damit zusammen, dass ein Aufruf von ToString() zu falschen Ergebnissen bzw. sogar zu einer Exception in Zeile 29 (beim Entschlüsseln) führt. Dies hängt offenbar damit zusammen, dass der String im Normalfall Steuerzeichen (Stichwort Backslash) enthält. Durch die gemachten Änderungen wird dies umgangen.

Für die endgültige Lösung möchte ich chico danken, der maßgeblich an der Entwicklung beteiligt war.

Mittwoch, 3. September 2008

Hashtable und der loadFactor Parameter

Ich bin heute über den "ominösen" float loadFactor Parameter im Hashtable Constructor beim Fremdcode debuggen gestoßen. Wer sich nicht mit theoretischen Verständnis belasten will, nicht das "letzte" bischen Performance braucht und auch noch nie eine "System.InvalidOperationException: Hashtable insert failed. Load factor too high" Exception gesehen hat, dem sei gesagt, laß den loadFactor in Ruhe. Der ist Standardmäßig auf 0.72 eingestellt und völlig ausreichend normalerweise.

Wer jetzt noch dabei ist, der muss sich nun darauf gefasst machen das ich recht weit ausholen muß. Nach etwas Recherche in Wesner Moise's legendärem .NET undocumented blog, ging mir ein Licht auf, was dass denn nun genau ist. 
Theorie
Die Grundidee der Hashtable ist, simpel gesagt, mittels des Keys den man besitzt direkt zum Eintrag in einer Liste springen zu können. Kein suchen und langes vergleichen, man hat den Key, berechnet daraus eine Art "Offset" und hat damit den Speicherplatz lokalisiert wo der zugehörige Value abgelegt ist. Die Berechnung dieses "Offsets" übernimmt die Hashfunktion in unserer Hashtable. 
Z.b. wir haben eine Mitarbeiternummer für 50 Mitarbeiter, um jeden Eindeutig zu identifizieren würde es reichen aus den letzten 2 Nummern der Mitarbeiternummer einen eindeutigen Hashcode zu berechnen, ergo reicht uns ein vorinitialisiertes leeres Array von 100 Einträgen (nennen wir dieses Array Buckets) und je nach Hashcode kann man dann zum berechneten Index springen (egal ob 0 oder 99). Nun kommt aber Mitarbeiter 51, und dessen letzten 2 Ziffern aus der Mitarbeiternummer stimmen ganz zufällig mit der eines anderen überein. Ein insert schlägt fehl (Hashtable insert failed), unsere Hashfunktion ist nicht ausreichend und es gibt das, was man eine Collision in der Hashtable nennt. 
Um diese Collision  zu vermeiden kann man nun z.B. die Hashfunktion neu implementieren, nur haben wir dann das dilema, um so komplizierter und einzigartiger dieser Hashcode berechnet  wird, um so langsamer wird jeder Zugriff auf unsere Hashtable. Ein anderer Ansatz geht vom Load Factor der Hashtable aus. haben wir unsere Buckets aus dem obigen Beispiel mit der Größe 100 und 50 Einträge drin, dann beträgt der Load Factor 50/100 = 0.5. Das heißt unsere Buckets sind zu 50% gefüllt, die Wahrscheinlichkeit das eine Collision stattfindet ist 0.5. Um so niedriger wir diesen Faktor drücken können, um so unwahrscheinlicher ist eine Collision, aber auch um so mehr freien Speicherplatz verpulvern wir sprichwörtlich. 
Aber selbst wenn wir die Wahrscheinlichkeit drücken, sie bleibt bestehen, daher benötigt man eine Technik die im Falle der Collision eine Gegenstrategie fährt. Buckets machen aus unserem 1-dimensionalen Array ein 2-dimensionales Array. Ist der Index belegt, wird einfach in der 2. Dimension am nächsten freien Index unser Kollidierter Hash abgelegt. Praktisch auffüllen wie bei einem Eimer (Bucket). Die Nachteile sind offentsichtlich, das eingangs erwähnte "Vergleichen", dass man vermeiden wollte ist plötzlich wieder da, auch noch eine Collisiondetektions und Handlingsroutine.
Ergo man kann es also auf den Punkt bringen, mehr Speicherplatz für mehr Performance in der Hashtable. Vermeidet man Kollisionen, vermeidet man unnötige Operationszeiten. Sowohl beim schreiben und vor allem auch beim auslesen.
.NET Praxis
Nach dem theoretischen Ausflug zurück zu .NET. Ganz so simpel wie das theoretische Modell ist es nicht ganz in der Praxis.
Buckets sind in der .NET Hashtable ein Array von Arrays, um genau zu sein ein Bucket[], wobei jedes Bucket drei Werte hält: hash_coll (int), key (object) und val (object):
Füge ich in die obige Hashtable den key "a" ein, berechnet die Hashfunktion einen Wert dazu und ordnet diesem Wert einen Index in DIESEM(!) Bucket Array zu (andere Bucket Array Größe, andere Zuordnungsweise). Daher wird in dieser Hashtable (100, 1.0f), der key "a" immer an Position 49 im Bucket Array landen (.NET 3.5). Hätte ich nun einen key zur Hand, dessen Hashcode zufällig identisch wäre, würde eine Collision eintreten und das Collision Handling tritt ein. Dazu wird der Hashkey neu berechnet und als "Offset" zu der Kollision Index Position aufgerechnet. Es wird also nicht wie im klassischen Bucket in die 2. Dimension gegangen, sondern, flapsig gesagt, flach "weitergesprungen" (was uns den Compare aus der Theorie von oben erspart!).
Das wiederrum erklärt warum das Bucket Array hier deutlich größer ist als der vorgegebene Initialwert von 100. Und jetzt kommen wir damit auch endlich zum loadFactor und dessen Auswirkung. Ich habe den Wert von 1.0f übergeben als loadFactor. Dies wird mit dem Initialwert 0.72 multipliziert. D.h. würde ich 0.5f übergeben, wäre der interne loadFactor 0.36:
Die Berechnung der Bucket Anzahl ist jetzt eher nebensächlich, grob gesagt die Initialgröße dividiert durch den loadFactor und dann die nächste Primzahl (aus statistischen Gründen für maximale Effektivität). Wesner Moise läßt sich in seinem Blog detailierter darüber aus, wen es genauer interessiert (siehe Verlinkung oben).
Fazit
D.h. zusammenfassend:
um so kleiner der loadFactor...
-> um so größer das Bucket Array
-> um so mehr Speicherverbrauch
-> um so weniger Collisionen
-> um so weniger Operationen
-> um so höhere Performance in Abhängigkeit von der steigenden Anzahl der keys (was wirklich durchaus in einem signifikanten Rahmen liegt, ...probierts aus)

Mittwoch, 20. August 2008

StyleCop v4.3 erschienen

Wie ich gerade anhand des Microsoft StyleCop-Blogs erfahren habe, ist die neue Version 4.3 des StyleCop erschienen.

Unter anderem wurden in dieser Version einige Bugs gefixt sowie neue Rules eingefügt. Sehr schön finde ich auch, dass die Definition der einzelnen Rules jetzt wohl im Visual Studio verfübar sein soll. Bis jetzt musste ich immer das WWW durchforsten, wenn ich eine mir unbekannte Rule-Verletzung im Code hatte. Sehr positive Entwicklung.

Zum Download geht es hier. Unter diesem Link steht auch eine Doku zur Verfügung, die ebenfalls die Rules und deren Erklärung enthält.

Montag, 18. August 2008

Microsoft StyleCop

Vor kurzem bin ich wieder einmal über ein sehr nettes Tool gestolpert, das Microsoft den C#-Programmierern zur Verfügung stellt, um die Codequalität zu verbessern, ähnlich dem FxCop. Anders als der angesprochene FxCop analysiert der StyleCop allerdings keine Assemblies sondern den geschriebenen Code direkt im VisualStudio. Auch werden von ihm natürlich andere Themengebiete abgedeckt als vom FxCop.

Hat man den StyleCop hier heruntergeladen und installiert, hat man im VisualStudio im Menü "Extras" ("Tools" in der englischen Version) zwei neue Menüpunkte, mämlich "Run Source Analysis" und "Run Source Analysis (Rescan all)". Die erste Option kann auch durch die Tastenkombination Ctrl + Shift + Y aufgerufen werden. Führt man die Quellcode-Analyse durch, erscheint ein neues Fenster, das die Ergebnise für die Solution enthält.

Hingewiesen wird hier unter anderem auf fehlende Dokumentationskommentare, "falsche" Formatierung und ähnliches. Weiterhin bekommt man eine Meldung, wenn man beispielsweise vergessen hat, einer Klasse explizit einen Zugriffsmodifizierer zuzuweisen, was ja doch schon mal passieren kann, wenn die Klasse sowieso private sein soll.

Interessant finde ich das AddOn hauptsächlich deswegen, weil es einem die Coding-Guidelines, die (zumindest teilweise) bei Microsoft verwendet werden. Ein paar Kleinigkeiten habe ich bereits gelernt, zum Beispiel, dass Using-Direktiven innerhalb eines Namespace gemacht werden und jede Datei einen definierten Header bekommen sollte. Die Sache mit den Using-Direktiven verstehe ich zwar noch nicht wirklich, doch den Grund hierfür werde ich schon noch herausfinden.  Falls da jemand etwas Genaueres sagen kann, so möge er dies doch bitte via Kommentar tun. Header verwende ich natürlich in meinen Sourcefiles, doch nicht in der Form, wie sie der StyleCop verlangt.

Für jeden, der sich für die Optimierung seines Codings interessiert, der möge sich den StyleCop herunterladen und installieren. Bei mir wird er jedenfalls jetzt regelmäßig zum Einsatz kommen.

Nachtrag: Wie ich gerade festgestellt habe, halten sich die von VisualStudio generierten Codedateien selbst nicht an die Vorgabe des StyleCop. Interessant...

Donnerstag, 7. August 2008

Kenne deine .NET Werkzeuge - HttpUtility Class

Ich denke eines der größten individuellen Mankos ist, dass man (ich) nicht sein Framework und die damit zur Verfügung gestellten Werkzeuge kennt. Ich vermute mal ein ASP.NET Entwickler wird die HttpUtility Class (http://msdn.microsoft.com/en-us/library/system.web.httputility(VS.80).aspx) kennen, ich kannte sie nicht. Es war unerlässlich das ich ein XML von Hand zusammenkleben mußte und mein LoadXML in ein XmlDocument schlug grandios fehl. Das Problem war schnell gefunden im XML Editior von Visual Studio, der hat mir einen Eintrag rot unterringelt der ein & enthielt. Ich wollte schon loslegen eine Hilfsklasse zu schreiben die mir meine Values &, ä, ö, ü, <, > usw. konvertiert nach &amp; etc. ... Doch "Halt!", meinte mein Kollege, da muss es doch was im Framework schon geben. Nach etwas "googeln" fand ich dann die HttpUtility Class. Und die hat eine HtmlEncode Methode die einem das wunderbar erschlägt :)

string encodedValue = System.Web.HttpUtility.HtmlEncode(unencodedValue);

Das Ganze funktioniert auch innerhalb von Biztalk. Die System.Web dll muss hier explizit referenziert werden, das dürfte vermutlich auch für alle Nicht-ASP.NET Projekte gelten.

Dienstag, 22. Juli 2008

OPC: DataChange-Event

Ein OPC-Client hat die Möglichkeit, vom Server Nachrichten über Aktualisierungen von Werten zu empfangen. Das ist natürlich sehr nützlich, wenn man gerne eine bestimmte Aktion ausführen möchte, wenn ein definierter Wert erreicht wird. Kommt ja auch nicht gerade selten vor, diese Anforderung, also hat die OPC Foundation da schon ganz schön schlau definiert.

Nicht ganz so schlau ist jedoch die Umsetzung innerhalb der OpcDAAuto.dll gelungen. Ich habe heute etwa einen halben Tag gekämpft, bis ich tatsächlich in der Lage war, den DataChange-Event, der vom Server bei Veränderungen von zuvor dafür angemeldeten Werten gesendet wird, zu empfangen. Ich habe Tutorials gewälzt, das Internet durchforstet und experimentiert - alles ohne Erfolg. Alle Voraussetzungen, die ich finden konnte, schienen mir erfüllt. Nach ewigem Herumprobieren gab es nur noch einen einzigen Unterschied zwischen den Tutorials - die, heruntergeladen und kompiliert im Übrigen einwandfrei funktionierten - und meiner Applikation. Dieser fand sich beim Anlegen von OPC-Items, also jenen Werten, die ich überwachen wollte. Mein Code - im Übrigen eigentlich absolut korrekt - sah folgendermaßen aus:

   1:  OPCGroupClass opcGroupTriggerItems;
   2:   
   3:  // Gruppe anlegen
   4:  opcGroupTriggerItems = ( OPCGroupClass ) opcServer.OPCGroups.Add( "TriggerItems" );
   5:   
   6:  // Items hinzufügen
   7:  opcGroupTriggerItems.OPCItems.AddItem( connection + "/" + alias + "/TriggerItem1", 0 ) );
   8:  opcGroupTriggerItems.OPCItems.AddItem( connection + "/" + alias + "/TriggerItem2", 1 ) );
   9:  opcGroupTriggerItems.OPCItems.AddItem( connection + "/" + alias + "/TriggerItem3", 2 ) );
  10:  opcGroupTriggerItems.OPCItems.AddItem( connection + "/" + alias + "/TriggerItem4", 3 ) );
  11:   
  12:  // Event Handler Methode registrieren
  13:  // WICHTIG: Die Event Handler Methode wird erst nach dem hinzufügen der Items zur Gruppe registriert,
  14:  //          da sonst beim Hinzufügen für jedes Item ein Event ausgelöst wird.
  15:  opcGroupTriggerItems.DataChange += new DIOPCGroupEvent_DataChangeEventHandler( opcGroupTriggerItems_DataChange );

Hier wird zuerst eine Gruppe auf dem OPC-Server angelegt, zu der dann anschließend die Items hinzugefügt werden, die die Werte darstellen, die überwacht werden sollen. Um zu vermeiden, dass für alle Items ein initialer Event ausgelöst wird, der über die Änderung von "nicht vorhanden" nach "vorhanden" und die damit verbundene Wertänderung benachrichtigt, wird die Event-Handler-Methode erst nach dem Anlegen der nötigen Items registriert. So wurden die Items korrekt angelegt, sie konnten ausgelesen werden, aber die Benachrichtigung über Wertänderungen funktionierte, wie bereits erwähnt, überhaupt nicht.

Der Code, der mir von den Tutorials aufgezeigt wurde und der jetzt, nachdem ich ihn so auch in meiner Applikation habe, zu dem gewünschten Ergebnis führt, sieht so aus:

   1:  OPCGroupClass opcGroupTriggerItems;
   2:  string[] itemIds = new string[5];
   3:  int[] clientHandles = new int[5];
   4:  int[] serverHandles;
   5:  int[] errors;
   6:   
   7:  // Gruppe anlegen
   8:  opcGroupTriggerItems = ( OPCGroupClass ) opcServer.OPCGroups.Add( "TriggerItems" );
   9:   
  10:  // Items erstellen
  11:  itemIds.SetValue( connection + "/" + alias + "/TriggerItem1", 1 );
  12:  itemIds.SetValue( connection + "/" + alias + "/TriggerItem2", 2 );
  13:  itemIds.SetValue( connection + "/" + alias + "/TriggerItem3", 3 );
  14:  itemIds.SetValue( connection + "/" + alias + "/TriggerItem4", 4 );
  15:   
  16:  // ClientHandles erstellen
  17:  clientHandles.SetValue( 1, 1 );
  18:  clientHandles.SetValue( 2, 2 );
  19:  clientHandles.SetValue( 3, 3 );
  20:  clientHandles.SetValue( 4, 4 );
  21:   
  22:  // Items der Gruppe hinzufügen
  23:  opcGroupTriggerItems.OPCItems.AddItems( 4, ref itemIds, ref clientHandles, out serverHandles, out errors, null, null );
  24:   
  25:  // Event Handler Methode registrieren
  26:  // WICHTIG: Die Event Handler Methode wird erst nach dem hinzufügen der Items zur Gruppe registriert,
  27:  //          da sonst beim Hinzufügen für jedes Item ein Event ausgelöst wird.
  28:  opcGroupTriggerItems.DataChange += new DIOPCGroupEvent_DataChangeEventHandler( opcGroupTriggerItems_DataChange );

Auch hier wird zunächst eine Gruppe angelegt. Es werden hier jedoch die Items nicht, wie zuvor, einzeln der Gruppe hinzugefügt. Statt dessen werden ein String-Array mit den Werten und ein Integer-Array mit den Client-Handles, unter denen die Items eindeutig zu identifizieren sind, angelegt. Hier ist die Besonderheit zu beachten, dass der erste Wert des Arrays jeweils nicht beachtet wird, das Array wird immer nur von 1 bis max durchlaufen. Das hängt, so weit ich informiert bin, damit zusammen, dass die von mir verwendete OpcDAAuto.dll in VB geschrieben ist. Jedenfalls werden anschließend die Items und ihre Client-Handles gesamt an eine Methode übergeben, die sie der Gruppe hinzufügen. Anschließend wieder die Event-Handler-Methode registriert. Test anwerfen - funzt.  Sehr erstaunlich und für mich nicht wirklich nachvollziehbar, aber im Endeffekt ist es jetzt auch egal, es läuft ja nun :)

log4net & 64Bit - Update

Zumindestens einen Teil des Problems aus dem Post http://justacodeblog.blogspot.com/2008/07/log4net-ibasecomponent-64bit-windows.html habe ich inzwischen aufgeklärt.

Unter Windows 2003 64Bit gibt es drei Gac Verzeichnisse: GAC_32 (für 32Bit kompilierte Assemblies), GAC_64 (für 64Bit kompilierte Assemblies) und GAC_MSIL (Assemblies die sowohl als auch ausgeführt werden können).

Mit Hilfe von Filemon habe ich dann herausgefunden das meine Anwendung log4net im GAC_32 verzweifelt sucht. Eigentlich sollte log4net mit Hilfe des Gacutils im GAC_32 Verzeichnis landen, tut es aber nicht, es landete bei mir im GAC_MSIL Verzeichnis.

Ich habe es dann "von Hand" dort hin kopiert und nun funktioniert es, jedoch habe ich keine Ahnung ob man bei Gacutil einen Switch setzen kann der die automatische Entscheidung überstimmt und das Deployment in einem bestimmten Verzeichniss erzwingen kann...

Montag, 21. Juli 2008

Custom Pipeline Components - Update

Seit meinem ersten Eintrag zum selber schreiben von Custom Pipeline Components (http://justacodeblog.blogspot.com/2008/05/custom-pipeline-components-wizard.html) hab ich inzwischen soviel Erfahrungen zu dem Thema gesammelt, das ich hierzu ein Update verfassen muss.

Ein paar Anmerkungen zum arbeiten mit Martijn Hoogendoorn's Component Wizard und Pipeline Komponenten generell:

- Der Beste Ansatz um die erstellten Komponenten stressfrei zu deployen und in der Toolbox des Pipeline Projektes zu verwenden ist es die fertige DLL Komponente: 1.) in den GAC zu deployen 2.) in das Pipeline Component Verzeichnis von Biztalk zu deployen (z.B.: C:\Program Files\Microsoft BizTalk Server 2006\Pipeline Components) Beides kann man gut im Post Built Event erledigen z.B. wie folgt (für 64Bit Windows mit angepasst, darum die relativen Pfade) "$(DevEnvDir)..\..\SDK\v2.0\Bin\gacutil.exe" /i "$(TargetPath)" xcopy "$(TargetPath)" "$(DevEnvDir)..\..\..\Microsoft BizTalk Server 2006\Pipeline Components" /R /Y

- Log4Net und 64Bit Windows können Probleme in Verbindung mit der Biztalk Toolbox Erkennung bereiten. Dabei kann entweder die korrekte Namenserkennung der Komponente, oder gar die komplete Komponenten Erkennung fehlschlagen. Das Problem vermute ich in den Windows Sicherheitseinstellungen (?). Ansonsten hilft als "Workaround" nur das komplette Entfernen von Log4Net aus der Komponente

- Beim Umbennen einer Pipeline Komponente (Namespace und/oder Klassenname) muss man zwei weitere Punkte beachten für die korrekte Anzeige in der Toolbox später. 1.) Unterhalb der Kompenenten Klassendeklaration wird der Name registriert:

   1:  private System.Resources.ResourceManager resourceManager = new System.Resources.ResourceManager(
   2:      "<Namespace>.<Pipeline Komponenten Name>",
   3:      Assembly.GetExecutingAssembly());

2.) Im Ressource File selbst: COMPONENTNAME = <Namespace>.<Pipeline Komponenten Name>

- Für einen rundimentären Test ist das pipeline.exe Tool selbst eine schnelle und schöne Sache. Jedoch empfiehlt es sich unbedingt auch eine BizTalk Test Orchestration zusammenzuklicken! Der Umstand ist simpel. Während die pipeline.exe unsere Komponente direkt aus dem lokalen Filesystem aufruft, wird in BizTalk ein COM Aufruf ausgeführt! Dies verändert den Kontext in dem sich der Stream befindet, den man gerade verarbeitet! Simples Beispiel: solange man über die pipeline.exe testet, kann der Stream nach FileStream gecastet werden, um z.B. den File Namen auszulesen. Führt man den selben Aufruf in BizTalk aus, sind die FileStream Informationen gar nicht mehr vorhanden. Ein Cast schlägt fehl!

- Für das Testen mit der pipeline.exe empfiehlt es sich eine eigene Solution Konfiguration anzulegen (z.B. Debug Pipeline). In den Projekteigenschaften konfiguriert man dann unter Debug:

  • Start external program: <pfad zum Pipeline Test Tool>\Pipeline.exe
  • Command line arguments z.B.: -pt <namespace>.<biztalk pipeline orchestration> -an <custom pipeline component name>-d <input flatfile> -v
  • und ggf. noch die Working directory anpassen wenn nötig

(nicht vergessen bei einer neuen Solution Konfiguration unter der Projekteigenschaft Build - Advanced Button die Debug Info mit erzeugen zu lassen (full))

- Bei der Implementierung ist darauf zu achten das BizTalk die Pipeline bei hoher Last automatisch parallel ausführt!! D.h. VORSICHT beim Umgang mit z.B. statischen Elementen, wer nicht darauf verzichten kann, muß Threadsicher programmieren!

Donnerstag, 3. Juli 2008

64Bit / 32Bit und universelles Postbuildevent mit gacutil

Nur als kleine Notiz für mich selbst, da die Pfade für die Programmordner unterschiedlich sind unter 64 und 32 Bit. Hab ich mir aus den Makros die Pfade zusammengeklickert und da ich allein schon mit dem setzen von " und \ herumeiere leg ich das hier mal lieber ab ;)

"$(DevEnvDir)..\..\SDK\v2.0\Bin\gacutil.exe" /u "$(TargetPath)"
"$(DevEnvDir)..\..\SDK\v2.0\Bin\gacutil.exe" /i "$(TargetPath)"

log4net, IBaseComponent & 64Bit Windows Server

log4net 1.2.10, IBaseComponent & 64Bit Windows Server funktionieren irgendwie nicht zusammen. Das witzige unter 32Bit funktioniert es!

Für eine Biztalk Pipeline habe ich zwei Komponenten entwickelt die aus einem Flatfile Format eine XML Datei erzeugt. Beim einbinden der Komponten war im Browser von VisualStudio bei einer der korrekte Name aus der IBaseComponent zu sehen, bei der anderen der Assembly Name.
Dumm denn auf meiner 32Bit Entwicklungsmaschine war dem nicht so, aber auf dem 64Bit Zielsystem. Nach einer etwas längeren Remotedebuggingsession stellte sich dann heraus das bei der Komponente mit dem "falschen Namen", nicht einmal der Konstruktor angesprungen wird, geschweige denn die Informationen aus IBaseComponent, wenn man diese in VisualStudio "an"browst.

Erst nach der vollständigen Entfernung der log4net Referenzierung (darauf muss man erstmal kommen), wurde der korrekte Name aus dem Interface angezeigt. Befand sich log4net gar im GAC war die Pipeline Komponente überhaupt nicht mehr auffindbar, beim manuellen Browsen gabs dann nur noch den lapidaren Hinweis das wohl etwas mit der Sicherheitseinstellung der Komponente nicht stimmen würde... sehr merkwürdig....

Eine Sicherheitseinstellung in Windows Server? Ein Problem mit log4net? :-/

Montag, 9. Juni 2008

Fallstrick: DateTime.AddXXX()

Ich stolpere immer wieder darüber und wie ich heute gesehen habe, bin ich tatsächlich nicht der einzige, dem es so geht:

Auf einen vorhandenen DateTime soll eine bestimmte Anzahl von Stunden (oder Minuten, Sekunden, ...) addiert werden. Natürlich, kein Problem, dafür bietet der DateTime ja die Methode AddHours(). Ein Aufruf, um auf das aktuelle Datum mit der aktuellen Uhrzeit fünf Stunden zu addieren, sähe demnach so aus:

   1:  DateTime dt = DateTime.Now;
   2:  dt.AddHours(5.0);

Verwendet man jetzt jedoch den Wert von dt weiter, um ihn beispielsweise anzuzeigen, fällt auf, dass die fünf Stunden nicht wie erwartet auf den aktuellen Wert addiert wurden. Der Grund hierfür ist ein ganz einfacher, der einem sogar von IntelliSense angezeigt wird.
Der Aufruf geht einfach deshalb ins Leere, weil er nicht auf den übergebenen DateTime addiert, sondern das Ergebnis der Addition zurück gibt. Irgendwo logisch, es handelt sich ja nicht um einen ref- oder out-Parameter, andererseits ist es dennoch irgendwie verwirrend, da es einfach logisch erscheint, dass auf den Ursprungswert addiert wird.

Wie auch immer, der korrekte Aufruf muss wie folgt aussehen:

   1:  DateTime dt = DateTime.Now;
   2:  dt = dt.AddHours(5.0);

Auf dass ich in Zukunft nicht mehr hierüber stolpern werde!

Vergleich VB.NET und C#

Ich springe in letzter Zeit öfter mal zwsichen VB.NET und C# hin und her, wobei ich ganz klar aus der C#-Ecke komme und mich daher mit der Syntax von VB.NET nicht wirklich auskenne.
Da kommt mir diese Seite mehr als gelegen. Hier wird ein direkter Vergleich angestellt, wie etwas in C# und VB.NET realisiert wird. Danke dafür an meinen Kollegen Sebastian.

Sollte ein Leser des Öfteren zwischen C# und Java wechseln müssen, für den gibt es hier eine ähnliche Übersicht.

Dienstag, 3. Juni 2008

Hashtable in Key Reihenfolge abarbeiten

Ich hatte in einer Hashtable Strings mit einem Integer Wert als Key abgelegt und wollte die nun in numerischer Folge abarbeiten, mein erster gedankenloser Ansatz war etwas umständlich. Mit etwas nachdenken hatte ich einen eleganteren Weg gefunden.

Dazu hab ich ersteinmal die Hashtable in ein generisches Dictionary umgewandelt, dann die Keys in eine generische Liste geschaufelt und dann einfach den Sort angewendet. Natürlich hätte dies auch mit einer Hashtable und einer Arraylist funktioniert, aber da beide in jeder Hinsicht in der Performance den Generics unterlegen sind hab ich das umgeschrieben nach diesem Beispiel:

   1:  ...
   2:  Dictionary<int, string> ht = new Dictionary<int, string>(10);
   3:  ht.Add(1, "hallo");
   4:  ht.Add(2, "welt");
   5:  ht.Add(3, "!");
   6:  ...
   7:             
   8:  List<int> keys = new List<int>(ht.Keys);
   9:  keys.Sort();
  10:   
  11:  foreach (int k in keys)
  12:  {
  13:     System.Console.WriteLine("key = " + k.ToString() + " value = " + (string)ht[k]);
  14:  }
  15:  ...

Das funktioniert dann auch analog mit anderen Datentypen, ggf. muss dann der Comparer implementiert/überladen werden, je nach Geschmack :)

Donnerstag, 29. Mai 2008

Seltsames Verhalten im Event Log

Zur Zeit schreibe ich, wenn es die Zeit und die Laune zulassen, an einem kleinen Tool zum Auslesen des Windows Event Log. Dabei habe ich eine recht interessante Feststellung gemacht.
Zum Auslesen der Einträge einer bestimmten Quelle, die im Event Log registriert ist, habe ich folgendes getan:

   1:  public static EventLogEntryCollection GetEntriesForSource( string logSource )
   2:  {
   3:      EventLog log;
   4:      EventLogEntryCollection entries;
   5:   
   6:      source = logSource;
   7:   
   8:      // EventLog vorbereiten
   9:      log = new EventLog();
  10:      log.Source = logSource;
  11:              
  12:      // EventLog-Einträge auslesen
  13:      entries = log.Entries;
  14:   
  15:      return entries;
  16:  }

Hier tue ich nichts anders, als ein EventLog-Objekt zu erstellen, das, wer hätte es gedacht, den Zugriff auf das Windows Event Log erlaubt.
Anschließend wird die Quelle (Source) gesetzt, deren Einträge ausgelesen werden sollen. Damit ist die Vorbereitung auch schon abgeschlossen und die Einträge innerhalb der Source können in eine EventLogEntryCollection eingelesen werden.

Damit ist das Auslesen aus der gewünschten Quelle auch schon abgeschlossen - dachte ich. Seltsamerweise werden trotz des Setzens der zu lesenden Quelle alle Einträge des Logs, in dem sich die Quelle befindet, in die EventLogEntryCollection geschrieben.

Ich möchte jetzt nicht so weit gehen zu sagen, dass es sich hierbei um einen Fehler im Konzept des Event Log-Handlings handelt, aber für mich ist es nicht logisch, alle Einträge in meine Collection geschrieben werden, obwohl ich - für meine Begriffe - eine Eingrenzung auf eine ganz bestimmte Source vorgenommen habe.

Das Schlimme daran ist nicht die Tatsache an sich, sondern viel eher, dass man sie nicht direkt wahrnimmt. Ich habe beispielsweise in eigenen Tests nie zwei mal hintereinander Quellen aus dem gleichen Log verwendet, so dass mir nie aufgefallen ist, dass alle Quellen des Logs exakt gleich viele (und vor allem die selben) Einträge hat. Das hat erst ein Betatester festgestellt (vielen Dank dafür an tropensturm!).

Die Beseitigung des Problems gestaltet sich glücklicherweise denkbar einfach, so dass in meiner Anzeige jetzt tatsächlich nur noch die erwarteten Einträge zu finden sind. Hierfür wird einfach nachträglich nochmals die Source eingegrenzt.

   1:  if( !entry.Source.Equals( source ) )