ASP.NET: Vertauschte Sessions (OutputCache und Cookieless Sessions)

by wolfgang@gehirnwindung.de (Wolfgang) März 22, 2013 22:50

Ich wurde vor kurzem mit einem Problem konfrontiert, bei dem ich meinem Kunden natürlich weiterhelfen wollte. Bei einem Web-Projekt wurden sporadisch Sessions "vertauscht" - so sah ein Benutzer A plötzlich die Userdaten eines Benutzers B. Das kam eher selten vor, aber jeder einzelne Fall ist natürlich unschön.

Meine erste Vermutung war, dass das Session-Objekt (oder eine der darin enthaltenen Variablen) evtl. in statischen Variablen gelandet ist. Das soll wohl öfter mal vorkommen und sieht z.B. so aus.

private static Session _sess;
public static void DoSomething() {
    if (_sess == null) {
        _sess = HttpContext.Current.Session;
    }
    if (_sess != null) {
        // ...
    }
}

Zugegeben, sieht recht harmlos aus - ist aber folgenschwer. Ich hab den Ersteller der Webseite kontaktiert und ihn gefragt, ob so etwas ähnliches evtl. enthalten ist. Der machte sich auch direkt auf die Suche nach ähnlichen Konstrukten, blieb aber erfolglos. Danach bekam ich sogar direkt Zugriff auf den Quellcode.

Ich hab den Code natürlich nochmal durchforstet (und die Fremd-Assemblies analysiert) - aber auch ich blieb ohne Erfolg bei meiner Suche (war ja auch überheblich zu denken, dass die anderen nicht richtig suchen würden - ich schäm mich *g*). Die Cache-Einstellungen schienen in Ordnung. Ich hab daraufhin mehrere Tests mit PhantomJS erstellt und durchgehend geprüft, ob die Session irgendwann verloren geht oder die Daten andere werden - aber auch hier ohne jeden Lichtblick. So ging das jetzt mehrere Tage.

Heute hatte ich was ganz anderes vor. Dabei musste ich Cookies in meinem Browser deaktiviert und kam eher zufällig auf die Seite - da hab ich es dann auch endlich mal gesehen: Userdaten eines anderen Benutzers. Und nun war auch endlich klar, wie es zum "vertauschen" der Sessions kam - nämlich gar nicht ;) Wir haben vielmehr - in einem Teilbereich - alle das Gleiche gesehen.

Die ASP.NET - Seite verwendet Cookies im "AutoDetect"-Modus (<session cookieless="AutoDetect" />). Es hätte aber auch "UseUrl" sein können - damit wäre es ebenso falsch gelaufen (allerdings wahrscheinlich leichter entdeckt).

Zusätzlich war bei einem ASP.NET Control (ein Footer) angegeben, dass die Ausgabe gecached werden kann. Das ist mir bei der vorherigen Durchforstung zwar aufgefallen, aber nicht wirklich negativ. Die Angabe sah so aus

<%@ OutputCache Duration="6000" VaryByParam="none" %>

Also auch hier an sich nichts schlimmes. Die HTML-Elemente waren allesamt Links zum Impressum und ähnlichen Seiten. Damit die Cookieless-Session beim Aufruf des Impressums nicht verloren geht, müssen natürlich auch diese Links mit Response.ApplyAppPathModifier() angepasst werden. Argh. Selbstverständlich wird die SessionID hier mitgecached - nur gesehen hab ich es nicht.

Der erste User nach den 6000 Sekunden gibt vor, wie der Footer aussieht.
Sind bei dem User Session-Cookies im Browser erlaubt, klappt es überall (die Cookieless-User verlieren Ihre Session, aber das ist weitaus weniger schlimm als fremde Userdaten zu sehen).
Ist der Browser des "führenden" Users dagegen so eingestellt, dass Session-Cookies abgelehnt werden, bekommen alle dessen Session angezeigt...

Nun kann man entweder den OutputCache ganz abstellen, oder aber die Angabe etwas erweitern zu

<%@ OutputCache Duration="6000" VaryByParam="none" VaryByCustom="session" %>

Hierfür muss aber in der global.asax noch die Funktion GetVaryByCustomString überschrieben werden, in etwa so:

