HR - Gehaltserhöhung Template
Dienstag, 6. Juli 2010 06:38
Mein Chef hat mir gezeigt, wie leicht es doch ist über die Abteilung HR eine Gehaltserhöhung zu bekommen. Ich fand das so gut, das muss ich nun hier Euch zeigen:
using System;
using MYCOMPANY.HelperFromHell;

namespace MYCOMPANY.Management.HR.HellWeDontWantThis
{
    public class IncreaseSalary
    {
        private string m_id;
        private decimal m_salary;

        public IncreaseSalary(string idNumberOfEmployee, decimal currentSalary)
        {
            m_id = idNumberOfEmployee;
            m_salary = currentSalary;
        }

        /// <summary>
        /// This is the increase of salary done by HR from time to time.
        /// </summary>
        /// <returns>New salary.</returns>
        public decimal IncreaseItsHr()
        {
            return m_salary;
        }

        /// <summary>
        /// Employee request a higher salary.
        /// </summary>
        /// <param name="whatEmployeeWants">Increase that results from the fucking brain of our brain dead employee.</param>
        /// <returns>New increased salary.</returns>
        public decimal OhNoEmployeeWantsMoreMoneyHowCouldThatHappen(decimal whatEmployeeWants)
        {
            if (Math.Sign(whatEmployeeWants) == 1)
            {
                m_salary = HrHelperFromHell.DoSomeSalaryMagic(m_salary);
                throw new ArgumentOutOfRangeException("whatEmployeeWants", whatEmployeeWants, "The increase is to high! HR Manager was brought to hospital.");
            }
            else
            {
                m_salary += whatEmployeeWants;
            }

            return m_salary;
        }
    }
}


Big Smile
XSD.exe /c - Klassengenerierung richtig gemacht
Samstag, 26. Juni 2010 15:07
Einleitung:
XSD.exe hat so seine Tücken, ich glaube das zeigt auch der große Unmut, der im Internet zu finden ist. Dabei muss das doch nicht sein Big Smile

Ich habe festgestellt, dass wenn man XSD verwenden möchte, dass es sinnvoll ist den Berg zum Propheten zu tragen. Im Folgenden erkläre ich, was das Problem eigentlich so dabei ist, und wie man das geschickt umschiffen kann.

Problembeschreibung:
XSD generiert lediglich aus den Teilen Klassen und Felder, von denen es denkt, dass diese zu gebrauchen sind. Dabei beachtet es nur die Elemente in den Schemas, Complex- und SimpleTypes für sich betrachtet lässt es außen vor.

Beispiel:
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein SimpleElement, das ebenfalls in der Klassengeneration beachtet werden soll -->
  <xs:simpleType name="SimpleRootElement">
    <xs:restriction base="xs:string">
      <xs:enumeration value="TestVal1" />
      <xs:enumeration value="TestVal2" />
    </xs:restriction>
  </xs:simpleType>

</xs:schema>

Hier meldet XSD die Fehlermeldung
Warning: cannot generate classes because no top-level elements with complex type were found.

Genauso sieht es aus, wenn das Schema so aussehen würde:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein normales Element, das kein ComplexType verwendet -->
  <xs:element name="CommonRootElement" type="xs:string" />

</xs:schema>


Und wiederum folgendes Schema, das ebenfalls die o.g. Fehlermeldung provoziert:
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein normales Element, das kein ComplexType verwendet -->
  <xs:element name="CommonRootElement" type="xs:string" />
  
  <!-- Ein ComplexType, aus dem eine Klasse generiert werden soll -->
  <xs:complexType name="ComplexTypeWhichIsReferenced">
    <xs:sequence>
      <xs:element name="CommonInnerElement1" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

</xs:schema>


Was will das "doofe" XSD denn überhaupt, warum erzeugt es mir aus dem Schema keine Klassen?

Problemerklärung:
XSD will zwingend ein ElementType, das im Root des Xml-Baumes des Schemas liegt. Weiterhin will es zwingend mindestens einen ComplexType definiert wissen, welches von solch einem Root-Element referenziert wird. Also folgendes Schema ist als Basis für XSD ausreichend, so dass es mit einer Klassengenerierung beginnt:

<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es allerdings ein Top level element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced2' veweist-->
  <xs:element name="TestElement2" type="ComplexTypeWhichIsReferenced2" />
  <xs:complexType name="ComplexTypeWhichIsReferenced2">
    <xs:sequence>
      <xs:element name="CommonInnerElement2" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

</xs:schema>


Ergebnis (Kommentare und Attribute entfernt):
public partial class ComplexTypeWhichIsReferenced2 {
    
    private string commonInnerElement2Field;
    
    /// <remarks/>
    public string CommonInnerElement2 {
        get {
            return this.commonInnerElement2Field;
        }
        set {
            this.commonInnerElement2Field = value;
        }
    }
}


Na das ist doch soweit super, oder? Jetzt hat XSD endlich ein Ergebnis produziert, das auch einem prüfenden Blick Stand hält.
Aber...

Erweiterte Problembeschreibung:
Wenn jetzt Kombinationen aus den oben genannten Schemas und deren Fehlermeldungen mit dem funktionierenden Beispiel zusammenspielen, dann wird es gefährlich. Denn dann wird XSD keine Fehlermeldung ausspucken und aber nur einen Teil für die Klassengenerierung heranziehen. Das ist denke ich das größte Manko, das diesem Tool angelastet wird. So denkt der Benutzer, dass das Tool eine gewisse undurchschaubare Eigenintelligenz verwendet.