public override string GetVaryByCustomString( HttpContext context, string custom ) {
    if (custom == "session") {
        if( context.Session != null ) {
            return context.Session.SessionID;
        }
    }
    return base.GetVaryByCustomString(context, custom);
}

So funktionieren auch OutputCache und Cookieless Sessions problemlos zusammen... Und beim nächsten Mal seh ich das auch gleich ;)

LINQ Extension Methods: Intersect

by wolfgang@gehirnwindung.de (Wolfgang) Mai 19, 2010 20:25

Mit der Intersect-Erweiterungsmethode vergleicht man zwei Sequenzen des gleichen Typs miteinander und erhält als Ergebnis eine neue Sequenz, in der die Elemente der ersten Sequenz vorhanden sind, die auch in der 2. Sequenz vorkommen (Gegenteilig zur Except-Erweiterungsmethode). Dabei wird jedes Element nur einmal aufgeführt. Verglichen wird - wenn man nichts anderes angibt - mit EqualityComparer<T>.Default.

Ohne Intersect Extension Method kann man das Verhalten in etwa so nachbilden

private List<T> Intersect<T>( List<T> list1, List<T> list2, EqualityComparer<T> comparer ) {
    var ret = new List<T>( );
    if( comparer == null ) comparer = EqualityComparer<T>.Default;

    for( int i = 0; i < list1.Count; i++ ) {
        if(
            !ret.Contains( list1[i], comparer ) &&
            list2.Contains( list1[i], comparer )
        ) {
            ret.Add( list1[i] );
        }
    }

    return ret;
}

Der Aufruf mit Erweiterungsmethode kann so erfolgen

list1.Intersect( list2 );
list1.Intersect( list2, EqualityComparer<T>.Default );

LINQ Extension Methods: Except

by wolfgang@gehirnwindung.de (Wolfgang) Mai 12, 2010 21:00

Mittels Except-Erweiterungsmethode vergleicht man zwei Sequenzen des gleichen Typs miteinander und erhält als Ergebnis eine neue Sequenz, in der die Elemente der ersten Sequenz vorhanden sind, die in der 2. Sequenz nicht vorkommen. Dabei wird jedes Element nur einmal aufgeführt. Verglichen wird - wenn man nichts anderes angibt - mit EqualityComparer<T>.Default.

Ohne Except Extension Method kann man das Verhalten in etwa so nachbilden

private List<T> Except<T>( List<T> list1, List<T> list2, EqualityComparer<T> comparer ) {
    var ret = new List<T>( );
    if( comparer == null ) comparer = EqualityComparer<T>.Default;

    for( int i = 0; i < list1.Count; i++ ) {
        if(
            !ret.Contains( list1[i], comparer ) &&
            !list2.Contains( list1[i], comparer )
        ) {
            ret.Add( list1[i] );
        }
    }

    return ret;
}

Doch zum Glück gibt es die Erweiterungsmethoden, der Aufruf erfolgt in etwa so

list1.Except( list2 );
list1.Except( list2, EqualityComparer<T>.Default );

LINQ Extension Methods: Union

by wolfgang@gehirnwindung.de (Wolfgang) Mai 12, 2010 01:00

Die Erweiterungsmethode Union bietet die Möglichkeit, 2 Sequenzen des gleichen Typs zu einer so zusammenzuführen, dass von jedem Element nur noch eine Entsprechung übrig bleibt. Damit verhält sich Union wie der Aufruf von Concat().Distinct().

Die Reihenfolge der Elemente in der Ergebnissequenz ergibt sich aus der Reihenfolge innerhalb der einzelnen Listen, die nacheinander abgearbeitet werden.

Ohne Union - Extension Method sieht das in etwa so aus

private List<T> Union<T>( List<T> list1, List<T> list2, EqualityComparer<T> comparer ) {
    var result = new List<T>( );
    if( comparer == null ) comparer = EqualityComparer<T>.Default;
    for( int i = 0; i < list1.Count; i++ ) {
        if( !result.Contains( list1[1], comparer ) ) {
            result.Add( list1[i] );
        }
    }

    for( int i = 0; i < list2.Count; i++ ) {
        if( !result.Contains( list2[1], comparer ) ) {
            result.Add( list2[i] );
        }
    }

    return result;
}