Ich habe hier einmal ein kleines Schema entwickelt, das sehr viele Probleme auf einmal betrachtet:
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <!-- Ein SimpleElement, das ebenfalls in der Klassengeneration beachtet werden soll -->
  <xs:simpleType name="SimpleRootElement">
    <xs:restriction base="xs:string">
      <xs:enumeration value="TestVal1" />
      <xs:enumeration value="TestVal2" />
    </xs:restriction>
  </xs:simpleType>
  
  <!-- Ein normales Element, das kein ComplexType verwendet -->
  <xs:element name="CommonRootElement" type="xs:string" />
  
  <!-- Ein ComplexType, aus dem eine Klasse generiert werden soll -->
  <xs:complexType name="ComplexTypeWhichIsReferenced">
    <xs:sequence>
      <xs:element name="CommonInnerElement1" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es allerdings ein Top level element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced2' veweist-->
  <xs:element name="TestElement2" type="ComplexTypeWhichIsReferenced2" />
  <xs:complexType name="ComplexTypeWhichIsReferenced2">
    <xs:sequence>
      <xs:element name="CommonInnerElement2" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es ein inneres Element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced' veweist-->
  <xs:complexType name="TestInnerLink">
    <xs:sequence>
      <xs:element name="ComplexInnerElement" type="ComplexTypeWhichIsReferenced" />
    </xs:sequence>
  </xs:complexType>
</xs:schema>


Wenn man das so mittels XSD durch den Klassengenerator jagt, dann entsteht folgender C# Code:
public partial class ComplexTypeWhichIsReferenced2 {
    
    private string commonInnerElement2Field;
    
    /// <remarks/>
    public string CommonInnerElement2 {
        get {
            return this.commonInnerElement2Field;
        }
        set {
            this.commonInnerElement2Field = value;
        }
    }
}


Was unschwer zu erkennen ist: Das ist genau das Gleiche, wie in dem oben genannten funktionierenden Minischema. Also hier hat XSD alles andere als das TestElement2 ignoriert!

Lösung:
Die Lösung ist aus der oben erwähnten Fehlermeldung heraus begründet. XSD betrachtet nur den Inhalt zur Klassengenerierung, welcher als Root-Element auf andere Complex-Typen verweist.
Das könnte man nun manuell machen, oder es automatisiert anwenden. Ich habe hier ein kleines Tool geschrieben, das die notwendigen Elemente einfach dem Schema hinzufügt. Die Logik im Schema ändert sich quasi nicht, aber XSD ist glücklich und das Ergebnis sieht sehr viel besser aus.
Das Tool macht von der Logik her folgendes (Codeausschnitt):
        /// <summary>
        /// Methode, die ein XML Schema erweitert, so dass keine ClassType ohne Element existieren,
        /// das darauf verweist.
        /// </summary>
        /// <remarks>Verhindert die Warnung:
        /// cannot generate classes because no top-level elements with complex type were found.</remarks>
        /// <param name="xsdFullFilename">Der voll qualifizierte Dateiname zur XSD Schemadatei.</param>
        /// <param name="throwError">Definiert, ob im Fehlerfall eine Exception geworfen werden soll.</param>
        /// <returns>True, wenn die Erweiterung der Schemadatei erfolgreich war, andernfalls False.</returns>
        public static bool GenerateDummyElementsForClassTypes(string xsdFullFilename, bool throwError)
        {
            XmlDocument schemaDoc = null;
            bool retVal = false;
            List<string> rootElements = new List<string>();
            Dictionary<string, string> rootElementsToAddToSchema = new Dictionary<string, string>();

            try
            {
                // Überprüfung, ob Schemadatei vorhanden
                if (!System.IO.File.Exists(xsdFullFilename))
                {
                    throw new System.IO.FileNotFoundException(String.Format("Konnte die Datei '{0}' nicht finden!", xsdFullFilename));
                }

                // Versuch das Schema in ein XmlDokument zu laden
                schemaDoc = new XmlDocument();
                schemaDoc.Load(xsdFullFilename);

                // Alle Root-Elements herausfinden
                ExtractAllElements(schemaDoc, rootElements);

                // Alle Root-ComplexTypes herausfinden
                ExtractAllClassTypes(schemaDoc, rootElements, rootElementsToAddToSchema);

                // Alle vorgemerkten Root-Elements dem Schema hinzufügen
                AddNecessaryRootElementsToSchemafile(xsdFullFilename, schemaDoc, rootElementsToAddToSchema);

                retVal = true;
            }
            catch
            {
                // wenn gewünscht, dann Fehler schmeißen, ansonsten nur den
                // Rückgabewert auf "False" setzen.
                if (throwError)
                {
                    throw;
                }
                retVal = false;
            }

            return retVal;
        }

Es gibt einen vollständigen Download des VisualStudio 2010 Projektes hier: XsdPrepare.zip