Der Vergleich wird - wenn nicht anders angegeben - mit dem Default - EqualityComparer<T> vollzogen.

Genauso verhält sich auch die Union-Erweiterungsmethode, die wie folgt aufgerufen wird

list1.Union( list2 );
list1.Union( list2, EqualityComparer<T>.Default );

LINQ Extension Methods: Zip

by wolfgang@gehirnwindung.de (Wolfgang) Mai 11, 2010 18:51

Mit der Zip-Erweiterungsmethode können die Elemente zweier Sequenzen 1:1 zusammengeführt werden. Das Ergebnis ist eine neue Sequenz.
Zip ist somit eine spezialisierte Form von Join, wobei der Index als Key verwendet wird. Daher ist es auch egal, wie viele Elemente die jeweiligen Listen haben - zurückgegeben werden nur diejenigen Elemente, die in beiden Listen vorkommen.

Angenommen, man hat die 2 Listen firstNames und lastNamens mit den Vor- bzw. Nachnamen und möchte diese miteinander so kombinieren, dass eine string-Sequenz herauskommt, in der diese Namen durch Komma getrennt zusammengesetzt stehen. Die beiden Listen sind entsprechend sortiert.

Ohne Zip-Methode würde das in etwa so aussehen

var fullNames = new List<string>();
int count = Math.Min( firstNames.Count, lastNames.Count );
for( int i=0; i<count; i++ ){
    fullNames.Add( string.Format( "{1}, {0}", firstNames[i], lastNames[i] ) );
}

wobei die 2. Zeile vereinfacht werden kann, wenn sichergestellt ist, dass die beiden Listen gleich viele Elemente enthalten.

Mit der Zip-Methode geht das wesentlich kürzer (und ist meiner Meinung nach auch etwas leichter lesbar).
Wer noch Probleme mit den Lambda-Ausdrücken hat, der kann sich mit dem Artikel Tanz den => Lambda mit mir... ein wenig einlesen.

var fullNames = firstNames.Zip(
                    lastNames,
                    ( firstName, lastName ) => string.Format( "{1}, {0}", firstName, lastName )
                );

Der Rückgabewert ist nicht auf string-Sequenzen beschränkt. Es kann eine beliebige Klasse (z.B. eine anonymen Klasse) angegeben werden.

Z.B. ist fullNames hier

var fullNames = firstNames.Zip(
                    lastNames,
                    ( firstName, lastName ) => new {
                                                     FirstName = firstName,
                                                     LastName = lastName
                                               }
                );

eine Sequenz von Klassen, in denen die Eigenschaften FirstName und LastName gesetzt sind.

SQL Server: Von wiederhergestellten Datenbanken und verwaisten Benutzern

by wolfgang@gehirnwindung.de (Wolfgang) Mai 10, 2010 23:04

Immer wieder - jedoch insgesamt selten und aus dem Grund auch leider immer wieder mit einer Suche verbunden - brauch ich nach der Wiederherstellung einer Datenbank eine Stored Procedure, um die Benutzer aus der wiederhergestellten Datenbank und die Logins des Servers wieder in "Einklang" zu bringen.

Das Problem tritt z.B. auf, wenn die DB in einem anderem Server wiederhergestellt wird. Die Ursache ist, dass zwar evtl. der Login- und der Username gleich, die im Hintergrund wirkenden ID's aber unterschiedlich sind.

Nun. Hierfür gibt es die SP sp_change_users_login. Mit einem Administratorkonto (sysadmin oder db_owner) an der Datenbank anmelden und folgendes Ausführen:

Um eine Übersicht über die Benutzer zu bekommen, die verwaist sind

EXEC sp_change_users_login @action='Report'


Wenn der Login inkl. Passwort bereits eingerichtet ist, der Benutzer aber noch zugeordnet werden muss.

EXEC sp_change_users_login @action='Auto_Fix', @userNamePattern='user'


Wenn der Benutzer zugeordnet und ein Login mit Passwort erstellt werden muss (ist der Login bereits vorhanden, wird @password ignoriert)

EXEC sp_change_users_login @action='Auto_Fix', @userNamePattern='user', @password='password'