Durch Anwendung der Methode GenerateDummyElementsForClassTypes entsteht aus dem kompletten Schema oben folgendes:
<xs:schema targetNamespace="http://MyTestSchemas/XMLSchema.xsd" elementFormDefault="qualified" xmlns="http://MyTestSchemas/XMLSchema.xsd" xmlns:mstns="http://tempuri.org/XMLSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <!-- Ein SimpleElement, das ebenfalls in der Klassengeneration beachtet werden soll -->
  <xs:simpleType name="SimpleRootElement">
    <xs:restriction base="xs:string">
      <xs:enumeration value="TestVal1" />
      <xs:enumeration value="TestVal2" />
    </xs:restriction>
  </xs:simpleType>
  <!-- Ein normales Element, das kein ComplexType verwendet -->
  <xs:element name="CommonRootElement" type="xs:string" />
  <!-- Ein ComplexType, aus dem eine Klasse generiert werden soll -->
  <xs:complexType name="ComplexTypeWhichIsReferenced">
    <xs:sequence>
      <xs:element name="CommonInnerElement1" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es allerdings ein Top level element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced2' veweist-->
  <xs:element name="TestElement2" type="ComplexTypeWhichIsReferenced2" />
  <xs:complexType name="ComplexTypeWhichIsReferenced2">
    <xs:sequence>
      <xs:element name="CommonInnerElement2" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  <!-- Ein weiterer ComplexType, aus dem eine Klasse generiert werden soll 
       Hier gibt es ein inneres Element, das auf den komplexen Typ 'ComplexTypeWhichIsReferenced' veweist-->
  <xs:complexType name="TestInnerLink">
    <xs:sequence>
      <xs:element name="ComplexInnerElement" type="ComplexTypeWhichIsReferenced" />
    </xs:sequence>
  </xs:complexType>
  <xs:element name="RootElement0" type="SimpleRootElement" />
  <xs:element name="RootElement1" type="TestInnerLink" />
</xs:schema>


Und wenn man dann XSD nochmal drüberlaufen lässt, dann entsteht folgender Code:
public partial class ComplexTypeWhichIsReferenced2 {
    
    private string commonInnerElement2Field;
    
    /// <remarks/>
    public string CommonInnerElement2 {
        get {
            return this.commonInnerElement2Field;
        }
        set {
            this.commonInnerElement2Field = value;
        }
    }
}

public partial class ComplexTypeWhichIsReferenced {
    
    private string commonInnerElement1Field;
    
    /// <remarks/>
    public string CommonInnerElement1 {
        get {
            return this.commonInnerElement1Field;
        }
        set {
            this.commonInnerElement1Field = value;
        }
    }
}

public enum SimpleRootElement {
    
    /// <remarks/>
    TestVal1,
    
    /// <remarks/>
    TestVal2,
}

public partial class TestInnerLink {
    
    private ComplexTypeWhichIsReferenced complexInnerElementField;
    
    /// <remarks/>
    public ComplexTypeWhichIsReferenced ComplexInnerElement {
        get {
            return this.complexInnerElementField;
        }
        set {
            this.complexInnerElementField = value;
        }
    }
}


Wenn man jetzt ein letztes Mal genau drauf schaut, dann hat XSD den Fall CommonRootElement nicht betrachtet, aber hier muss man zu dessen Schutz sagen, hier wüsste ich auch nicht, wie man hieraus eine Klasse basteln soll.

Aber mit meinem kleinen Helfertool gehen nun (hoffentlich) keine Inhalte mehr durch die Klassengenerierung verloren, und so kann man das auch in automatisierten Prozessen dann einsetzen.

Ich hoffe, dass die nähere Betrachtung hilfreich war, und dass ihr etwas mit dem Tool anfangen könnt. Über Kommentare und Verbesserungsvorschläge würde ich mich freuen.

Es gibt einen vollständigen Download des VisualStudio 2010 Projektes hier: XsdPrepare.zip
Mock Objekt - Wie generiere ich eine SQLException ohne Datenbank?
Freitag, 4. Juni 2010 09:27
Folgendes Problem: Wie kann man für Unit Tests bestimmte SQL Exceptions provozieren? Vielleicht sogar noch ohne Datenbank? Bei mir war das Problem, dass wir standardisierte Retry-Mechanismen verwenden, die aber bei bestimmten Fehlerszenarien keinen Sinn machen. So haben wir diverse SQL Fehlermeldungen / Errorcodes definiert, bei denen kein Retry stattfinden soll. Wie testet man nun so etwas? Hier die Antwort:

Mit Hilfe der folgenden Klasse kann man beliebige SqlExceptions erzeugen lassen, und somit Datanbankfehlerszenarien testen, ohne die Fehler mit einer Datenbank reproduzieren zu lassen.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Reflection;

namespace Testing.MockObjects
{
    /// <summary>
    /// Hilfsklasse, um SQL Exceptions zu generieren. Diese Klasse bietet nur statische
    /// Methoden an.
    /// </summary>
    public class MockSqlException
    {
        #region Constructor

        /// <summary>
        /// Standard Konstruktor, nicht öffentlich, da nur statische Methoden verfügbar sein sollen.
        /// </summary>
        protected MockSqlException()
        {}

        #endregion

        #region Public methods

        /// <summary>
        /// Erstellt eine SqlException über Reflection.
        /// </summary>
        /// <param name="errorMessage">Die Fehlermeldung, die für die SqlException.Message
        /// Eigenschaft vorgesehen werden soll.</param>
        /// <param name="errorNumber">Die Sqlspezifische Fehlernummer.</param>
        /// <returns>Die generierte SqlException.</returns>
        public static SqlException CreateSqlException(string errorMessage, int errorNumber)
        {
            // Eine SqlException enthält immer eine SqlErrorCollection
            SqlErrorCollection collection = GetErrorCollection();

            // Wir brauchen nur einen Fehler, den wir hiermit generieren
            SqlError error = GetError(errorNumber, errorMessage);

            // Dieser Fehler wird der Collection über die Eigenschaft "add" hinzugefügt
            MethodInfo addMethod = collection.GetType().GetMethod("Add", BindingFlags.NonPublic | BindingFlags.Instance);
            addMethod.Invoke(collection, new object[] { error });

            // Hier wird der Konstruktor der SqlException aufgerufen und die SqlErrorCollection
            // als Parameter übergeben (Standard Konstruktor)
            Type[] types = new Type[] { typeof(string), typeof(SqlErrorCollection) };
            object[] parameters = new object[] { errorMessage, collection };
            ConstructorInfo constructor = typeof(SqlException).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
            SqlException exception = (SqlException)constructor.Invoke(parameters);

            // Die SQLException wird zurückgegeben
            return exception;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Generiert über Reflection einen SqlError, der einer SqlErrorCollection hinzugefügt werden kann.
        /// </summary>
        /// <param name="errorCode">Die Sqlspezifische Fehlernummer.</param>
        /// <param name="message">>Die Fehlermeldung, die für die SqlException.Message
        /// Eigenschaft vorgesehen werden soll.</param>
        /// <returns>Das generierte SqlError Objetkt.</returns>
        private static SqlError GetError(int errorCode, string message)
        {
            // generiert über den Konstruktor und dessen Argumente eine SqlError Instanz
            object[] parameters = new object[] { errorCode, (byte)0, (byte)10, "server", message, "procedure", 0 };
            Type[] types = new Type[] { typeof(int), typeof(byte), typeof(byte), typeof(string), typeof(string), typeof(string), typeof(int) };
            ConstructorInfo constructor = typeof(SqlError).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
            
            return  (SqlError)constructor.Invoke(parameters);
        }
        
        /// <summary>
        /// Generiert über Reflection eine leere SqlErrorCollection Instanz.
        /// </summary>
        /// <returns>Die generierte SqlErrorCollection Instanz.</returns>
        private static SqlErrorCollection GetErrorCollection()
        {
            // generiert über den Konstruktor eine SqlErrorCollection Instanz
            ConstructorInfo constructor = typeof(SqlErrorCollection).
            GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { }, null);
            SqlErrorCollection collection = (SqlErrorCollection)constructor.Invoke(new object[] { });

            return collection;
        }

        #endregion
    }
}


Wie aus dem Code gut zu erkennen ist, wird der ganze Aufbau der SqlException über Reflection getätigt, weil die SqlException leider nicht öffentlich zugänglich ist.

Die SqlException ist auch immer so aufgebaut, dass sie eine ErrorCollection entgegennimmt, deshalb der umständliche Weg über die Collection. Weiterhin wichtig ist, dass die Eigenschaft SqlException.ErrorCode NICHT den ErrorCode vom SqlServer liefert, sondern den HRESULT, mit dem meist nichts anzufangen ist. Für den SqlErrorCode muss man die Eigenschaft SqlException.Number abfragen. Witzigerweise entspricht der SqlErrorcode der SqlException immer des SqlErrorCodes des ersten SqlErrors aus der Collection (hardcoded!).

Ein Anwendungsbeispiel ist dann z.b. folgender:
        static void Main(string[] args)
        {
            try
            {
                throw MockSqlException.CreateSqlException(
                    "The conversion from datetime data type to smalldatetime data type resulted in a smalldatetime overflow error",
                    298);
            }
            catch (SqlException sqlex)
            {
                int test = sqlex.ErrorCode;
                Console.WriteLine(String.Format("Eine SQL Exception ist aufgetreten: ErrorCode: {0} Message {1}", sqlex.Number, sqlex.Message));
            }

            Console.WriteLine("If this is displayed, a sql exception was thrown, and everything is fine ;-)");
            Console.ReadKey();
        }


Übrigens eine Auflistung aller SQL Fehlermeldungen findet man in der Datenbank selbst:
In SQL 2005: sys.messages
In SQL 2000: master.dbo.sysmessages

Mit Hilfe des Verfahrens lassen sich Datenbank-Negativtests erheblich beschleunigen, weil keine Verbindung zu einer Datenbank notwendig ist.
[deprecated] XslTransform wird zu XslCompiledTransform -> Umstellungsprobleme
Mittwoch, 2. Juni 2010 02:59
Mit unserer Umstellung auf das neue Framework habe ich mich mit dem Problem beschäftigt, dass XslTransform veraltet ist. Die Anweisung, dass nun XslCompiledTransform zu verwenden ist, ist zwar schön und gut, aber wir stießen dabei auf 2 markante Probleme:

Fehler 1:
For security reasons DTD is prohibited in this XML document. To enable DTD processing set the DtdProcessing property on XmlReaderSettings to Parse and pass the settings into XmlReader.Create method.

Fehler 2:
The 'xsl:stylesheet' element is not declared.

Im Folgenden erkläre ich nun Schritt für Schritt, worin die Probleme lagen. Hierfür soll folgendes Ausgangsszenario gelten:

Ausgangssituation:

Gegeben sei folgender Quellcode:
        static void Main(string[] args)
        {
            XPathDocument doc = new XPathDocument("PocTest1_log.xml");
            StreamWriter swriter = new StreamWriter("PocTest1_log.html");

            XslTransform trans = new XslTransform();
            trans.Load("XSLTFile1.xslt");
            trans.Transform(doc, null, swriter);
        }

dabei ist das zu transformierende Dokument "PocTest1_log.xml" erst einmal unwichtig, es enthält ein valides und vollständiges Xml DOM Objekt.
Interessant ist das XSLT Stylesheet, das für eine Transformation zu einem HTML Dokument verwendet werden soll. Die Besonderheit ist, dass dieses einen DTD Teil definiert, also eine Entity mit definiert. Zu Demozwecken, und damit den Fehler zu provozieren, genügt folgendes Dummy Stylesheet:
<!DOCTYPE xsl:stylesheet [ <!ENTITY jquerysrc SYSTEM 'test.js'> ]>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="xml" indent="yes"/>

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Wenn nun der o.g. Code ausgeführt wird läuft alles zunächst wunderbar, außer, dass die Warnung XslTransform ist deprecated ausgegeben wird.