Man kann auch einen Benutzer mit einem Login verbinden, wenn die beiden nicht den gleiche Namen haben. Dann sieht es so aus (auch hier kann noch optional ein Passwort angegeben werden)

EXEC sp_change_users_login @action='Update_One', @userNamePattern='user', @loginName='login'

 

So, und beim nächsten Mal weiß ich, wo ich suchen muss ;)

BizTalk: Maps mit Referenzen zu externen Assemblies debuggen

by wolfgang@gehirnwindung.de (Wolfgang) Mai 05, 2010 14:25

Um BizTalk-Maps zu debuggen, die eine exterene Assembly aufrufen, reichen die Standard-Möglichkeiten nicht mehr aus. Es erscheint die Fehlermeldung

Cannot find the script or external object that implements prefix 'http://schemas.microsoft.com/BizTalk/2003/ScriptNS0'

Um das zu umgehen, müssen die externen Assemblies dem Debugger bekannt gemacht werden. Um allgemein XSLT zu debuggen kann, neben der Möglichkeit dies über das Menü "XML" -> "Debug XSLT" zu erledigen, auch das Objekt XslCompiledTransform verwendet werden. Hierdurch ist man wesentlich flexibler und kann z.B. StartParameter angeben und/oder eben Extension Objects". Und da eine BizTalk-Map letztendlich auch nur ein XSLT-Dokument darstellt, gilt das auch dafür.

Als erstes sollte man ein C#-Projekt zur BizTalk-Solution hinzufügen. Darin wird lediglich unten stehende Methode eingefügt und die "Main"-Methode so angepasst, dass ein Aufruf der Methode "DebugXslt" mit den jeweiligen Parametern erfolgt.

/// <summary>
/// Helper Method to debug Maps with Extension Objects
/// </summary>
/// <param name="mapXslt">Path to xslt-file generated by the MapperCompiler (Validate Map)</param>
/// <param name="inputXml">Path to input-file (Test Map)</param>
/// <param name="extensionObjectsXml">Path to Extension Object XML (Validate Map)</param>
static void DebugXslt( string mapXslt, string inputXml, string extensionObjectsXml ) {
    if( !string.IsNullOrEmpty( mapXslt ) && !string.IsNullOrEmpty( inputXml ) && File.Exists( mapXslt ) && File.Exists( inputXml ) ) {
        XslCompiledTransform xslt = new XslCompiledTransform( true );

        // allow embedded scripts and document()-function
        XsltSettings xsltSettings = new XsltSettings( true, true );

        // Load the style sheet.
        xslt.Load( mapXslt, xsltSettings, null );

        // resolve extension objects
        XsltArgumentList xsltArgs = new XsltArgumentList( );
        if( !string.IsNullOrEmpty(extensionObjectsXml) && File.Exists( extensionObjectsXml ) ) {
            XmlReaderSettings rsettings = new XmlReaderSettings( );
            rsettings.IgnoreComments = true;
            rsettings.IgnoreProcessingInstructions = true;
            rsettings.IgnoreWhitespace = true;

            using( XmlReader reader = XmlReader.Create( extensionObjectsXml, rsettings ) ) {
                reader.ReadStartElement( "ExtensionObjects" );

                do {
                    string ns = "", an = "", cn = "";
                    while( reader.MoveToNextAttribute( ) ) {
                        switch( reader.LocalName ) {
                            case "Namespace":
                                reader.ReadAttributeValue( );
                                ns = reader.Value;
                                break;
                            case "AssemblyName":
                                reader.ReadAttributeValue( );
                                an = reader.Value;
                                break;
                            case "ClassName":
                                reader.ReadAttributeValue( );
                                cn = reader.Value;
                                break;
                        }
                    }

                    // load type and add instance to xslt-Arguments
                    if( !string.IsNullOrEmpty( ns ) && !string.IsNullOrEmpty( an ) && !string.IsNullOrEmpty( cn ) ) {
                        Type t = Type.GetType( Assembly.CreateQualifiedName( an, cn ) );
                        xsltArgs.AddExtensionObject( ns, System.Activator.CreateInstance( t, false ) );
                    }
                } while( reader.ReadToNextSibling( "ExtensionObject" ) );

                reader.Close( );
            }
        }

        // Execute the transformation.
        using( XmlWriter writer = XmlWriter.Create( TextWriter.Null ) ) {
            xslt.Transform( inputXml, xsltArgs, writer );
            writer.Close( );
        }

        xsltArgs.Clear( );
    }
}