Umstellung auf XslCompiledTransform Schritt 1:

Eine Umstellung auf XslCompiledTransform ergibt zunächst folgender Code:
        static void Main(string[] args)
        {
            XPathDocument doc = new XPathDocument("PocTest1_log.xml");
            StreamWriter swriter = new StreamWriter("PocTest1_log.html");

            XslCompiledTransform trans = new XslCompiledTransform();
            trans.Load("XSLTFile1.xslt");
            trans.Transform(doc, null, swriter);
        }

Die Laufzeit ergibt den o.g. Fehler 1 (Zeile trans.Load): "For security reasons DTD is prohibited in this XML document ..."

Umstellung auf XslCompiledTransform Schritt 2 (Lösung zu Schritt 1):

Im Netz findet sich hier die bereits etablierte Lösung, dass man mittels einem XmlReader und der darin enthaltenen Möglichkeit XmlReaderSettings mitzugeben die Lösung enthalten sei:
        static void Main(string[] args)
        {
            XPathDocument doc = new XPathDocument("PocTest1_log.xml");
            StreamWriter swriter = new StreamWriter("PocTest1_log.html");

            XmlReaderSettings xmlReadSet = new XmlReaderSettings();
            xmlReadSet.ValidationType = ValidationType.DTD;
            xmlReadSet.DtdProcessing = DtdProcessing.Parse;

            XslCompiledTransform trans = new XslCompiledTransform();
            trans.Load(XmlReader.Create("XSLTFile1.xslt", xmlReadSet));
            trans.Transform(doc, null, swriter);
        }

Übrigens, die Eigenschaft XmlReaderSettings.ProhibitDtd ist ebenso veraltet, hierzu kann man wie im Code angegeben die Eigenschaft XmlReaderSettings.DtdProcessing verwenden.

Die Laufzeit ergibt allerdings den o.g. Fehler 2 (Zeile trans.Load): "The 'xsl:stylesheet' element is not declared."

Diese Fehlermeldung ist alles andere als selbsterklärend, und total irreführend. Ich habe lange herumgespielt, aber im Netz war nichts zu finden, und auch ein intensives Debugging brachte leider keine Lösung. Ich habe nur folgende Erkenntnisse gehbt (zunächst):
- Entfernung des DTD (DOCTYPE Knoten) -> Code funktioniert
- Verwendung von XsltSettings mit diversen Einstellmöglichkeiten (z.B. TrustedXslt) -> keine Veränderung, Code funktioniert nicht
- XsltArgumentList kann nicht verwendet werden, da das Problem schon bei Load auftritt, nicht erst bei Transform
- ...

Umstellung auf XslCompiledTransform vollständige Lösung:
Ich weiß nicht warum ich versucht habe noch einmal mit den XmlReaderSettings herumzuspielen, aber die Änderung des ValidationType brachte dann die funktionierende Lösung:
        static void Main(string[] args)
        {
            XPathDocument doc = new XPathDocument("PocTest1_log.xml");
            StreamWriter swriter = new StreamWriter("PocTest1_log.html");

            XmlReaderSettings xmlReadSet = new XmlReaderSettings();
            xmlReadSet.DtdProcessing = DtdProcessing.Parse;
            xmlReadSet.ValidationType = ValidationType.Schema;

            XslCompiledTransform trans = new XslCompiledTransform();
            trans.Load(XmlReader.Create("XSLTFile1.xslt", xmlReadSet));
            trans.Transform(doc, null, swriter);
        }

So ganz kann ich die Fehlermeldung nicht nachvollziehen, ich wüsste nicht, wenn ich das Stylesheet als DTD behandle, was darin syntaktisch verkehrt wäre, und warum eine Validierung fehlschlägt, aber eine Umstellung auf Schema brachte dann schlußendlich die Lösung.
Common Tools: TreeSize Free
Mittwoch, 26. Mai 2010 12:17
Ein sehr nützliches Freeware Tool, um die Verzeichnisse herauszufinden die auf der Platte den meisten Platz verbrauchen. Ideal um mal auszukehren:



Zu finden unter JAM Software - Treesize Free. Die Private bzw. Professional Version bietet dann deutlich neuere Versionen mit noch mehr Features, die aber für den allgemeinen Hausgebrauch aber nicht notwendig sind.

Viel Spaß mit dem Tool
Von Thunderbird zurück zu Outlook
Dienstag, 25. Mai 2010 05:45
Einleitung:
Ich habe mich nach langer Nutzungsdauer dazu entschieden vom Mailclient Thunderbird zurück zu Outlook zu wechseln. Ich bin von Office2010 und dessen Outlook derart begeistert, dass ich diesen Schritt wagen wollte. Leider stellte sich heraus, dass das aber nicht so einfach war, denn so komfortabel Thunderbird auch ist, es gibt darin keine Export Funktion, und auch keine Möglichkeit, dass ein externes Programm bequem auf die E-Mails zugreifen kann.


Quelle: http://www.office2010-blog.de

Problembeschreibung:
Alle im Internet gefundenen Vorschläge waren sehr schwierig umzusetzen, oder bereits veraltet. So gibt es sehr viele Anleitungen, die besagen, dass Thunderbird E-Mails im Profilordner im MBox Format abspeichere und dass es Konvertierungsprogramme wie MBox2Eml möglich machen würden E-Mails zu konvertieren und sie für Outlook Express zugänglich zu machen. Also wäre hier der Weg MBox2Eml -> Outlook Express -> Outlook, aber...
Ja das war so, jedoch ab Version 3.x (oder schon früher) verwendet Thunderbird die Datenbank SQL Compact. Das macht das Konzept schon wieder hinfällig. Eine kurze Verwendung des SQLCE Viewer brachte zwar Einblicke in die Datenbankstruktur, und auch E-Mails habe ich gefunden, jedoch ist das deutlich zu unkomfortabel und auch zu unsicher daraus manuell Daten zu extrahieren.

Lösungsfindung:
Es gibt auch im Internet die zahlreichen Hinweise, dass man den Umweg über IMAP gehen kann. Dieser Weg habe ich zunächst verfolgt und als Lösungsweg erachtet. Aber auch hier gibt es genügend Probleme, um damit einen ganzen Tag zu verbringen. Ich wollte nicht den Umweg über meinen Mail-Provider (Freenet oder T-Online) zu gehen, um alle Mails erst Online zu bringen, weil ich doch eine recht beschränkte DSL Leitung habe, und weil meine Online Postfachgröße auch nur beschränkt ist. Also wollte ich einen Mailserver lokal aufsetzen...

Unter der englischen Wikipadia Seite Comparison of mail servers findet sich schnell ein Überblick über alle gängigen Mail Server, vor allem auch welche, die kostenfrei einsetzbar sind. Ich möchte es kurz machen: kein E-Mail Server, der kostenlos ist, und sich unter Windows installieren lässt war dazu geeignet, einmal schnell einen lokalen Mailserver aufzusetzen. Ausprobiert habe ich folgende Server mit folgendem Ergebnis:
- Apache James -> gescheitert aufgrund der fehlenden Java-Umgebung
- hMailServer -> dieser kann leider nur SMTP Forwarding, weshalb ich nicht wusste, was bei IMAP Zugriff und Mailupload letztendlich passiert
- UW Imap -> Die Webseite ist sehr undurchschaubar, und es gibt dort nur die Möglichkeit zig Dateien per FTP herunterzuladen, aber keine anständige Anleitung was und wie ich da vorgehen sollte, also habe ich es erst gar nicht probiert
- Meldware Mail Server (Pegasusmail aka pMail) -> sehr komplexe Bedienung, und irgendwie habe ich es nicht geschafft, das Ding nur unter lokalem Netzwerk zum Fliegen zu bringen (macht dem Namen nicht die Ehre ;-)

Lösung:
Ich erinnerte mich, dass ich vor nicht allzu langer Zeit einen SME Server bei mir laufen hatte. Das ist ein File/Mail/Internet Server, dessen Fokus auf die Bedienbarkeit und Installationsleichtigkeit gelegt wurde.


Quelle: http://www.linux-user.de/ausgabe/2006/10/014-sme-server/index.html

Also nur kurz über VMWare eine neue VM aufgesetzt, diesen SME Server installiert, so konfiguriert, dass er als "nur Server" betrieben wird, und dass die VM netzwerktechnisch so eingebunden ist, dass die VM wie ein Rechner im lokalen Netzwerk sich verhält (bridged). Dem SME Server noch eine IP Adresse aus dem lokalen Bereich angegeben, und dann über den Host via Web-Admin Schnittstelle auf die Serverkonfiguration zugegriffen (Bild oben). Als letztes ToDo ist noch ein neuer Benutzer anzulegen, und ein neues Passwort ihm zu verpassen. Ansonsten ist nichts zu konfigurieren, alles andere macht standardmäßig SME Server für einen!

Nun kann man im Thunderbird ein neues Konto anlegen, und auf den SME Server verweisen lassen (ich hab das z.B. so realisiert, dass die Mailadresse dann timo@192.168.0.123 hieß). Thunderbird erkennt automatisch den möglichen IMAP Zugriff, bringt dann aber eine Warnung, dass das SMTP Zertifikat nicht vertrauenswürdig sei. Klar, es ist ein von SME Server erstelltes Zertifikat, das in keiner Zertifizierungsstelle bekannt ist; diese Meldung kann man aber getrost ignorieren. Als letztes Feintuning habe ich in den Konteneinstellungen noch das Häkchen gelöscht, das bewirkt eine lokale Kopie vorzuhalten und dass E-Mails größer als 1 KB nicht heruntergeladen werden. Sorry, ich habe bereits Thunderbird deinstalliert, ich hätte gleich dran denken sollen Screenshotbilder zu machen, nun habe ich keine.

Ist das neue Konto eingerichtet, dann kann man alle Ordner per Drag & Drop in das neue IMAP Konto ziehen. Dabei werden alle darin enthaltenen Mails auf den SME Server transferiert, die Ordner werden automatisch mit angelegt. Abschließend zu diesem Vorgang darf man die E-Mails in den Standardordnern "Posteingang", "Gesendete Mails" etc. nicht vergessen, dort muss man alle Mails durch "Alle markieren" in entsprechende IMAP Ordner (gegebenenfalls neue anlegen) schieben. Dann Thunderbird schließen.

Zum Schluss Outlook2010 starten, das gleiche IMAP Konto anlegen, und kurz warten, bis Outlook alle Mails heruntergeladen hat (standardmäßig macht Outlook eine lokale Kopie). Sobald Outlook fertig ist, alle Ordner wieder per Drag & Drop in die lokale Datendatei ziehen und fertig.

Zum Aufräumen empfiehlt es sich dann Thunderbird zu deinstallieren, das lokale Profilverzeichnis manuell zu löschen (bei mir waren dort noch Reste zu finden), in Outlook das lokale IMAP Konto wieder entfernen, dann die Virtuelle Maschine wieder entfernen. Ich würde schätzen, dass je nach Menge von Mails, das Ganze zwischen einer halben Stunde und 1 Stunde erledigt sein sollte (inkl. Installation SME Server, aber ohne Download).