Parameter Beschreibung
mapXslt Pfad zur XSLT-Datei
inputXml Pfad zur der Datei, die die Testdaten enthält
extensionObjectsXml

Pfad zur Datei, in der die Erweiterungsobjekte stehen (darf leer sein)

Nun müssen noch die einzelnen Pfade ermittelt werden. Mittels "Validate Map" (rot im Bild) werden die XSLT-Datei und die extensionObjects-Datei erstellt. Im Output-Window erscheinen die jeweiligen Pfade und müssen nur noch übernommen werden.
Hat man kein XML-Dokument, mit dem man Testen kann, sondern ein natives Format, so hilft "Test Map". Auch hier erscheint im Output-Window der Pfad mit dem Hinweis "Test Map used the following file".

 

Sind nun alle 3 Pfade ermittelt und eingetragen, kann es auch endlich losgehen. Damit der Debugger nicht einfach durchläuft, sollte die XSLT-Datei geöffnet und eine Haltepunkt eingefügt werden.

 

Nun muss das Projekt gestartet werden. Dazu mit der rechten Maustaste auf das Projekt zeigen und "Debug" -> "Start new instance" ausführen.
Alternativ das Projekt als StartUp-Project einrichten und F5 drücken...

The Type initializer for '{0}' threw an exception - oder warum die Reihenfolge statischer Felder nicht egal ist...

by wolfgang@gehirnwindung.de (Wolfgang) April 29, 2010 18:06

Heute hab ich in einem Projekt meine Klassen etwas aufgeräumt und dabei auch die Reihenfolge der Methoden, Eigenschaften und Variablen angepasst. Der Build des Projekts war erfolgreich - die Tests danach aber nicht. Es erschien der Fehler:

The Type initializer for '{0}' threw an exception.

Da ich mir aber sicher war, dass ich wirklich nur Elemente auf Klassenebene verschoben hab, hat mich das wirklich sehr verwundert... Aus dem StackTrace konnte man entnehmen, dass es sich um den statische Konstruktor (.cctor()) handelt. Nun, ich schreibe oft statische Konstruktoren, aber in aller Regel ohne jeglichen Inhalt, weswegen der Grad der Verwunderung dementsprechend stieg.

Kurzum, der Fehler liegt wirklich an der Reihenfolge, allerdings geht es hierbei ausschließlich um statische Felder, die in der richtigen Reihenfolge definiert sein müssen. Bei genauerer Betrachtung ist das auch nur logisch. Ich hätte mir an der Stelle eine schöne Kompiler-Fehlermeldung gewünscht.

Ein einfaches Beispiel zeigt, worum es genau geht.

public class Test {
    private static readonly decimal x = 1 / y;
    private static readonly int y = 8;

    static Test() { }
}

Sobald eine Instanz der Klasse erstellt wird, kommt es zu einer Division durch 0 (bzw. zum Versuch desselben) und damit zu besagter Ausnahme.
Zur Veranschaulichung hier noch das Äquivalent. y ist bei der ersten Verwendung noch mit dem default-Wert belegt - und der ist nunmal 0.

public class Test {
    private static readonly decimal x;
    private static readonly int y;

    static Test() {
        x = 1 / y;
        y = 8;
    }
}

Damit ist aber auch klar, dass man die Initialisierung statischer Variablen innerhalb des statischen Konstruktors selbst vornehmen sollte - zumindest wenn diese von anderen Variablen abhängen.

Tags:

C#

BizTalk: Zugriff auf UNB-Segment und AS2-Eigenschaften innerhalb Orchestration

by wolfgang@gehirnwindung.de (Wolfgang) Februar 15, 2010 21:30