Ich hoffe, es war nützlich, über Kommentare würde ich mich freuen.
PowerShell Provider für BizTalk
Donnerstag, 6. Mai 2010 11:28
Heute wurde ich darauf aufmerksam gemacht, dass es auf Codeplex einen Powershell Provider für MS BizTalk 2006 R2 / 2009 gibt.

Das ist durchaus eine sinnvolle Erweiterung zur BizTalk Administration Console, vor allem wenn man mal schnell eine manuelle Tätigkeit (z.B. Analyse vom MessageFlow) durchführen möchte.

Alles weitere kann man auf der Codeplex Seite nachlesen.
von Timo Rehl | 1 comment(s)
Abgelegt unter: ,
Windows Batch - Tricks
Dienstag, 13. April 2010 10:19
Ich habe zwecks einem Lasttest ein Batch Programm geschrieben, das dauerhaft ein Testtool mit Zufallsabfragen angeschmissen hat und dann ein Ergebnis auswertet und dieses in einer CSV Datei protokolliert. Dabei bin ich auf folgende Stolpersteine gestossen:
  1. Zufallszahlen in einer Schleife
  2. Ausgabe von Text in einer Datei ohne Zeilenumbruch
  3. Ermitteln von Dateigrößen
  4. In Aufrufverzeichnis wechseln (und wieder zurück)
1. Zufallszahlen in einer Schleife:

Ich habe festgestellt, dass Zufallszahlen, die in einer Schleife erzeugt werden, z.B. so:
FOR /L %%i in (1,1,5) DO (
    SET /A rnd=10*%random%/32768
    ECHO Zahl: %rnd%
)

immer wieder die gleiche Zahl liefern, also nicht wirklich zufällig Stick out tongue

Lösung:
Man muss einen Zwischenaufruf tätigen, sozusagen einen neuen Call-Stack aufbauen lassen, dann wird wirklich immer wieder eine neue Zufallszahl erzeugt (meiner Ansicht nach nicht sehr plausibel, aber wenns dann funktioniert):
FOR /L %%i in (1,1,5) DO (
    CALL :ZWISCHENCALL
)
GOTO :EOF

:ZWISCHENCALL
    SET /A rnd=10*%random%/32768
    ECHO Zahl: %rnd%
GOTO :EOF


2. Ausgabe von Text in einer Datei ohne Zeilenumbruch:

Diese Anforderung war meines Erachtens schon schwierig umzusetzen, ich habe auch lange dafür im Netz suchen müssen, bis ich zum gewünschten Ergebnis kam. Ich hätte ja gerne Zwischenschritte als CSV in einer Datei protokolliert, jedoch um eine komfortable Auswertung z.B. mit Excel zu haben, kann dann nicht nach jeder Ausgabe ein Zeilenumbruch erfolgen.
Ein simples "ECHO" erzeugt immer einen Zeilenumbruch!

Lösung:
set /p =TextToDumpIntoFile;<nul>> Result.csv


Hier muss noch ein bischen Erklärung hinzu: ein "set /p" ist eine Benutzereingabeaufforderung ("/p" für prompt), in herkömmlicher Benutzung ist dann normalerweise das "set" so verwendet, dass die Benutzereingabe in einer Variablen gespeichert wird:
set /p %%userinput=Bitte Text eingeben:

In der obigen Lösung wird die Variable weggelassen (ja das geht!), und gleichzeitig die Usereingabe durch das "<nul" vorbestimmt. Übrig bleibt also der Prompt-Text (hier:"TextToDumpIntoFile;") welcher durch die herkömmliche Umleitung ">>Result.csv" an die Datei angehängt wird.
Sehr strange, aber auch das funktioniert ;-)

3. Ermitteln von Dateigrößen:

Hier kann mit einer For-Schleife gearbeitet werden (auch wenn nur eine Datei letztendlich interessant ist). Ich kann das nicht im vollen Umfang erklären, ich habe die Lösung bereits fertig im Netz gefunden:

Lösung:
for /f "delims=" %%i in ('dir FileToCheck.txt /s /B') do (
    if !%%~zi!==!1396! (
        REM Do whatever you want
    )
)


Knackpunkt ist das "delims" mit der Kombination "dir /s /B", worüber dann auch die Dateigröße mittels  "!%%~zi!" überprüft werden kann. In dem Beispiel wird eine Datei "FileToCheck.txt" überprüft, und diese muss dann 1396 Bytes groß sein.

3. In Aufrufverzeichnis wechseln (und wieder zurück):

Das ist ein Standardproblem, welches dann auch sehr einfach zu lösen ist. Dennoch muss man es wissen ;-) Grundproblem ist es, wenn man die Batchdatei nicht aus dem Verzeichnis aufruft, in dem die Batch Datei liegt. Man kann mit der angegebenen Lösung zum Verzeichnis der Batch-Datei wechseln und zum Schluß dann wieder zurück zum Aufrufverzeichnis.

Lösung:
REM wechselt ins Ausführungsverzeichnis
PushD "%~dp0"

REM hier kann z.B. ein externes Tool, was im selben Verzeichnis wie die Batch Datei liegt, aufgerufen werden.

REM wechselt ins Aufrufverzeichnis zurück
PopD


Ich hoffe diese Tricks helfen Euch auch weiter?
WMI Abfrage für Ereignisprotokoll nicht zuverlässig?
Donnerstag, 25. Februar 2010 09:34
Ich habe die letzten 2 Tage an einem Phänomen gekämpft, das hat mich wirklich an meinen Fähigkeiten als Entwickler zweifeln lassen.