In einer Orchestration bekommt man es normalerweise nur noch mit den eigentlichen Daten einer Nachricht zu tun. So sind bei EDIFACT-Nachrichten z.B. die UNA- und UNB-Segmente nicht Teil des Schemas. Weiterhin fehlen auch die AS2-Header und eben alles, was nicht unbedingt benötigt wird. Das hat seine Vorteile (ist aufgeräumt), manchmal wird aber auch der Zugriff auf diese Daten benötigt.

Läuft eine Nachricht mal auf einen Fehler, so kann man in den "Message Details" erkennen, dass verschiedene "Context-Properties" aus eben diesen sonst nicht sichtbaren Segmenten (oder aus dem Protokoll) zu den einzelnen Nachrichten zugewiesen sind.

Auf diese Kontexteigenschaften kann man mittels Message(ContextProperty) innerhalb einer BizTalk Server Expression Shape in Orchestration Expression Shape zugreifen. Im Falle von EDIFACT-Eigenschaften muss aber - damit man die Auswahl der Context Properties überhaupt zu sehen bekommt - noch eine Referenz auf die DLL Microsoft.BizTalk.Edi.BaseArtifacts (oft zu finden unter C:\Program Files\Microsoft BizTalk Server 2006\Microsoft.BizTalk.Edi.BaseArtifacts.dll) zum Projekt hinzugefügt werden.

Mit dieser Referenz sind dann Kontexteigenschaften in den Namespaces EDI (für EDIFACT und X12 - Eigenschaften) und EdiIntAs (für AS2 - Eigenschaften) vorhanden.

BizTalk: Checkliste für das Senden von AS2-Nachrichten

by wolfgang@gehirnwindung.de (Wolfgang) Januar 20, 2010 21:29

Nach der Checkliste für das Empfangen von Nachrichten über AS2, muss quasi ja auch eine Checkliste für das Senden bereitgestellt werden ;) Diese Liste ist wesentlich kürzer, wenn es auch mehr Möglichkeiten gibt, als hier dargestellt...

  1. Send Port
    - für asynchrone MDN: One-way
    - für synchrone MDN: Solicit-Response
    • Type: HTTP
      • Destionation URL: Die URL, die beim Partner eingerichtet wird (auf beiden Seiten sollte die URL exakt gleich angegeben werden - inkl. Groß-/Kleinschreibung)
      • Enabled chunked encoding deaktivieren
    • Send pipeline
      • AS2Send (für alle möglichen Daten),
      • AS2EDISend (für EDI-Daten) oder
      • eine eigene Send Pipeline, in der die AS2 Encoder-Component verwendet wird
  2. Sender Party
    • General
      • Send Ports (den oben erstellen Port auswählen, alternativ geht die Zuweisung auch über einen Role Link oder wie unten beschrieben über den Alias-Namen)
    • AS2 Properties -> Party as AS2 Message Receiver (nicht verwirren lassen)
      • Einstellungen ab "Outbound AS2 Message" entsprechend der Vereinbarung mit dem Partner einstellen
      • AS2-From und AS2-To erscheinen wie hier angegeben im Head-Bereich der Nachricht und damit beim Partner
  3. Asynchrone MDNs müssen - wie eine beliebig andere AS2-Nachricht - empfangen werden können. An welche Adresse die MDN geschickt wird, kann in den Party-Einstellungen angegeben (Receipt-Delivery-Option) werden. Um die Verarbeitung dieser Nachrichten kümmert sich allerdings der BizTalk-Server selbst.

Die Party Resolution für ausgehende Nachrichten (also das Finden der Party, die die Nachricht empfängt und bei der die entsprechenden Einstellungen vorgenommen wurden) kann auch anders erfolgen. Das zuweisen des Send-Ports zu einer Party wird aber (meiner Erfahrung nach) sehr häufig verwendet. Es sollte allerdings auch ausreichen, einer Party einen EDIINT-AS2-To - Alias und der ausgehenden Nachricht den AS2-To - Header zuzuweisen - getestet hab ich das noch nicht.

Für die Signierung und Verschlüsselung der Nachricht und der Signierung der MDN müssen zusätzlich Zertifikate installiert und an bestimmten Stellen ausgewählt werden, die in dieser kleinen Übersicht beschrieben werden.

Powered by BlogEngine.NET 1.6.1.6
Theme by Mads Kristensen | Modified by Mooglegiant and me ;)