Folgende Situation:
  • Ein Testfall erzeugt durch Systemzeitumstellungen definierte Einträge in verschiedene Ereignisprotokolle.
  • Nach jedem Schreiben gibt es im Testframework eine Sicherheitsüberprüfung, ob der Eintrag vorhanden ist.
  • Nachdem alle Einträge geschrieben wurden, wird meine Komponente getestet, dessen Aufgabe es ist Ereignisse von verchiedenen Rechnern und Ereignisprotokollen zu holen.
  • Aufgrund der hohen Datenmengen werden die Ereignisse zeitscheibenweise per WMI abgeholt (siehe auch: Blog Rehl - Quota violation)
Fehlerbeschreibung:
Unter Umständen kam es vor, dass für den ersten konfigurierten Rechner und das erste Ereignisprotokoll in der ersten Zeitscheibe keine Ergebnisse vorlagen (trotz richtig konzipiertem Testfall), ich schätze das Auftreten auf ca. 30%.

Details:
Meine Vorgehensweise ist so codetechnisch umgesetzt worden:
                    ManagementObjectSearcher searcher = null;
                    ManagementScope scope = null;
                    SelectQuery query = null;

                    // Vorbereitungen und WMI Query Objekt Initialisierungen
                    searcher = new ManagementObjectSearcher();
                    scope = new ManagementScope(string.Format("\\\\{0}", computerName));
                    scope.Path = new ManagementPath("root\\CIMV2");
                    scope.Path.Server = computerName;
                    scope.Connect();
                    searcher.Scope = scope;
                    query = new SelectQuery();

                    // Datum WMI gerecht umwandeln
                    strIntervalStartTime = ManagementDateTimeConverter.ToDmtfDateTime(intervalStartTime);
                    strIntervalEndTime = ManagementDateTimeConverter.ToDmtfDateTime(intervalEndTime);

                    // Hiermit werden die Eventlogs ausgelesen
                    query.QueryString = string.Format("SELECT * FROM Win32_NTLogEvent WHERE (LogFile = '{0}' And TimeGenerated > '{1}' And TimeGenerated < '{2}')", eventLog, strIntervalStartTime, strIntervalEndTime);

                    // Die Abfrage dem Suchobjekt zuweisen
                    searcher.Query = query;

                    // Ergebnisse ermitteln
                    retVal = searcher.Get();


Analyse:
Zunächst dachte ich, dass es etwas mit den Zeitscheiben zu tun hatte, die Trace-Ausgaben waren aber eindeutig, dass alles richtig laufen sollte. Ein nachträglich eingebautes Retry (10 Stück an der Zahl), das durchgeführt wurde, wenn keine Ergebnisse ermittelt wurden brachte kein Ergebnis.

Also haben wir einen Alternativweg gesucht um Ereignisse zu ermitteln und stießen im .NET Framework auf System.Diagnostic.EventLog. Die EventLog Klasse verwendet intern die Windows API anstatt WMI. Erste Tests brachten hier ein besseres Ergebnis. Es wurden immer alle Ereignisse zurückgeliefert. Problem: Die EventLog Klasse kann keine Filterung durchführen. Es holt immer alle Ereignisse aus einem Protokoll (zumindest die Headerinformationen hierzu). Ein Lasttest brachte auch die Erkenntnis, dass das bei großen Ereignisprotokollen nicht zu empfehlen ist.
                EventLog log = new EventLog(eventLog, computerName);
                foreach (EventLogEntry entry in log.Entries)
                {
                    if (entry.TimeGenerated > startTime && entry.TimeGenerated < endTime)
                    {
                        retVal = false;
                        break;
                    }
                }


Ergebnis:
Also machten wir einen abschließenden Test: Bringt die WMI Abfrage keine Ergebnisse, dann ermitteln wir mittels der EventLog Klasse die Anzahl der Ereignisse, die in der Zeitscheibe liegen. Nur dann wenn WMI kein Ergebnis liefert, und die Windows API auch nicht, dann überspringen wir die Zeitscheibe, ansonsten wird die Verarbeitung aufgrund des Datenverlusts abgebrochen.

Fazit:
Ich würde aufgrund unserer Tests, WMI als nicht zuverlässig einstufen, wenn es um das Holen von Ereignissen geht!
Übrigens ein Count(*) ist grundsätzlich für WMI gültig, aber für die Ereignisse nicht verfügbar.
von Timo Rehl | mit no comments
Abgelegt unter: , ,
Visual Studio Source Control Explorer (TFS) Encoding Detection
Dienstag, 2. Februar 2010 15:31
Heute bin ich auf ein interessantes Feature in Visual Studio im Source Control Explorer gestossen. Über Rechts-Klick auf eine Datei (die vorab per Get auf die Platte geholt werden muss) -> Properties kommt man unter anderem auf die Möglichkeit "Set Encoding...", das man problemlos anklicken kann.

Das Ganze sieht dann so aus:

Mittels "Detect" kann man Visual Studio erkennen lassen in welchem Format aktuell die Datei abgespeichert ist. Man kann hier auch das Encoding direkt setzen, aber bei Verlusten (z.b. von UTF-8 zu Windows-1252) möchte ich nicht wissen was Visual Studio dann verschluckt.

Zum Erkennen von Encodings aber durchaus brauchbar, ganz besonders wenn es über UTF-8 und Ansi hinausgeht.
von Timo Rehl | mit no comments
Abgelegt unter: ,
Mehr Beiträge « Vorige Seite - Nächste Seite »